4、Modbus TCP帧结构:MBAP报文头、单元标识符、协议数据单元(PDU)、与RTU的映射关系
好,咱们今天来聊聊Modbus TCP的帧结构。说实话,很多工程师一开始接触TCP版Modbus时,总觉得它跟RTU是两码事。其实不然,TCP只是在RTU外面套了一层“马甲”。这层马甲,就是MBAP报文头。
我个人习惯把Modbus TCP的帧拆成两段来看:前面7个字节的MBAP头,后面是大家熟悉的PDU(协议数据单元)。PDU这部分跟RTU几乎一模一样,只是没有了CRC校验。嗯,这里要注意,TCP/IP协议栈自己会保证数据完整性,所以Modbus TCP就把CRC给省了。
4.1 MBAP报文头详解
MBAP,全称是Modbus Application Protocol header。说白了,它就是给TCP传输用的一个信封。一共7个字节,结构如下:
| 字段 | 长度(字节) | 描述 |
|---|---|---|
| 事务处理标识符 | 2 | 用于请求/响应的配对,一般由客户端生成 |
| 协议标识符 | 2 | 固定为0x0000,表示Modbus协议 |
| 长度 | 2 | 后续字节的总长度(包括单元标识符) |
| 单元标识符 | 1 | 相当于RTU中的从站地址 |
我在项目中遇到过好几次这样的问题:有人把事务处理标识符设成了固定值,结果多线程请求时,响应全乱套了。你想想看,如果同时发了两个请求,一个读温度,一个读压力,返回的数据就分不清谁是谁了。
关键点:事务处理标识符必须唯一。客户端每发一个请求,这个值就要加1。服务器响应时,会原样返回这个值。这样客户端就能把响应和请求一一对应起来。
协议标识符就简单了,永远是0x0000。我记得有一次调试时,看到抓包工具里这个字段是0x0001,结果设备根本不搭理。后来才发现是协议栈库的版本问题,它把协议标识符给填错了。
4.2 单元标识符的秘密
单元标识符,这个字段很有意思。它只有1个字节,取值范围0-255。在RTU里,这个叫从站地址。但在TCP里,它的作用变了。
为什么会这样?因为TCP本身是一对一的通信。一个TCP连接只对应一个设备。那单元标识符用来干嘛?
- 网关场景:当TCP客户端通过网关连接多个RTU从站时,单元标识符就用来区分不同的从站。比如网关后面挂了3个RTU设备,单元标识符就填1、2、3。
- 直接连接:如果PLC或上位机直接连Modbus TCP设备,单元标识符通常填0x00或0xFF。有些设备比较死板,必须填0x00才认。
我的经验:如果你不确定单元标识符该填什么,先试试0x00。不行再试0xFF。我曾经被一个德国品牌的驱动器坑过,它要求单元标识符必须等于设备本身的IP地址最后一位。你说这设计是不是有点奇葩?
4.3 协议数据单元(PDU)
PDU这部分,跟RTU完全一样。它由功能码和数据组成。功能码1个字节,数据部分最长253个字节。
举个例子,读保持寄存器的PDU:
请求:0x03 0x00 0x00 0x00 0x0A
| |______| |______|
功能码 起始地址 寄存器数量
响应:0x03 0x14 0x00 0x01 ...(共20个字节的数据)
| | |______|
功能码 字节数 寄存器值
你看,跟RTU的PDU一模一样。只是RTU在外面套了从站地址和CRC,TCP套了MBAP头。
4.4 与RTU的映射关系
说白了,Modbus TCP就是把RTU报文中的从站地址挪到了MBAP头里,把CRC校验去掉了。映射关系如下:
| RTU帧 | TCP帧 | 说明 |
|---|---|---|
| 从站地址(1字节) | 单元标识符(1字节) | 位置变了,但值可以一样 |
| 功能码(1字节) | 功能码(1字节) | 完全不变 |
| 数据(N字节) | 数据(N字节) | 完全不变 |
| CRC(2字节) | 无 | 由TCP/IP保证完整性 |
我刚开始做网关开发时,就利用了这个映射关系。直接把RTU报文中的从站地址提取出来,塞进MBAP头的单元标识符里,然后去掉CRC,加上MBAP头的前6个字节,就拼成了TCP帧。反过来也一样,从TCP帧里取出单元标识符,加上CRC,就还原成RTU帧了。
避坑指南:我曾经在做一个项目时,发现TCP转RTU网关偶尔会丢包。查了两天才找到原因——RTU的CRC校验在转换时没重新计算。因为TCP帧里没有CRC,网关在转发时必须重新计算CRC。如果你直接拿TCP帧里的数据去当RTU用,CRC肯定对不上。
4.5 一个完整的TCP请求示例
咱们来看一个完整的例子。假设要读取从站地址为1的设备,保持寄存器起始地址0,读取10个寄存器。
RTU请求帧:
01 03 00 00 00 0A C5 CD
| | |______| |____| |__|
地址 功能码 起始地址 数量 CRC
TCP请求帧:
00 01 00 00 00 06 01 03 00 00 00 0A
|____| |____| |____| | | |______|
事务ID 协议ID 长度 单元 功能码 数据
标识符
注意看长度字段是0x0006,它表示从单元标识符开始到结尾一共6个字节(1个单元标识符 + 1个功能码 + 4个数据)。
我个人习惯在调试时先用Wireshark抓包,看看MBAP头里的长度字段对不对。很多新手会犯一个错误:把长度字段算成了整个TCP帧的长度,包括MBAP头本身。其实长度字段只算后面的数据,不包括MBAP头的前6个字节。
好了,关于Modbus TCP的帧结构就讲这么多。记住一句话:TCP就是RTU去掉了CRC,加了个7字节的MBAP头。搞懂了这一点,你就能在两种协议之间自由切换了。