目录
- 一、前言
- 二、Modbus RTU 帧格式
- 三、四种寄存器模型
- 四、常用功能码报文拆解
- 五、IDLE 中断与 Modbus 帧边界的天然契合
- 六、从 OOP 视角看 Modbus 后端
- 七、ModbusPoll 工具验证
- 八、常见坑
- 九、结尾
一、前言
大家好,这里是Hello_Embed。
上一篇我们把 UART 封装成了统一的UART_Device接口——Init、Send、RecvByte三个方法,底层换串口上层。
本篇进入项目核心协议:Modbus RTU。它是整个"工业互联设备管理系统"中,PC 上位机和 STM32H5 中控之间、中控和各路传感器之间的通用通信语言。
二、Modbus RTU 帧格式
| 地址(1B) | 功能码(1B) | 数据(NB) | CRC16(2B) | |----------|-----------|---------|-----------|- 地址:1-247(从站地址),0(广播,所有从站都执行,不回应)
- 功能码:告诉从站"读还是写、读写哪种寄存器"
- 数据:长度由功能码决定,可能包含寄存器地址、数量、写入值
- CRC16:校验整帧完整性,出错则丢弃
帧与帧之间用3.5 字符时间的静默(IDLE)分隔。在 115200 波特率下,1 字符 ≈ 87μs,3.5 字符 ≈ 305μs。
三、四种寄存器模型
Modbus 从站内部维护四张"表",每种表对应不同的功能码:
| 类型 | 前缀 | 功能码(读/写) | 大小 | 用途举例 |
|---|---|---|---|---|
| 线圈 Coil | 0x | 01 / 05,15 | 1 bit | 继电器输出、LED 开关 |
| 离散输入 DI | 1x | 02 / 无写 | 1 bit | 按键、限位开关 |
| 保持寄存器 HR | 4x | 03 / 06,16 | 16 bit | 参数配置、设定值 |
| 输入寄存器 IR | 3x | 04 / 无写 | 16 bit | 温度、湿度、光照读数 |
编号从 1 开始,但协议地址从 0 开始。例如"保持寄存器 1"→ 协议地址
0x0000。
四、常用功能码报文拆解
4.1 读保持寄存器 (03)
请求:从站 1,读寄存器地址 0,读 1 个
01 03 00 00 00 01 84 0A │ │ └───┘ └───┘ └───┘ │ │ │ │ │ │ │ │ │ CRC16 │ │ │ 读取数量 = 1 个寄存器 │ │ 起始地址 = 0x0000 │ 功能码 03 = 读保持寄存器 从站地址 1响应:
01 03 02 00 05 38 47 │ │ │ └───┘ └───┘ │ │ │ │ │ │ │ │ │ CRC16 │ │ │ 寄存器值 = 0x0005 │ │ 数据字节数 = 2 │ 功能码 从站地址4.2 写单个保持寄存器 (06)
请求:写地址 1 的寄存器,值为 10
01 06 00 01 00 0A 19 CD │ │ └───┘ └───┘ └───┘ │ │ │ │ │ │ │ │ │ CRC16 │ │ │ 写入值 = 0x000A (10) │ │ 寄存器地址 = 0x0001 │ 功能码 06 = 写单个寄存器 从站地址 1响应:原样回传(确认写入成功)。
4.3 读离散输入 (02)
请求:读从站 1 的离散输入,起始地址 0,读 3 个
01 02 00 00 00 03 38 0B响应:返回 1 字节,低 3 位对应 3 个离散输入的状态。
五、IDLE 中断与 Modbus 帧边界的天然契合
DMA+IDLE 接收模式是专门为 Modbus RTU "量身定制"的:
Modbus 帧 1 (N1字节) → 静默 305μs → Modbus 帧 2 (N2字节) → 静默 305μs → ... HAL_UARTEx_ReceiveToIdle_DMA 的行为: 收到帧 1 最后一个字节 → 1字符时间无数据 → IDLE 中断 → RxEventCallback(Size=N1) ← 完美!Size 就是帧长度 → 入队 → 重启 DMA 收到帧 2 ...不需要自己拼帧——IDLE 中断天然告诉你了每帧的边界。上层只需从 Queue 取出 Size 个字节就是一帧完整报文。
六、从 OOP 视角看 Modbus 后端
libmodbus 通过modbus_new_rtu()接收设备名:
modbus_t*ctx=modbus_new_rtu("uart4",115200,'N',8,1);内部流程:
modbus_new_rtu("uart4") → GetUARTDevice("uart4") // 拿到 OOP 设备指针 → ctx->backend.send = pDev->send // 绑定 Send → ctx->backend.recv = pDev->RecvByte // 绑定 RecvByte → ctx->backend.connect = ... // 调用 pDev->Init这就是 OOP 封装的终极意义——libmodbus 完全不关心底层是 UART2、UART4 还是 USB CDC。协议栈只管send()和RecvByte(),物理层由设备表管理。
// 用板载 UART4(接到 RS-485)modbus_t*ch2=modbus_new_rtu("uart4",115200,'N',8,1);// 用 USB CDC(虚拟串口,接到 PC 上位机)modbus_t*usb=modbus_new_rtu("usb",115200,'N',8,1);// 上层 modbus_reply / modbus_receive 代码完全一样!七、ModbusPoll 工具验证
PC 端工具路径:Tools/Modbus/(ModbusPoll 主站 + ModbusSlave 从站模拟器)
调试流程:
- 用 ModbusSlave 模拟从站:设置地址、寄存器初始值
- 用 ModbusPoll 发命令:观察请求帧和响应帧的原始报文
- STM32 上先用 ModbusSlave 软件验证 PC 端主站逻辑正确
- 再替换为 STM32 从站(代码写好后)
验证链路:
PC ModbusPoll ←─USB CDC──→ STM32H5 ←─UART4(RS-485)──→ STM32F030传感器 主站 中控 从站八、常见坑
8.1 CRC 计算
CRC 是整帧计算(地址+功能码+数据),不包含 CRC 字段本身。
8.2 寄存器地址偏移
"保持寄存器 1"在协议里地址是0x0000,不是0x0001。Modbus 编号从 1 开始,协议地址从 0 开始。
8.3 帧间间隔不够
如果连续发两帧 Modbus 报文,间隔小于 3.5 字符(< 305μs @115200),从机会把两帧当成一帧,CRC 校验失败。发送时必须保证至少 305μs 的静默。
8.4 broadcast 无响应
地址 0 是广播地址,所有从站执行命令但不回应。不要等广播的响应。
九、结尾
本篇把 Modbus RTU 的核心概念全串起来了:
- 帧格式(地址+功能码+数据+CRC)
- 四种寄存器模型(Coil/DI/HR/IR)
- IDLE 中断天然适合 Modbus 帧边界检测
UART_Device封装如何让 libmodbus 做到"与硬件无关"
学习路径回顾:
Note 7-8: UART 三种方式(查询/中断/DMA) Note 9: SPI DMA Note 10: DMA+IDLE Note 11: RTOS 信号量 Note 12: UART_Device OOP 封装 Note 13: Modbus 协议分析 ← 本篇下一篇预告:libmodbus 源码走读——核心数据结构、发送/接收/回复的全景分析,以及移植到 STM32H5 的关键步骤。