跳到主要内容

Modbus

Zeus 的 Modbus 模块帮你读写线圈和寄存器,不需要手拼功能码、CRC 或 MBAP 头。

先分清三个名字

名字例子含义
通道名busZeus 里这条通信线路的名字,可以是串口、TCP 或虚拟通道
设备名ovenZeus 里这台设备的名字
从站地址unitId: 1Modbus 协议里的从站地址 / 单元标识

很多问题来自把这三个混在一起。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]);
方法功能码用途
ReadCoilsAsync01读线圈
ReadDiscreteInputsAsync02读离散输入
ReadHoldingRegistersAsync03读保持寄存器
ReadInputRegistersAsync04读输入寄存器
WriteSingleCoilAsync05写单个线圈
WriteSingleRegisterAsync06写单个保持寄存器
WriteMultipleCoilsAsync0F写多个线圈
WriteMultipleRegistersAsync10写多个保持寄存器

地址从 0 开始

Zeus 使用报文里的真实地址,从 0 开始。

设备文档写法Zeus 里通常写
40001address: 0
40002address: 1
30001address: 0,但用输入寄存器 API
00001address: 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

下一步:周期读取数据看 点表与采集