3. Modbus RTU帧结构:报文格式详解

好,咱们今天来聊聊Modbus RTU最核心的东西——报文格式。说白了,就是设备之间到底怎么“说话”的。我刚开始接触这个协议的时候,看着那一串十六进制数,说实话有点懵。但后来摸透了规律,发现其实特别简单。

3.1 报文的基本骨架

一个完整的Modbus RTU报文,就像一封信。它由四个部分组成:

  • 地址码(1字节)—— 这信是写给谁的
  • 功能码(1字节)—— 你要对方干什么
  • 数据域(N字节)—— 具体的内容或参数
  • CRC校验(2字节)—— 确保信没被篡改

你看,就这么简单。我当年在调试一个污水处理项目时,发现PLC和仪表怎么都对不上话。折腾了半天,最后发现是地址码写错了。嗯,这种低级错误,谁还没犯过呢?

3.2 地址码:设备的“身份证”

地址码占1个字节,取值范围是1到247。0是广播地址,248到255保留不用。

关键点:每个从站设备必须有唯一的地址。主站发报文时,地址码告诉总线:“我在找谁”。从站收到后,一看地址不是自己的,直接忽略。

我曾经在一个项目中,现场有30多个仪表,地址从1排到30。结果有个同事图省事,把两个仪表都设成了5。你想想看,主站发指令给5号设备,两个设备同时响应,总线直接乱套。所以我的建议是:设备地址一定要做好台账,最好用标签贴在设备上。

3.3 功能码:告诉设备做什么

功能码也是1个字节。常用的就那么几个:

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

这里有个小技巧:如果从站返回的功能码最高位是1(比如0x83),说明出错了。这是Modbus的异常响应机制。我刚开始做项目时,看到返回0x83还以为是设备坏了,后来才知道是功能码不支持。

3.4 数据域:真正的“干货”

数据域的长度是可变的,从0字节到252字节不等。它具体包含什么,取决于功能码。

举个例子,读取保持寄存器(功能码0x03):

  • 主站发送:地址码 + 0x03 + 起始地址(2字节)+ 寄存器数量(2字节)+ CRC
  • 从站响应:地址码 + 0x03 + 数据字节数(1字节)+ 数据(N字节)+ CRC

我习惯用实际报文来说明。假设我们要读取1号从站,从地址0x0000开始,读2个寄存器:

主站发送:01 03 00 00 00 02 C4 0B
从站响应:01 03 04 00 0A 00 14 3A 86

你看,从站返回了4个字节的数据(0x000A和0x0014),也就是十进制的10和20。是不是很直观?

我的经验:调试时最好用串口助手抓一下报文。很多问题一看原始数据就明白了。比如数据全是0xFF,那多半是通信线接反了。

3.5 CRC校验:通信的“安全锁”

CRC校验占2个字节,放在报文的最后。它是对地址码、功能码、数据域进行循环冗余校验的结果。

计算CRC的步骤:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC寄存器的低8位异或
  3. 右移1位,如果移出的位是1,则与0xA001异或
  4. 重复步骤3共8次
  5. 处理完所有字节后,得到最终的CRC值

说实话,我从来不自己手算CRC。都是直接用现成的函数库。但你要知道它的原理——CRC校验能检测出99.99%以上的传输错误。

注意:CRC在报文中的字节序是低字节在前,高字节在后。比如CRC值是0xC40B,发送时先发0x0B,再发0xC4。这个顺序搞反了,通信肯定失败。我曾经因为这个坑,排查了整整一个下午。

3.6 字节序与传输规则

Modbus RTU的字节序遵循“小端模式”。什么意思呢?就是多字节数据,低字节在前,高字节在后。

举个例子,寄存器值0x1234,在报文中的顺序是:

先发 0x34,再发 0x12

而不是我们习惯的0x12、0x34。这一点特别容易搞混。我刚开始做Modbus开发时,就因为这个字节序问题,读出来的数据全是错的。

另外,传输规则还有几点要注意:

  • 每个字节的格式:1个起始位 + 8个数据位 + 无校验/奇校验/偶校验 + 1个停止位
  • 波特率:9600、19200、38400等,主从站必须一致
  • 报文间隔:两个报文之间至少要有3.5个字符的静默时间

为什么要有3.5个字符的间隔?说白了,就是让从站知道“上一个报文结束了,下一个报文要开始了”。如果没有这个间隔,从站可能把两个报文当成一个来处理,那数据就全乱了。

3.7 一个完整的报文示例

咱们来看一个完整的读写例子。假设我们要把1号从站的寄存器0x0001的值改为0x00FF:

主站发送:01 06 00 01 00 FF D8 1A
从站响应:01 06 00 01 00 FF D8 1A

注意看,写单个寄存器的响应报文,就是把发送报文原样返回。这是Modbus协议规定的。如果返回的不一样,说明出错了。

我遇到过一种情况:从站返回了异常码0x86(功能码0x06的最高位置1),表示“非法数据值”。后来查手册才知道,那个寄存器是只读的,不能写。所以,拿到设备手册后,一定要先看清楚哪些寄存器可读可写,哪些只能读。

3.8 避坑指南

最后,我总结几个常见的坑:

  • 地址重复:同一总线上不能有两个相同地址的设备
  • CRC错误:计算CRC时别忘了包含地址码和功能码
  • 字节序搞反:多字节数据一定是低字节在前
  • 波特率不匹配:主从站的波特率、校验位、停止位必须完全一致
  • 报文间隔太短:两个报文之间至少间隔3.5个字符时间

嗯,这一章的内容就到这里。报文格式是Modbus RTU的基础,搞懂了它,后面的通信调试就会顺畅很多。下一章咱们聊聊实际项目中怎么用代码实现Modbus RTU通信。