RS485 是什么

RS485 是工业现场最普及的串行总线:两根差分线(A、B)就能挂几十台设备,抗干扰强,最远 1200 米,速率从 9600 到 10Mbps 都能跑。电表、变频器、传感器、PLC 几乎都留了 RS485 口。

它是半双工总线:同一时刻只能有一台设备发送,其他设备接收。谁发、什么时候发,要靠协议仲裁,最常见的就是 Modbus 的主从轮询。

本文核心知识点
  • 差分信号A/B 两线抗干扰。
  • 半双工同一时刻单方发送。
  • 方向控制DE/RE 引脚切换。
  • Modbus RTU工业标准主从协议。

RS232 只能点对点、电平摆幅大、距离短;RS485 改成了差分电平,两线共享总线,天然适合多机采集场景。理解 RS485,本质就是理解"把 UART 的电平换成差分、再加一根方向线"。

RS485 配图:蓝调渐变背景上带天线的方形设备盒通过管线连接压力表,中段脉冲波形的扁平科技插画
两根差分线,串起一整条工业总线。

收发电路与方向控制

MCU 的 UART 是 TTL 电平,要经过收发器芯片转成差分。最常用的是 MAX485 或 SP3485:RO 接 RX、DI 接 TX、RE 和 DE 合并成一个方向引脚。

  • RE 低电平有效:拉低使能接收。
  • DE 高电平有效:拉高使能发送。
  • 两个引脚合到一起:输出高就发送,输出低就接收。
  • 总线两端要各接一只 120 欧姆终端电阻,匹配阻抗防反射。
  • 偏置电阻(上拉 A、下拉 B)保证空闲时电平确定,防止收到乱码。

方向切换有个关键细节:从发送切回接收要留一点延时,等最后一个字节发完、总线电平稳定后再拉低 DE,否则会把最后一个字节的尾巴截掉。自动方向电路(用 RC 延时)能省一个 GPIO,但高速下不如软件控制可靠。

UART 配置与收发驱动

UART 配置和普通串口一样,注意波特率、数据位、停止位要跟对端一致,Modbus RTU 通常用 8 数据位、1 停止位、无校验或偶校验。

// USART2 配置:115200, 8N1
UART_HandleTypeDef huart;
huart.Instance = USART2;
huart.Init.BaudRate = 115200;
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;
HAL_UART_Init(&huart);

// 发送一帧:先拉高 DE,发完等发完再拉低
void rs485_send(uint8_t *buf, uint16_t len) {
  RS485_DE_HIGH();                    // 进入发送态
  HAL_UART_Transmit(&huart, buf, len, 100);
  while (__HAL_UART_GET_FLAG(&huart, UART_FLAG_TC) == RESET);
  RS485_DE_LOW();                     // 回接收态
}

// 接收用空闲中断或 DMA,收到一整帧再处理
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
  if (huart->Instance == USART2) {
    rs485_frame_done(rx_buf, Size);   // 一帧到齐
  }
}

接收一帧的边界怎么定?Modbus RTU 规定帧间间隔至少 3.5 个字符时间,超过这个时间没数据就算一帧结束。用 UART 空闲中断(IDLE)最简单,也可以用接收超时定时器。

Modbus RTU 协议

Modbus RTU 的帧格式很紧凑:从机地址(1 字节)加功能码(1 字节)加数据(N 字节)加 CRC16(2 字节,低字节在前)。常用功能码:01 读线圈、02 读离散输入、03 读保持寄存器、04 读输入寄存器、05 写单线圈、06 写单寄存器。

// 主机读从机地址 0x01 的保持寄存器:起始 0x0000,读 2 个
// 请求帧:01 03 00 00 00 02 C4 0B
// 从机应答:01 03 04 12 34 56 78 

// CRC16 计算(MODBUS 多项式 0xA001)
uint16_t modbus_crc(uint8_t *buf, uint16_t len) {
  uint16_t crc = 0xFFFF;
  for (uint16_t i = 0; i < len; i++) {
    crc ^= buf[i];
    for (uint8_t j = 0; j < 8; j++) {
      if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
      else crc >>= 1;
    }
  }
  return crc;
}

寄存器里存的是什么,由厂商的寄存器地址表决定,协议本身不解释数据含义。所以对接设备前先要拿到对方的寄存器手册,把地址、数据类型(16 位还是 32 位、大小端)、缩放系数搞清楚。