第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协议的核心脉络串起来了。从物理层到应用层,从帧结构到地址映射,一目了然。
4.5 异常码与调试技巧
通信不可能永远一帆风顺。Modbus协议定义了异常响应机制:当从站收到非法请求时,会把功能码的最高位置1,然后返回一个异常码。
| 异常码 | 名称 | 常见原因 |
|---|---|---|
| 01 | 非法功能码 | 从站不支持该功能码 |
| 02 | 非法数据地址 | 地址超出范围 |
| 03 | 非法数据值 | 数据值超出允许范围 |
| 04 | 从站设备故障 | 硬件或固件错误 |
| 06 | 从站忙 | 正在处理其他任务,稍后重试 |
调试三板斧:
- 抓原始报文:用Wireshark(TCP)或串口监视器(RTU)看实际收发的字节流。别信代码里的打印日志,有时候是代码自己拼错了。
- 检查CRC:RTU通信90%的问题出在CRC计算上。网上有在线CRC计算器,把报文贴进去算一下,对比就知道对不对。
- 确认地址偏移:PLC手册里写的40001,协议里到底传0x0000还是0x0001?拿功能码03读一下,看返回的数据对不对得上。
嗯,Modbus协议的原理就这些。说白了,它不复杂,但细节特别多。尤其是地址映射和字节序,每个厂家都有自己的“小九九”。做网关集成时,一定要先拿到设备手册的Modbus地址表,对着表写代码,别自己猜。