第二十八章 485实验
本章我们将向大家介绍如何使用STM32G474的串口实现485通信(半双工)。在本章中,我们将使用STM32G474的串口3来实现两块开发板之间的485通信,并将结果显示在TFTLCD模块上。
本章分为如下几个部分:
28.1 485简介
28.2 硬件设计
28.3 程序设计
28.4 下载验证
28.1 485简介
485(一般称作 RS485/EIA-485)隶属于OSI模型物理层,是串行通讯的一种。电气特性规定为2线,半双工,多点通信的类型。它的电气特性和RS-232大不一样。用缆线两端的电压差值来表示传递信号。RS485仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。
RS485的特点包括:
1. 接口电平低,不易损坏芯片。RS485的电气特性:逻辑“1”以两线间的电压差为+(2~6)V表示;逻辑“0”以两线间的电压差为-(2~6)V表示。接口信号电平比RS232降低了,不易损坏接口电路的芯片,且该电平与TTL电平兼容,可方便与TTL电路连接。
2. 传输速率高。10米时,RS485的数据最高传输速率可达35Mbps,在1200m时,传输速度可达100Kbps。
3. 抗干扰能力强。RS485接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗噪声干扰性好。
4. 传输距离远,支持节点多。RS485总线最长可以传输1200m左右,更远的距离则需要中继传输设备支持(速率≤100Kbps才能稳定传输),一般最大支持32个节点,如果使用特制的485芯片,可以达到128个或者256个节点,最大的可以支持到400个节点。
RS485推荐使用在点对点网络中,比如:线型,总线型网络等,而不能是星型,环型网络。理想情况下RS485需要2个终端匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为120Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终端匹配电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。485推荐的一主多从连接方式如图28.1.1所示:

图28.1.1 RS485连接
在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备4上面各加一个120Ω的匹配电阻。
由于RS485具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以RS485有很广泛的应用。实际多设备时收发器有范围为-7V到+12V的共模电压,为了稳定传输,也有使用3线的布线方式,即在原有的A、B两线上多增加一条地线。(4线制只能实现点对点的全双工通讯方式,这种也叫RS422,由于布线的难度和通讯局限,相对使用得比较少)。
TP8485E/SP3485可作为RS485的收发器,该芯片支持3.3V~5.5V供电,最大传输速度可达250Kbps,支持多达256个节点(单位负载为1/8的条件下),并且支持输出短路保护。该芯片的框图如图28.1.2所示:

图28.1.2 TP8485E/SP3485框图
图中A、B总线接口,用于连接485总线。RO是接收输出端,DI是发送数据收入端,RE是接收使能信号(低电平有效),DE是发送使能信号(高电平有效)。
28.2 硬件设计
1. 例程功能
经过前面的学习我们知道实际的RS485仍是串行通讯的一种电平传输方式,那么我们实际通讯时可以使用串口进行实际数据的收发处理,使用485转换芯片将串口信号转换为485的电平信号进行传输,本章,我们只需要配置好串口3,就可以实现正常的485通信了,串口3的配置和串口1基本类似,只是串口3的时钟来自APB1,最大频率为170Mhz。
本章将实现这样的功能:通过连接两个电机开发板的RS485接口,然后由KEY0控制发送,当按下一个开发板的KEY0的时候,就发送5个数据给另外一个开发板,并在两个开发板上分别显示发送的值和接收到的值。
2. 硬件资源
1)LED灯
LED0 – PE0
2)USART3,用于485信号串行通讯。
3)正点原子1.3寸TFTLCD模块(SPI接口)
4)RS485收发芯片TP8485/SP3485
5)开发板两块(485半双式模式无法自收发,我们需要用两个开发板或者USB转485调试器+串口助手来帮助我们完成测试,大家根据自己的实际条件选择)
3. 原理图
根据我们需要实现的程序功能,我们设计电路原理如下:

图28.2.1 RS485连接原理设计
从上图可以看出:开发板的串口3(PB10、PB11)连接在TP8485上,485A和485B是接线端子,图中的R30和R33是两个偏置电阻,用来保证总线空闲时,A、B之间的电压差都会大于200mV(逻辑1),从而避免因总线空闲时因A、B压差不稳定,可能出现的乱码。我们只需要用2根导线将两个开发板RS485端子的A和A,B和B连接起来。这里注意不要接反了(A接B),接反了会导致通讯异常!!
28.3 程序设计
28.3.1 RS485的HAL库驱动
由于485实际上是串口通讯,我们参照串口实验一节使用类似的HAL库驱动即可,在这里分析一下RS485配置步骤。
RS485配置步骤
1)使能串口和GPIO口时钟
本实验用到USART3口,使用PB10和PB11作为串口的TX和RX脚,因此需要先使能USART3和GPIOB时钟。参考代码如下:
__HAL_RCC_USART3_CLK_ENABLE(); /* 使能USART3时钟 */ __HAL_RCC_GPIOB_CLK_ENABLE(); /* 使能GPIOB时钟 */
2) 串口参数初始化(波特率、字长、奇偶校验等)
HAL库通过调用串口初始化函数HAL_UART_Init完成对串口参数初始化,详见例程源码。
该函数通常会调用HAL_UART_MspInit函数来完成对串口底层的初始化,包括:串口及GPIO时钟使能、GPIO模式设置、中断设置等。但是本实验避免与USART1冲突,所以没有把串口底层初始化放在HAL_UART_MspInit函数里。
3)GPIO模式设置(速度,上下拉,复用功能等)
GPIO模式设置通过调用HAL_GPIO_Init函数实现,详见本例程源码。
4)开启串口相关中断,配置串口中断优先级
本实验我们使用串口中断来接收数据。我们使用HAL_UART_Receive_IT函数开启串口中断接收,并设置接收buffer及其长度。通过HAL_NVIC_EnableIRQ函数使能串口中断,通过HAL_NVIC_SetPriority函数设置中断优先级。
5)编写中断服务函数
串口3中断服务函数为:USART3_IRQHandler,当发生中断的时候,程序就会执行中断服务函数,在这里就可以对接收到的数据进行处理,详见本例程源码。
6)串口数据接收和发送
最后我们可以通过读写USART_RDR/USART_TDR寄存器,完成串口数据的接收和发送,HAL库也给我们提供了:HAL_UART_Receive和HAL_UART_Transmit两个函数用于串口数据的接收和发送。
大家可以根据实际情况选择使用哪种方式来收发串口数据。
28.3.2 程序流程图

图28.3.2.1 RS485实验程序流程图
28.3.3 程序解析
1. RS485驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RS485驱动相关源码包括两个文件:rs485.c和rs485.h。
为方便修改,我们在rs485.h中使用宏定义485相关的控制引脚和串口编号,如果需要使用其它的引脚或者串口,修改宏和串口的定义即可,它们在rs485.h中定义,它们列出如下:
/* RS485 引脚 和 串口 定义 */
#define RS485_TX_GPIO_PORT GPIOB
#define RS485_TX_GPIO_PIN GPIO_PIN_10
#define RS485_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define RS485_RX_GPIO_PORT GPIOB
#define RS485_RX_GPIO_PIN GPIO_PIN_11
#define RS485_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define RS485_UX USART3
#define RS485_UX_IRQn USART3_IRQn
#define RS485_UX_IRQHandler USART3_IRQHandler
#define RS485_UX_CLK_ENABLE() do{ __HAL_RCC_USART3_CLK_ENABLE(); }while(0)
接下来介绍rs485.c文件里面的函数:
1)rs485_init函数
rs485_init的配置与串口类似,也需要设置波特率等参数,另外还需要配置收发模式的驱动引脚,我们的程序设计如下:
/**
* @brief RS485初始化函数
* @note 该函数主要是初始化串口
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @retval 无
*/
void rs485_init(uint32_t baudrate)
{
/* IO 及 串口时钟配置 */
RS485_TX_GPIO_CLK_ENABLE(); /* 使能 串口TX脚 时钟 */
RS485_RX_GPIO_CLK_ENABLE(); /* 使能 串口RX脚 时钟 */
RS485_UX_CLK_ENABLE(); /* 使能 串口 时钟 */
GPIO_InitTypeDef gpio_initure;
gpio_initure.Pin = RS485_TX_GPIO_PIN;
gpio_initure.Mode = GPIO_MODE_AF_PP;
gpio_initure.Pull = GPIO_PULLUP;
gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_initure.Alternate = GPIO_AF7_USART3; /* 复用为串口3 */
HAL_GPIO_Init(RS485_TX_GPIO_PORT, &gpio_initure); /* 串口TX 脚 模式设置 */
gpio_initure.Pin = RS485_RX_GPIO_PIN;
/* 串口RX 脚 必须设置成输入模式 */
HAL_GPIO_Init(RS485_RX_GPIO_PORT, &gpio_initure);
/* USART 初始化设置 */
g_rs458_handler.Instance = RS485_UX; /* 选择485对应的串口 */
g_rs458_handler.Init.BaudRate = baudrate; /* 波特率 */
g_rs458_handler.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
g_rs458_handler.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
g_rs458_handler.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
g_rs458_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
g_rs458_handler.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&g_rs458_handler); /* 使能对应的串口, 调用Msp */
__HAL_UART_DISABLE_IT(&g_rs458_handler, UART_IT_TC);
#if RS485_EN_RX /* 如果使能了接收 */
__HAL_UART_ENABLE_IT(&g_rs458_handler, UART_IT_RXNE); /* 开启接收中断 */
HAL_NVIC_EnableIRQ(RS485_UX_IRQn); /* 使能USART3中断 */
HAL_NVIC_SetPriority(RS485_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */
#endif
}
可以看到代码基本跟串口的配置一样,只是多了收发控制引脚的配置。
2)发送函数
发送函数用于输出485信号到485总线上,默认的485方式一般空闲时为接收状态,只有发送数据时我们才控制485芯片进入发送状态,发送完成后马上回到空闲接收状态,这样可以保证操作过程中485的数据丢失最小。我们实现的发送函数如下:
/**
* @brief RS485发送len个字节
* @param buf : 发送区首地址
* @param len : 发送字节数(为了和本代码接收匹配,这里不要超过RS485_REC_LEN个字节)
* @retval 无
*/
void rs485_send_data(uint8_t *buf, uint8_t len)
{
HAL_UART_Transmit(&g_rs458_handler, buf, len, 1000); /* 串口3发送数据 */
g_RS485_rx_cnt = 0;
}
3)485接收中断函数
RS485的接收与串口中断一样,不过要注意空闲时要切换回接收状态,否则会收不到数据。我们定义了一个全局的缓冲区g_RS485_rx_buf进行接收测试,通过串口中断接收数据,编写的接收代码如下:
uint8_t g_RS485_rx_buf[RS485_REC_LEN]; /* 接收缓冲, 最大 RS485_REC_LEN 个字节. */
uint8_t g_RS485_rx_cnt = 0; /* 接收到的数据长度 */
void RS485_UX_IRQHandler(void)
{
uint8_t res;
/* 接收到数据 */
if ((__HAL_UART_GET_FLAG(&g_rs458_handler, UART_FLAG_RXNE) != RESET))
{
HAL_UART_Receive(&g_rs458_handler, &res, 1, 1000);
if (g_RS485_rx_cnt < RS485_REC_LEN) /* 缓冲区未满 */
{
g_RS485_rx_buf[g_RS485_rx_cnt] = res; /* 记录接收到的值 */
g_RS485_rx_cnt++; /* 接收数据增加1 */
}
}
}
4)485查询接收数据函数
该函数用于查询485总线上接收到的数据,主要实现的逻辑是:一开始进入函数时,先记录下当前接收计数器的值,再来一个延时去判断接收是否结束(即该期间有无接收到数据),假如说接收计数器的值没有改变,就证明接收结束,我们就可以把当前接收缓冲区传递出去。函数实现如下:
/**
* @brief RS485查询接收到的数据
* @param buf : 接收缓冲区首地址
* @param len : 接收到的数据长度
* @arg 0 , 表示没有接收到任何数据
* @arg 其他, 表示接收到的数据长度
* @retval 无
*/
void rs485_receive_data(uint8_t *buf, uint8_t *len)
{
uint8_t rxlen = g_RS485_rx_cnt;
uint8_t i = 0;
*len = 0; /* 默认为0 */
delay_ms(10); /* 等待10ms,连续超过10ms没有接收到一个数据,则认为接收结束 */
if (rxlen == g_RS485_rx_cnt && rxlen) /* 接收到了数据,且接收完成了 */
{
for (i = 0; i < rxlen; i++)
{
buf[i] = g_RS485_rx_buf[i];
}
*len = g_RS485_rx_cnt; /* 记录本次数据长度 */
g_RS485_rx_cnt = 0; /* 清零 */
}
}
RS485的代码就讲到这里,基本是串口的知识,大家不明白的配置可以翻看之前串口章节的知识。
2. main.c代码
在main.c中编写如下代码:
int main(void)
{
uint8_t key;
uint8_t i = 0, t = 0;
uint8_t cnt = 0;
uint8_t rs485buf[5];
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(170); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
rs485_init(9600); /* 初始化RS485 */
/* 显示提示信息 */
lcd_show_string(10, 10, 140, 32, 32, "STM32", RED);
lcd_show_string(10, 42, 140, 24, 24, "485 TEST", RED);
lcd_show_string(10, 66, 140, 24, 24, "ATOM@ALIENTEK", RED);
lcd_show_string(10, 100, 200, 16, 16, "KEY0:Send", RED);
lcd_show_string(10, 130, 200, 16, 16, "Count:", RED); /* 显示当前计数值 */
lcd_show_string(10, 150, 200, 16, 16, "Send Data:", RED); /* 提示发送的数据 */
/* 提示接收到的数据 */
lcd_show_string(10, 190, 200, 16, 16, "Receive Data:", RED);
while (1)
{
key = key_scan(0);
if (key == KEY0_PRES) /* KEY0按下,发送一次数据 */
{
for (i = 0; i < 5; i++)
{
rs485buf[i] = cnt + i; /* 填充发送缓冲区 */
/* 显示数据 */
lcd_show_xnum(10 + i * 32, 170, rs485buf[i], 3, 16, 0x80, BLUE);
}
rs485_send_data(rs485buf, 5); /* 发送5个字节 */
}
rs485_receive_data(rs485buf, &key);
if (key) /* 接收到有数据 */
{
if (key > 5)
{
key = 5; /* 最大是5个数据. */
}
for (i = 0; i < key; i++)
{
/* 显示数据 */
lcd_show_xnum(10 + i * 32, 210, rs485buf[i], 3, 16, 0x80, BLUE);
}
}
t++;
delay_ms(10);
if (t == 20)
{
LED0_TOGGLE(); /* LED0闪烁, 提示系统正在运行 */
t = 0;
cnt++;
lcd_show_xnum(10 + 48, 130, cnt, 3, 16, 0x80, BLUE); /* 显示数据 */
}
}
}
我们是通过按键控制数据的发送。在此部分代码中,cnt是一个累加数,一旦KEY0按下,就以这个数位基准连续发送5个数据。当485总线收到数据得时候,就将收到的数据直接显示在LCD屏幕上。
28.4 下载验证
在代码编译成功之后,将代码下载到两块开发板,程序运行效果如图28.4.1所示:

图28.4.1 程序运行效果图
LED0不停闪烁,提示程序在运行。此时,按下KEY0,在另外一个开发板上面收到这个开发板发送的数据。如图28.4.2和图28.4.3所示:

图28.4.2 发送RS485数据的开发板界面

图28.4.3 接收RS485数据的开发板
图28.4.2来自开发板A,发送了5个数据,图28.4.3来自开发板B,接收到了来自开发板A的5个数据。
本章介绍的485总线时通过串口控制收发的,只需要连接两块开发板RS485的A和B就可以进行数据传输了。
另外,利用USMART测试的部分,我们这里就不做介绍了,大家可自行验证下。