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 计算步骤:

  1. 初始化 CRC 寄存器为 0xFFFF。
  2. 对每个字节,与 CRC 低字节异或。
  3. 右移 1 位,如果移出的位是 1,则与多项式 0xA001 异或。
  4. 重复步骤 3 共 8 次。
  5. 处理完所有字节后,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:解析响应报文

  1. 从站地址:0x02,确认是目标设备。
  2. 功能码:0x03,确认是读操作响应。
  3. 数据字节数:0x04,表示后面有 4 个字节数据。
  4. 数据:
    • 寄存器 0x0000(温度):0x01F4 = 500,单位 0.1℃,实际温度 = 50.0℃
    • 寄存器 0x0001(湿度):0x0032 = 50,单位 0.1%RH,实际湿度 = 5.0%RH
  5. CRC:0x7A 0x9B,用同样的算法验证,确保数据无误。

避坑指南:我曾经遇到一个设备,响应报文里的数据字节数总是比预期多 1。后来发现,它把「设备状态」字节也塞进来了。所以,解析时一定要严格按照设备手册来,不要想当然。

好了,这一章的内容就到这里。Modbus RTU 协议其实不复杂,你只要记住:地址映射是地图,功能码是动作,CRC 是安全锁。下一章,咱们会把这些知识串起来,用 Python 写一个完整的温度巡检仪数据采集程序。

记住,多动手抓包分析,比看十遍文档都管用。找个串口调试助手,发几条报文试试,你很快就能上手。