跳到主要内容

Modbus

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

先分清三个名字​

名字例子含义
通道名busZeus 里这条通信线路的名字,可以是串口、TCP、UDP 或虚拟通道
设备名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, 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 从站;需要 ASCII 时改成 ModbusTransport.Ascii
AddVirtualChannel("bus", responder)把虚拟从站挂到虚拟通道上
AddModbusRtu("oven", "bus", unitId: 1)在 bus 通道上登记一台名为 oven 的 Modbus RTU 设备
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 总线时。

换成 Modbus ASCII​

一些老仪表或串口网关使用 ASCII 帧:冒号起始、十六进制文本、LRC 校验、CRLF 结束。通道仍然是串口,设备注册换成 AddModbusAscii:

builder.AddSerialPort("bus", "COM3", 9600);
builder.AddModbusAscii("oven", "bus", unitId: 1);

虚拟从站也支持同一封装,便于先验证主站逻辑:

builder.AddVirtualChannel("bus", new ModbusSlaveResponder(1, ModbusTransport.Ascii));
builder.AddModbusAscii("oven", "bus", unitId: 1);

读写 API​

除常规线圈/寄存器读写外,Zeus 也提供诊断和设备识别类功能码:异常状态 07、诊断回显 08、报告服务器 ID 11、读/写文件记录 14/15、掩码写 16、读写多个保持寄存器 17、读设备识别 2B/0E。

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);
var exceptionStatus = await oven.ReadExceptionStatusAsync();
var echo = await oven.DiagnosticsReturnQueryDataAsync(0xA55A);
var serverId = await oven.ReportServerIdAsync();
var identity = await oven.ReadDeviceIdentificationAsync();
await oven.WriteFileRecordAsync(fileNumber: 4, recordNumber: 1, values: [11, 22]);
var record = await oven.ReadFileRecordAsync(fileNumber: 4, recordNumber: 1, recordLength: 2);

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]);
await oven.MaskWriteRegisterAsync(address: 30, andMask: 0xFFF0, orMask: 0x0005);
var afterWrite = await oven.ReadWriteMultipleRegistersAsync(
readAddress: 10,
readQuantity: 2,
writeAddress: 20,
writeValues: [300, 400]);
方法功能码用途
ReadCoilsAsync01读线圈
ReadDiscreteInputsAsync02读离散输入
ReadHoldingRegistersAsync03读保持寄存器
ReadInputRegistersAsync04读输入寄存器
WriteSingleCoilAsync05写单个线圈
WriteSingleRegisterAsync06写单个保持寄存器
ReadExceptionStatusAsync07读异常状态字节
DiagnosticsReturnQueryDataAsync08诊断回显,用于确认从站诊断通路
WriteMultipleCoilsAsync0F写多个线圈
WriteMultipleRegistersAsync10写多个保持寄存器
ReportServerIdAsync11读取服务器 ID、运行状态和厂商附加数据
ReadFileRecordAsync14读一条文件记录
WriteFileRecordAsync15写一条文件记录
MaskWriteRegisterAsync16按 AND / OR 掩码修改单个保持寄存器
ReadWriteMultipleRegistersAsync17一次事务中先写多个保持寄存器,再读回保持寄存器
ReadDeviceIdentificationAsync2B/0E读取厂商名、产品代码和版本等识别对象

地址从 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, 0.1);
map.HoldingRegister("setpoint", 1, 0.1).Writable("setpoint");
map.Coil("heater", 2).Writable("heater");
});

然后读取或写回点表:

var temperature = app.Points.Get<double>("temperature");
await app.Points.WriteAsync("setpoint", 80.0);
await app.Points.WriteAsync("heater", true);

WriteAsync 传入工程值。上例中 80.0 会按 scale: 0.1 反算成保持寄存器 800。一次性调试、批量写或手册上的特殊地址,仍可直接调用 ModbusDevice 的写方法。

常见问题​

现象最可能原因处理
一直超时从站地址、串口参数、RTU/TCP/ASCII 类型不对先用虚拟从站确认主机逻辑,再查现场参数
非法数据地址地址偏移错了,或数量超过范围检查 40001 是否要换算成 0
CRC/LRC 失败波特率、校验位、停止位不一致,或线路干扰对照设备手册和串口助手抓包
TCP 事务号不匹配同一 TCP 通道上并发发了多路请求同一客户端串行请求,或拆分通道
读到的温度大 10 倍没做比例换算在点表里用 HoldingRegister(..., 0.1)
写回被拒绝点没标可写,或写了输入区对保持寄存器/线圈调用 .Writable

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