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Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。

RS485连接

  在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备 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 的条件下),并且支持输出短路保护。该芯片的框图如下图所示:

SP3485框图

  图中 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 为低电平。

三、原理图

RS485原理图

USART2接口引脚

RS485使能引脚

  从电路图中可以看到,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;
}
posted @ 2023-11-12 17:45  星光映梦  阅读(107)  评论(0编辑  收藏  举报