2. Modbus协议基础:Modbus RTU与TCP帧结构、功能码详解(03/06/16)、地址映射规则
好,咱们正式开始啃Modbus这块硬骨头。说实话,做工业通信网关这么多年,Modbus是我打交道最多的协议,没有之一。你想想看,从PLC到变频器,从温控表到智能电表,几乎每个设备都挂着Modbus接口。今天我就把Modbus RTU和TCP的底裤扒开,让你看得明明白白。
2.1 Modbus RTU帧结构:串口上的“一问一答”
Modbus RTU跑在串口上,RS-232或者RS-485。它的帧结构其实很简单,我习惯把它拆成四段来看:
- 地址域(1字节):从站地址,范围1-247。0是广播地址,248-255保留。
- 功能码(1字节):告诉从站要干什么。读还是写?读线圈还是读寄存器?
- 数据域(N字节):具体参数,比如起始地址、数据个数、要写入的值。
- 校验域(2字节):CRC16校验,保证数据在传输过程中没被干扰。
举个例子,主站要读取从站地址为1的设备,从寄存器地址0x0000开始读2个寄存器:
请求帧:01 03 00 00 00 02 C4 0B
| | | | CRC16
地址 功能码 起始地址 寄存器数量
从站正常响应:
响应帧:01 03 04 00 0A 00 14 3A 0C
| | | | | CRC16
地址 功能码 字节数 寄存器值
嗯,这里要注意:RTU帧里没有帧头帧尾标记,完全靠时间间隔来区分一帧数据。3.5个字符时间的静默表示帧开始,1.5个字符时间表示字节间隔。我在项目中遇到过,有些国产设备对时间要求不严格,导致帧粘连,调试时折腾了我一整天。
2.2 Modbus TCP帧结构:以太网上的“快车道”
Modbus TCP说白了就是把RTU的帧封装到TCP包里,去掉了CRC校验(因为TCP/IP协议栈自己会做校验),加了一个MBAP头。我个人觉得,TCP版本比RTU省心多了,至少不用操心串口参数。
MBAP头共7个字节:
| 字段 | 长度 | 说明 |
|---|---|---|
| 事务处理标识符 | 2字节 | 用于匹配请求和响应,一般从0开始递增 |
| 协议标识符 | 2字节 | 固定为0x0000,表示Modbus协议 |
| 长度 | 2字节 | 后续数据的字节数(从单元标识符开始算) |
| 单元标识符 | 1字节 | 相当于RTU里的从站地址 |
同样的读寄存器操作,TCP帧长这样:
请求帧:00 01 00 00 00 06 01 03 00 00 00 02
| | | | | | |
事务ID 协议ID 长度 单元ID 功能码 起始地址 数量
你发现没有?TCP帧里没有CRC,长度字段代替了RTU里的时间间隔判断。说白了,TCP帧是“自描述”的,收到多少字节就是多少字节,不用猜。
2.3 功能码详解:03、06、16,这三个够用了
Modbus功能码有几十个,但实际项目中,我个人觉得掌握03、06、16这三个就能搞定90%的场景。为什么?因为大部分工业设备的数据都放在保持寄存器里,而这三个功能码就是专门操作保持寄存器的。
功能码03:读保持寄存器
这个最常用。读取从站中连续的多个保持寄存器,每个寄存器16位。请求时指定起始地址和数量,响应时返回对应的寄存器值。
举个例子,读取地址0x0100开始的3个寄存器:
请求:01 03 01 00 00 03 45 C6
响应:01 03 06 00 01 00 02 00 03 3A 0C
| | | | | |
地址 功能码 字节数 寄存器1 寄存器2 寄存器3
功能码06:写单个寄存器
这个简单,就是往一个寄存器里写一个16位的值。请求帧里包含寄存器地址和要写入的值,从站响应时原样返回请求帧表示成功。
请求:01 06 00 01 00 0A 18 0A
| | | | CRC
地址 功能码 地址 值(10)
响应:01 06 00 01 00 0A 18 0A (原样返回)
嗯,这里要注意:06功能码只能写一个寄存器。如果你想一次写多个,就得用16功能码。
功能码16:写多个寄存器
这个比06复杂一点,但效率高。请求帧里要指定起始地址、寄存器数量、字节数,然后跟上要写入的数据。
请求:01 10 00 01 00 02 04 00 0A 00 14 3A 0C
| | | | | | |
地址 功能码 起始地址 数量 字节数 值1(10) 值2(20)
响应:01 10 00 01 00 02 10 08
| | | | CRC
地址 功能码 起始地址 数量
响应帧只返回起始地址和数量,不返回具体值。这是协议规定的,别指望从站会回传写入的值。
2.4 地址映射规则:别被“地址偏移”坑了
地址映射是Modbus协议里最容易出问题的地方。我见过太多工程师在这里翻车了。说白了,Modbus地址有四种表示方式:
- 物理地址:协议帧里实际传输的地址,从0x0000开始。
- 逻辑地址:PLC或HMI里看到的地址,比如40001、30001这种。
- 功能码关联地址:不同功能码对应不同的地址空间,比如03功能码对应4xxxx(保持寄存器),01功能码对应0xxxx(线圈)。
- 设备内部地址:设备手册里写的地址,可能和协议地址差一个偏移量。
举个例子,你看到HMI上显示“40001”,这个地址对应的是:
- 功能码:03(读保持寄存器)或06/16(写保持寄存器)
- 协议地址:0x0000(因为40001 - 40001 = 0)
- 设备内部地址:可能是0x0000,也可能是0x1000,看设备厂家怎么定义
我习惯的做法是:先看设备手册,找到“Modbus地址映射表”,搞清楚协议地址和设备内部地址的对应关系。然后写代码时,统一用协议地址,不做任何偏移。这样最不容易出错。
好了,Modbus协议基础就讲到这里。下一节我们会讲CANopen协议,到时候你会看到,这两种协议虽然都是工业通信,但设计思路完全不同。嗯,做好准备,CANopen比Modbus复杂多了。