GitHub 项目地址https://github.com/lidecong133/YModbus前面已经讲了 Modbus 协议也写了一篇 YModbus 的快速上手。这一篇不继续讲协议细节重点放在 YModbus 这个库本身。也就是回答一个很直接的问题YModbus 现在到底能做什么如果你准备在自己的上位机、调试工具、采集程序、测试程序里用它这篇可以先看一遍。它不只是一个“读寄存器”的小工具而是准备慢慢做成一套完整的 Modbus 通讯底座。YModbus 分成哪几块现在项目里主要有这几部分项目作用YModbus核心库包含协议、client、TCP transport、功能码、寄存器转换等YModbus.Serial串口支持用来做 Modbus RTU / ASCIIYModbus.Slave从站 / server 模拟用来做 TCP、RTU、ASCII 响应侧YModbus.Cli命令行工具适合脚本、测试、自动化samples示例工程方便直接跑起来看效果如果你只是写一个上位机去读设备大多数时候会用到YModbusYModbus.Serial如果你要做设备模拟、调试软件里的从站模拟、网关测试就会用到YModbus.Slave支持哪些通讯方式YModbus 目前支持三种常见 Modbus 形式通讯方式常见场景Modbus RTU串口、RS485、USB 转 RS485Modbus TCP以太网、串口服务器、网关、PLCModbus ASCII老设备、特殊串口场景这三个方式底层报文不一样但上层读写方法尽量保持一致。比如读保持寄存器不管是 RTU 还是 TCP最后都是ushort[]registersawaitclient.ReadHoldingRegistersAsync(0,4);这样用起来会轻松很多。你不用在业务代码里到处判断 RTU 怎么拼帧、TCP 怎么加 MBAP Header、ASCII 怎么转字符。这些底层细节交给库处理。Client主动读写设备最常用的就是 client。它适合这种场景我的程序主动去读设备、写设备。比如上位机读仪表、采集软件读 IO 模块、调试工具读 PLC 或网关。YModbus 的ModbusClient是面向单个slaveID/unitId的。比如 RTUusingSystem.IO.Ports;usingYModbus.Clients;usingYModbus.Serial;usingSerialPortportnew(COM3){BaudRate9600,DataBits8,ParityParity.None,StopBitsStopBits.One};port.Open();awaitusingModbusClientclientModbusSerialClientFactory.CreateRtu(slaveId:1,serialPort:port,leaveOpen:true);ushort[]valuesawaitclient.ReadHoldingRegistersAsync(0,4);比如 TCPusingYModbus.Clients;awaitusingModbusClientclientawaitModbusClientFactory.CreateTcpAsync(host:192.168.1.10,port:502,unitId:1);ushort[]valuesawaitclient.ReadHoldingRegistersAsync(0,4);这里有个命名点要注意。Modbus TCP 里的 client/server 是网络角色不是简单等于主站/从站。不过在使用上你可以先这样理解ModbusClient主动发请求的一侧远端设备或模拟器接收请求并返回响应的一侧常用读写功能YModbus 里常用功能码都有对应方法。功能码方法说明01ReadCoilsAsync读线圈02ReadDiscreteInputsAsync读离散输入03ReadHoldingRegistersAsync读保持寄存器04ReadInputRegistersAsync读输入寄存器05WriteSingleCoilAsync写单个线圈06WriteSingleRegisterAsync写单个保持寄存器15/0x0FWriteMultipleCoilsAsync写多个线圈16/0x10WriteMultipleRegistersAsync写多个保持寄存器23/0x17ReadWriteMultipleRegistersAsync一条报文里读写保持寄存器比如写一个保持寄存器awaitclient.WriteSingleRegisterAsync(100,123);比如写多个保持寄存器ushort[]registersnewushort[]{1,2,3};awaitclient.WriteMultipleRegistersAsync(100,registers);这些方法的参数都尽量贴近 Modbus 协议本身。比如地址用的是协议地址通常从0开始。如果设备手册写40001代码里很多时候应该填0不是40001。ModbusClient 和 ModbusMultiUnitClientYModbus 里有两个容易混的 clientModbusClientModbusMultiUnitClient简单说类型适合场景ModbusClient创建时就固定一个slaveID/unitIdModbusMultiUnitClient每次调用方法时再传入slaveID/unitId这个名字是故意这样取的。它不是想表达 TCP 里的 client/server也不是想重新解释主站/从站。它只表达一件事同一个通讯连接里可以访问多个 UnitId 或 slaveID。比如你只读一个设备awaitusingModbusClientclientawaitModbusClientFactory.CreateTcpAsync(192.168.1.10,502,unitId:1);ushort[]valuesawaitclient.ReadHoldingRegistersAsync(0,10);如果你通过一个 TCP 转 RTU 网关后面挂了多个设备就可以用ModbusMultiUnitClientawaitusingModbusMultiUnitClientmultiUnitClientawaitModbusClientFactory.CreateTcpMultiUnitAsync(192.168.1.10,502);ushort[]unit1awaitmultiUnitClient.ReadHoldingRegistersAsync(1,0,10);ushort[]unit2awaitmultiUnitClient.ReadHoldingRegistersAsync(2,0,10);这时每次调用时传入的第一个参数就是目标 UnitId。对网关、多设备轮询、多站号采集来说这个会比较方便。SerialRTU 和 ASCII 串口支持核心库YModbus本身不直接依赖SerialPort。串口相关的东西放在YModbus.Serial里。这样做的好处是边界更清楚核心库负责 Modbus 协议和 TCP串口扩展库负责把SerialPort接进来RTU 创建方式awaitusingModbusClientclientModbusSerialClientFactory.CreateRtu(slaveId:1,serialPort:port,leaveOpen:true);ASCII 创建方式awaitusingModbusClientclientModbusSerialClientFactory.CreateAscii(slaveId:1,serialPort:port,leaveOpen:true);大多数新设备用 RTU 更多ASCII 相对少一些。但既然工业现场会遇到老设备库里还是把 ASCII 留出来。Slave / Server模拟设备YModbus 不只支持主动读写也支持响应侧。也就是可以让你的程序模拟一个 Modbus 设备。这个功能很适合下面这些场景调试主站软件做上位机联调模拟仪表数据测试 Modbus TCP 网关做自动化测试做主站/从站调试工具比如启动一个 TCP slave network模拟两个 UnitIdusingSystem.Net;usingYModbus.Slave;ModbusSlaveDataStoreunitOneStorenew(pointCount:100);unitOneStore.SetHoldingRegister(0,1234);unitOneStore.SetHoldingRegister(1,5678);ModbusSlaveDataStoreunitTwoStorenew(pointCount:100);unitTwoStore.SetHoldingRegister(0,2222);unitTwoStore.SetHoldingRegister(1,3333);awaitusingModbusTcpSlaveNetworknetworknew(newModbusTcpSlaveNetworkOptions{ListenAddressIPAddress.Loopback,Port1502});network.AddSlave(newModbusSlaveDefinition{UnitId1},unitOneStore);network.AddSlave(newModbusSlaveDefinition{UnitId2},unitTwoStore);awaitnetwork.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第一个读 UnitId1第二个读 UnitId2。这对以后做调试软件很重要。因为一个好的 Modbus 调试工具不应该只能当 client也应该能模拟 server / slave让别人来连。SlaveDataStore模拟数据区做从站模拟时最核心的是数据区。YModbus 里用ModbusSlaveDataStore来保存这些数据CoilsDiscrete InputsHolding RegistersInput Registers比如ModbusSlaveDataStorestorenew(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。比如直接读一个 floatusingYModbus.Clients;usingYModbus.Protocol;floattemperatureawaitclient.ReadHoldingRegisterSingleAsync(startAddress:0,wordOrder:ModbusWordOrder.HighWordFirst,byteOrder:ModbusByteOrder.BigEndian);比如写一个 floatawaitclient.WriteHoldingRegisterSingleAsync(startAddress:10,value:23.5F,wordOrder:ModbusWordOrder.HighWordFirst,byteOrder:ModbusByteOrder.BigEndian);这里的重点是wordOrder和byteOrder。如果读出来的数特别离谱比如温度变成一个很大的数不要第一时间怀疑库。先看设备手册里的字节序。RetryOptions处理偶发失败工业通讯现场不一定每次都稳定。有时候设备忙有时候网关后面的设备没响应有时候串口偶发超时。YModbus 可以通过ModbusRetryOptions加重试usingYModbus.Transports;ModbusRetryOptionsretryOptionsnew(){RetryCount2,RetryDelayMilliseconds100};awaitusingModbusClientclientawaitModbusClientFactory.CreateTcpAsync(192.168.1.10,502,unitId:1,retryOptions:retryOptions);这里RetryCount 2的意思是第一次请求失败后最多再尝试 2 次。重试适合处理偶发问题。但地址错、站号错、功能码错重试是救不了的。批量读写辅助Modbus 单条报文有数量限制。比如一次不能无限读几千个寄存器。如果业务上确实要读一大片地址可以用 block helper让库帮你拆成多次请求。比如ushort[]registersawaitclient.ReadHoldingRegistersInBlocksAsync(startAddress:0,quantity:1000);写多个寄存器也有类似方法awaitclient.WriteHoldingRegistersInBlocksAsync(0,registers);这个功能适合采集系统、配置备份、整段寄存器读取。普通小范围调试时不一定需要它。Custom Function厂家私有功能码工业设备里经常会有厂家私有功能码。有些功能不在标准 Modbus 功能码里但设备手册会告诉你使用功能码0x41后面跟某某数据。这种时候如果库只支持固定功能码就很难扩展。YModbus 提供了自定义功能码入口ModbusResponseresponseawaitclient.ExecuteCustomAsync(functionCode:0x41,payload:newbyte[]{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.TcpClientTCP client 读保持寄存器YModbus.Sample.TcpSlaveTCP slave network模拟多个 UnitIdYModbus.Sample.RtuClientRTU 串口读保持寄存器YModbus.Sample.AsciiClientASCII 串口读保持寄存器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.CreateTcpAsyncRTU 读真实设备ModbusSerialClientFactory.CreateRtu同一个网关后面多个 UnitIdModbusMultiUnitClient模拟 TCP 设备ModbusTcpSlaveServer或ModbusTcpSlaveNetwork模拟多个 UnitIdModbusTcpSlaveNetwork做寄存器表格模拟ModbusSlaveDataStore读写 float / int32typed helper 或RegisterConverter偶发超时需要容错ModbusRetryOptions命令行快速测试YModbus.Cli这样看就比较清楚了。YModbus 不是只解决一个点而是把 Modbus 调试和开发里常见的几块能力都放到了一起。写在最后我做 YModbus不是想把协议完全藏起来。Modbus 这种工业协议完全藏起来反而不好。因为现场一出问题你还是要看功能码地址slaveID / unitId报文异常码字节序所以 YModbus 的方向是常用功能简单用关键细节看得见。你可以用高级方法快速读写也可以在需要的时候回到底层报文和协议概念。这对后面做主站调试工具、从站模拟工具、串口服务器、Modbus 网关、工业物联网模块都会是一个比较稳的基础。