(六)YModbus读写数据:线圈、离散输入、保持寄存器、输入寄存器
2026/6/13 23:40:56 网站建设 项目流程

GitHub 项目地址:https://github.com/lidecong133/YModbus

Client 创建好以后,下一步就是选功能码、填地址、读数据。

这一步看起来简单,现场却经常出问题。

很多设备手册不会直接告诉你“请用 03 功能码从地址 0 开始读”。它可能写40001,也可能写Holding Register 1,还有些只给一张寄存器表,默认你懂它的习惯。

所以读写前,先把 Modbus 四类数据区分清楚。

数据区常见用途读功能码标准写功能码YModbus 返回
Coils可读写开关量0105/0Fbool[]
Discrete Inputs只读开关量02bool[]
Holding Registers可读写寄存器0306/10ushort[]
Input Registers只读寄存器04ushort[]

一句话记:线圈和保持寄存器通常能写,离散输入和输入寄存器通常只读。

当然,设备厂家不一定完全按名字设计业务。有些保持寄存器虽然理论上能写,实际也可能只允许读。能不能写,最终还是看设备手册和设备响应。

线圈:读写开关量

线圈用来表示布尔状态,常见的是输出点、启动位、复位位、报警清除位。

读线圈:

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读取,用0610写入。

读保持寄存器:

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 多站号轮询。

读写前先核对这几件事

我自己接设备时,会先看这几项:

  • 手册写的是哪类数据区
  • 功能码是010203还是04
  • 地址是协议地址,还是40001这种显示地址
  • 一次读的数量有没有超过设备支持范围
  • 这个地址到底能不能写
  • 写入会不会触发设备动作

能读,不代表能写。

写成功,也不代表设备业务一定执行了。有些设备写完参数还要保存命令,有些设备必须在停机或远程模式下才接受。

到这里

YModbus 读写四类数据区,对应的方法其实很直接:

  • ReadCoilsAsync
  • ReadDiscreteInputsAsync
  • ReadHoldingRegistersAsync
  • ReadInputRegistersAsync
  • WriteSingleCoilAsync
  • WriteSingleRegisterAsync
  • WriteMultipleCoilsAsync
  • WriteMultipleRegistersAsync

真正要花心思的,不是记方法名,而是把功能码、地址、数量、站号和设备手册对上。

这些对上了,读写代码反而很简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询