4、协议帧结构:Modbus TCP 报文格式详解

好,咱们今天聊聊Modbus TCP的报文结构。

说实话,很多工程师刚接触Modbus TCP时,第一反应是:“这不就是把串口数据包直接塞进TCP里吗?”
嗯,差不多,但又不完全一样。我当年第一次调试时也这么想,结果报文死活对不上,折腾了一下午才发现——多了一个MBAP头,少了一个校验码

4.1 整体结构:比RTU多了什么,少了什么?

先看一张对比图,你就明白了。

Modbus RTU vs Modbus TCP 报文结构对比 Modbus RTU: 地址 功能码 数据区 CRC校验 Modbus TCP: MBAP报文头(7字节) 功能码 数据区 ✘ 没有设备地址(由IP区分) ✘ 没有CRC校验(由TCP/IP保证) ✔ 增加了MBAP报文头(7字节) ✔ 增加了单元标识符(替代设备地址)

看到了吧?
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。

💡 我的小技巧: 如果你只连一台设备,事务ID可以固定为0x0000。但如果你连多台设备,或者要并发请求,事务ID必须递增。否则你分不清哪条回复对应哪条请求。

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)。

⚠️ 我曾经踩过的坑: 有一次我写代码时,把长度字段算成了整个报文长度(包括MBAP头)。结果服务器一直不回包。调试了半天才发现——长度字段不应该包含MBAP头自身。记住:长度是从单元标识符开始算的。

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(异常标志)
🔑 核心要点: Modbus TCP的报文结构可以概括为:
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时,常用Wireshark抓包。过滤条件写"modbus",就能看到完整的报文结构。你可以对照着报文,逐字节分析,很快就能上手。

好了,Modbus TCP的报文结构就讲到这里。
说白了就是:MBAP头 + 功能码 + 数据,去掉CRC,加上事务ID。
你只要记住这几点,写代码、抓包分析都不会出大问题。


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