12. RS485通信协议
一、RS485简介
RS485(一般称作 RS485/EIA-485)隶属于 OSI 模型物理层,是串行通讯的一种。电气特性规定为 2 线,半双工,多点通信的类型。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。
RS485 的特点包括:
- 接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为 +(2~6)V 表示;逻辑“0”以两线间的电压差为 -(2~6)V 表示。接口信号电平比 RS232 降低了,不易损坏接口电路的芯片,且该电平与 TTL 电平兼容,可方便与 TTL 电路连接。
- 传输速率高。10 米时,RS485 的数据最高传输速率可达 35Mbps,在 1200m 时,传输速度可达 100Kbps。
- 抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗噪声干扰性好。
- 传输距离远,支持节点多。RS485 总线最长可以传输 1200m 左右,更远的距离则需要中继传输设备支持但这时(速率≤100Kbps)才能稳定传输,一般最大支持 32 个节点,如果使用特制的 485 芯片,可以达到 128 个或者 256 个节点,最大的可以支持到 400 个节点。
RS485 推荐使用在点对点网络中,比如:线型,总线型网络等,而不能是星型,环型网络。理想情况下 RS485 需要 2 个终端匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为 120Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。
在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备 4 上面各加一个 120Ω的匹配电阻。
由于 RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以 RS485 有很广泛的应用。实际多设备时收发器有范围为 -7V 到 +12V 的共模电压,为了稳定传输,也有使用 3 线的布线方式,即在原有的 A、B 两线上多增加一条地线。(4 线制只能实现点对点的全双工通讯方式,这种也叫 RS422,由于布线的难度和通讯局限,相对使用得比较少)。
通信接口 | 通信方式 | 信号线 | 电平标准 | 拓扑结构 | 通信距离 | 通信速率 | 抗干扰能力 |
---|---|---|---|---|---|---|---|
TTL | 全双工 | TX/RX | 逻辑 0:0 ~ 0.4V 逻辑 1:2.4V ~ 5V |
点对点 | 1 米 | 100kbps | 弱 |
RS232 | 全双工 | TX/RX | 逻辑 0:-(15 ~ 3)V 逻辑 1:+(3 ~ 15)V |
点对点 | 100 米 | 20kbps | 较弱 |
RS485 | 半双工 | 差分线 AB | 逻辑 0:-(2 ~ 6)V 逻辑 1:+(2 ~ 6)V |
多点双向 | 1200 米 | 100kbps | 强 |
二、RS485驱动芯片
TP8485E/SP3485 可作为 RS485 的收发器,该芯片支持 3.3V~5.5V 供电,最大传输速度可达 250Kbps,支持多达 256 个节点(单位负载为 1/8 的条件下),并且支持输出短路保护。该芯片的框图如下图所示:
图中 A、B 总线接口,用于连接 485 总线。RO 是接收输出端,DI 是发送数据收入端,RE 是接收使能信号(低电平有效),DE 是发送使能信号(高电平有效)。因为 RS485 为半双工通信,通过 RE 和 DE 就能控制发送与接收。
当驱动器使能引脚 DE 为逻辑高电平时,差分输出 A 和 B 遵循数据输入 DI 处的逻辑状态。DI 处的逻辑高导致 A 转为高电平,B 转为低电平。当 DI 为低电平时,输出状态反转,B 变高电平,A 变低电平。
当接收器使能引脚 RE 为逻辑低电平时,接收器被激活。当 \(V_{A}-V_{B}\) 的差分输入电压为正且高于 0.2V 时,接收器输出 RO 为高电平。当 \(V_{A}-V_{B}\) 的差分输入电压为负且低于 -0.2V 时,接收器输出 RO 为低电平。
三、原理图
从电路图中可以看到,SP3485 芯片的 RO 和 DI 管脚连接在 STM32F4 芯片的串口 2 管脚 PA3(USART2 RXD) 和 PA2(USART2 TXD) 上,SP3485 芯片的 DE 与 RE 短接在一起连接在 STM32F4 芯片的 PG8 上,通过 PG8 管脚就可以控制 SP3485 的收发,当 PG8=0 时,为接收模式,当 PG8=1 时,为发送模式。图中的 R2 电阻为匹配电阻,大小为 120 欧。图中另外 2 个电阻 R80 和 R81 为偏置电阻,用来保证总线空闲时,A、B 之间的电压差都会大于 0.2V(逻辑 1),从而避免因总线空闲时, A、 B 压差不定,引起逻辑错乱导致出现乱码。
四、程序源码
4.1、串口初始化函数
UART 初始化函数内容如下:
#define UART_RECEIVE_LENGTH 200
UART_HandleTypeDef g_usart2_handle; // USART2句柄
uint8_t g_usart2_rx_buffer[UART_RECEIVE_LENGTH]; // USART2接收数据缓冲区
uint16_t g_usart2_rx_status = 0; // USART2接收状态标记
/**
* @brief 串口初始化函数
*
* @param huart 串口句柄
* @param UARTx 串口寄存器基地址
* @param band 波特率
*/
void UART_Init(UART_HandleTypeDef *huart, USART_TypeDef *UARTx, uint32_t band, uint8_t *rx_buffer)
{
huart->Instance = UARTx; // 寄存器基地址
huart->Init.BaudRate = band; // 波特率
huart->Init.WordLength = UART_WORDLENGTH_8B; // 数据位
huart->Init.StopBits = UART_STOPBITS_1; // 停止位
huart->Init.Parity = UART_PARITY_NONE; // 奇偶校验位
huart->Init.Mode = UART_MODE_TX_RX; // 收发模式
huart->Init.HwFlowCtl = UART_HWCONTROL_NONE; // 硬件流控制
huart->Init.OverSampling = UART_OVERSAMPLING_16; // 过采样
HAL_UART_Init(huart);
// 开启空闲中断,第一个参数是串口句柄,第二个参数是接收数据缓冲区,第三个参数是接收数据缓冲区的最大长度
HAL_UARTEx_ReceiveToIdle_IT(&g_usart2_handle, rx_buffer, UART_RECEIVE_LENGTH);
}
USART2 底层初始化函数内容如下:
/**
* @brief 串口底层初始化函数
*
* @param huart 串口句柄
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART2) // 初始化的串口是否是USART2
{
__HAL_RCC_USART2_CLK_ENABLE(); // 使能USART2时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能对应GPIO的时钟
// PA2 -> USART2 TXD
GPIO_InitStruct.Pin = GPIO_PIN_2; // USART2 TXD的引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽式复用
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.Alternate = GPIO_AF7_USART2; // 复用功能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA3 -> USART2 RXD
GPIO_InitStruct.Pin = GPIO_PIN_3; // USART2 RXD的引脚
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_EnableIRQ(USART2_IRQn); // 使能USART2中断
HAL_NVIC_SetPriority(USART2_IRQn, 2, 0); // 设置中断优先级
}
}
串口 2 中断服务函数内容如下:
/**
* @brief USART2中断服务函数
*
*/
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&g_usart2_handle); // 调用HAL库公共处理函数
// 再次开启空闲中断,第一个参数是串口句柄,第二个参数是接收数据缓冲区,第三个参数是接收数据缓冲区的最大长度
HAL_UARTEx_ReceiveToIdle_IT(&g_usart2_handle, (uint8_t *)g_usart2_rx_buffer, UART_RECEIVE_LENGTH);
}
从代码逻辑可以看出,在中断服务函数内部通过调用空闲中断回调函数 HAL_UARTEx_RxEventCallback()
进行处理。然后,再调用 HAL_UARTEx_ReceiveToIdle_IT()
函数重新开启空闲中断。HAL_UARTEx_ReceiveToIdle_IT()
函数的用于在中断模式下接收串行数据直到检测到空闲线状态(即 UART 线路变为 idle 状态)。这个函数常用于实现 UART 异步通信中连续的数据接收,直到 UART 线路没有数据为止。
空闲中断回调函数内容如下:
/**
* @brief USART空闲中断回调函数
*
* @param huart 串口句柄
* @param Size 实际接收数据大小
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2)
{
g_usart2_rx_status = Size;
}
}
4.2、RS485初始化函数
RS485 引脚定义:
#define RS485_RE_GPIO_CLK_ENABLE() __HAL_RCC_GPIOG_CLK_ENABLE()
#define RS485_RE_GPIO_PORT GPIOG
#define RS485_RE_GPIO_PIN GPIO_PIN_8
#define RS485_RE(x) do { x ? \
HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(RS485_RE_GPIO_PORT, RS485_RE_GPIO_PIN, GPIO_PIN_RESET); \
} while (0);
RS485 初始化函数:
void RS485_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
RS485_RE_GPIO_CLK_ENABLE(); // 使能RS485 RE引脚时钟
GPIO_InitStruct.Pin = RS485_RE_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(RS485_RE_GPIO_PORT, &GPIO_InitStruct);
RS485_RE(0); // 进入接收模式
}
RS485 发送数据函数内容如下:
/**
* @brief RS485发送数据函数
*
* @param buffer 要发送数据缓冲区的指针
* @param length 要发送数据的大小
*/
void RS485_SendData(uint8_t *buffer, uint16_t length)
{
RS485_RE(1); // 进入发送模式
HAL_UART_Transmit(&g_usart2_handle, buffer, length, 0xFFFF); // UART发送数据
g_usart2_rx_status = 0; // 接收数据长度为0
RS485_RE(0); // 进入接收模式
}
4.3、main()函数
main() 函数内容如下:
int main(void)
{
char data[] = "Hello World!\r\n";
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
UART_Init(&g_usart2_handle, USART2, 115200, g_usart2_rx_buffer);
RS485_Init();
RS485_SendData((uint8_t *)data, strlen(data));
while (1)
{
if (g_usart2_rx_status)
{
RS485_SendData(g_usart2_rx_buffer, strlen((char *)(g_usart2_rx_buffer)));
g_usart2_rx_status = 0;
}
}
return 0;
}