2、Modbus寻址与数据模型:线圈、离散输入、保持寄存器、输入寄存器的地址映射规则

好,咱们接着聊。上一章我们把Modbus的物理层和协议帧结构理清了,这一章我重点讲讲Modbus里最容易让人犯迷糊的东西——寻址与数据模型

说实话,我刚开始接触Modbus时,也被这四种对象搞得有点晕。线圈、离散输入、保持寄存器、输入寄存器……名字听着都差不多,地址还容易记混。后来在产线上调试一台老式PLC时,因为地址映射搞反了,折腾了整整一个下午。嗯,从那以后我就把这套规则刻在脑子里了。

2.1 四种基本数据对象

Modbus协议定义了四种基本的数据对象。你想想看,这其实就是在模拟一个PLC或者RTU内部的数据存储区。它们分别是:

对象类型 对象名称 数据宽度 访问权限 物理意义
离散量输入 Discrete Input 1 bit 只读 现场开关、按钮、限位信号
线圈 Coil 1 bit 读写 继电器、指示灯、电磁阀
输入寄存器 Input Register 16 bit 只读 模拟量输入、传感器数值
保持寄存器 Holding Register 16 bit 读写 设定值、PID参数、累计值

我个人习惯把这四种对象分成两组来记:位对象字对象。位对象就是线圈和离散输入,一个字节能塞8个;字对象就是保持寄存器和输入寄存器,一个寄存器占两个字节。

2.2 地址映射规则——别搞混了!

这里有个关键点,也是很多新手栽跟头的地方:Modbus协议中的地址是0-based的,但很多设备手册和组态软件里用的是1-based的地址

什么意思呢?

在Modbus协议报文里,数据地址是从0开始的。比如你要读第一个线圈,协议里填的地址是0x0000。但你去翻某个变频器的说明书,它告诉你“运行状态”的地址是00001。这个00001就是1-based的地址,对应协议里的地址0。

⚠️ 避坑指南: 我曾经在调试一个温控仪表时,手册上写保持寄存器地址是40001,我直接在代码里填了40001,结果死活读不到数据。后来才发现,协议层要填的是40001 - 40001 = 0。说白了,就是要把手册上的地址减去起始偏移量,再减1。

标准的地址映射规则如下:

对象类型 PLC地址范围(1-based) 协议地址范围(0-based) 功能码
线圈 00001 ~ 09999 0x0000 ~ 0x270E 01(读)、05(写单)、15(写多)
离散输入 10001 ~ 19999 0x0000 ~ 0x270E 02(读)
输入寄存器 30001 ~ 39999 0x0000 ~ 0x270E 04(读)
保持寄存器 40001 ~ 49999 0x0000 ~ 0x270E 03(读)、06(写单)、16(写多)

你看,协议地址范围其实是一样的,都是0x0000到0x270E(也就是0到9999)。区别在于PLC地址的前缀数字:0代表线圈,1代表离散输入,3代表输入寄存器,4代表保持寄存器。

2.3 实际项目中的地址换算

我在智能工厂项目里,经常要跟不同品牌的设备打交道。西门子的S7-1200、三菱的FX系列、还有各种国产温控器,它们的地址表示方式五花八门。但底层都是Modbus,所以换算方法万变不离其宗。

举个例子:

你拿到一个设备手册,上面写着“当前温度”的地址是30012。那么:

  1. 先看前缀:3开头,说明是输入寄存器(只读)。
  2. 去掉前缀:剩下0012,这是1-based地址。
  3. 换算成协议地址:12 - 1 = 11,也就是0x000B。
  4. 功能码用04(读输入寄存器)。

再比如,你要控制一个继电器,手册上写线圈地址是00005

  1. 前缀0,说明是线圈(可读写)。
  2. 去掉前缀:剩下0005。
  3. 协议地址:5 - 1 = 4,即0x0004。
  4. 功能码用05(写单个线圈)或15(写多个线圈)。
💡 个人小技巧: 我习惯在代码里写一个地址转换函数。输入是手册上的PLC地址(比如40001),输出是协议地址(0)。这样不管换什么设备,只要改一下地址表就行,不用每次都手动算。

2.4 数据模型在内存中的布局

你可能会问:这四种对象在设备内部是怎么存的?

其实,Modbus协议并没有规定具体的物理存储方式。它只定义了逻辑上的数据模型。也就是说,设备厂商可以自由安排这些数据在内存里的位置,只要保证通过Modbus能正确读写就行。

但实际项目中,大部分设备都是这样做的:

  • 线圈和离散输入:通常放在一个连续的bit区域。比如地址0到9999对应线圈,地址10000到19999对应离散输入。但注意,这两个区域是独立的,地址0的线圈和地址0的离散输入不是同一个东西。
  • 保持寄存器和输入寄存器:也是独立的16位字区域。地址0的保持寄存器和地址0的输入寄存器,物理上可能是两个不同的内存单元。

我记得有一次,一个同事问我:“为什么我读保持寄存器地址0能读到数据,但读输入寄存器地址0却是0?”

我告诉他:“这很正常。保持寄存器和输入寄存器是两个不同的存储区。保持寄存器0存的是设定值,输入寄存器0存的是实际值。你读的是两个不同的变量。”

2.5 地址映射的常见坑

做Modbus集成,地址映射这块有几个坑,我帮你列出来:

🔴 坑1:地址偏移搞错
很多组态软件(比如组态王、WinCC)在配置变量时,会让你填“寄存器地址”。它们默认用的是1-based地址。但如果你在代码里直接写Modbus协议,一定要记得减1。

🔴 坑2:功能码用错
读线圈用01,读离散输入用02,读保持寄存器用03,读输入寄存器用04。这四个功能码千万别搞混。我见过有人用03功能码去读离散输入,结果返回的数据全是乱的。

🔴 坑3:数据类型不匹配
保持寄存器是16位的,但很多设备会把32位浮点数或32位整数放在两个连续的寄存器里。比如地址40001和40002组合成一个float。这时候你要注意字节序(Big Endian还是Little Endian)。

🔴 坑4:地址范围超限
标准Modbus的地址范围是0到65535(0x0000到0xFFFF)。但很多设备只实现了0到9999。如果你访问超出范围的地址,设备会返回异常码02(非法数据地址)。

2.6 一个完整的地址映射示例

最后,我拿一个实际项目中的例子来收尾。假设我们要读取一个Modbus温控器的数据,它的手册上给出了这样的地址表:

参数名称 PLC地址 数据类型 读写属性
当前温度 30001 16位无符号整数 只读
设定温度 40001 16位无符号整数 读写
运行状态 00001 1位布尔 只读
启动/停止 00002 1位布尔 读写

那么,在Modbus协议层面,我们应该这样操作:

  • 读当前温度:功能码04,地址0x0000(30001 - 30001 = 0),读1个寄存器。
  • 读设定温度:功能码03,地址0x0000(40001 - 40001 = 0),读1个寄存器。
  • 写设定温度:功能码06,地址0x0000,写入1个寄存器。
  • 读运行状态:功能码02,地址0x0000(00001 - 00001 = 0),读1个位。
  • 写启动/停止:功能码05,地址0x0001(00002 - 00001 = 1),写入1个位。

你看,只要掌握了这个换算规则,不管设备手册怎么写,你都能准确找到对应的数据。

好了,这一章就到这里。下一章我们聊聊Modbus的通信模式——RTU和ASCII,以及它们在实际应用中的选择。到时候我会分享一个我在污水处理厂项目中踩过的坑,保证让你印象深刻。