4、协议帧结构:Modbus TCP 报文格式详解
好,咱们今天聊聊Modbus TCP的报文结构。
说实话,很多工程师刚接触Modbus TCP时,第一反应是:“这不就是把串口数据包直接塞进TCP里吗?”
嗯,差不多,但又不完全一样。我当年第一次调试时也这么想,结果报文死活对不上,折腾了一下午才发现——多了一个MBAP头,少了一个校验码。
4.1 整体结构:比RTU多了什么,少了什么?
先看一张对比图,你就明白了。
看到了吧?
Modbus TCP 把 RTU 里的设备地址和CRC校验都去掉了。
取而代之的是一个7字节的MBAP报文头(Modbus Application Protocol Header)。
为什么去掉CRC?
因为TCP/IP协议栈自己会做校验。你再做一遍,纯属浪费CPU。我见过有人硬要在TCP报文里加CRC,结果两边都对不上,折腾半天才发现是画蛇添足。
4.2 MBAP报文头:7个字节,一个都不能少
MBAP头是整个Modbus TCP的核心。它长这样:
| 字段 | 长度 | 描述 | 取值范围 |
|---|---|---|---|
| 事务处理标识符 | 2字节 | 用于匹配请求和响应 | 0x0000 - 0xFFFF |
| 协议标识符 | 2字节 | 固定为0x0000(Modbus协议) | 0x0000 |
| 长度 | 2字节 | 后续字节数(从单元标识符开始算) | 0x0000 - 0xFFFF |
| 单元标识符 | 1字节 | 替代RTU的设备地址 | 0x00 - 0xFF |
咱们一个一个说。
4.2.1 事务处理标识符(Transaction Identifier)
这个字段说白了就是请求的编号。
你发一个请求,给它一个ID。服务器回复时,会把同样的ID带回来。
这样你就能知道——这条回复是对应哪条请求的。
我习惯的做法是:每次发送请求时,把事务ID加1。从0开始,到65535循环。
但要注意,有些设备不支持事务ID循环,你发0xFFFF后下一个发0x0000,它可能就懵了。
稳妥的做法是:用0x0001到0x7FFF,别用0x0000和0xFFFF。
4.2.2 协议标识符(Protocol Identifier)
这个字段固定为0x0000。
表示这是Modbus协议。目前没有其他值,将来也不会有。
你如果收到非0x0000的值,直接丢弃就好。
嗯,这里要注意:
有些网关设备会在协议标识符里做文章,比如填0x0001表示走其他协议。但那是厂家私有做法,不是标准Modbus TCP。你如果遇到这种情况,得看厂家的文档。
4.2.3 长度字段(Length)
这个字段容易搞错。
它表示从单元标识符开始,到报文结束的总字节数。
也就是说:长度 = 1(单元标识符)+ 1(功能码)+ N(数据区)。
举个例子:
如果你发一个读保持寄存器请求(功能码0x03),读取2个寄存器。
数据区是:起始地址(2字节)+ 寄存器数量(2字节)= 4字节。
那么长度字段 = 1 + 1 + 4 = 6(0x0006)。
4.2.4 单元标识符(Unit Identifier)
这个字段替代了RTU里的设备地址。
在纯Modbus TCP网络中,它通常填0x00或0xFF。
但如果你通过网关连接Modbus RTU设备,这个字段就用来指定下游设备的地址。
举个例子:
你有一个Modbus TCP转RTU的网关。网关后面挂了3个RTU设备,地址分别是1、2、3。
你发Modbus TCP请求时,单元标识符填1,网关就知道——这条请求要转发给地址为1的RTU设备。
我个人的习惯是:
如果直接连Modbus TCP设备,单元标识符固定填0x00。
如果通过网关连RTU设备,单元标识符填RTU设备的地址(1-247)。
4.3 无校验:为什么Modbus TCP不需要CRC?
这个问题我经常被问到。
答案其实很简单:TCP/IP协议栈已经做了校验。
TCP层有16位的校验和,IP层也有头部校验。
数据在传输过程中如果出错,TCP会重传。所以Modbus TCP不需要再做一次CRC。
但要注意:
这并不意味着你的应用层不需要做任何校验。
我建议你在应用层至少做以下检查:
- 事务ID匹配:回复的事务ID必须和请求一致
- 协议ID检查:必须是0x0000
- 长度检查:实际收到的字节数必须等于长度字段+6(MBAP头)
- 功能码检查:回复的功能码最高位是否为1(异常标志)
MBAP头(7字节)+ 功能码(1字节)+ 数据区(N字节)
没有设备地址,没有CRC校验。就这么简单。
4.4 完整报文示例:读保持寄存器
光说不练假把式。咱们看一个完整的例子。
请求报文(读取从地址0开始的2个寄存器):
事务标识符: 0x0001 (2字节)
协议标识符: 0x0000 (2字节)
长度: 0x0006 (2字节) ← 后续6字节
单元标识符: 0x00 (1字节)
功能码: 0x03 (1字节) ← 读保持寄存器
起始地址: 0x0000 (2字节) ← 从地址0开始
寄存器数量: 0x0002 (2字节) ← 读2个寄存器
完整报文(十六进制):
01 00 00 00 00 06 00 03 00 00 00 02
响应报文(返回2个寄存器的值:0x1234和0x5678):
事务标识符: 0x0001 (2字节) ← 和请求一致
协议标识符: 0x0000 (2字节)
长度: 0x0007 (2字节) ← 后续7字节
单元标识符: 0x00 (1字节)
功能码: 0x03 (1字节) ← 正常响应
字节数: 0x04 (1字节) ← 4字节数据
寄存器1值: 0x12 0x34 (2字节)
寄存器2值: 0x56 0x78 (2字节)
完整报文(十六进制):
01 00 00 00 00 07 00 03 04 12 34 56 78
你看,请求和响应的事务ID都是0x0001,这样你就能把请求和响应配对起来。
4.5 常见错误与避坑指南
最后,分享几个我实际项目中遇到的坑:
- 长度字段算错:这是最常见的错误。记住,长度是从单元标识符开始算,不包括MBAP头。
- 事务ID不匹配:有些设备要求事务ID必须递增,不能重复。我建议每次请求都递增。
- 单元标识符乱填:直接连TCP设备时填0x00,连网关时填下游设备地址。别搞混。
- 忘记大端序:Modbus TCP使用大端序(Big-Endian)。多字节字段的高字节在前。这个和Modbus RTU一样。
好了,Modbus TCP的报文结构就讲到这里。
说白了就是:MBAP头 + 功能码 + 数据,去掉CRC,加上事务ID。
你只要记住这几点,写代码、抓包分析都不会出大问题。
公众号:蓝海资料掘金营,微信deep3321