3. Modbus TCP协议详解:报文结构、功能码、寻址方式

好,咱们今天来聊聊Modbus TCP。说实话,在工业以太网协议里,Modbus TCP算是最「亲民」的一个了。我最早接触它是在一个水处理项目上,当时PLC和上位机要通讯,老工程师直接甩给我一句话:「用Modbus TCP,简单得很。」后来我才明白,他说的「简单」背后,其实藏着不少门道。

3.1 报文结构:拆开看看里面长啥样

Modbus TCP的报文结构,说白了就是「MBAP头 + 数据帧」。嗯,就这么简单。但简单归简单,你得知道每一块是干嘛的。

先看MBAP头,全称是Modbus Application Protocol Header。它一共7个字节,我习惯把它拆成四部分:

字段 长度 说明
事务处理标识符 2字节 用于匹配请求和响应,我一般从0开始递增
协议标识符 2字节 固定为0x0000,表示Modbus协议
长度 2字节 后续字节数,包括单元标识符
单元标识符 1字节 相当于设备地址,范围0-255

这里有个坑,我当年踩过。事务处理标识符,很多人直接填0,结果多设备并发时全乱套了。我个人习惯是每个请求递增1,这样响应回来我能知道是哪个请求的回复。你想想看,要是同时发了10个请求,回来一堆0,你分得清谁是谁吗?

MBAP头后面跟着的就是PDU(Protocol Data Unit)。PDU的结构更简单:功能码 + 数据。功能码1个字节,数据长度可变。

完整的Modbus TCP报文 = MBAP头(7字节) + 功能码(1字节) + 数据(N字节)

3.2 功能码:你让设备干啥

功能码就是指令。你告诉设备:「我要读线圈」或者「我要写寄存器」。Modbus TCP支持的功能码和串口Modbus完全一样,只是传输层换成了TCP/IP。

常用的功能码就这几个,我列个表给你看:

功能码 名称 作用
0x01 读线圈 读取DO(数字量输出)状态
0x02 读离散输入 读取DI(数字量输入)状态
0x03 读保持寄存器 读取AO(模拟量输出)或参数
0x04 读输入寄存器 读取AI(模拟量输入)值
0x05 写单个线圈 控制单个DO
0x06 写单个寄存器 设置单个AO或参数
0x0F 写多个线圈 批量控制DO
0x10 写多个寄存器 批量设置AO或参数

我记得有一次,现场工程师跟我说设备没反应。我一看代码,他用功能码0x03去读数字量输入。这能读到才怪呢!数字量输入得用0x02。所以啊,功能码别搞混,每个码对应什么数据类型,心里要有数。

小技巧:调试时先用Modbus Poll或ModScan这类工具发一下功能码,看看设备怎么回。设备正常响应了,再写代码。这能省你半天排查时间。

3.3 寻址方式:数据在哪儿

Modbus的寻址,说白了就是「起始地址 + 数量」。你告诉设备:「从地址100开始,读5个寄存器。」设备就把这5个寄存器的值打包发给你。

但这里有个容易混淆的地方——地址编号。Modbus协议里,地址是从0开始的。比如保持寄存器的地址范围是0x0000到0xFFFF。但很多设备厂商的说明书上,地址是从1开始的。比如说明书上写「寄存器40001」,对应协议里的地址就是0x0000。

为什么会这样?因为早期Modbus用PLC的地址表示法,40001代表「4」开头的数据区(保持寄存器),后面「0001」是序号。但在协议报文里,你得减去偏移量。我见过不少新手直接拿40001去发报文,结果设备根本不搭理。

举个例子,你要读保持寄存器地址40001:

// 正确的报文
MBAP头: 0x0001 0x0000 0x0006 0x01
功能码: 0x03
起始地址: 0x0000  // 注意这里是0,不是40001
寄存器数量: 0x0001

再比如,你要读输入寄存器地址30001:

// 正确的报文
MBAP头: 0x0002 0x0000 0x0006 0x01
功能码: 0x04
起始地址: 0x0000  // 30001对应协议地址0
寄存器数量: 0x0001

注意:我曾经在一个项目中,设备说明书上写「寄存器地址从1开始」,但实际报文里地址是从0开始的。后来发现是设备固件版本不同导致的。所以拿到新设备,先用工具扫一遍地址范围,确认偏移量。

3.4 实际报文示例:看个完整的

咱们来看一个完整的请求-响应过程。假设我要读设备地址为1的保持寄存器,从地址100开始读3个寄存器:

请求报文:

事务标识符: 0x0001
协议标识符: 0x0000
长度: 0x0006
单元标识符: 0x01
功能码: 0x03
起始地址: 0x0064  // 100的十六进制
寄存器数量: 0x0003

完整报文: 00 01 00 00 00 06 01 03 00 64 00 03

响应报文:

事务标识符: 0x0001
协议标识符: 0x0000
长度: 0x0009
单元标识符: 0x01
功能码: 0x03
数据长度: 0x06  // 3个寄存器,每个2字节
数据: 0x0012 0x0034 0x0056

完整报文: 00 01 00 00 00 09 01 03 06 00 12 00 34 00 56

你看,响应里的事务标识符和请求是一样的,这样你就能把请求和响应配对。我习惯在代码里维护一个请求队列,每个请求带一个唯一ID,响应回来就根据ID找到对应的请求,处理数据。

3.5 避坑指南:我踩过的那些坑

做Modbus TCP开发,有几个坑我印象特别深:

  • 字节序问题:Modbus默认是大端模式(Big-Endian),高字节在前。但有些设备厂商自作聪明用了小端。我遇到过一台温控器,读回来的温度值高低字节是反的,折腾了我一上午。
  • 超时设置:TCP连接本身有超时,但Modbus请求也有超时。我一般设500ms,如果设备响应慢,可以适当延长。但别设太长,否则故障时系统会卡死。
  • 连接管理:Modbus TCP默认使用502端口。每个设备一个连接,别复用。我见过有人用一个连接轮询多个设备,结果设备多了就丢包。
  • 异常响应:设备返回异常码时,功能码的最高位会被置1。比如你发0x03,设备返回0x83,后面跟一个异常码。常见的异常码有0x01(非法功能码)、0x02(非法数据地址)、0x03(非法数据值)。

调试建议:先用Wireshark抓包看看报文对不对。Wireshark能自动解析Modbus TCP,一眼就能看出问题。我每次调试新设备,第一步就是抓包确认报文格式。

好了,Modbus TCP的核心内容就这些。报文结构、功能码、寻址方式,这三样搞明白了,剩下的就是多练。下一章咱们聊聊EtherNet/IP,那个协议比Modbus TCP复杂不少,但功能也更强大。到时候我给你们讲讲我在汽车生产线上的实战经验。