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 示例工程,方便直接跑起来看效果

如果你只是写一个上位机去读设备,大多数时候会用到:

  • YModbus
  • YModbus.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:

  • ModbusClient
  • ModbusMultiUnitClient

简单说:

类型 适合场景
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 位。

但现场数据经常是:

  • short
  • int
  • long
  • float
  • double

这些类型需要多个寄存器组合。

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);

这里的重点是 wordOrderbyteOrder

如果读出来的数特别离谱,比如温度变成一个很大的数,不要第一时间怀疑库。

先看设备手册里的字节序。

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 设备 ModbusTcpSlaveServerModbusTcpSlaveNetwork
模拟多个 UnitId ModbusTcpSlaveNetwork
做寄存器表格模拟 ModbusSlaveDataStore
读写 float / int32 typed helper 或 RegisterConverter
偶发超时需要容错 ModbusRetryOptions
命令行快速测试 YModbus.Cli

这样看就比较清楚了。

YModbus 不是只解决一个点,而是把 Modbus 调试和开发里常见的几块能力都放到了一起。

写在最后

我做 YModbus,不是想把协议完全藏起来。

Modbus 这种工业协议,完全藏起来反而不好。

因为现场一出问题,你还是要看:

  • 功能码
  • 地址
  • slaveID / unitId
  • 报文
  • 异常码
  • 字节序

所以 YModbus 的方向是:

常用功能简单用,关键细节看得见。

你可以用高级方法快速读写,也可以在需要的时候回到底层报文和协议概念。

这对后面做主站调试工具、从站模拟工具、串口服务器、Modbus 网关、工业物联网模块,都会是一个比较稳的基础。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐