简介NMODBUS.zip 是一套面向 C# 开发者的 MODBUS TCP/IP 通信库资源适合刚接触工业自动化协议、需要快速与 PLC 建立数据交互的新手与中级开发者。它封装了 ModbusTcpMaster 等核心类提供读写保持寄存器、线圈以及数据转换、异常处理等常用能力可帮助读者省去从零实现协议栈的繁琐过程直接集成到上位机或采集项目中。压缩包共 30 个文件约 529KB包含 5 个 dll 动态库、5 个 xml 文档、3 个 cs 源码、3 个 config 配置、2 个 exe 示例程序以及 nupkg 包、sln 解决方案和 csproj 工程文件覆盖从引用库到运行示例的完整链路。目录中可见 ModbusTcpip 示例工程与 NModbus 3.0.62 依赖便于对照理解客户端创建、请求发送与响应解析流程。目前已有 305 人学习适合作为 MODBUS 通信入门与项目集成的参考素材。1. 从一次现场联调说起NMODBUS.zip 到底能解决什么去年冬天某产线改造项目PLC 侧用的是 Modbus TCP上位机是 C# 写的 WinForm 监控界面。现场调试时发现用现成的商业库要么授权费用高要么在 .NET Framework 4.0 环境下跑不起来。折腾了两天最后翻出一个叫 NMODBUS 的开源库解压后直接引用 DLL半小时跑通读写保持寄存器。那次之后我手里常备一份 NMODBUS.zip遇到 Modbus TCP/IP 的活儿基本先拿它试。NMODBUS 是一个纯 C# 实现的 Modbus 协议库支持 Modbus TCP、Modbus RTU、Modbus ASCII 三种模式覆盖主站Master和从站Slave两种角色。它不依赖任何第三方组件源码结构清晰适合嵌入到工控上位机、数据采集网关、设备模拟器等场景。如果你正在用 C# 做 Modbus TCP/IP 通信又不想被商业库的授权和平台限制卡住这份源码包值得拆开看看。它适合谁一是做工业自动化上位机的开发者需要快速读写 PLC 寄存器二是做设备模拟测试的工程师要用从站模式模拟一台 Modbus 设备三是想理解 Modbus 协议帧结构的学习者源码里把功能码、事务标识、单元标识都拆得很清楚。不适合谁如果你的项目要求跨平台到 Linux 且用 .NET Core需要确认版本兼容性如果只是偶尔发一两条 Modbus 指令用串口调试助手可能更快。2. 拆开 NMODBUS.zip源码结构、核心类与选型理由2.1 源码目录里有什么解压 NMODBUS.zip 后通常能看到这样的结构不同版本略有差异以实际为准NMODBUS/ ├── Modbus/ │ ├── Device/ │ │ ├── ModbusMaster.cs │ │ ├── ModbusSlave.cs │ │ └── ModbusSerialMaster.cs │ ├── Data/ │ │ ├── DataStore.cs │ │ └── RegisterEventArgs.cs │ ├── IO/ │ │ ├── ModbusTransport.cs │ │ └── TcpTransport.cs │ └── Utility/ │ └── ModbusUtility.cs ├── Samples/ │ ├── TcpMasterSample.cs │ └── TcpSlaveSample.cs └── NMODBUS.csproj核心类集中在Modbus.Device命名空间下。ModbusIpMaster是 TCP 主站入口ModbusSerialMaster是串口主站入口ModbusSlave用于从站模拟。DataStore是从站的数据存储容器负责维护线圈、离散输入、保持寄存器、输入寄存器的值。2.2 为什么选 NMODBUS 而不是自己写 Socket自己用TcpClient拼 Modbus TCP 帧不是不行但有几个坑事务标识符Transaction ID需要自己维护递增多线程并发时容易重复异常码解析要对照协议表粘包处理需要自己写缓冲区。NMODBUS 把这些都封装好了对外暴露的是ReadHoldingRegisters、WriteSingleCoil这类方法调用时不用关心底层字节序。另一个选型理由是它的从站实现。很多轻量库只做 Master但 NMODBUS 的ModbusSlave配合DataStore可以快速搭一个模拟设备用来测试上位机逻辑不用真接 PLC。这在开发阶段很省事。2.3 环境准备与引用方式NMODBUS 通常以源码形式分发可以直接把Modbus文件夹拖进你的项目也可以编译成 DLL 后引用。我一般用后者方便多个项目复用。# 假设已安装 .NET SDK进入源码目录 cd NMODBUS dotnet build -c Release # 编译产物在 bin/Release/ 下找到 NMODBUS.dll如果项目是 .NET Framework 4.0 及以上用 Visual Studio 新建类库项目把源码文件添加进去编译即可。注意部分老版本 NMODBUS 使用了System.IO.Ports在 .NET Core 下需要额外安装System.IO.PortsNuGet 包。提示编译前确认目标框架。.NET Framework 项目直接引用源码最省事.NET Core / .NET 5 项目建议先编译成 DLL再通过项目引用或 NuGet 本地源引入。3. 用 NMODBUS 写一个 Modbus TCP 主站连接、读写与参数配置3.1 建立 TCP 连接与主站实例Modbus TCP 默认端口是 502但现场设备经常改成 503 或 1502。连接时用TcpClient指定 IP 和端口然后传给ModbusIpMaster.CreateIp。using System.Net.Sockets; using Modbus.Device; // 创建 TCP 客户端连接目标设备 var tcpClient new TcpClient(192.168.1.10, 502); // 基于 TCP 客户端创建 Modbus 主站 var master ModbusIpMaster.CreateIp(tcpClient); // 设置超时避免网络异常时线程卡死 tcpClient.ReceiveTimeout 2000; tcpClient.SendTimeout 2000;ModbusIpMaster.CreateIp接收一个TcpClient对象内部会创建TcpTransport处理收发。超时设置建议放在TcpClient上而不是 Modbus 层因为底层 Socket 超时更直接。如果现场网络抖动大ReceiveTimeout 可以放宽到 3000 毫秒。3.2 读保持寄存器的参数怎么填读保持寄存器用ReadHoldingRegisters两个参数起始地址和数量。注意这里的地址是协议地址不是 PLC 手册上的 Modbus 地址。比如手册写 40001对应协议地址 0写 40010对应协议地址 9。// 从站地址单元标识为 1起始地址 0读 10 个寄存器 byte slaveId 1; ushort startAddress 0; ushort numRegisters 10; ushort[] registers master.ReadHoldingRegisters(slaveId, startAddress, numRegisters); // 遍历输出实际项目中通常按业务解析 for (int i 0; i registers.Length; i) { Console.WriteLine($寄存器 {startAddress i}: {registers[i]}); }slaveId在 Modbus TCP 里对应 MBAP 头中的单元标识如果设备不支持多从站填 1 即可。startAddress是 0 基地址numRegisters一次最多读 125 个协议限制超了会抛异常。读回来的ushort[]每个元素是一个 16 位寄存器值32 位浮点数需要自己拼两个寄存器。3.3 写单个线圈与写多个寄存器写线圈用WriteSingleCoil值用bool写多个寄存器用WriteMultipleRegisters传ushort[]。// 写单个线圈地址 5置为 true master.WriteSingleCoil(1, 5, true); // 写多个保持寄存器起始地址 10写入 3 个值 ushort[] values { 100, 200, 300 }; master.WriteMultipleRegisters(1, 10, values);写操作没有返回值成功即静默。如果从站返回异常码NMODBUS 会抛SlaveException里面包含SlaveExceptionCode可以据此判断是非法地址还是非法数据值。实际项目中建议用 try-catch 包住记录日志。3.4 从站模拟用 DataStore 搭一个虚拟设备开发阶段没有真实 PLC 时可以用 NMODBUS 的从站功能模拟。核心是DataStore和ModbusSlave。using Modbus.Data; using Modbus.Device; // 创建数据存储默认包含线圈、离散输入、保持寄存器、输入寄存器 var dataStore DataStoreFactory.CreateDefaultDataStore(); // 初始化保持寄存器前 10 个值 for (ushort i 0; i 10; i) { dataStore.HoldingRegisters[i] (ushort)(i * 10); } // 监听 502 端口创建从站 var listener new TcpListener(System.Net.IPAddress.Any, 502); listener.Start(); var slave ModbusTcpSlave.CreateTcp(1, listener); slave.DataStore dataStore; slave.Listen();DataStoreFactory.CreateDefaultDataStore()会初始化四个数据区默认大小各 100 个。ModbusTcpSlave.CreateTcp的第一个参数是从站地址第二个是TcpListener。Listen()是阻塞调用通常放在独立线程里。这样上位机连上来读写数据就从dataStore里取方便验证逻辑。4. 避坑与排查NMODBUS 实战中容易翻车的五个点4.1 现象读寄存器返回异常码 0x02原因起始地址或数量超出从站支持范围。很多 PLC 的保持寄存器只开放前 100 个你从 200 开始读从站直接回非法数据地址。解决先确认从站手册里的寄存器映射表把地址换算成 0 基协议地址。不确定时从地址 0 读 1 个寄存器试起逐步加数量。4.2 现象连接成功但读写超时原因Modbus TCP 的单元标识填错或者目标端口不是 502。有些网关设备把 Modbus TCP 映射到非标端口但默认按 502 连。解决用TcpClient先测端口通不通再确认slaveId。如果设备文档写“站号 2”那slaveId就填 2不要填 1。4.3 现象多线程调用时数据错乱原因ModbusIpMaster实例不是线程安全的。多个线程同时调ReadHoldingRegisters事务标识符会冲突返回的数据可能串包。解决每个线程创建独立的TcpClient和ModbusIpMaster或者加锁串行化。我一般用lock包住读写操作简单有效。4.4 现象32 位浮点数解析出来是乱值原因Modbus 寄存器是 16 位的32 位浮点需要两个寄存器拼接且字节序有 ABCD、CDAB、BADC、DCBA 四种。NMODBUS 返回的是ushort[]不负责浮点转换。解决确认设备用的是哪种字节序用BitConverter手动拼。常见做法是先把两个ushort转成byte[]再按顺序组合后转float。4.5 现象从站模拟时上位机连不上原因TcpListener绑定的是IPAddress.Any但防火墙拦了 502 端口或者slave.Listen()没在独立线程里跑主线程被阻塞。解决检查防火墙入站规则放行 502 端口。Listen()用Task.Run包起来避免阻塞 UI 线程。注意NMODBUS 的从站模拟仅用于测试不要直接暴露在公网环境。生产环境建议加访问控制。5. 进阶技巧用 NMODBUS 做批量采集与异常重连5.1 批量读取的合并策略一条 Modbus TCP 报文最多读 125 个寄存器。如果业务需要读 500 个寄存器不要分 4 次从地址 0、125、250、375 读而是先确认这些寄存器在从站里是否连续。连续的话分 4 次读不连续的话按块合并减少请求次数。// 按 125 个一组分批读取避免单次超限 ushort totalStart 0; ushort totalCount 500; ushort batchSize 125; for (ushort offset 0; offset totalCount; offset batchSize) { ushort count (ushort)Math.Min(batchSize, totalCount - offset); ushort[] batch master.ReadHoldingRegisters(1, (ushort)(totalStart offset), count); // 处理 batch存入本地缓存 }Math.Min保证最后一批不超限。每批之间可以加Thread.Sleep(10)给从站喘息时间尤其是低端 PLC连续请求太快会丢包。5.2 断线重连的封装现场网络不稳定TcpClient断开后ModbusIpMaster不会自动重连。我一般封装一个ModbusClientWrapper在读写异常时重建连接。public class ModbusClientWrapper { private TcpClient _tcpClient; private ModbusIpMaster _master; private string _ip; private int _port; public ModbusClientWrapper(string ip, int port) { _ip ip; _port port; Connect(); } private void Connect() { _tcpClient?.Close(); _tcpClient new TcpClient(_ip, _port); _tcpClient.ReceiveTimeout 2000; _master ModbusIpMaster.CreateIp(_tcpClient); } public ushort[] ReadHoldingRegisters(byte slaveId, ushort start, ushort count) { try { return _master.ReadHoldingRegisters(slaveId, start, count); } catch (Exception) { // 发生异常时重连一次再试 Connect(); return _master.ReadHoldingRegisters(slaveId, start, count); } } }这个封装把重连逻辑收在ReadHoldingRegisters里调用方不用关心连接状态。注意重连只做一次如果第二次还失败就让异常抛出去避免死循环。5.3 验证方法用 Modbus Poll 对照写完代码后怎么确认读上来的值是对的我习惯用 Modbus Poll 这类工具同时连同一个从站对比两边读到的寄存器值。如果 Modbus Poll 读出来是 1234你的代码读出来也是 1234说明地址和解析没问题。如果 Modbus Poll 能读到你读不到检查slaveId和起始地址。另一个验证手段是用 NMODBUS 的从站模拟自己写值自己读闭环测试。比如在dataStore.HoldingRegisters[0]里写 999然后主站读地址 0看是不是 999。这个法子能排除 PLC 侧的问题快速定位是代码问题还是设备问题。从那以后我每次拿到新的 Modbus 设备都先用 NMODBUS 从站模拟跑一遍读写闭环确认代码逻辑没问题再接真实设备。这样现场调试时少了很多“到底是代码还是设备”的纠结。希望帮到你。本文还有配套的精品资源点击获取