第3章:Modbus帧结构——报文格式解析、地址域、功能码、数据域、CRC校验原理

各位同学,今天我们来聊聊Modbus协议里最核心的东西——帧结构。

说实话,我刚开始接触Modbus时,看着那一串十六进制数,脑袋都是懵的。后来在工厂调试一个温湿度传感器,死活读不到数据,折腾了两天,最后发现是CRC校验算错了。从那以后,我对帧结构的每个字节都格外敏感。

好,咱们直接进入正题。

3.1 报文格式:Modbus RTU的“骨架”

Modbus RTU的报文,说白了就是一段有固定格式的二进制数据。我习惯把它想象成一封挂号信:有收件人地址、信件内容、还有防伪封条。

一个完整的Modbus RTU帧,长这样:

| 地址域 | 功能码 | 数据域 | CRC校验 |
| 1字节  | 1字节  | N字节  | 2字节   |

嗯,就这么简单。总共最少4个字节,最多256个字节。为什么是256?因为数据域最长能到252个字节,加上地址、功能码和CRC,刚好256。

我在项目中见过有人把数据域塞得满满当当,结果从机处理不过来,直接超时。所以啊,别贪心,数据量大的时候分几次读更稳妥。

3.2 地址域:谁是你的通信对象

地址域占1个字节,取值范围是0~247。其中:

  • 0:广播地址,所有从机都要接收,但不回复
  • 1~247:从机地址,每个设备一个唯一编号
  • 248~255:保留,别用
我的经验:给设备分配地址时,我习惯从1开始,按顺序排。比如第一台温控器是1,第二台是2。千万别跳着用,否则后期维护时你自己都记不住。

你想想看,如果总线上挂了20个设备,地址乱糟糟的,排查故障时得多痛苦?

3.3 功能码:告诉从机你要干什么

功能码也是1个字节。它定义了主站想让从站做什么。常用的就那么几个:

功能码 含义 我常用的场景
0x01 读取线圈状态 读取继电器输出
0x02 读取离散输入 读取按钮、限位开关
0x03 读取保持寄存器 读取传感器数值、参数
0x04 读取输入寄存器 读取模拟量输入
0x05 写单个线圈 控制单个继电器
0x06 写单个寄存器 设置单个参数
0x0F 写多个线圈 批量控制输出
0x10 写多个寄存器 批量设置参数

这里有个坑,我曾经踩过:有些国产设备的功能码实现不标准,比如0x03和0x04混用。你发0x03读输入寄存器,它也能响应,但数据是错的。所以拿到新设备,第一件事就是看它的Modbus协议文档。

3.4 数据域:真正干活的地方

数据域是帧的“正文”。它包含:

  • 起始地址:从哪个寄存器开始读/写(2字节)
  • 寄存器数量:读/写多少个(2字节)
  • 数据内容:写操作时,这里放要写入的值

举个例子,我要读取地址为1的从机,从寄存器0x0000开始读2个寄存器:

发送:01 03 00 00 00 02 C4 0B
      |  |  |     |     |
      |  |  |     |     +-- CRC校验
      |  |  |     +-------- 寄存器数量(2个)
      |  |  +-------------- 起始地址(0x0000)
      |  +----------------- 功能码(0x03读保持寄存器)
      +-------------------- 从机地址(1)

从机回复:

接收:01 03 04 00 64 00 C8 7A 3F
      |  |  |  |        |
      |  |  |  |        +-- CRC校验
      |  |  |  +----------- 数据(0x0064=100, 0x00C8=200)
      |  |  +-------------- 字节数(4个字节)
      |  +----------------- 功能码(0x03)
      +-------------------- 从机地址(1)
注意:Modbus RTU采用大端字节序(Big-Endian)。也就是说,高字节在前,低字节在后。比如0x0064,先发0x00,再发0x64。这一点在解析数据时千万不能搞反。

3.5 CRC校验:数据的“指纹”

CRC(循环冗余校验)是Modbus RTU的防伪封条。它占2个字节,放在帧的末尾。

为什么需要CRC?因为工业现场干扰多,电磁噪声、线路老化都可能导致数据出错。没有CRC,你读到100℃可能实际上是150℃,后果嘛...你懂的。

CRC的计算过程,说白了就是:

  1. 把整个帧(地址+功能码+数据)当成一串二进制数
  2. 用这个数除以一个固定的多项式(0x8005)
  3. 余数就是CRC值

嗯,数学上就是这么回事。但实际编程时,我们不会真的去做除法,太慢了。我一般用查表法:

def crc16_modbus(data):
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc

这段代码我用了快十年了,从单片机到树莓派,从Python到C,基本没改过。你直接拿去用就行。

我曾经踩过的坑:有一次我写CRC计算,把多项式搞反了。Modbus用的是0xA001(逆序),我用了0x8005(正序)。结果算出来的校验码总是不对,从机一直不回包。排查了整整一个下午才发现。所以,千万别搞错多项式的方向!

3.6 异常响应:当事情不顺利时

如果从机收到请求,但无法执行,它会返回一个异常帧:

| 地址域 | 功能码+0x80 | 异常码 | CRC校验 |
| 1字节  | 1字节       | 1字节  | 2字节   |

常见的异常码:

  • 01:非法功能码——从机不支持这个功能
  • 02:非法数据地址——你读的寄存器不存在
  • 03:非法数据值——你写的值超出范围
  • 04:从机故障——设备内部出问题了

我个人习惯,在调试阶段把异常码打印出来。这样能快速定位问题,省得瞎猜。

3.7 本章小结

Modbus帧结构,说白了就是一套约定:谁说话、说什么、怎么说、怎么防伪。掌握了这四点,你就掌握了Modbus通信的精髓。

下一章,我们会用Python代码把这些帧结构变成真正的通信程序。到时候,你就能亲手抓包、解析、发送Modbus报文了。

课后练习:用手算一下发送帧 01 03 00 00 00 02 的CRC值,看看是不是 C4 0B。算不出来没关系,用上面的Python代码跑一遍也行。

Modbus RTU帧结构全景图 地址域 1字节 功能码 1字节 数据域 N字节(0~252) CRC校验 2字节 数据域内部结构(以读保持寄存器为例) 起始地址(2字节) 寄存器数量(2字节) 数据内容(可选) 关键要点: • 总长度:最少4字节,最多256字节 • 字节序:大端模式(高字节在前) • CRC范围:地址域 + 功能码 + 数据域(不含CRC本身) • 异常响应:功能码最高位置1,后跟异常码

好了,帧结构就讲到这里。记住,Modbus通信的本质就是拼帧、发帧、收帧、拆帧。把这四个步骤搞明白,你就入门了。

公众号:蓝海资料掘金营,微信deep3321