4、Modbus TCP:TCP报文封装、MBAP头结构、与RTU的区别、实际组网案例

好,咱们今天聊聊Modbus TCP。说实话,这是我在工业现场用得最多的协议之一。很多刚入行的朋友觉得Modbus RTU和Modbus TCP差不多,不就是把串口换成了网口嘛。嗯,这么理解也没大错,但细节上差别还挺大的。我当年第一次调试Modbus TCP的时候,就因为在报文格式上栽了个跟头,折腾了大半天。

4.1 TCP报文封装:数据是怎么塞进TCP里的?

Modbus TCP本质上就是把Modbus的协议数据单元(PDU)直接塞进TCP的载荷里。但这里有个关键问题:TCP是流式协议,没有帧的概念。你发一串数据过来,接收方怎么知道这一帧从哪开始、到哪结束?

Modbus RTU靠的是3.5个字符的静默时间来区分帧。但TCP里不行,你没法在网络上等3.5个字符的时间,那太慢了。所以Modbus TCP引入了一个额外的头部——MBAP头,专门用来解决这个问题。

说白了,Modbus TCP的报文结构就是:

MBAP头(7字节) + 功能码(1字节) + 数据(N字节)

这个MBAP头里最重要的字段就是长度字段。接收方先读7个字节的MBAP头,解析出长度,就知道后面还有多少数据要收。这样就能完整地拼出一帧Modbus报文。

核心要点:Modbus TCP没有CRC校验,因为TCP/IP协议栈自己会做校验。这一点和RTU完全不同,别搞混了。

4.2 MBAP头结构:7个字节的门道

MBAP头全称是Modbus Application Protocol Header。一共7个字节,结构如下:

字段 长度(字节) 说明
事务处理标识符 2 用于匹配请求和响应。客户端每发一个请求,这个值加1
协议标识符 2 固定为0x0000,表示Modbus协议
长度 2 后续数据的字节数(包括功能码+数据)
单元标识符 1 相当于RTU里的从站地址,用于标识目标设备

我解释一下这几个字段的实际用法。

事务处理标识符:这个字段太重要了。你想想看,一个PLC可能同时给多个设备发请求,响应回来的顺序可能是乱的。没有这个标识符,你都不知道哪个响应对应哪个请求。我习惯的做法是每次请求递增这个值,收到响应后匹配一下,确保数据不乱。

协议标识符:这个字段目前只有0x0000一个值。但设计上留了扩展空间,以后如果有别的协议也想用这个框架,可以换一个标识符。不过目前我没见过实际使用的案例。

长度字段:注意,这个长度不包括MBAP头本身的7个字节。它只算功能码+数据的长度。举个例子,如果功能码是03,数据是4个字节,那长度字段就是5。

单元标识符:这个字段在TCP里其实有点尴尬。因为TCP本身已经通过IP地址和端口号定位到具体设备了,为什么还要一个单元标识符?

我的经验:单元标识符主要用于网关场景。比如一个Modbus TCP网关下面挂了多个RTU从站,网关收到TCP请求后,根据单元标识符决定转发给哪个RTU从站。如果是一对一的TCP连接,这个字段通常设为0x01或0xFF。

4.3 与RTU的区别:不只是接口不同

很多人觉得Modbus TCP就是把RTU的串口换成了以太网。其实没那么简单。我列个表,大家一看就明白:

对比项 Modbus RTU Modbus TCP
物理层 RS-232/RS-485 以太网
地址 从站地址(1字节) 单元标识符(1字节)+ IP地址
校验 CRC16(2字节) 无(由TCP/IP保证)
帧界定 3.5字符静默时间 MBAP头中的长度字段
最大节点数 32(RS-485典型值) 理论上无限制
通信模式 半双工 全双工

这里有个容易踩的坑:RTU的从站地址在TCP里变成了单元标识符,但位置变了。RTU里地址在报文最前面,TCP里地址在MBAP头的最后一个字节。我见过有人直接把RTU报文前面加个MBAP头发出去,结果地址重复了,设备根本不响应。

避坑指南:我曾经在调试一个项目时,把RTU报文直接塞进TCP里,忘了去掉CRC。结果设备一直返回异常码。查了半天才发现,接收方把CRC当成了数据的一部分。记住:Modbus TCP没有CRC,别画蛇添足。

4.4 实际组网案例:一个典型的SCADA系统

我拿一个实际项目来说吧。去年我帮一个水处理厂做自动化改造,现场是这样的:

  • 1台PLC作为Modbus TCP客户端(主站)
  • 3台变频器,每台都支持Modbus TCP
  • 1台智能电表,只支持Modbus RTU
  • 1台Modbus TCP转RTU网关

组网结构很简单:PLC通过交换机连接3台变频器和网关。网关下面再接电表。

PLC读取变频器数据时,直接发Modbus TCP请求。比如读变频器1的当前频率:

请求报文(十六进制):
00 01 00 00 00 06 01 03 00 00 00 01

解释:
00 01 - 事务处理标识符(第1次请求)
00 00 - 协议标识符(Modbus)
00 06 - 长度(后面6个字节)
01    - 单元标识符(变频器1)
03    - 功能码(读保持寄存器)
00 00 - 起始地址
00 01 - 读取数量(1个寄存器)

变频器1的响应:

响应报文:
00 01 00 00 00 05 01 03 02 0F A0

解释:
00 01 - 事务处理标识符(和请求匹配)
00 00 - 协议标识符
00 05 - 长度(后面5个字节)
01    - 单元标识符
03    - 功能码
02    - 数据字节数(2个字节)
0F A0 - 频率值(4000,表示40.00Hz)

那台电表是RTU的,PLC怎么读?通过网关。PLC发给网关的TCP报文里,单元标识符设为电表的RTU地址。网关收到后,剥掉MBAP头,加上CRC,转成RTU报文发给电表。电表响应后,网关再封装成TCP报文返回给PLC。

关键点:在这个组网里,PLC不需要知道电表是RTU还是TCP。它只需要知道单元标识符对应哪个设备。网关把底层的差异全部屏蔽掉了。这就是Modbus TCP设计的巧妙之处——上层应用完全不用关心物理层是什么。

嗯,说到这儿,我想起一个细节。当时调试的时候,变频器2总是偶尔不响应。我抓包一看,发现PLC发的请求里事务处理标识符重复了。变频器2的固件有个bug,收到相同的事务处理标识符会直接丢弃。解决办法很简单:每次请求递增事务处理标识符,不要复用。

所以你看,Modbus TCP虽然简单,但实际用起来还是有不少门道的。尤其是事务处理标识符的管理,我建议用一个循环递增的计数器,不要用随机数,也不要复用得太频繁。65535个值循环一遍,对于大多数应用场景完全够用了。