3. Modbus RTU协议帧结构:地址域、功能码、数据域、CRC校验、报文实例逐字节解析

好,咱们今天来啃一块硬骨头——Modbus RTU的帧结构。说实话,很多搞了三五年工控的老手,遇到通讯故障时还是只会拿万用表量通断。其实只要把帧结构搞明白,大部分问题都能迎刃而解。

Modbus RTU的报文,说白了就是一串字节流。它不像TCP/IP那么复杂,也没有七层模型。它的帧结构非常简洁,就四个部分:地址域、功能码、数据域、CRC校验。我习惯把这四个部分称为「四件套」,缺一不可。

3.1 地址域——谁在说话?

地址域只有一个字节,取值范围是1到247。0地址是广播地址,所有从站都要响应,但不会回复。248到255是保留地址,我建议你别碰。

你想想看,一条485总线上挂着几十个设备,主站发一个报文,怎么知道是发给谁的?就是靠这个地址域。每个从站的地址必须唯一,这是基本功。我在项目中遇到过两次地址冲突的情况,一次是PLC和变频器都设成了3,结果变频器乱跳,PLC还以为是正常数据。嗯,排查了整整一个下午。

关键点:地址域0x00是广播,从站收到后执行命令但不回复。地址域0xFF在某些私有协议中用作特殊用途,标准Modbus RTU不建议使用。

3.2 功能码——要干什么?

功能码也是一个字节。它告诉从站:你要读数据还是写数据?读线圈还是读寄存器?

常用的功能码就那么几个:

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

注意,如果从站返回的报文里功能码最高位是1(比如0x83),那就说明出错了。这是Modbus的异常响应机制。我曾经调试一个温控器,读寄存器一直返回0x83,查了半天才发现是寄存器地址写错了。

3.3 数据域——具体内容

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

举个例子:

  • 功能码0x03(读保持寄存器):数据域包含起始地址(2字节)和寄存器数量(2字节)
  • 功能码0x06(写单个寄存器):数据域包含寄存器地址(2字节)和要写入的值(2字节)
  • 功能码0x10(写多个寄存器):数据域包含起始地址、寄存器数量、字节数、实际数据

这里有个坑:Modbus RTU的数据是大端模式。什么意思?就是高位字节在前,低位字节在后。比如寄存器地址0x1234,在报文里先发0x12,再发0x34。我见过有人用串口助手调试,把高低位搞反了,读出来的数据全是错的。

我的小技巧:抓包时如果发现数据对不上,先检查字节序。很多国产设备支持小端模式,但标准Modbus RTU必须用大端。遇到这种情况,我一般会在代码里加一个字节序转换函数,一劳永逸。

3.4 CRC校验——数据对不对?

CRC校验占两个字节,放在报文末尾。它校验的是从地址域到数据域的所有字节。计算方法是CRC-16/MODBUS,多项式是0x8005。

说实话,CRC的计算逻辑有点绕,我一般不手算。但你要知道它的作用:防止数据在传输过程中被干扰。工业现场电磁环境恶劣,电机启停、变频器开关都会产生干扰。没有CRC校验,你读到的数据可能是错的,但你自己不知道。

我曾经在一个钢铁厂的项目里,PLC读取高温计的数值偶尔会跳变。排查了传感器、线路、接地,最后发现是CRC校验没做对。从站返回的CRC是错的,但主站没检查,直接当有效数据用了。嗯,从那以后我写通讯程序,第一件事就是加CRC校验。

警告:CRC校验是Modbus RTU的底线。不要为了省几个CPU周期而跳过CRC检查。我曾经见过有人为了「提高效率」关闭CRC校验,结果设备偶尔误动作,差点酿成事故。

3.5 报文实例逐字节解析

光说不练假把式。咱们来看一个真实的报文。

场景:主站(地址1)要读取从站(地址2)的3个保持寄存器,起始地址是0x0100。

主站发送:

01 03 01 00 00 03 95 CB

逐字节解析:

  • 01:地址域,目标从站地址为1
  • 03:功能码,读保持寄存器
  • 01 00:起始地址,0x0100(大端模式)
  • 00 03:寄存器数量,读3个寄存器
  • 95 CB:CRC校验,对前面5个字节计算得到

从站响应(正常):

01 03 06 00 0A 00 14 00 1E 48 72

逐字节解析:

  • 01:地址域,从站地址为1
  • 03:功能码,与请求一致
  • 06:数据域字节数,6个字节(3个寄存器 × 2字节)
  • 00 0A:第一个寄存器的值,0x000A = 10
  • 00 14:第二个寄存器的值,0x0014 = 20
  • 00 1E:第三个寄存器的值,0x001E = 30
  • 48 72:CRC校验

从站响应(异常):

01 83 02 C0 F1

逐字节解析:

  • 01:地址域
  • 83:功能码,0x80 | 0x03,最高位为1表示异常
  • 02:异常码,02表示非法数据地址(起始地址或寄存器数量不对)
  • C0 F1:CRC校验

常见异常码:

  • 01:非法功能码(从站不支持该功能)
  • 02:非法数据地址(寄存器地址超出范围)
  • 03:非法数据值(数据值超出允许范围)
  • 04:从站设备故障

好了,帧结构就讲到这里。你想想看,Modbus RTU其实很简单,就是四个部分拼在一起。但越简单的东西,越容易在细节上出错。我建议你拿一个真实的报文,用串口助手抓下来,对着这个解析过程逐字节看一遍。嗯,看一遍顶我讲十遍。

下一章咱们聊聊Modbus RTU的时序和波特率设置,那个才是真正让人头疼的地方。