(四)YModbus功能介绍:这个库到底能做什么
GitHub 项目地址:https://github.com/lidecong133/YModbus
前面已经讲了 Modbus 协议,也写了一篇 YModbus 的快速上手。
这一篇不继续讲协议细节,重点放在 YModbus 这个库本身。
也就是回答一个很直接的问题:
YModbus 现在到底能做什么?
如果你准备在自己的上位机、调试工具、采集程序、测试程序里用它,这篇可以先看一遍。
它不只是一个“读寄存器”的小工具,而是准备慢慢做成一套完整的 Modbus 通讯底座。
YModbus 分成哪几块
现在项目里主要有这几部分:
| 项目 | 作用 |
|---|---|
YModbus |
核心库,包含协议、client、TCP transport、功能码、寄存器转换等 |
YModbus.Serial |
串口支持,用来做 Modbus RTU / ASCII |
YModbus.Slave |
从站 / server 模拟,用来做 TCP、RTU、ASCII 响应侧 |
YModbus.Cli |
命令行工具,适合脚本、测试、自动化 |
samples |
示例工程,方便直接跑起来看效果 |
如果你只是写一个上位机去读设备,大多数时候会用到:
YModbusYModbus.Serial
如果你要做设备模拟、调试软件里的从站模拟、网关测试,就会用到:
YModbus.Slave
支持哪些通讯方式
YModbus 目前支持三种常见 Modbus 形式:
| 通讯方式 | 常见场景 |
|---|---|
| Modbus RTU | 串口、RS485、USB 转 RS485 |
| Modbus TCP | 以太网、串口服务器、网关、PLC |
| Modbus ASCII | 老设备、特殊串口场景 |
这三个方式底层报文不一样,但上层读写方法尽量保持一致。
比如读保持寄存器,不管是 RTU 还是 TCP,最后都是:
ushort[] registers = await client.ReadHoldingRegistersAsync(0, 4);
这样用起来会轻松很多。
你不用在业务代码里到处判断 RTU 怎么拼帧、TCP 怎么加 MBAP Header、ASCII 怎么转字符。
这些底层细节交给库处理。
Client:主动读写设备
最常用的就是 client。
它适合这种场景:
我的程序主动去读设备、写设备。
比如上位机读仪表、采集软件读 IO 模块、调试工具读 PLC 或网关。
YModbus 的 ModbusClient 是面向单个 slaveID / unitId 的。
比如 RTU:
using System.IO.Ports;
using YModbus.Clients;
using YModbus.Serial;
using SerialPort port = new("COM3")
{
BaudRate = 9600,
DataBits = 8,
Parity = Parity.None,
StopBits = StopBits.One
};
port.Open();
await using ModbusClient client = ModbusSerialClientFactory.CreateRtu(
slaveId: 1,
serialPort: port,
leaveOpen: true);
ushort[] values = await client.ReadHoldingRegistersAsync(0, 4);
比如 TCP:
using YModbus.Clients;
await using ModbusClient client = await ModbusClientFactory.CreateTcpAsync(
host: "192.168.1.10",
port: 502,
unitId: 1);
ushort[] values = await client.ReadHoldingRegistersAsync(0, 4);
这里有个命名点要注意。
Modbus TCP 里的 client/server 是网络角色,不是简单等于主站/从站。
不过在使用上,你可以先这样理解:
ModbusClient:主动发请求的一侧- 远端设备或模拟器:接收请求并返回响应的一侧
常用读写功能
YModbus 里常用功能码都有对应方法。
| 功能码 | 方法 | 说明 |
|---|---|---|
01 |
ReadCoilsAsync |
读线圈 |
02 |
ReadDiscreteInputsAsync |
读离散输入 |
03 |
ReadHoldingRegistersAsync |
读保持寄存器 |
04 |
ReadInputRegistersAsync |
读输入寄存器 |
05 |
WriteSingleCoilAsync |
写单个线圈 |
06 |
WriteSingleRegisterAsync |
写单个保持寄存器 |
15 / 0x0F |
WriteMultipleCoilsAsync |
写多个线圈 |
16 / 0x10 |
WriteMultipleRegistersAsync |
写多个保持寄存器 |
23 / 0x17 |
ReadWriteMultipleRegistersAsync |
一条报文里读写保持寄存器 |
比如写一个保持寄存器:
await client.WriteSingleRegisterAsync(100, 123);
比如写多个保持寄存器:
ushort[] registers = new ushort[] { 1, 2, 3 };
await client.WriteMultipleRegistersAsync(100, registers);
这些方法的参数都尽量贴近 Modbus 协议本身。
比如地址用的是协议地址,通常从 0 开始。
如果设备手册写 40001,代码里很多时候应该填 0,不是 40001。
ModbusClient 和 ModbusMultiUnitClient
YModbus 里有两个容易混的 client:
ModbusClientModbusMultiUnitClient
简单说:
| 类型 | 适合场景 |
|---|---|
ModbusClient |
创建时就固定一个 slaveID / unitId |
ModbusMultiUnitClient |
每次调用方法时再传入 slaveID / unitId |
这个名字是故意这样取的。
它不是想表达 TCP 里的 client/server,也不是想重新解释主站/从站。
它只表达一件事:同一个通讯连接里,可以访问多个 UnitId 或 slaveID。
比如你只读一个设备:
await using ModbusClient client = await ModbusClientFactory.CreateTcpAsync(
"192.168.1.10",
502,
unitId: 1);
ushort[] values = await client.ReadHoldingRegistersAsync(0, 10);
如果你通过一个 TCP 转 RTU 网关,后面挂了多个设备,就可以用 ModbusMultiUnitClient:
await using ModbusMultiUnitClient multiUnitClient =
await ModbusClientFactory.CreateTcpMultiUnitAsync("192.168.1.10", 502);
ushort[] unit1 = await multiUnitClient.ReadHoldingRegistersAsync(1, 0, 10);
ushort[] unit2 = await multiUnitClient.ReadHoldingRegistersAsync(2, 0, 10);
这时每次调用时传入的第一个参数就是目标 UnitId。
对网关、多设备轮询、多站号采集来说,这个会比较方便。
Serial:RTU 和 ASCII 串口支持
核心库 YModbus 本身不直接依赖 SerialPort。
串口相关的东西放在 YModbus.Serial 里。
这样做的好处是边界更清楚:
- 核心库负责 Modbus 协议和 TCP
- 串口扩展库负责把
SerialPort接进来
RTU 创建方式:
await using ModbusClient client = ModbusSerialClientFactory.CreateRtu(
slaveId: 1,
serialPort: port,
leaveOpen: true);
ASCII 创建方式:
await using ModbusClient client = ModbusSerialClientFactory.CreateAscii(
slaveId: 1,
serialPort: port,
leaveOpen: true);
大多数新设备用 RTU 更多,ASCII 相对少一些。
但既然工业现场会遇到老设备,库里还是把 ASCII 留出来。
Slave / Server:模拟设备
YModbus 不只支持主动读写,也支持响应侧。
也就是可以让你的程序模拟一个 Modbus 设备。
这个功能很适合下面这些场景:
- 调试主站软件
- 做上位机联调
- 模拟仪表数据
- 测试 Modbus TCP 网关
- 做自动化测试
- 做主站/从站调试工具
比如启动一个 TCP slave network,模拟两个 UnitId:
using System.Net;
using YModbus.Slave;
ModbusSlaveDataStore unitOneStore = new(pointCount: 100);
unitOneStore.SetHoldingRegister(0, 1234);
unitOneStore.SetHoldingRegister(1, 5678);
ModbusSlaveDataStore unitTwoStore = new(pointCount: 100);
unitTwoStore.SetHoldingRegister(0, 2222);
unitTwoStore.SetHoldingRegister(1, 3333);
await using ModbusTcpSlaveNetwork network = new(new ModbusTcpSlaveNetworkOptions
{
ListenAddress = IPAddress.Loopback,
Port = 1502
});
network.AddSlave(new ModbusSlaveDefinition { UnitId = 1 }, unitOneStore);
network.AddSlave(new ModbusSlaveDefinition { UnitId = 2 }, unitTwoStore);
await network.StartAsync();
这样一个 TCP 端口就能模拟多个 UnitId。
你可以用自己的 client 去读:
dotnet run --project .\samples\YModbus.Sample.TcpClient -- 127.0.0.1 1502 1 0 2
dotnet run --project .\samples\YModbus.Sample.TcpClient -- 127.0.0.1 1502 2 0 2
第一个读 UnitId 1,第二个读 UnitId 2。
这对以后做调试软件很重要。
因为一个好的 Modbus 调试工具,不应该只能当 client,也应该能模拟 server / slave,让别人来连。
SlaveDataStore:模拟数据区
做从站模拟时,最核心的是数据区。
YModbus 里用 ModbusSlaveDataStore 来保存这些数据:
- Coils
- Discrete Inputs
- Holding Registers
- Input Registers
比如:
ModbusSlaveDataStore store = new(pointCount: 100);
store.SetCoil(0, true);
store.SetDiscreteInput(0, true);
store.SetHoldingRegister(0, 1234);
store.SetInputRegister(0, 5678);
主站来读的时候,读到的就是这里面的数据。
如果主站写线圈或写保持寄存器,DataStore 里的值也会跟着变化。
它还暴露了 IModbusSlaveDataStore 接口。
这意味着以后你可以自己实现数据存储,比如:
- 从内存读写
- 从数据库读写
- 和设备状态绑定
- 和 UI 表格绑定
- 和脚本引擎绑定
对调试工具来说,这个接口很有价值。
因为你不一定只想模拟固定数据,可能还想让用户在界面上实时改寄存器值。
寄存器类型转换
Modbus 寄存器是 16 位。
但现场数据经常是:
shortintlongfloatdouble
这些类型需要多个寄存器组合。
YModbus 提供了 RegisterConverter,也提供了一些 typed read / write helper。
比如直接读一个 float:
using YModbus.Clients;
using YModbus.Protocol;
float temperature = await client.ReadHoldingRegisterSingleAsync(
startAddress: 0,
wordOrder: ModbusWordOrder.HighWordFirst,
byteOrder: ModbusByteOrder.BigEndian);
比如写一个 float:
await client.WriteHoldingRegisterSingleAsync(
startAddress: 10,
value: 23.5F,
wordOrder: ModbusWordOrder.HighWordFirst,
byteOrder: ModbusByteOrder.BigEndian);
这里的重点是 wordOrder 和 byteOrder。
如果读出来的数特别离谱,比如温度变成一个很大的数,不要第一时间怀疑库。
先看设备手册里的字节序。
RetryOptions:处理偶发失败
工业通讯现场不一定每次都稳定。
有时候设备忙,有时候网关后面的设备没响应,有时候串口偶发超时。
YModbus 可以通过 ModbusRetryOptions 加重试:
using YModbus.Transports;
ModbusRetryOptions retryOptions = new()
{
RetryCount = 2,
RetryDelayMilliseconds = 100
};
await using ModbusClient client = await ModbusClientFactory.CreateTcpAsync(
"192.168.1.10",
502,
unitId: 1,
retryOptions: retryOptions);
这里 RetryCount = 2 的意思是:第一次请求失败后,最多再尝试 2 次。
重试适合处理偶发问题。
但地址错、站号错、功能码错,重试是救不了的。
批量读写辅助
Modbus 单条报文有数量限制。
比如一次不能无限读几千个寄存器。
如果业务上确实要读一大片地址,可以用 block helper,让库帮你拆成多次请求。
比如:
ushort[] registers = await client.ReadHoldingRegistersInBlocksAsync(
startAddress: 0,
quantity: 1000);
写多个寄存器也有类似方法:
await client.WriteHoldingRegistersInBlocksAsync(0, registers);
这个功能适合采集系统、配置备份、整段寄存器读取。
普通小范围调试时,不一定需要它。
Custom Function:厂家私有功能码
工业设备里经常会有厂家私有功能码。
有些功能不在标准 Modbus 功能码里,但设备手册会告诉你:
使用功能码
0x41,后面跟某某数据。
这种时候,如果库只支持固定功能码,就很难扩展。
YModbus 提供了自定义功能码入口:
ModbusResponse response = await client.ExecuteCustomAsync(
functionCode: 0x41,
payload: new byte[] { 0x00, 0x01 });
从站 / server 侧也可以注册自定义功能码处理器。
这对后面做硬件产品会有用。
因为自己的设备以后可能会有一些标准 Modbus 之外的扩展指令。
CLI:命令行调试
项目里还有 YModbus.Cli。
它适合脚本、自动化测试、快速验证。
比如读保持寄存器:
dotnet run --project .\src\YModbus.Cli\YModbus.Cli.csproj -- read-holding-registers --host 127.0.0.1 --port 502 --unit-id 1 --address 0 --quantity 10
写操作默认是 dry-run,需要加 --confirm 才会真正发出去。
这一点我觉得很重要。
因为工业现场写错参数的风险比读错数据大得多。
Samples:直接跑起来看
库里放了几个 sample:
| 示例 | 作用 |
|---|---|
YModbus.Sample.TcpClient |
TCP client 读保持寄存器 |
YModbus.Sample.TcpSlave |
TCP slave network,模拟多个 UnitId |
YModbus.Sample.RtuClient |
RTU 串口读保持寄存器 |
YModbus.Sample.AsciiClient |
ASCII 串口读保持寄存器 |
YModbus.Sample.RegisterConversion |
寄存器和 float / int32 等类型转换 |
如果你刚开始用这个库,我建议先跑 sample。
TCP 最容易验证:
dotnet run --project .\samples\YModbus.Sample.TcpSlave
dotnet run --project .\samples\YModbus.Sample.TcpClient -- 127.0.0.1 1502 1 0 4
一个终端启动模拟设备,另一个终端读它。
能跑通以后,再接真实设备。
用库时怎么选
可以按这个思路选:
| 你要做什么 | 用什么 |
|---|---|
| TCP 读真实设备 | ModbusClientFactory.CreateTcpAsync |
| RTU 读真实设备 | ModbusSerialClientFactory.CreateRtu |
| 同一个网关后面多个 UnitId | ModbusMultiUnitClient |
| 模拟 TCP 设备 | ModbusTcpSlaveServer 或 ModbusTcpSlaveNetwork |
| 模拟多个 UnitId | ModbusTcpSlaveNetwork |
| 做寄存器表格模拟 | ModbusSlaveDataStore |
| 读写 float / int32 | typed helper 或 RegisterConverter |
| 偶发超时需要容错 | ModbusRetryOptions |
| 命令行快速测试 | YModbus.Cli |
这样看就比较清楚了。
YModbus 不是只解决一个点,而是把 Modbus 调试和开发里常见的几块能力都放到了一起。
写在最后
我做 YModbus,不是想把协议完全藏起来。
Modbus 这种工业协议,完全藏起来反而不好。
因为现场一出问题,你还是要看:
- 功能码
- 地址
- slaveID / unitId
- 报文
- 异常码
- 字节序
所以 YModbus 的方向是:
常用功能简单用,关键细节看得见。
你可以用高级方法快速读写,也可以在需要的时候回到底层报文和协议概念。
这对后面做主站调试工具、从站模拟工具、串口服务器、Modbus 网关、工业物联网模块,都会是一个比较稳的基础。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)