s苦瓜大王

ModBus RTU 从站知识手册

ModBus RTU 从站知识手册

面向嵌入式开发者,系统化整理协议原理、寄存器模型、DMA+空闲中断接收、RTU 帧解析、异常码、FreeRTOS 任务集成与常见踩坑。

  • 对应架构:串口 + DMA + 空闲中断
  • CH32 / STM32 / ESP32 通用
  • 配套 FreeRTOS

目录

  1. ModBus 协议族概览
  2. RTU 帧结构与时序
  3. 数据模型与功能码
  4. 从站寄存器规划
  5. CRC-16 校验
  6. 从站状态机
  7. DMA + 空闲中断接收
  8. RS-485 收发切换
  9. FreeRTOS 任务集成
  10. 异常码与错误处理
  11. 常见踩坑与调试
  12. 测试方法与工具

1. ModBus 协议族概览

ModBus 是施耐德 1979 年发布的工业串行通信协议,因开放、简单、互操作性强被广泛用于 PLC、变频器、传感器、电表等设备。

变体 物理层 数据格式 典型应用
ModBus-RTU RS-485 / RS-232 二进制(紧凑) 工业现场总线(最常见)
ModBus-ASCII RS-485 / RS-232 ASCII(可读) 老旧设备、调试
ModBus-TCP Ethernet MBAP 头 + PDU 工业以太网、网关
ModBus-RTU over TCP TCP 裸 RTU 帧(无 MBAP) 串口服务器透传

你做的是 ModBus-RTU 从站(Slave/Server):响应上位机(主站 Master/Client)的请求,被动执行读写。


2. RTU 帧结构与时序

2.1 帧格式

| 地址(1B) | 功能码(1B) | 数据(N 字节) | CRC-16(2B, 低位在前) |

每帧 4~256 字节(典型),地址 0x00 为广播(从站不应答,仅写),0x01~0xF7 为从站地址。

2.2 帧间隔(关键!)

ModBus-RTU 用静默时间区分帧边界:

  • 3.5 字符时间的静默 → 视为帧结束。
  • 帧内字节间隔 ≤ 1.5 字符时间(否则主站/从站丢弃)。

以 9600 bps, 11 位/字符(1 起始 + 8 数据 + 1 校验 + 1 停止)为例:1 字符 ≈ 1.146 ms,
3.5 字符 ≈ 4 ms(≥ 1.75 ms 时按 4 ms 算),1.5 字符 ≈ 1.75 ms。这就是为什么 9600bps 以上通常取 4ms 作为帧超时阈值

2.3 字符格式

  • 8N1(最常见,无校验)、8E1(偶校验)、8O1(奇校验) — 主从必须一致
  • 校验位通常用于检测物理层错误,但 RTU 帧仍必须有 CRC-16 校验。

3. 数据模型与功能码

ModBus 把数据抽象成 4 类存储区(寄存器):

存储区 类型 访问 地址空间 常用功能码
Coil(线圈) 1 bit(只读/读写) R / RW 00001~09999 01 读 / 05 写单 / 0F 写多
Discrete Input(离散输入) 1 bit(只读) R 10001~19999 02 读
Holding Register(保持寄存器) 16 bit(读写) RW 40001~49999 03 读 / 06 写单 / 10 写多
Input Register(输入寄存器) 16 bit(只读) R 30001~39999 04 读

注意:PDU 协议层用 0x0000 基址,如"40001"在请求中是 0x0000。常用换算:协议地址 = 人读地址 - 偏移(40001→-40001, 30001→-30001)。

3.1 8 个常用功能码

名称 PDU 请求 PDU 响应
0x01 读线圈 起始地址(2B) + 数量(2B) 字节数(1B) + 线圈值(按位打包)
0x02 读离散输入 同 01 同 01
0x03 读保持寄存器 起始地址(2B) + 数量(2B) 字节数(1B) + 寄存器值(大端)
0x04 读输入寄存器 同 03 同 03
0x05 写单线圈 地址(2B) + 值(0xFF00=ON, 0x0000=OFF) 回显请求(8B PDU)
0x06 写单寄存器 地址(2B) + 值(2B) 回显请求(8B PDU)
0x0F 写多线圈 地址(2B) + 数量(2B) + 字节数(1B) + 数据 地址(2B) + 数量(2B)
0x10 写多寄存器 地址(2B) + 数量(2B) + 字节数(1B) + 数据 地址(2B) + 数量(2B)

4. 从站寄存器规划

建议在 C 端用结构体/数组统一管理,避免在协议层散落数据。

// 保持寄存器(40001 起)
typedef struct {
    uint16_t mv_out;        // 测量值
    uint16_t setpoint;      // 设定值
    uint16_t kp, ki, kd;     // PID 参数
    uint16_t ctrl_word;     // 控制字
    uint16_t baud_idx;      // 波特率索引
} modbus_holding_t;

// 输入寄存器(30001 起,只读)
typedef struct {
    int16_t  temperature;
    uint16_t pressure;
    uint16_t status_word;
} modbus_input_t;

// 线圈(00001 起,位操作)
typedef struct {
    uint8_t relay1 : 1;
    uint8_t relay2 : 1;
    uint8_t enable : 1;
    uint8_t alarm  : 1;
} modbus_coil_t;

extern modbus_holding_t g_holding;
extern modbus_input_t   g_input;
extern modbus_coil_t    g_coil;

大端 vs 小端: ModBus 协议层是大端(网络字节序),但 MCU 多为小端。推荐在发送/接收时统一用 __REV16() 或位操作做转换,不要假设编译器自动转换。


5. CRC-16 校验

ModBus RTU 使用多项式 0xA001(即 x^16 + x^15 + x^2 + 1反序)的 16 位 CRC,低位先发

5.1 查表法(推荐,O(1) 每字节)

static const uint16_t crc16_table[256] = { /* 256 项,生成工具可一次输出 */ };

uint16_t modbus_crc16(const uint8_t *buf, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    while (len--) {
        crc = (crc >> 8) ^ crc16_table[(crc ^ *buf++) & 0xFF];
    }
    return crc;   // 返回时低字节是 crc_lo
}

// 发送时:
buf[len] = crc & 0xFF;        // CRC-Lo 先发
buf[len+1] = (crc >> 8) & 0xFF; // CRC-Hi 后发

// 接收校验:
uint16_t crc_calc = modbus_crc16(rx_buf, rx_len - 2);
uint16_t crc_recv = rx_buf[rx_len-1] << 8 | rx_buf[rx_len-2];
if (crc_calc != crc_recv) return;   // 校验失败直接丢弃

256 项表约 512 字节 Flash,极小。如需零占用,可用 0xA001 多项式的位移计算版(约 20 行代码,执行稍慢)。


6. 从站状态机

stateDiagram-v2 [*] --> IDLE IDLE --> RECEIVING: 收到首字节 RECEIVING --> IDLE: 空闲超时(>3.5字符) RECEIVING --> FRAME_READY: 收到完整帧 FRAME_READY --> CRC_CHECK: 解析完成 CRC_CHECK --> ADDR_MATCH: CRC 通过 CRC_CHECK --> IDLE: CRC 失败,丢弃 ADDR_MATCH --> PROCESS: 地址匹配或广播 ADDR_MATCH --> IDLE: 地址不匹配 PROCESS --> SEND_RESP: 执行读写,组织响应 PROCESS --> SEND_EXC: 异常,组织异常码 SEND_RESP --> IDLE: DMA 发送完成 SEND_EXC --> IDLE: DMA 发送完成

收到一帧的完整流程:

  1. 校验帧长度 ≥ 4(地址+功能码+CRC)。
  2. 校验 CRC-16。
  3. 校验地址 == 本机或 == 广播(0x00 且为写命令)。
  4. 解析功能码,执行对应读写,组织响应 PDU。
  5. 计算响应 CRC,启动 DMA 发送。

7. DMA + 空闲中断接收

这是你架构图里选的方案,最省 CPU 也最稳。原理:用 DMA 循环接收,当 UART 总线出现 ≥ 3.5 字符的静默时触发 IDLE 中断,此时 DMA 计数器值差就是本帧长度。

7.1 初始化

// 注意:必须 先开 DMA,后开 UART,最后使能 IDLE 中断
#define MODBUS_RX_BUF_SIZE  256
static uint8_t rx_buf[MODBUS_RX_BUF_SIZE];

void modbus_uart_init(void)
{
    // 1. UART 115200 8N1(示例)
    uart_init(MODBUS_UART, 115200);

    // 2. DMA 循环模式: UART RX → rx_buf
    dma_circular_rx(MODBUS_UART_DMA, MODBUS_UART, rx_buf, MODBUS_RX_BUF_SIZE);

    // 3. 使能 UART 空闲中断
    USART_ITConfig(MODBUS_UART, USART_IT_IDLE, ENABLE);
    USART_ITConfig(MODBUS_UART, USART_IT_TC,   ENABLE); // 发送完成(切回接收)
    NVIC_EnableIRQ(MODBUS_UART_IRQn);
}

7.2 空闲中断服务程序

void MODBUS_UART_IRQHandler(void)
{
    // --- 接收: 空闲中断 = 一帧结束 ---
    if (USART_GetITStatus(MODBUS_UART, USART_IT_IDLE)) {
        // 必须:读 SR + 读 DR 才能清 IDLE 标志(写入 0 也不行)
        volatile uint8_t sr = MODBUS_UART->STATR;
        volatile uint8_t dr = MODBUS_UART->DATAR;
        (void)sr; (void)dr;

        // 计算本次接收字节数
        uint16_t recv_len = MODBUS_RX_BUF_SIZE
                          - DMA_GetCurrDataCounter(MODBUS_UART_DMA);

        // 复制出整帧,防止下一帧覆盖(双缓冲)
        memcpy(frame_buf, rx_buf, recv_len);
        frame_len = recv_len;
        xSemaphoreGiveFromISR(s_frame_ready, NULL);  // 通知协议任务

        // 重置 DMA 指针(对部分 MCU 必须显式重新设置)
        DMA_Cmd(MODBUS_UART_DMA, DISABLE);
        DMA_SetCurrDataCounter(MODBUS_UART_DMA, MODBUS_RX_BUF_SIZE);
        DMA_Cmd(MODBUS_UART_DMA, ENABLE);
    }

    // --- 发送完成: 切回接收 ---
    if (USART_GetITStatus(MODBUS_UART, USART_IT_TC)) {
        USART_ClearITPendingBit(MODBUS_UART, USART_IT_TC);
        rs485_set_tx_mode(false);   // RS-485 收发切换
    }
}

易错: CH32/STM32 的 IDLE 中断清除必须 SR 读 + DR 读,写 0 清不掉!
另外,DMA 计数器重置是"安全网",循环模式本身会自动回卷,但显式重置可避免历史数据干扰。


8. RS-485 收发切换

RS-485 是半双工,主/从站都需要控制 DE/RE 引脚:

  • 接收态:DE=0,RE=0(或者 RE=1,看芯片)。
  • 发送态:DE=1,RE=1。
// 切换要在字节发送开始前,且发送完成时切回
void rs485_set_tx_mode(bool en) {
    GPIO_WriteBit(RS485_DE_PORT, RS485_DE_PIN, en ? Bit_SET : Bit_RESET);
    // 留几 us 切换时间,典型电路需 > 1 字符时间
    delay_us(20);
}

// 发送响应时:
void modbus_send(const uint8_t *buf, uint16_t len)
{
    rs485_set_tx_mode(true);
    DMA_TxStart(MODBUS_UART_DMA, buf, len);  // 发送完成后由 TC 中断切回
}

隐患: 部分廉价 RS-485 收发器在切换方向时会产生短暂毛刺,可能让对端误识别为"空闲"而结束一帧。建议在 DE 下降沿后再加 1~2 字符时间的等待,再开始下一帧接收。


9. FreeRTOS 任务集成

flowchart LR IDLE_IRQ[UART 空闲中断] -->|xSemaphoreGive| RX_TASK[ModBus RX 任务] RX_TASK -->|处理/解析| TX[组织响应 + DMA 发送] TX -->|TC 中断| BACK[切回接收] APP_TASK[应用任务] -.查询/通知.- REG[共享寄存器区] RX_TASK -.写.- REG

推荐的任务划分:

任务 优先级 职责
ModBus 任务 中(响应延迟 ≤ 1ms) 解析请求、查/改寄存器、组响应
采集任务 高(实时性) ADC/DMA、算法处理
存储任务 SDIO/FATFS 写入
  • 共享数据保护:应用任务读取寄存器时,使用 xSemaphoreCreateMutex() 或 Critical Section。
  • 零拷贝:把寄存器定义为全局 volatile,RX 任务只做地址/数量边界检查,直接读写。
  • 原子写:写多寄存器(0x10)时要分两步:先写入影子区 + 标志位,然后由应用任务一次性 commit,避免读到半新半旧数据。

10. 异常码与错误处理

当请求合法但从站无法执行时,返回 0x80 | 功能码 + 异常码:

异常码 名称 典型场景
0x01 非法功能码 不支持的功能码
0x02 非法数据地址 起始地址或结束地址越界
0x03 非法数据值 写入值非法(超出范围)
0x04 从站设备故障 底层传感器/执行器故障
0x05 确认(长处理) 已接受,但尚未完成
0x06 从站设备忙 正在处理上一个请求
0x08 存储奇偶校验差错 内部存储异常
0x0A 网关路径不可用 仅网关用
0x0B 网关目标无响应 仅网关用
// 异常响应 PDU: [0x80|func] [exception_code]
void modbus_send_exception(uint8_t func, uint8_t exc)
{
    tx_buf[0] = slave_addr;
    tx_buf[1] = func | 0x80;
    tx_buf[2] = exc;
    uint16_t crc = modbus_crc16(tx_buf, 3);
    tx_buf[3] = crc & 0xFF;
    tx_buf[4] = crc >> 8;
    modbus_send(tx_buf, 5);
}

11. 常见踩坑与调试

症状 根因 解法
主站读不到任何响应 DE/RE 方向未切换 / 切换太快 DE 拉高后等 ≥ 1 字符再发;TC 中断切回
CRC 校验始终失败 发送时高低字节反了 CRC-Lo 先发;接收按 lo|hi<<8 拼
偶发丢帧 3.5 字符判定被干扰 提高总线质量、加终端电阻 120Ω
同一帧被当成两帧处理 两帧间隔不足 3.5 字符 主站加帧间隔;提高 MCU 晶振精度
地址匹配却无响应 广播(0x00)用了读指令 广播只对 05/06/0F/10 写指令有效,读不响应
读多寄存器值全 0 或全 1 未做字节序转换 协议是大端,MCU 小端要 __REV16
长时间运行后死机 DMA 缓冲区溢出 / 中断未清 增大缓冲;确保 IDLE 标志清理顺序正确
多个主站冲突 总线有多个主站 ModBus 不支持多主,需应用层协调

调试黄金组合: USB 转 RS-485 工具 + ModBus Poll/ModbusMaster(主站模拟器) + 串口助手(logic 抓帧)。三者配合能定位 99% 的问题。


12. 测试方法与工具

12.1 上位机工具

  • ModBus Poll(Win):主站仿真,功能齐全。
  • ModbusMasterTool / libmodbus:开源,适合集成自测。
  • 维博智控 / 各类 HMI 配置软件:国产主站,直接验证互通性。
  • VS Code 插件 + Wireshark + pymodbus:Python 端快速验证。

12.2 自测关键点

  • 所有支持的 8 个功能码各跑一遍正常/越界/超长场景。
  • 广播写(0x00)测试。
  • CRC 故意造 1 位错 → 应不响应。
  • 连续 1000 帧压测,统计丢包率、响应时间分布。
  • 长时间(24h+) 跑,看是否有内存泄漏、计数器溢出。
  • 波特率阶梯测试:1200 / 2400 / 9600 / 19200 / 38400 / 115200。

12.3 一致性认证(可选)

ModBus-IDA(现 ModBus Organization)提供官方 Conformance Test,费用 500~1500 美元。如果产品要销售海外或对接客户系统,通过认证可大幅减少兼容性投诉。


参考资料

  1. ModBus Organization 官方协议规范(英文,ModBus Application Protocol V1.1b3 / ModBus Serial Line V1.02)
  2. ModBus Tools - 官方维护的工具与厂商资源目录
  3. libmodbus - 开源 C 库,支持 RTU/TCP,工业级实现
  4. FreeMODBUS - 嵌入式 ModBus RTU/ASCII 从站协议栈(经典参考)
  5. pymodbus - Python 实现的 ModBus 协议栈,适合自测脚本
  6. STM32 / CH32 HAL 库串口 DMA+空闲中断接收模板(ST 官方应用笔记)
  7. 100ask ModBus 中文教程与社区资源

posted on 2026-07-15 14:31  s苦瓜大王  阅读(6)  评论(0)    收藏  举报