Modbus
Zeus 的 Modbus 模块帮你读写线圈和寄存器,不需要手拼功能码、CRC 或 MBAP 头。
先分清三个名字
| 名字 | 例子 | 含义 |
|---|---|---|
| 通道名 | bus | Zeus 里这条通信线路的名字,可以是串口、TCP 或虚拟通道 |
| 设备名 | oven | Zeus 里这台设备的名字 |
| 从站地址 | unitId: 1 | Modbus 协议里的从站地址 / 单元标识 |
很多问题来自把这三个混在一起。bus 不是 COM 口,oven 不是从站地址,unitId 也不是 Zeus 的设备名。
没硬件时先跑虚拟从站
var memory = new ModbusSlaveMemory();
memory.HoldingRegisters[0] = 185;
await using var app = ZeusHost.Create(builder =>
{
builder.AddVirtualChannel("bus", new ModbusSlaveResponder(1, ModbusTransport.Rtu, memory));
builder.AddModbusRtu("oven", "bus", unitId: 1, points: map =>
{
map.HoldingRegister("temperature", 0, raw => raw * 0.1);
});
});
await app.StartAsync();
var oven = app.Devices.Get<ModbusDevice>("oven");
var values = await oven.ReadHoldingRegistersAsync(0, 1);
var temperature = app.Points.Get<double>("temperature");
这段代码里:
| 代码 | 含义 |
|---|---|
ModbusSlaveMemory | 虚拟从站的寄存器内存 |
HoldingRegisters[0] = 185 | 地址 0 的保持寄存器值是 185 |
ModbusSlaveResponder(1, Rtu, memory) | 模拟一个地址为 1 的 RTU 从站 |
AddVirtualChannel("bus", responder) | 把虚拟从站挂到虚拟通道上 |
AddModbusRtu("oven", "bus", unitId: 1) | 在 bus 通道上登记一台名为 oven 的 Modbus RTU 设备 |
raw => raw * 0.1 | 把原始寄存器值 185 转成 18.5 |
可运行示例:samples/Zeus.Samples.Console.Modbus。
换成现场串口
虚拟从站跑通后,把通道换成真实串口:
builder.AddSerialPort("bus", "COM3", 9600);
builder.AddModbusRtu("oven", "bus", unitId: 1);
AddModbusRtu 的第二个参数仍然是 Zeus 通道名 bus,不是 COM3。
换成现场网口
Modbus TCP 通常端口是 502:
builder.AddTcpClient("bus", "192.168.1.10", 502);
builder.AddModbusTcp("oven", "bus", unitId: 1);
unitId 在 Modbus TCP 里仍然可能有用,尤其是网关转 RTU 总线时。
读写 API
var oven = app.Devices.Get<ModbusDevice>("oven");
var holding = await oven.ReadHoldingRegistersAsync(address: 0, quantity: 2);
var input = await oven.ReadInputRegistersAsync(address: 0, quantity: 2);
var coils = await oven.ReadCoilsAsync(address: 0, quantity: 8);
var discrete = await oven.ReadDiscreteInputsAsync(address: 0, quantity: 8);
await oven.WriteSingleRegisterAsync(address: 1, value: 200);
await oven.WriteSingleCoilAsync(address: 2, value: true);
await oven.WriteMultipleRegistersAsync(address: 10, values: [100, 200]);
await oven.WriteMultipleCoilsAsync(address: 20, values: [true, false, true]);
| 方法 | 功能码 | 用途 |
|---|---|---|
ReadCoilsAsync | 01 | 读线圈 |
ReadDiscreteInputsAsync | 02 | 读离散输入 |
ReadHoldingRegistersAsync | 03 | 读保持寄存器 |
ReadInputRegistersAsync | 04 | 读输入寄存器 |
WriteSingleCoilAsync | 05 | 写单个线圈 |
WriteSingleRegisterAsync | 06 | 写单个保持寄存器 |
WriteMultipleCoilsAsync | 0F | 写多个线圈 |
WriteMultipleRegistersAsync | 10 | 写多个保持寄存器 |
地址从 0 开始
Zeus 使用报文里的真实地址,从 0 开始。
| 设备文档写法 | Zeus 里通常写 |
|---|---|
40001 | address: 0 |
40002 | address: 1 |
30001 | address: 0,但用输入寄存器 API |
00001 | address: 0,但用线圈 API |
如果厂家文档明确写的是 0 基地址,就按文档来。遇到 非法数据地址 时,优先检查这里。
和点表采集配合
如果某些数据要周期读取,不要自己写 while 循环。直接声明点:
builder.AddAcquisition(TimeSpan.FromMilliseconds(500));
builder.AddModbusRtu("oven", "bus", unitId: 1, points: map =>
{
map.HoldingRegister("temperature", 0, raw => raw * 0.1);
map.HoldingRegister("setpoint", 1);
map.Coil("heater", 2);
});
然后读取点表:
var temperature = app.Points.Get<double>("temperature");
常见问题
| 现象 | 最可能原因 | 处理 |
|---|---|---|
| 一直超时 | 从站地址、串口参数、RTU/TCP 类型不对 | 先用虚拟从站确认主机逻辑,再查现场参数 |
非法数据地址 | 地址偏移错了,或数量超过范围 | 检查 40001 是否要换算成 0 |
| CRC 失败 | 波特率、校验位、停止位不一致,或线路干扰 | 对照设备手册和串口助手抓包 |
| TCP 事务号不匹配 | 同一 TCP 通道上并发发了多路请求 | 同一客户端串行请求,或拆分通道 |
| 读到的温度大 10 倍 | 没做比例换算 | 在点表里用 raw => raw * 0.1 |
下一步:周期读取数据看 点表与采集。