RS485 是什么
RS485 是工业现场最普及的串行总线:两根差分线(A、B)就能挂几十台设备,抗干扰强,最远 1200 米,速率从 9600 到 10Mbps 都能跑。电表、变频器、传感器、PLC 几乎都留了 RS485 口。
它是半双工总线:同一时刻只能有一台设备发送,其他设备接收。谁发、什么时候发,要靠协议仲裁,最常见的就是 Modbus 的主从轮询。
- 差分信号A/B 两线抗干扰。
- 半双工同一时刻单方发送。
- 方向控制DE/RE 引脚切换。
- Modbus RTU工业标准主从协议。
RS232 只能点对点、电平摆幅大、距离短;RS485 改成了差分电平,两线共享总线,天然适合多机采集场景。理解 RS485,本质就是理解"把 UART 的电平换成差分、再加一根方向线"。
收发电路与方向控制
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 位、大小端)、缩放系数搞清楚。
主从收发实现
主机按地址轮询,从机收到自己的请求才应答。从机侧要处理异常:功能码不支持返回 01,寄存器越界返回 02,值非法返回 03。
// 从机接收处理伪代码
void modbus_slave_handle(uint8_t *frame, uint16_t len) {
uint8_t addr = frame[0];
uint8_t func = frame[1];
if (addr != MY_ADDR) return; // 不是我的帧
if (crc_check(frame, len) != OK) return;
switch (func) {
case 0x03: read_registers(frame); break;
case 0x06: write_register(frame); break;
default: send_exception(func, 0x01); break;
}
}
// 主机轮询:发请求,等应答,超时则记故障
bool modbus_master_poll(uint8_t slave, uint16_t reg, uint16_t n,
uint16_t *out) {
uint8_t req[8] = {slave, 0x03, reg >> 8, reg & 0xFF,
n >> 8, n & 0xFF, 0, 0};
uint16_t crc = modbus_crc(req, 6);
req[6] = crc & 0xFF; req[7] = crc >> 8;
rs485_send(req, 8);
if (rs485_wait_frame(100)) { // 100ms 超时
return parse_response(rx_buf, rx_len, out);
}
return false; // 超时,记录掉线
}
轮询周期要设计好:设备越多、每帧越长,单轮越慢。可以把慢速参数(温度、状态)和快速参数(电流、频率)拆成两组,不同周期轮询,既保实时性又省总线带宽。
总线故障与调试
- A/B 接反:整个网络乱码或通信失败,万用表量 A 对 B 的电压极性可判断。
- 缺终端电阻:长线末端反射,数据偶尔错一两个字节。
- 缺偏置电阻:总线空闲时电平不确定,上电就收到 0xFF 乱码。
- 共地问题:多设备地电位差大,加隔离(光耦或隔离收发器)解决。
- 方向切换过快:帧尾被截断,接收端 CRC 老不过。
调试神器是 USB-485 分析仪加串口助手:先单独回环测一块板,再接总线测两块对发,最后挂满全网络压测。抓包看十六进制流,CRC 错还是地址错一目了然。
RS485 的坑大多不在软件,而在总线物理层:电阻、接地、方向,查完这三样再谈协议。
常见问题
// 1. 波特率不一致,对端收到乱码
// 2. 方向引脚没切换,收发冲突
// 3. 没接终端电阻,长线数据错位
// 4. 多机地址重复,应答冲突
// 5. 隔离没做,雷击或共地烧收发器
// 6. CRC 校验失败,多半是帧边界没切对
把 RS485 和 Modbus 跑通,电表、变频器、传感器都能接进来,这是嵌入式工程师进工业领域的必修课。