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个值循环一遍,对于大多数应用场景完全够用了。