4、Modbus RTU 协议详解:帧结构、功能码、CRC校验、地址与寄存器
好,咱们进入正题。Modbus RTU 协议,可以说是工业物联网网关最常打交道的协议之一。我做了这么多年网关适配,跟它打交道最多。说白了,它就是个“老古董”,但稳定可靠,几乎所有 PLC、传感器、仪表都支持它。
今天我就把它的核心细节掰开揉碎了讲。你只要掌握了帧结构、功能码、CRC 校验、地址和寄存器这四块,基本就能搞定 90% 的 Modbus RTU 设备。
4.1 帧结构:报文长什么样?
Modbus RTU 的报文,其实特别简单。它没有复杂的头部,也没有花哨的封装。一个完整的请求或响应帧,就长这样:
| 地址码 (1字节) | 功能码 (1字节) | 数据区 (N字节) | CRC校验 (2字节) |
嗯,这里要注意:整个帧是连续的字节流,没有起始位和停止位。那怎么判断一帧结束了?靠的是“静默时间”。总线空闲 3.5 个字符时间以上,就认为上一帧结束了,新的一帧开始了。
我在项目中遇到过一个问题:有个客户说他的网关老是读到乱码。我排查了半天,发现是他用的串口转以太网模块,把帧之间的间隔时间搞乱了。Modbus RTU 对时序要求很严格,你想想看,如果两个字节之间的间隔超过了 1.5 个字符时间,接收端就会认为帧结束了,后面的字节就成了下一帧。这就是典型的“粘包”或“断帧”。
4.2 功能码:你想对设备做什么?
功能码就是告诉设备:你想干嘛。常用的功能码其实就几个,我列个表给你看:
| 功能码 | 名称 | 作用 | 我常用的场景 |
|---|---|---|---|
| 0x01 | 读线圈 | 读取开关量输出 | 控制继电器状态 |
| 0x02 | 读离散输入 | 读取开关量输入 | 读取按钮、限位开关 |
| 0x03 | 读保持寄存器 | 读取模拟量输出/参数 | 读取温度、压力、设定值 |
| 0x04 | 读输入寄存器 | 读取模拟量输入 | 读取传感器原始值 |
| 0x05 | 写单个线圈 | 控制单个开关量 | 启动/停止电机 |
| 0x06 | 写单个寄存器 | 设置单个参数 | 修改 PID 系数 |
| 0x0F | 写多个线圈 | 批量控制开关量 | 批量复位报警 |
| 0x10 | 写多个寄存器 | 批量设置参数 | 下载配置表 |
我个人习惯,最常用的是 0x03 和 0x06。为什么?因为保持寄存器既能读又能写,而且一个寄存器 16 位,能表示 0-65535 或 -32768-32767,大部分传感器数据都够用了。
举个例子,读一个温度传感器:
请求:01 03 00 00 00 01 84 0A
响应:01 03 02 01 2C B9 33
解释一下:
- 01:设备地址,1号设备
- 03:读保持寄存器
- 00 00:起始寄存器地址,从 0 号寄存器开始
- 00 01:读取 1 个寄存器
- 84 0A:CRC 校验
响应里的 01 2C 就是数据,换算成十进制是 300。如果这个传感器量程是 0-100℃,分辨率是 0.1℃,那实际温度就是 30.0℃。你看,是不是很简单?
4.3 CRC 校验:数据对不对?
CRC 校验是 Modbus RTU 的“安全锁”。它用的是 CRC-16/MODBUS 算法,多项式是 0x8005。计算范围是从地址码到数据区的最后一个字节,不包括 CRC 本身。
我刚开始做的时候,CRC 校验总是算不对。后来发现,CRC 的低字节在前,高字节在后。比如上面例子里的 84 0A,实际计算出来的 CRC 值是 0x0A84,发送时先发低字节 0x84,再发高字节 0x0A。
这里我贴一个 C 语言的 CRC 计算函数,你直接拿去用:
unsigned short crc16_modbus(unsigned char *buf, int len) {
unsigned short crc = 0xFFFF;
for (int i = 0; i < len; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
4.4 地址与寄存器:设备在哪?数据在哪?
Modbus 的地址分两种:设备地址和寄存器地址。
设备地址就是挂在总线上的每个设备的“门牌号”。范围是 1-247,0 是广播地址。我建议你给每个设备分配一个唯一的地址,不要用默认的 1。为什么?因为如果两个设备都是地址 1,它们会同时响应,总线就乱套了。
寄存器地址是设备内部的数据“抽屉”。Modbus 协议里,寄存器地址是从 0 开始的。但很多设备厂商的说明书里,写的是“寄存器编号”,比如 40001、30001 这种。这里有个换算关系:
- 0x 开头的地址:协议层地址,从 0 开始
- 4xxxx 开头的编号:保持寄存器,编号从 40001 开始
- 3xxxx 开头的编号:输入寄存器,编号从 30001 开始
- 0xxxx 开头的编号:线圈,编号从 00001 开始
- 1xxxx 开头的编号:离散输入,编号从 10001 开始
举个例子,说明书上写“温度数据在 40003 寄存器”,那协议层的地址就是 40003 - 40001 = 2,也就是 0x0002。这个坑我踩过好几次,每次都要提醒自己:协议层地址 = 编号 - 起始编号。
- 帧结构:地址 + 功能码 + 数据 + CRC
- 功能码:03 读保持寄存器,06 写单个寄存器,最常用
- CRC:低字节在前,高字节在后,计算范围不含 CRC 本身
- 地址:设备地址 1-247,寄存器地址从 0 开始
好了,Modbus RTU 的核心内容就这些。你把这些搞懂了,再去适配各种设备,基本就是“降维打击”。下一章我会讲 Modbus TCP,它跟 RTU 的区别和联系,到时候你就知道为什么我说“RTU 是基础,TCP 是变种”了。