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的时序和波特率设置,那个才是真正让人头疼的地方。