3、Modbus RTU协议详解:寄存器地址映射、功能码(03/06/16)、CRC校验计算、报文解析实战
好,咱们进入第三章。这一章可以说是整个数据采集方案的「地基」。Modbus RTU 协议,你搞懂了它,后面所有设备通信、数据解析、上位机开发,都会顺风顺水。
我个人习惯,把 Modbus RTU 看作一种「工业界的普通话」。不管你是国产温控器,还是进口 PLC,只要支持 Modbus,大家就能用同一套规则聊天。说白了,它定义了「谁先说、说多长、怎么校验、怎么理解对方的意思」。
3.1 寄存器地址映射:设备的数据「户口本」
每个 Modbus 设备内部,都有一张「寄存器地图」。这张地图告诉你:温度数据存在哪个房间(地址),湿度数据又存在哪个房间。我刚开始接触时,最头疼的就是搞混「线圈」和「寄存器」——其实没那么复杂。
Modbus 定义了四种数据对象,但咱们做温度巡检仪,最常用的是 保持寄存器(Holding Register)。它可读可写,16位宽,专门存模拟量数据,比如温度值、设定阈值。
| 数据对象 | 访问类型 | 地址范围(PLC习惯) | Modbus 协议地址 | 位宽 |
|---|---|---|---|---|
| 线圈(Coil) | 读写 | 00001-09999 | 0x0000-0x270E | 1 bit |
| 离散输入(Discrete Input) | 只读 | 10001-19999 | 0x0000-0x270E | 1 bit |
| 输入寄存器(Input Register) | 只读 | 30001-39999 | 0x0000-0x270E | 16 bit |
| 保持寄存器 | 读写 | 40001-49999 | 0x0000-0x270E | 16 bit |
重点来了:PLC 习惯用 40001 这种「五位数地址」,但 Modbus 报文里实际用的是 协议地址(0x0000 开始)。比如 40001 对应协议地址 0x0000,40002 对应 0x0001。你写代码时,一定要做这个减法。
举个例子。我手头有个温控器,它的温度值存在保持寄存器地址 40001(协议地址 0x0000),温度单位是 0.1℃。如果读回来是 0x01F4(十进制 500),那实际温度就是 50.0℃。嗯,这里要注意:不同厂家的数据格式可能不一样,有的用有符号整数,有的用 IEEE754 浮点数。我踩过这个坑——读回来 65535,以为是传感器坏了,结果人家定义 0xFFFF 表示「传感器开路」。
3.2 功能码 03、06、16:三个最常用的「指令」
功能码就是 Modbus 的「动词」。你告诉设备:我要干嘛。做温度巡检,你只需要掌握三个功能码:03(读)、06(写单个)、16(写多个)。
3.2.1 功能码 03:读取保持寄存器
这是最常用的功能码。你想知道当前温度、湿度、压力,都用它。
请求报文格式:
从站地址 + 0x03 + 起始地址高字节 + 起始地址低字节 + 寄存器数量高字节 + 寄存器数量低字节 + CRC低字节 + CRC高字节
响应报文格式:
从站地址 + 0x03 + 数据字节数 + 数据1高字节 + 数据1低字节 + ... + CRC低字节 + CRC高字节
举个例子。我要读从站地址 0x01 的保持寄存器,从地址 0x0000 开始,读 2 个寄存器(4 个字节)。
请求: 01 03 00 00 00 02 C4 0B
响应: 01 03 04 01 F4 00 32 7A 9B
解析一下:数据字节数是 04(4 个字节),第一个寄存器值是 0x01F4(500,即 50.0℃),第二个寄存器值是 0x0032(50,即 5.0%RH)。
我的习惯:一次读取多个连续寄存器,比多次单读效率高得多。比如读 10 个通道的温度,一次发 03 功能码读 10 个寄存器,比发 10 次报文快 10 倍。但注意,有些设备限制一次最多读 125 个寄存器(0x7D)。
3.2.2 功能码 06:写单个保持寄存器
你想修改某个参数,比如设定温度阈值、切换工作模式,就用 06 功能码。
请求报文格式:
从站地址 + 0x06 + 寄存器地址高字节 + 寄存器地址低字节 + 数据高字节 + 数据低字节 + CRC低字节 + CRC高字节
比如我要把从站 0x01 的寄存器 0x0001(设定温度)改成 0x00C8(200,即 20.0℃)。
请求: 01 06 00 01 00 C8 19 8A
响应: 正常响应就是原样返回请求报文,表示写入成功。
注意:写操作一定要谨慎。我曾经在调试时,不小心把设备的「波特率寄存器」写成了 0,结果设备直接「失联」了。后来只能拆机用串口工具重新配置。所以,写操作前,务必确认寄存器地址和数据类型。
3.2.3 功能码 16:写多个保持寄存器
当你要一次性设置多个参数,比如同时修改温度上限、下限、回差,用 16 功能码比连续发 06 高效得多。
请求报文格式:
从站地址 + 0x10 + 起始地址高字节 + 起始地址低字节 + 寄存器数量高字节 + 寄存器数量低字节 + 数据字节数 + 数据1高字节 + 数据1低字节 + ... + CRC低字节 + CRC高字节
比如我要写从站 0x01 的寄存器 0x0002 和 0x0003,分别写入 0x00C8 和 0x0032。
请求: 01 10 00 02 00 02 04 00 C8 00 32 6F 9A
响应: 01 10 00 02 00 02 20 08
响应报文里,返回了起始地址和寄存器数量,表示写入成功。
3.3 CRC校验计算:确保数据没「串味」
Modbus RTU 使用 CRC-16(循环冗余校验)来检测数据传输错误。说白了,就是发送方算出一个「校验值」附在报文末尾,接收方用同样的算法再算一遍,如果对不上,说明数据在传输过程中被干扰了。
CRC 计算有点繁琐,但好在不用手算。我一般用查表法,速度快,代码也简洁。
CRC-16 计算步骤:
- 初始化 CRC 寄存器为 0xFFFF。
- 对每个字节,与 CRC 低字节异或。
- 右移 1 位,如果移出的位是 1,则与多项式 0xA001 异或。
- 重复步骤 3 共 8 次。
- 处理完所有字节后,CRC 寄存器的高低位互换(低字节在前,高字节在后)。
下面是我常用的 Python 实现,你直接拿去用:
def crc16_modbus(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
# 高低字节互换
return ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF)
# 示例:计算 01 03 00 00 00 02 的 CRC
data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x02])
crc = crc16_modbus(data)
print(f"CRC: {crc:04X}") # 输出:C40B
小技巧:你可以用在线 CRC 计算工具验证结果。但记住,Modbus 的 CRC 是「低字节在前,高字节在后」。比如计算结果是 0xC40B,报文里要写成 0x0B 0xC4。我刚开始就搞反了顺序,折腾了半天才发现。
3.4 报文解析实战:手撕一个完整通信过程
光说不练假把式。咱们来实战一把,完整解析一个温度读取过程。
场景:从站地址 0x02,读取保持寄存器 0x0000(温度)和 0x0001(湿度)。
步骤 1:构造请求报文
从站地址: 0x02
功能码: 0x03
起始地址: 0x0000
寄存器数量: 0x0002
CRC: 计算得到 0xC4 0x0B(低字节在前)
请求报文: 02 03 00 00 00 02 C4 0B
步骤 2:发送并接收响应
假设设备正常响应:
02 03 04 01 F4 00 32 7A 9B
步骤 3:解析响应报文
- 从站地址:0x02,确认是目标设备。
- 功能码:0x03,确认是读操作响应。
- 数据字节数:0x04,表示后面有 4 个字节数据。
- 数据:
- 寄存器 0x0000(温度):0x01F4 = 500,单位 0.1℃,实际温度 = 50.0℃
- 寄存器 0x0001(湿度):0x0032 = 50,单位 0.1%RH,实际湿度 = 5.0%RH
- CRC:0x7A 0x9B,用同样的算法验证,确保数据无误。
避坑指南:我曾经遇到一个设备,响应报文里的数据字节数总是比预期多 1。后来发现,它把「设备状态」字节也塞进来了。所以,解析时一定要严格按照设备手册来,不要想当然。
好了,这一章的内容就到这里。Modbus RTU 协议其实不复杂,你只要记住:地址映射是地图,功能码是动作,CRC 是安全锁。下一章,咱们会把这些知识串起来,用 Python 写一个完整的温度巡检仪数据采集程序。
记住,多动手抓包分析,比看十遍文档都管用。找个串口调试助手,发几条报文试试,你很快就能上手。