RS485 总线与 Modbus 从机实现:从物理层到协议栈的完整指南
👁 8 阅读 · 2026-08-21 · 嵌入式 协议
RS485 总线凭借其差分传输、多节点支持和长距离通信能力,成为工业现场最常用的物理层标准,而 Modbus 协议则以其简单、开放的特性成为事实上的应用层标准。本文面向有一定基础的嵌入式开发者,深入剖析 RS485 物理层特性与 Modbus RTU 从机实现的关键技术,包括收发切换、时序控制、CRC 校验、状态机解析等核心要点,并给出基于 STM32 的完整代码示例,帮助读者快速构建稳定可靠的工业通信节点。
## 1. RS485 物理层基础
RS485 采用差分信号传输,A、B 两线之间的电压差表示逻辑状态(>200mV 为 1,<-200mV 为 0),因此具有很强的抗共模干扰能力。其关键特性包括:
- **多节点能力**:标准驱动芯片(如 MAX485)支持 32 个节点,若使用低负载芯片(如 ISL3170)可达 256 个节点。
- **传输距离**:在 100kbps 速率下可达 1200 米,速率越高,距离越短(如 10Mbps 时约 15 米)。
- **半双工模式**:同一时刻只能发送或接收,需要控制 DE/RE 引脚切换方向。
**收发切换的时序陷阱**:
- 发送完成后,必须等待最后一个字节完全移出移位寄存器(通常需要 2-3 个字符时间),才能关闭发送模式。
- 若切换过快,总线上的最后一位可能被截断,导致接收方帧错误。
- 推荐使用 `__HAL_UART_GET_FLAG(&huart, UART_FLAG_TC)` 等待发送完成,再拉低 DE。
## 2. Modbus RTU 协议帧格式
Modbus RTU 以字节流形式传输,帧结构如下:
```
[地址域 1字节] [功能码 1字节] [数据域 N字节] [CRC16 低字节] [CRC16 高字节]
```
- **地址域**:从机地址 1-247,0 为广播地址。
- **功能码**:常用 0x03(读保持寄存器)、0x06(写单个寄存器)、0x10(写多个寄存器)。
- **数据域**:寄存器地址、数量、数据等,大端序(高字节在前)。
- **CRC16**:对地址到数据域末尾的所有字节计算,多项式 0xA001,初始值 0xFFFF,低字节先发送。
**帧间隔要求**:RTU 模式下,帧与帧之间至少需要 3.5 个字符时间的静默(如 9600bps 下约 4ms),帧内字节间隔不能超过 1.5 个字符时间,否则视为帧断裂。
## 3. 从机实现核心设计
### 3.1 状态机解析
推荐使用状态机处理字节流,避免阻塞式接收:
```c
typedef enum {
IDLE, // 空闲,等待起始字节
RECEIVING, // 接收中
COMPLETE, // 帧接收完成
} FrameState;
```
### 3.2 定时器管理帧超时
使用一个 1ms 或 0.1ms 的定时器中断,在每次收到字节时重置计数值,若超过 3.5 字符时间未收到新字节,则判定帧结束。
## 4. 完整代码示例(STM32 HAL 库)
以下代码基于 STM32F1xx 和 MAX485 芯片,使用 USART2 + 定时器 TIM6。
### 4.1 初始化配置
```c
// USART2 初始化:9600, 8N1
void MX_USART2_UART_Init(void) {
huart2.Instance = USART2;
huart2.Init.BaudRate = 9600;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart2);
// 使能接收中断
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
}
// 控制 DE/RE 引脚(PC1)
#define RS485_DE_HIGH() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET)
#define RS485_DE_LOW() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET)
```
### 4.2 定时器配置(1ms 中断)
```c
void MX_TIM6_Init(void) {
TIM6->PSC = 72-1; // 72MHz/72 = 1MHz
TIM6->ARR = 1000-1; // 1ms 中断
HAL_TIM_Base_Start_IT(&htim6);
}
volatile uint16_t frame_timer = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM6) {
if (frame_timer < 0xFFFF) frame_timer++;
if (rx_state == RECEIVING && frame_timer > 4) { // 4ms > 3.5字符@9600
rx_state = COMPLETE;
}
}
}
```
### 4.3 接收中断处理
```c
volatile uint8_t rx_byte;
volatile uint8_t rx_buf[256];
volatile uint16_t rx_len = 0;
volatile FrameState rx_state = IDLE;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART2) {
if (rx_state == IDLE) {
rx_len = 0;
rx_state = RECEIVING;
}
if (rx_len < 256) {
rx_buf[rx_len++] = rx_byte;
frame_timer = 0; // 重置超时
}
HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // 继续接收
}
}
```
### 4.4 主循环处理帧
```c
void Modbus_Poll(void) {
if (rx_state == COMPLETE) {
// 1. 校验 CRC
uint16_t crc = Modbus_CRC16(rx_buf, rx_len - 2);
if (crc != (rx_buf[rx_len-1] << 8 | rx_buf[rx_len-2])) {
rx_state = IDLE; // CRC 错误,丢弃
return;
}
// 2. 检查地址
if (rx_buf[0] != 0x01 && rx_buf[0] != 0x00) { // 本机地址 1
rx_state = IDLE;
return;
}
// 3. 处理功能码
uint8_t func = rx_buf[1];
uint8_t tx_len = 0;
switch (func) {
case 0x03: // 读保持寄存器
tx_len = Handle_ReadHolding(rx_buf, tx_buf);
break;
case 0x06: // 写单个寄存器
tx_len = Handle_WriteSingle(rx_buf, tx_buf);
break;
default: // 异常响应
tx_buf[0] = rx_buf[0];
tx_buf[1] = func | 0x80;
tx_buf[2] = 0x01; // 非法功能码
tx_len = 3;
break;
}
// 4. 发送响应
if (tx_len > 0) {
uint16_t crc_tx = Modbus_CRC16(tx_buf, tx_len);
tx_buf[tx_len++] = crc_tx & 0xFF;
tx_buf[tx_len++] = (crc_tx >> 8) & 0xFF;
RS485_DE_HIGH();
HAL_UART_Transmit(&huart2, tx_buf, tx_len, 100);
while (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET);
RS485_DE_LOW();
}
rx_state = IDLE;
}
}
```
### 4.5 CRC16 计算函数
```c
uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
```
### 4.6 功能码处理示例(读保持寄存器)
```c
uint8_t Handle_ReadHolding(uint8_t *rx, uint8_t *tx) {
uint16_t start_reg = (rx[2] << 8) | rx[3];
uint16_t reg_cnt = (rx[4] << 8) | rx[5];
if (reg_cnt > 125 || start_reg + reg_cnt > 10) { // 假设有10个寄存器
tx[0] = rx[0]; tx[1] = 0x83; tx[2] = 0x02; // 非法数据地址
return 3;
}
tx[0] = rx[0];
tx[1] = 0x03;
tx[2] = reg_cnt * 2;
for (uint16_t i = 0; i < reg_cnt; i++) {
uint16_t val = holding_regs[start_reg + i];
tx[3 + i*2] = val >> 8;
tx[4 + i*2] = val & 0xFF;
}
return 3 + reg_cnt * 2;
}
```
## 5. 注意事项与调试技巧
- **总线终端电阻**:在总线两端各接 120Ω 匹配电阻,防止信号反射。若节点数少且距离短,可省略。
- **偏置电阻**:在 A、B 间加 390Ω 上拉/下拉,确保空闲时总线电平确定(A 高 B 低)。
- **隔离与地电位**:长距离通信建议使用隔离收发器(如 ADM2483),避免地环路。
- **调试工具**:使用 USB-RS485 适配器配合串口助手,先用 PC 模拟主机测试从机响应。
- **时序验证**:用示波器观察 A-B 差分波形,确认发送切换时序是否正确。
- **异常处理**:从机应能处理非法功能码、非法地址、CRC 错误,并返回异常响应或静默丢弃。
## 6. 总结
实现 RS485 Modbus 从机的关键在于:物理层正确控制收发切换、协议层严格遵循帧时序和 CRC 校验。本文给出的状态机框架可轻松扩展至其他功能码,且不占用 CPU 阻塞时间。在实际项目中,建议进一步加入看门狗、错误计数和诊断寄存器,提升通信的鲁棒性。掌握这些基础,你就能快速构建出符合工业标准的 Modbus 设备。