4、Modbus协议深度解析:Modbus RTU与TCP帧结构、功能码详解、地址映射规则

各位同学,咱们今天来啃一块硬骨头——Modbus协议。说实话,这玩意儿在工业现场太常见了,几乎每个做自动化的人都会碰到。但很多人只是会用,真问到帧结构里每个字节是干嘛的,就说不清了。

我当年刚入行时也吃过亏。有一次调试一个变频器,怎么都读不到数据,折腾了一下午。最后发现是RTU帧的CRC校验算错了。嗯,从那以后,我就把Modbus协议从头到尾捋了一遍。今天把这些经验分享给你们。

4.1 Modbus协议家族:RTU vs TCP

Modbus协议主要有两种形态:RTU和TCP。它们的关系,说白了就是同一套“语言”跑在不同的“传输通道”上。

核心区别一句话:RTU走串口(RS-232/485),TCP走以太网。RTU有CRC校验,TCP没有(因为TCP/IP协议栈自己会做校验)。

咱们先看一张对比表,把两者的差异搞清楚:

对比项 Modbus RTU Modbus TCP
物理层 RS-232 / RS-485 以太网 (RJ45)
传输方式 串行异步,半双工 TCP/IP,全双工
帧格式 地址 + 功能码 + 数据 + CRC MBAP头 + 功能码 + 数据
地址域 1字节,0为广播,1-247为从站 单元标识符(1字节),通常填0xFF
校验方式 16位CRC校验 无应用层校验(依赖TCP)
典型波特率 9600 / 19200 / 115200 bps 10/100/1000 Mbps
最大从站数 32个(RS-485无中继) 理论上无限制

我个人习惯是:如果现场有现成的以太网,优先用TCP,调试方便。但很多老旧设备只有串口,那就必须用RTU。咱们做网关转换时,经常要把RTU转成TCP,或者反过来。

4.2 帧结构深度拆解

4.2.1 Modbus RTU帧结构

RTU的帧结构,我建议你死记硬背下来。因为调试时经常要对着报文看:

| 地址域(1B) | 功能码(1B) | 数据域(NB) | CRC校验(2B) |

举个例子,读取从站1的保持寄存器,起始地址0x0000,读取2个寄存器:

请求帧:01 03 00 00 00 02 C4 0B
响应帧:01 03 04 00 0A 00 14 3A 1B

咱们逐字节拆解:

  • 01:从站地址,表示1号从站
  • 03:功能码,读取保持寄存器
  • 00 00:起始地址,高字节在前(大端序)
  • 00 02:读取数量,2个寄存器
  • C4 0B:CRC校验,低字节在前

响应帧里:

  • 01:从站地址回显
  • 03:功能码回显
  • 04:数据字节数,4个字节(2个寄存器×2字节)
  • 00 0A:第一个寄存器值,十进制10
  • 00 14:第二个寄存器值,十进制20
  • 3A 1B:CRC校验

实战小技巧:调试RTU时,我习惯先用串口助手抓报文,看CRC对不对。很多问题都是CRC计算错误导致的。Linux下可以用 modbus-rtu 工具加 -d 参数打印原始报文。

4.2.2 Modbus TCP帧结构

TCP帧比RTU多了一个MBAP头(Modbus Application Protocol Header),共7个字节:

| 事务标识(2B) | 协议标识(2B) | 长度(2B) | 单元标识(1B) | 功能码(1B) | 数据域(NB) |

同样的读取操作,TCP请求帧长这样:

00 01 00 00 00 06 01 03 00 00 00 02

拆解一下:

  • 00 01:事务标识,用于匹配请求和响应
  • 00 00:协议标识,固定为0(Modbus协议)
  • 00 06:后续字节长度,6个字节(单元标识+功能码+数据)
  • 01:单元标识符,相当于RTU的从站地址
  • 03:功能码
  • 00 00:起始地址
  • 00 02:读取数量

注意:TCP帧的“长度”字段不包括MBAP头自身,只算后面的数据。这个坑我踩过——有一次写网关程序,长度算多了2个字节,结果对端一直报错。查了半天才发现是长度字段包含了单元标识符。

4.3 功能码详解

功能码是Modbus协议的核心,告诉从站你要干什么。常用的就那么几个,我列个表:

功能码 名称 操作对象 读写方向
01 (0x01) 读取线圈状态 DO(数字量输出) 只读
02 (0x02) 读取离散输入 DI(数字量输入) 只读
03 (0x03) 读取保持寄存器 AO(模拟量输出/参数) 只读
04 (0x04) 读取输入寄存器 AI(模拟量输入) 只读
05 (0x05) 写单个线圈 DO 只写
06 (0x06) 写单个寄存器 AO 只写
15 (0x0F) 写多个线圈 DO 只写
16 (0x10) 写多个寄存器 AO 只写

这里有个容易混淆的点:保持寄存器和输入寄存器的区别。保持寄存器可读可写,一般存参数、设定值;输入寄存器只读,存传感器采集的数值。我在项目中见过有人用03功能码去读输入寄存器,结果读回来的全是0——因为地址空间根本不对。

4.4 地址映射规则

地址映射是Modbus转PROFINET时最头疼的部分。Modbus的地址是“平面”的,而PROFINET的GSD文件里定义的是“结构化”的数据块。怎么把两者对应起来?

先看Modbus的地址空间划分:

地址范围 数据类型 对应功能码 PLC中常见表示
00001 - 09999 线圈(DO) 01, 05, 15 Q区
10001 - 19999 离散输入(DI) 02 I区
30001 - 39999 输入寄存器(AI) 04 PIW
40001 - 49999 保持寄存器(AO) 03, 06, 16 PQW / DB

注意,Modbus地址是从1开始的,但协议报文里的地址是从0开始的。比如你要读40001,报文里填的地址是0x0000。这个偏移量很多人会搞错。

地址映射的核心原则:在网关配置时,把Modbus的“地址+功能码”映射到PROFINET的“槽号+子槽号+字节偏移”。说白了,就是告诉网关:Modbus的哪个数据,放到PROFINET的哪个位置。

举个例子,我在一个项目中这样配:

  • Modbus保持寄存器40001-40010(10个寄存器,20字节)→ PROFINET输入数据槽1,偏移0
  • Modbus线圈00001-00016(16个位,2字节)→ PROFINET输出数据槽2,偏移0

这样PLC侧读PROFINET输入数据时,就能直接拿到Modbus从站的模拟量值;写PROFINET输出数据时,就能控制Modbus从站的数字量输出。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了,建议你保存下来对照着看:

Modbus协议知识体系 Modbus RTU Modbus TCP RTU帧结构 地址(1B) + 功能码(1B) + 数据(NB) + CRC(2B) TCP帧结构 MBAP头(7B) + 功能码(1B) + 数据(NB) 常用功能码 01读线圈 | 02读离散输入 | 03读保持寄存器 | 04读输入寄存器 05写单线圈 | 06写单寄存器 | 15写多线圈 | 16写多寄存器 地址映射规则(Modbus → PROFINET) 00001-09999 线圈 → PROFINET 数字量输出 30001-39999 输入寄存器 → PROFINET 模拟量输入 40001-49999 保持寄存器 → PROFINET 模拟量输出/参数

4.6 实战中的避坑指南

最后,分享几个我在项目中踩过的坑:

坑1:CRC校验字节序搞反

我曾经在写一个RTU转TCP的网关程序时,CRC算对了,但低字节和高字节放反了。从站一直返回异常码0x03(非法数据值)。后来用串口对比工具才发现,CRC应该是 C4 0B,我写成了 0B C4。记住:RTU的CRC是低字节在前。

坑2:TCP的单元标识符乱填

有些Modbus TCP从站不检查单元标识符,填什么都行。但有些会严格校验。我遇到过一台温控器,单元标识符必须填0x01,填0xFF就报错。所以做网关时,最好把这个字段做成可配置的。

调试利器:Linux下用 modpoll 和 mbpoll 工具,可以快速测试Modbus设备。加 -v 参数能看到原始报文,比用Windows的串口助手方便多了。