第4章 Modbus协议原理:RTU与TCP帧结构、功能码详解、地址映射规则

Modbus这玩意儿,说实话,是工控圈子里最老牌、也最顽强的协议之一。我入行那会儿,几乎所有PLC都标配它。到现在做工业物联网网关,Modbus依然是绕不开的坎儿。你想想看,一个1979年诞生的协议,四十多年了还在大量使用,一定有它的道理——简单、稳定、开放。

这一章,咱们就把Modbus的底裤扒干净。从帧结构到功能码,再到地址映射,全是实战中天天要打交道的硬知识。

4.1 Modbus的两种“方言”:RTU vs TCP

Modbus其实分好几种变体,但最常用的就两个:RTU(串行通信)和TCP(以太网通信)。它们的数据模型完全一样,但打包方式不同。

核心区别一句话:RTU用CRC校验,TCP用TCP/IP协议栈保证可靠传输。RTU没有IP头,TCP没有CRC。

4.1.1 Modbus RTU帧结构

RTU帧是二进制格式,紧凑、高效。我最早用RS485接变频器时,就是跟这玩意儿打交道。帧结构如下:

字段 长度(字节) 说明
地址码 1 从站地址(1-247),0为广播
功能码 1 操作类型(读/写/诊断等)
数据区 N 寄存器地址、数量、数据值
CRC校验 2 循环冗余校验(低字节在前)

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

请求帧:01 03 00 00 00 02 C4 0B
        01  → 从站地址
        03  → 读保持寄存器
        00 00 → 起始地址
        00 02 → 读取数量
        C4 0B → CRC校验

我的习惯:调试RTU通信时,先用串口助手看原始字节流。CRC算不对是新手最常见的坑。我曾经花了一下午,最后发现是高低字节顺序搞反了——RTU的CRC是低字节在前。

4.1.2 Modbus TCP帧结构

TCP版本去掉了CRC,加了个MBAP头(Modbus Application Protocol Header)。因为TCP/IP协议栈自己会做校验,没必要重复。

字段 长度(字节) 说明
事务标识符 2 用于匹配请求和响应
协议标识符 2 固定为0x0000(Modbus协议)
长度 2 后续字节数(从单元标识符开始算)
单元标识符 1 相当于RTU的地址码
功能码 1 同RTU
数据区 N 同RTU

同样的请求,TCP版本长这样:

请求帧:00 01 00 00 00 06 01 03 00 00 00 02
        00 01 → 事务ID(随便起,能对上就行)
        00 00 → 协议ID(固定)
        00 06 → 长度(后面还有6个字节)
        01    → 单元ID(相当于从站地址)
        03    → 功能码
        00 00 → 起始地址
        00 02 → 读取数量

注意:TCP帧里的事务ID必须和响应帧一致。我见过有人用多线程并发请求时,事务ID重复了,结果响应数据全乱套。建议每个请求用递增计数器生成唯一ID。

4.2 功能码详解——Modbus的“指令集”

功能码就是告诉从站:你要干什么。Modbus协议定义了三大类功能码:公共功能码、用户定义功能码和保留功能码。咱们只聊最常用的那几个。

4.2.1 位操作功能码

功能码 名称 操作对象 说明
01 (0x01) 读线圈 DO(数字量输出) 读取0/1状态
02 (0x02) 读离散输入 DI(数字量输入) 只读,不可写
05 (0x05) 写单个线圈 DO 置位或复位
15 (0x0F) 写多个线圈 DO 批量操作

4.2.2 字操作功能码

功能码 名称 操作对象 说明
03 (0x03) 读保持寄存器 AO(模拟量输出/参数) 最常用,可读可写
04 (0x04) 读输入寄存器 AI(模拟量输入) 只读,传感器数据
06 (0x06) 写单个寄存器 AO 写一个16位值
16 (0x10) 写多个寄存器 AO 批量写,效率高

实战经验:功能码03和04最容易混淆。记住:03对应的是可读可写的参数(比如设定值、PID参数),04对应的是只读的测量值(比如温度、压力)。我见过有人用04去写参数,结果从站直接返回异常码02(非法数据地址)。

4.3 地址映射规则——PLC里的“门牌号”

Modbus的地址空间是分区的。每个区对应不同的数据类型。你想想看,PLC里既有开关量,又有模拟量,还有各种参数,总得有个规矩来区分吧?

4.3.1 标准地址分区

地址范围(PLC习惯) Modbus协议地址 数据类型 功能码
00001 - 09999 0x0000 - 0x270E 线圈(DO) 01, 05, 15
10001 - 19999 0x0000 - 0x270E 离散输入(DI) 02
30001 - 39999 0x0000 - 0x270E 输入寄存器(AI) 04
40001 - 49999 0x0000 - 0x270E 保持寄存器(AO) 03, 06, 16

这里有个大坑:PLC习惯用“40001”这种5位十进制数表示地址,但Modbus协议里实际传的是偏移量(0x0000)。也就是说,PLC里的40001,在协议帧里地址是0x0000;40002对应0x0001,以此类推。

我曾经踩过的坑:给一个西门子S7-200写程序,PLC里配的地址是40001,我直接在代码里写地址0x0000,结果死活读不到数据。后来发现,有些PLC的地址映射是从1开始的,而协议里是从0开始的。差1都不行。所以,一定要看设备手册的地址映射表,别想当然。

4.3.2 地址映射的“潜规则”

实际项目中,地址映射往往没那么规整。我总结了几条经验:

  • 保持寄存器最常用:90%的数据都映射在40001-4xxxx区间。不管是温度、压力还是转速,全塞这里。
  • 32位数据占两个寄存器:比如一个32位浮点数,需要连续两个16位寄存器。字节序(大端/小端)和字序(高字在前/低字在前)各厂家不一样。ABB和西门子就经常不一样。
  • 位打包:有些设备把多个状态位打包在一个寄存器里。比如寄存器40010的bit0表示运行,bit1表示故障。这时候要用位掩码来解析。

我的建议:做网关地址映射时,先画一张地址映射表。列清楚:PLC地址、协议偏移量、数据类型、缩放系数、字节序。这张表就是你的“通信宪法”,后面所有代码都按它来写。

4.4 知识结构总览

下面这张图,把Modbus协议的核心脉络串起来了。从物理层到应用层,从帧结构到地址映射,一目了然。

Modbus协议知识结构图 Modbus RTU(串行) Modbus TCP(以太网) RTU帧结构 地址码(1B) | 功能码(1B) | 数据区(NB) | CRC(2B) 二进制紧凑格式,CRC校验 TCP帧结构 MBAP头(7B) | 功能码(1B) | 数据区(NB) 无CRC,依赖TCP/IP校验 位操作功能码 01读线圈 | 02读离散输入 | 05写单线圈 | 15写多线圈 操作对象:DO/DI(数字量) 字操作功能码 03读保持寄存器 | 04读输入寄存器 06写单寄存器 | 16写多寄存器 地址映射规则 00001-09999: 线圈(DO) | 10001-19999: 离散输入(DI) 30001-39999: 输入寄存器(AI) | 40001-49999: 保持寄存器(AO) ⚠ PLC地址与协议偏移量差1,32位数据需跨寄存器组合

4.5 异常码与调试技巧

通信不可能永远一帆风顺。Modbus协议定义了异常响应机制:当从站收到非法请求时,会把功能码的最高位置1,然后返回一个异常码。

异常码 名称 常见原因
01 非法功能码 从站不支持该功能码
02 非法数据地址 地址超出范围
03 非法数据值 数据值超出允许范围
04 从站设备故障 硬件或固件错误
06 从站忙 正在处理其他任务,稍后重试

调试三板斧:

  1. 抓原始报文:用Wireshark(TCP)或串口监视器(RTU)看实际收发的字节流。别信代码里的打印日志,有时候是代码自己拼错了。
  2. 检查CRC:RTU通信90%的问题出在CRC计算上。网上有在线CRC计算器,把报文贴进去算一下,对比就知道对不对。
  3. 确认地址偏移:PLC手册里写的40001,协议里到底传0x0000还是0x0001?拿功能码03读一下,看返回的数据对不对得上。

嗯,Modbus协议的原理就这些。说白了,它不复杂,但细节特别多。尤其是地址映射和字节序,每个厂家都有自己的“小九九”。做网关集成时,一定要先拿到设备手册的Modbus地址表,对着表写代码,别自己猜。