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 实战中的几个坑

讲几个我踩过的坑,你们以后遇到了心里有数。

  1. 字节序问题:Modbus RTU默认是大端模式(高位在前),但有些PLC是小端模式。比如一个16位值0x1234,大端传输是12 34,小端传输是34 12。如果你发现读出来的数值不对,先检查字节序。
  2. 寄存器对齐:32位浮点数需要占用2个寄存器。不同PLC的浮点存储格式可能不同,有的用ABCD,有的用CDAB。我建议你在SCADA里配置时,先写一个固定值,然后读回来看看对不对。
  3. 通信超时:Modbus RTU是主从模式,主机发出请求后,从机必须在规定时间内响应。如果从机没响应,主机会重试。这个超时时间设置很关键,设太短容易误判,设太长影响效率。我个人习惯设500ms,重试3次。

我的经验:调试Modbus通信时,先用一个简单的功能码(比如03读几个寄存器)验证通路,确认没问题了再上复杂的操作。别一上来就搞批量读写,出了问题都不知道是协议问题还是设备问题。

好了,这节课的内容就到这里。Modbus RTU和TCP的报文结构、功能码、地址映射,这些都是基本功。下节课我们聊点更实战的——多从站轮询、异常处理、以及如何优化通信效率。到时候见。