4、PLC与SCADA的通信协议(上):Modbus RTU/TCP协议详解——报文结构、功能码、地址映射
各位同学,今天我们来聊聊工业通信里最经典、也最绕不开的一个协议——Modbus。说实话,我入行那会儿,第一个接触的协议就是它。那时候师傅丢给我一本英文手册,说“把这个啃下来,你就算入门了”。现在回想起来,Modbus确实是个很好的起点,结构简单,逻辑清晰,但坑也不少。
咱们分两节课来讲。这节课先搞定Modbus RTU和Modbus TCP的核心内容:报文长什么样、功能码怎么用、地址怎么映射。下节课再聊一些实战中的疑难杂症。
4.1 Modbus的前世今生
Modbus是1979年由Modicon公司(就是现在的施耐德电气)发明的。你想想看,快50年的老协议了,现在还在大量使用,说明什么?说明它足够简单、足够稳定。
Modbus主要有两种变体:
- Modbus RTU:串行通信,RS-232/RS-485物理层。数据用二进制传输,效率高。
- Modbus TCP:以太网通信,TCP/IP协议栈。其实就是把RTU的报文封装到TCP包里。
我个人习惯:现场设备距离近、数量少,用RTU;如果走以太网或者需要远程访问,用TCP。但要注意,RTU和TCP的报文结构略有不同,别搞混了。
4.2 报文结构——数据在线上怎么跑?
我们先看RTU的报文。Modbus RTU的报文结构非常规整,就像一列火车,每节车厢都有固定用途。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 地址码 | 1 | 从站地址,范围1-247 |
| 功能码 | 1 | 告诉从站要做什么 |
| 数据区 | N | 具体参数,长度可变 |
| CRC校验 | 2 | 循环冗余校验,保证数据完整性 |
举个例子,我要读取1号从站的保持寄存器,起始地址0,读2个寄存器。报文是这样的:
请求:01 03 00 00 00 02 C4 0B
响应:01 03 04 00 0A 00 14 3A 0F
我来拆解一下:
01:从站地址03:功能码,读取保持寄存器00 00:起始地址00 02:读取数量C4 0B:CRC校验
响应报文里,04表示后面有4个字节数据,00 0A是第一个寄存器的值(10),00 14是第二个寄存器的值(20)。
重点:Modbus RTU的报文之间要有至少3.5个字符时间的静默间隔,用来区分不同的报文。这个在编程时很容易忽略,我曾经遇到过因为波特率设置不对,导致报文粘连的问题,排查了好久。
再看Modbus TCP。它的报文结构稍微不同,去掉了CRC校验,加了一个MBAP头。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 事务标识符 | 2 | 用于匹配请求和响应 |
| 协议标识符 | 2 | 固定为0x0000 |
| 长度 | 2 | 后续数据的字节数 |
| 单元标识符 | 1 | 相当于RTU的地址码 |
| 功能码 | 1 | 同RTU |
| 数据区 | N | 具体参数 |
同样的读取操作,TCP报文是这样的:
请求:00 01 00 00 00 06 01 03 00 00 00 02
响应:00 01 00 00 00 07 01 03 04 00 0A 00 14
注意看,00 06表示后面有6个字节(单元标识符+功能码+数据区)。TCP因为底层有TCP的校验,所以不再需要CRC,省了点事。
小技巧:调试Modbus通信时,我习惯先用串口助手或者Modbus Poll工具抓一下报文,看看原始数据对不对。很多时候问题出在地址或者数据格式上,看一眼报文就明白了。
4.3 功能码——PLC能听懂的命令
功能码就是告诉PLC“你要干什么”。常用的就那么几个,我列个表:
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
| 01 | 读取线圈状态 | DO(数字量输出) | 读0/1状态 |
| 02 | 读取离散输入 | DI(数字量输入) | 只读 |
| 03 | 读取保持寄存器 | AO(模拟量输出) | 最常用,读写均可 |
| 04 | 读取输入寄存器 | AI(模拟量输入) | 只读 |
| 05 | 写单个线圈 | DO | 控制单个输出 |
| 06 | 写单个寄存器 | AO | 设置单个值 |
| 15 | 写多个线圈 | DO | 批量控制 |
| 16 | 写多个寄存器 | AO | 批量设置 |
实际项目中,功能码03和06用得最多。比如你要读取一个温度值,通常就是03功能码;要设定一个PID参数,就用06功能码。
注意:有些PLC厂商会自定义功能码,或者对标准功能码做扩展。比如西门子的S7-200 SMART,虽然支持Modbus,但地址映射方式跟标准Modbus不太一样。我建议你在用之前,一定先看PLC的Modbus通信手册,别想当然。
4.4 地址映射——PLC里的数据怎么找到?
这是初学者最容易懵的地方。Modbus的地址和PLC里的地址不是一回事,需要映射。
标准Modbus定义了四种数据对象:
- 线圈(Coil):对应PLC的DO,地址范围00001-09999
- 离散输入(Discrete Input):对应PLC的DI,地址范围10001-19999
- 输入寄存器(Input Register):对应PLC的AI,地址范围30001-39999
- 保持寄存器(Holding Register):对应PLC的AO,地址范围40001-49999
但注意,Modbus报文里用的地址是协议地址,从0开始。比如你要读保持寄存器40001,报文里填的地址是0x0000;读40002,填0x0001。这个映射关系一定要搞清楚。
举个例子:
PLC地址:40001 → 协议地址:0x0000
PLC地址:40002 → 协议地址:0x0001
PLC地址:40100 → 协议地址:0x0063
换算公式很简单:协议地址 = PLC地址 - 40001。其他类型的地址同理。
避坑指南:我曾经在一个项目里,用SCADA读西门子S7-1200的Modbus数据,怎么读都是0。后来发现,西门子的Modbus地址是从0开始的,但它的数据块地址又是从1开始的。说白了,就是地址偏移没算对。从那以后,我每次做地址映射都会画个表格,两边对照着看。
4.5 实战中的几个坑
讲几个我踩过的坑,你们以后遇到了心里有数。
- 字节序问题:Modbus RTU默认是大端模式(高位在前),但有些PLC是小端模式。比如一个16位值0x1234,大端传输是12 34,小端传输是34 12。如果你发现读出来的数值不对,先检查字节序。
- 寄存器对齐:32位浮点数需要占用2个寄存器。不同PLC的浮点存储格式可能不同,有的用ABCD,有的用CDAB。我建议你在SCADA里配置时,先写一个固定值,然后读回来看看对不对。
- 通信超时:Modbus RTU是主从模式,主机发出请求后,从机必须在规定时间内响应。如果从机没响应,主机会重试。这个超时时间设置很关键,设太短容易误判,设太长影响效率。我个人习惯设500ms,重试3次。
我的经验:调试Modbus通信时,先用一个简单的功能码(比如03读几个寄存器)验证通路,确认没问题了再上复杂的操作。别一上来就搞批量读写,出了问题都不知道是协议问题还是设备问题。
好了,这节课的内容就到这里。Modbus RTU和TCP的报文结构、功能码、地址映射,这些都是基本功。下节课我们聊点更实战的——多从站轮询、异常处理、以及如何优化通信效率。到时候见。