第3章 Modbus协议详解:RTU与TCP帧结构、功能码、寄存器映射、CRC校验
各位工程师朋友,咱们今天来啃一块硬骨头——Modbus协议。说实话,这协议我用了十几年,从早期的RS485串口到现在的以太网,它就像个老黄牛,朴实但可靠。很多刚入行的朋友觉得Modbus太简单,不就是读读写写吗?嗯,真到现场调试的时候,坑可不少。
我个人习惯把Modbus理解成「工业界的通用语言」。不管你是PLC、变频器、传感器还是执行器,只要支持Modbus,大家就能对话。今天咱们重点讲清楚四个核心:帧结构、功能码、寄存器映射、CRC校验。把这四个吃透了,Modbus就算入门了。
3.1 帧结构:RTU vs TCP,到底差在哪?
Modbus有两种主流传输模式:RTU(远程终端单元)和TCP(以太网)。说白了,RTU跑在串口上(RS232/RS485),TCP跑在以太网上。帧结构有区别,但核心思想一样——都是「请求-应答」模式。
3.1.1 Modbus RTU帧结构
RTU帧是二进制格式,紧凑高效。我当年在一条老生产线上调试,串口波特率只有9600,一帧数据就那么几个字节,省着用。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 地址码 | 1 | 从站地址,范围1~247,0为广播 |
| 功能码 | 1 | 操作类型,如03读保持寄存器 |
| 数据区 | N | 寄存器地址、数量、数据内容 |
| CRC校验 | 2 | 循环冗余校验,低字节在前 |
RTU帧示例(读保持寄存器):
请求:01 03 00 00 00 0A C5 CD
应答:01 03 14 00 01 00 02 ... [20字节数据] ... CRC_H CRC_L
3.1.2 Modbus TCP帧结构
TCP帧去掉了CRC校验(交给TCP/IP协议栈处理),加了一个MBAP头。说白了,TCP/IP已经保证了传输可靠性,Modbus就不需要再自己校验了。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 事务标识符 | 2 | 用于匹配请求和应答 |
| 协议标识符 | 2 | 固定为0x0000 |
| 长度 | 2 | 后续字节数(从单元标识符开始) |
| 单元标识符 | 1 | 相当于RTU的地址码 |
| 功能码 | 1 | 同RTU |
| 数据区 | N | 同RTU |
TCP帧示例(读保持寄存器):
请求:00 01 00 00 00 06 01 03 00 00 00 0A
应答:00 01 00 00 00 17 01 03 14 00 01 00 02 ... [20字节数据]
3.2 功能码:Modbus的指令集
功能码就是Modbus的「动词」。你想读数据?用03。你想写数据?用06或10。你想控制线圈?用05或0F。功能码分两类:位操作和字操作。
3.2.1 常用功能码一览
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
| 01 | 读线圈 | 线圈(DO) | 读取0/1状态 |
| 02 | 读离散输入 | 离散输入(DI) | 只读 |
| 03 | 读保持寄存器 | 保持寄存器 | 最常用,读写均可 |
| 04 | 读输入寄存器 | 输入寄存器 | 只读 |
| 05 | 写单个线圈 | 线圈 | 置位或复位 |
| 06 | 写单个寄存器 | 保持寄存器 | 写一个16位数据 |
| 0F | 写多个线圈 | 线圈 | 批量操作 |
| 10 | 写多个寄存器 | 保持寄存器 | 批量写,我最常用 |
| 2B | 封装接口 | 扩展功能 | 用于设备识别等 |
3.2.2 异常响应
如果从站收到非法请求,会返回异常码。异常码 = 功能码 + 0x80。比如你发03读寄存器,从站返回83,说明出错了。
常见异常码:
- 01:非法功能码——从站不支持这个功能
- 02:非法数据地址——寄存器地址不存在
- 03:非法数据值——数据超出范围
- 04:从站设备故障——硬件出问题了
3.3 寄存器映射:把物理量映射到地址空间
寄存器映射是Modbus的灵魂。说白了,就是把温度、速度、位置这些物理量,映射到一个个16位的寄存器地址上。你读地址100,得到的是当前温度;你写地址200,设置的是目标速度。
3.3.1 四种数据对象
| 对象类型 | 访问方式 | 数据宽度 | 典型用途 |
|---|---|---|---|
| 线圈(Coil) | 读写 | 1位 | 启停控制、报警复位 |
| 离散输入(DI) | 只读 | 1位 | 按钮状态、限位开关 |
| 输入寄存器(Input Register) | 只读 | 16位 | 模拟量输入、编码器值 |
| 保持寄存器(Holding Register) | 读写 | 16位 | 参数设置、速度指令 |
3.3.2 实际案例:伺服驱动器的寄存器映射
我做过一个项目,用Modbus控制台达伺服。它的寄存器映射长这样:
地址(十六进制) 内容 数据类型 读写
0x0000 状态字 uint16 只读
0x0001 控制字 uint16 读写
0x0002 当前位置(低16位) uint16 只读
0x0003 当前位置(高16位) uint16 只读
0x0004 目标速度(低16位) uint16 读写
0x0005 目标速度(高16位) uint16 读写
0x0006 加速度设定 uint16 读写
...
注意:32位数据(比如位置、速度)需要占用两个连续的寄存器。低16位在前,高16位在后。这是Modbus的默认字节序,但有些设备可能相反。你想想看,如果字节序搞反了,你写进去的速度值可能变成天文数字。
3.4 CRC校验:确保数据不出错
CRC校验是Modbus RTU的「安全锁」。串口通信环境嘈杂,电磁干扰、信号衰减都可能导致数据出错。CRC就是用来检测这些错误的。
3.4.1 CRC算法原理
Modbus RTU使用CRC-16,多项式为0x8005。计算过程不复杂,但手算很麻烦。我一般直接用查表法,速度快,代码也简洁。
CRC计算步骤:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC低字节异或
- 右移一位,如果移出的位为1,则与多项式异或
- 重复8次
- 处理完所有字节后,取反得到最终CRC
3.4.2 C语言实现(查表法)
// CRC-16 查表法实现
uint16_t crc16_modbus(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
uint16_t i;
while (len--) {
crc ^= *data++;
for (i = 0; i < 8; i++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001; // 0xA001是0x8005的反转
} else {
crc >>= 1;
}
}
}
return crc;
}
3.4.3 常见CRC错误
- 字节序搞反:CRC低字节在前,高字节在后。很多新手写反了。
- 漏算地址码:CRC计算范围包括地址码、功能码和数据区,不包括CRC本身。
- 初始值错误:Modbus RTU的CRC初始值是0xFFFF,不是0x0000。
好了,今天就聊到这儿。下一章咱们讲Modbus在运动控制中的实战应用,包括如何用Modbus控制伺服驱动器、如何优化通信效率。到时候我会分享一个我亲手调试过的案例,保证干货满满。
公众号:蓝海资料掘金营,微信deep3321