2、Modbus TCP协议详解:报文结构、功能码分类、TCP/IP封装、典型通信流程

好,咱们今天来聊聊Modbus TCP。说实话,这个协议在工业现场太常见了。我做了这么多年自动化,几乎每个项目都离不开它。你想想看,从PLC到变频器,从仪表到机器人,大家都在用Modbus TCP通信。为什么?因为它简单、可靠、容易上手。

但简单归简单,真要把它用好,还是得把底层的东西吃透。我记得刚入行那会儿,就因为在报文结构上栽了个跟头,调试了整整两天。嗯,咱们今天就把它彻底讲明白。

2.1 Modbus TCP报文结构

Modbus TCP的报文,说白了就是在标准Modbus串口协议外面包了一层TCP/IP的外衣。它的报文结构分为两部分:MBAP报文头协议数据单元(PDU)

咱们先看MBAP头。这个头一共7个字节,结构如下:

字段 长度(字节) 描述
事务处理标识符 2 用于匹配请求和响应,一般由客户端生成
协议标识符 2 固定为0x0000,表示Modbus协议
长度 2 后续字节的总长度(从单元标识符开始算)
单元标识符 1 相当于串口协议中的从站地址

这里我要特别说一下事务处理标识符。我在项目中遇到过有人把它设成固定值,结果多客户端同时访问时,响应报文全乱套了。我个人习惯是用一个递增计数器来生成这个值,这样每个请求都有唯一标识,绝对不会搞混。

关键点:MBAP头中的长度字段,只包含单元标识符+PDU的长度,不包含MBAP头本身的7个字节。这个坑我踩过,大家注意。

PDU部分就简单了,它包含:

  • 功能码(1字节):告诉设备要做什么操作
  • 数据(N字节):具体的操作参数或数据

2.2 功能码分类

功能码是Modbus协议的核心。我把它分成三大类,这样好记:

2.2.1 位操作类

功能码 名称 说明
0x01 读线圈 读取DO(数字量输出)状态
0x02 读离散输入 读取DI(数字量输入)状态
0x05 写单个线圈 控制单个DO输出
0x0F 写多个线圈 批量控制DO输出

2.2.2 寄存器操作类

功能码 名称 说明
0x03 读保持寄存器 最常用,读取模拟量或参数
0x04 读输入寄存器 读取AI(模拟量输入)值
0x06 写单个寄存器 设置单个参数
0x10 写多个寄存器 批量设置参数

2.2.3 诊断与特殊类

  • 0x07:读取异常状态
  • 0x08:诊断(回环测试等)
  • 0x11:读取事件计数
  • 0x17:读/写多个寄存器(一次搞定读写)

我的经验:实际项目中,80%以上的通信都在用0x03和0x06这两个功能码。读保持寄存器、写单个寄存器,这两个搞明白了,基本就能应付大多数场景。

2.3 TCP/IP封装过程

Modbus TCP怎么在网络上跑?说白了就是层层打包。我画个简单的流程:

应用层:  [MBAP头] + [功能码] + [数据]
传输层:  TCP头 + [上述数据]
网络层:  IP头 + [上述数据]
链路层:  以太网帧头 + [上述数据] + 帧尾

这里有个关键点:Modbus TCP默认使用端口502。为什么是502?嗯,历史原因,这个端口是IANA专门分配给Modbus协议的。我在项目中见过有人改成其他端口,结果防火墙策略没配好,通信死活不通。所以,除非你有特殊需求,否则别改端口。

注意:Modbus TCP没有串口协议中的CRC校验。为什么?因为TCP/IP协议栈本身已经提供了可靠的传输和校验。如果你在Modbus TCP报文里还加CRC,那就是画蛇添足了。我曾经见过一个外包团队这么干,结果两边对不上,折腾了好几天。

2.4 典型通信流程

咱们用一个实际例子来说明。假设我要读取一台变频器的当前频率,地址是40001(保持寄存器)。

步骤1:客户端发送请求

事务标识符:0x0001
协议标识符:0x0000
长度:0x0006
单元标识符:0x01
功能码:0x03
起始地址:0x0000(对应40001)
寄存器数量:0x0001

注意看,这里的起始地址是0x0000,对应的是40001。为什么?因为Modbus的地址映射规则是:40001对应地址0,40002对应地址1,以此类推。这个映射关系我刚开始也搞混过,后来养成了习惯,每次都在代码里写清楚注释。

步骤2:服务器响应

事务标识符:0x0001(和请求一致)
协议标识符:0x0000
长度:0x0005
单元标识符:0x01
功能码:0x03
数据长度:0x02
数据:0x0BB8(假设频率是30.00Hz,对应3000)

这里有个细节:响应报文的事务标识符必须和请求一致。如果对不上,客户端应该丢弃这个报文。我在调试时遇到过设备固件有bug,事务标识符总是返回0,结果客户端一直报超时。

步骤3:异常处理

如果请求出错,服务器会返回异常响应:

功能码:0x83(0x03 + 0x80,最高位置1)
异常码:0x02(非法数据地址)

常见的异常码有:

  • 0x01:非法功能码
  • 0x02:非法数据地址
  • 0x03:非法数据值
  • 0x04:从站设备故障

避坑指南:我曾经遇到一个项目,设备返回异常码0x04,我以为是设备坏了,换了三台都不行。后来才发现,是请求的寄存器数量超过了设备支持的最大值。所以,收到异常码时,先检查自己的请求参数,别急着怀疑设备。

2.5 实际部署建议

最后,给大家几个我在项目中总结的经验:

  1. 超时设置:Modbus TCP的默认超时一般是1秒。但如果网络环境差,或者设备响应慢,建议调到3-5秒。我一般设3秒,重试3次。
  2. 连接管理:不要每个请求都新建TCP连接。建立连接的开销很大,建议用长连接。我习惯用一个连接池来管理。
  3. 报文间隔:虽然TCP没有像串口那样的帧间隔要求,但建议请求之间留10-20ms的间隔,给设备一点处理时间。
  4. 字节序:Modbus默认是大端模式(Big-Endian)。如果你用的是小端模式的系统(比如x86),记得做字节序转换。这个坑我踩过不止一次。

好了,Modbus TCP的核心内容就这些。说白了,它就是一套约定好的数据交换规则。你只要把报文结构、功能码、封装流程搞清楚了,剩下的就是多练。下一章咱们聊Modbus RTU,那个和TCP有些不同,但核心思想是一样的。