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头。搞懂了这一点,你就能在两种协议之间自由切换了。