(六)YModbus读写数据:线圈、离散输入、保持寄存器、输入寄存器
GitHub 项目地址:https://github.com/lidecong133/YModbus
Client 创建好以后,下一步就是选功能码、填地址、读数据。
这一步看起来简单,现场却经常出问题。
很多设备手册不会直接告诉你“请用 03 功能码从地址 0 开始读”。它可能写40001,也可能写Holding Register 1,还有些只给一张寄存器表,默认你懂它的习惯。
所以读写前,先把 Modbus 四类数据区分清楚。
| 数据区 | 常见用途 | 读功能码 | 标准写功能码 | YModbus 返回 |
|---|---|---|---|---|
| Coils | 可读写开关量 | 01 | 05/0F | bool[] |
| Discrete Inputs | 只读开关量 | 02 | 无 | bool[] |
| Holding Registers | 可读写寄存器 | 03 | 06/10 | ushort[] |
| Input Registers | 只读寄存器 | 04 | 无 | ushort[] |
一句话记:线圈和保持寄存器通常能写,离散输入和输入寄存器通常只读。
当然,设备厂家不一定完全按名字设计业务。有些保持寄存器虽然理论上能写,实际也可能只允许读。能不能写,最终还是看设备手册和设备响应。
线圈:读写开关量
线圈用来表示布尔状态,常见的是输出点、启动位、复位位、报警清除位。
读线圈:
bool[]coils=awaitclient.ReadCoilsAsync(0,8);写单个线圈:
awaitclient.WriteSingleCoilAsync(0,true);写多个线圈:
awaitclient.WriteMultipleCoilsAsync(startAddress:0,values:new[]{true,false,true,true});写线圈要小心。很多设备会把线圈映射成动作命令,比如启动、停止、复位。你在调试软件里点一下,设备那边可能真的动作。
我的习惯是,第一次接设备只读不写。等确认站号、地址、功能码都对,再做写入测试。
离散输入:只读开关量
离散输入也是布尔量,但一般是只读。
读离散输入:
bool[]inputs=awaitclient.ReadDiscreteInputsAsync(0,8);它常见于这些状态:
- 急停输入
- 限位开关
- 光电信号
- 设备就绪
- 报警状态
标准 Modbus 没有“写离散输入”的功能码。你如果在从站模拟器里改离散输入,那是模拟器内部改值,不是主站通过标准功能码写进去。
这个区别要分清楚,否则调试时会误以为主站能写所有状态。
保持寄存器:最常用,也最容易踩坑
保持寄存器用功能码03读取,用06或10写入。
读保持寄存器:
ushort[]registers=awaitclient.ReadHoldingRegistersAsync(0,10);写单个保持寄存器:
awaitclient.WriteSingleRegisterAsync(100,123);写多个保持寄存器:
awaitclient.WriteMultipleRegistersAsync(startAddress:100,values:newushort[]{123,456,789});保持寄存器返回的是ushort[],每个元素就是一个 16 位寄存器。
如果设备手册写40001 当前温度,程序里不一定填40001。多数情况下,40001是给人看的编号,真正协议地址要填0。
这就是 Modbus 最常见的地址差 1。
第一次读保持寄存器时,我建议这样试:
ushort[]values=awaitclient.ReadHoldingRegistersAsync(0,1);先读一个。通了,再扩大数量。不要上来就读一大段,报错以后反而不好判断是起始地址错、数量太大,还是跨了非法区域。
输入寄存器:读测量值很常见
输入寄存器用功能码04。
ushort[]registers=awaitclient.ReadInputRegistersAsync(0,10);很多仪表会把温度、压力、流量、重量放在输入寄存器里。但也有设备把这些值放在保持寄存器。
所以看到“测量值”不要直接猜功能码。手册写30001通常对应04,写40001通常对应03,但还是要看厂家说明。
如果你用03读不到,可以试04。反过来也一样。只要设备返回异常码或超时,就回到功能码和地址表上查。
地址一律按协议地址传
YModbus 的 API 里,地址都是协议地址,也就是从0开始。
比如设备手册写:
40001 当前速度 40002 当前压力程序里通常写:
ushort[]values=awaitclient.ReadHoldingRegistersAsync(0,2);如果手册直接写:
地址 0 当前速度 地址 1 当前压力那程序里也从0开始。
麻烦就在于不同厂家手册写法不统一。你要判断它给的是显示地址,还是协议地址。
读出来不对时,不要马上怀疑 YModbus。先把地址基准确认清楚。
一次读很多,用分块方法
Modbus 协议对一次读取数量有限制。
保持寄存器一次最多读 125 个,线圈一次也有数量限制。你要读几百个、上千个点位,就应该拆成多次请求。
YModbus 提供了分块辅助方法:
ushort[]registers=awaitclient.ReadHoldingRegistersInBlocksAsync(startAddress:0,quantity:1000);写一大段保持寄存器也可以分块:
awaitclient.WriteHoldingRegistersInBlocksAsync(0,registers);这些方法适合参数备份、地址表导出、批量采集。
不过现场第一次联调还是那句话:先小范围读通,再扩大。
MultiUnitClient只是多了站号参数
如果你用的是ModbusMultiUnitClient,方法名基本一样,只是第一个参数多了 UnitId / SlaveId。
ushort[]unit1=awaitclient.ReadHoldingRegistersAsync(1,0,10);ushort[]unit2=awaitclient.ReadHoldingRegistersAsync(2,0,10);写入也是:
awaitclient.WriteSingleRegisterAsync(unitId:1,address:100,value:123);这种写法很适合 TCP 网关和 RS485 多站号轮询。
读写前先核对这几件事
我自己接设备时,会先看这几项:
- 手册写的是哪类数据区
- 功能码是
01、02、03还是04 - 地址是协议地址,还是
40001这种显示地址 - 一次读的数量有没有超过设备支持范围
- 这个地址到底能不能写
- 写入会不会触发设备动作
能读,不代表能写。
写成功,也不代表设备业务一定执行了。有些设备写完参数还要保存命令,有些设备必须在停机或远程模式下才接受。
到这里
YModbus 读写四类数据区,对应的方法其实很直接:
ReadCoilsAsyncReadDiscreteInputsAsyncReadHoldingRegistersAsyncReadInputRegistersAsyncWriteSingleCoilAsyncWriteSingleRegisterAsyncWriteMultipleCoilsAsyncWriteMultipleRegistersAsync
真正要花心思的,不是记方法名,而是把功能码、地址、数量、站号和设备手册对上。
这些对上了,读写代码反而很简单。
