GitHub 项目地址:https://github.com/lidecong133/YModbus
保持寄存器读出来是ushort[]。
这很符合 Modbus 协议,但不太符合人看数据的习惯。
现场你想看的通常不是16856这种原始寄存器值,而是温度23.5、压力1.25、计数器123456。这些值在设备内部可能是整数缩放,也可能是float,还可能是 32 位或 64 位整数。
所以读寄存器只是第一步。第二步是把寄存器解释成人能看懂的值。
先确认它到底是什么类型
一个 Modbus 寄存器是 16 位,也就是 2 个字节。
| 类型 | 常见占用 |
|---|---|
short/ushort | 1 个寄存器 |
int/uint | 2 个寄存器 |
float/Single | 2 个寄存器 |
long/ulong | 4 个寄存器 |
double | 4 个寄存器 |
如果手册写温度是float32,一般就要读两个寄存器。
ushort[]registers=awaitclient.ReadHoldingRegistersAsync(100,2);如果手册写温度是UINT16,倍率 0.1,那就只读一个寄存器,然后自己除以 10。
这两种处理方式完全不同。
比例系数不是字节序
这个坑很常见。
设备显示23.5,寄存器读出来是235,这时候不要急着按float解析。
它很可能只是整数缩放:
ushortraw=(awaitclient.ReadHoldingRegistersAsync(100,1))[0];decimaltemperature=raw/10m;字节序解决的是“几个字节怎么拼成一个类型”。
比例系数解决的是“原始数值怎么换算成工程量”。
这两个问题不要混在一起。混在一起以后,现场就会开始盲试:一会儿换字节序,一会儿换地址,一会儿换功能码,最后哪个改动生效都说不清。
再看字序和字节序
如果设备确实存的是float或int32,那就要看顺序。
一个float占 4 个字节,比如:
41 BC 00 00放进两个寄存器时,可能是:
寄存器 0: 41 BC 寄存器 1: 00 00也可能是:
寄存器 0: 00 00 寄存器 1: 41 BCYModbus 把它拆成两个参数:
| 参数 | 说明 |
|---|---|
ModbusWordOrder | 多个 16 位寄存器之间,哪个字在前 |
ModbusByteOrder | 一个寄存器内部,高字节还是低字节在前 |
常见组合是HighWordFirst+BigEndian,但不能假定所有设备都这样。
直接读成 typed 值
如果你只想读一个 typed 值,可以用扩展方法。
读float:
usingYModbus.Clients;usingYModbus.Protocol;floattemperature=awaitclient.ReadHoldingRegisterSingleAsync(startAddress:100,wordOrder:ModbusWordOrder.HighWordFirst,byteOrder:ModbusByteOrder.BigEndian);读int32:
intcounter=awaitclient.ReadHoldingRegisterInt32Async(startAddress:120,wordOrder:ModbusWordOrder.LowWordFirst,byteOrder:ModbusByteOrder.BigEndian);输入寄存器也有对应方法:
floatpressure=awaitclient.ReadInputRegisterSingleAsync(startAddress:0,wordOrder:ModbusWordOrder.HighWordFirst,byteOrder:ModbusByteOrder.BigEndian);区别只是功能码不同。保持寄存器走03,输入寄存器走04。
批量读完再转换
实际项目里,我更常用另一种方式:先读一段寄存器,再按地址表解析。
例如你一次读 100 个保持寄存器,里面有温度、压力、状态字、计数器。每个字段单独发请求,效率低,也不方便保持同一时刻的数据。
这时候用RegisterConverter:
usingYModbus.Protocol;ushort[]registers=awaitclient.ReadHoldingRegistersAsync(100,20);floattemperature=RegisterConverter.ToSingle(registers.AsSpan(0,2).ToArray(),ModbusWordOrder.HighWordFirst,ModbusByteOrder.BigEndian);intcounter=RegisterConverter.ToInt32(registers.AsSpan(2,2).ToArray(),ModbusWordOrder.LowWordFirst,ModbusByteOrder.BigEndian);写入时也可以反过来转:
ushort[]values=RegisterConverter.FromSingle(23.5F,ModbusWordOrder.HighWordFirst,ModbusByteOrder.BigEndian);awaitclient.WriteMultipleRegistersAsync(100,values);怎么判断顺序对不对
最稳的办法是找一个已知值。
比如设备屏幕显示温度23.5,你读出来两个寄存器。然后用不同组合解析,看哪个结果接近23.5。
可以临时这样试:
foreach(ModbusWordOrderwordOrderinEnum.GetValues<ModbusWordOrder>()){foreach(ModbusByteOrderbyteOrderinEnum.GetValues<ModbusByteOrder>()){floatvalue=RegisterConverter.ToSingle(registers,wordOrder,byteOrder);Console.WriteLine($"{wordOrder},{byteOrder}:{value}");}}如果某个组合解析出来是4.17E-41这种离谱值,基本就不是它。
但这个办法只是为了确认设备格式。确认以后,最好把格式写进地址表,不要每次靠试。
地址表里要记录格式
一个可维护的地址表,至少应该记录这些东西:
| 字段 | 示例 |
|---|---|
| 地址 | 100 |
| 功能码 | 03 |
| 数据类型 | Float32 |
| 字序 | HighWordFirst |
| 字节序 | BigEndian |
| 比例系数 | 1.0 |
| 单位 | ℃ |
| 名称 | 当前温度 |
只记录“地址”和“名称”是不够的。等项目过几个月再维护,谁也不记得当时这个值为什么要低字在前,为什么要除以 10。
写入 typed 值要更谨慎
读错,通常只是显示不对。
写错,可能会改设备参数。
YModbus 有 typed write helper:
awaitclient.WriteHoldingRegisterSingleAsync(startAddress:100,value:23.5F,wordOrder:ModbusWordOrder.HighWordFirst,byteOrder:ModbusByteOrder.BigEndian);写int32:
awaitclient.WriteHoldingRegisterInt32Async(startAddress:120,value:123456,wordOrder:ModbusWordOrder.LowWordFirst,byteOrder:ModbusByteOrder.BigEndian);真正对设备写之前,先确认地址可写、数值范围安全、字序字节序和手册一致。最好写完以后再读回一次,确认设备里实际变成了什么。
到这里
寄存器转换不是 YModbus 特有的问题,是 Modbus 本身就这样。
YModbus 能帮你做两件事:
- 直接用 typed helper 读写
int、float、double - 用
RegisterConverter在批量读出的寄存器上做转换
但设备到底是整数缩放、还是 IEEE754 浮点数,字序怎么排,比例系数是多少,还是要回到设备手册和现场验证。