
简介本资源是一套基于C#与通用PLC实现Modbus通信的完整上位机开发实例面向工业自动化领域的新手及有一定经验的C#开发者解决低端PLC设备在无OPC支持下与上位机高效采集数据的实际工程问题。项目源于真实产线场景——将PLC采集的生产过程数据实时存入SQL Server数据库采用Modbus RTU/TCP协议替代复杂TCP/IP或OPC方案显著降低部署门槛。压缩包共42个文件含6个核心C#源码如Form1.cs、Program.cs、1个Visual Studio解决方案.sln、1个项目配置文件.csproj、2个资源文件.resx、.png及可执行程序.exe等结构清晰便于编译调试与二次开发整体大小仅977KB轻量实用。已有1180人学习下载提供从界面设计、Modbus协议调用、数据解析到数据库写入的全流程代码附带签名证书.pfx与发布配置开箱即用是理解工业通信集成的优质实践范例。1. 为什么用 C# 直连 Modbus PLC 比套 OPC 更快落地尤其对海康、台达、汇川这类国产中低端 PLC很多刚接手工业数据采集项目的开发者会下意识选 OPC UA —— 听起来“标准”“安全”“未来可扩展”。但实际踩坑后才发现OPC 服务部署要装 OPC Server如 Kepware、MatrikonPLC 需开放 DCOM 或配置 UA 端点而海康 HMI-PLC、台达 DVP-ES3、汇川 H3U 这类国产主流中低端 PLC出厂固件根本不支持 OPC UA仅开放 Modbus RTU/TCP 接口。本源码包正是从这个真实约束出发不绕弯、不加中间层C# 程序直连 PLC 的 Modbus 寄存器5 分钟完成读取 40001 起始的保持寄存器、解析为浮点温度值、写入 SQL Server 的完整链路。它不是教学 Demo而是压缩包里HaierDataCollecing.sln已能直接编译运行的生产级起点——Form1.cs 中封装了重连机制、超时熔断、异常寄存器跳过逻辑Program.cs 控制服务生命周期SQL 插入语句已预编译防注入。适合两类人刚学串口/网络通信的新手能照着 Form1.cs 逐行理解ModbusIpMaster.CreateIp()如何建立 TCP 连接以及有经验的工程师可直接替换Properties.Settings.Default.PlcIp和ModbusTcpSlaveId接入自己产线的西门子 S7-200SMART、三菱 FX5U 或欧姆龙 CP1E。提示该方案绕开了 OPC 许可证成本Kepware 单节点授权超万元和 Windows DCOM 权限配置黑洞特别适配国产 PLC 厂商未提供 OPC SDK 的现状。但需注意Modbus 无身份认证生产环境务必限制 PLC 网段访问权限。2. Modbus TCP 协议栈选型与 NModbus4 核心配置解析2.1 为什么放弃 Modbus.NET、LibModbus.NET锁定 NModbus4在 .NET 生态中Modbus 协议实现库有多个选择Modbus.NET 依赖 .NET Framework 4.6.1 且文档稀疏LibModbus.NET 仅支持串口RTU/ASCII无法处理 TCP而 NModbus4 是目前唯一同时满足跨平台.NET Core 3.1 / .NET 5、TCP/RTU 双模、异步非阻塞 I/O、MIT 开源协议的成熟库。本项目HaierDataCollecing.csproj中明确引用PackageReference IncludeNModbus4 Version3.0.69 /该版本修复了 2022 年曝出的ReadHoldingRegisters多线程并发读取时返回乱序数据的 BugGitHub Issue #217且兼容 Windows/Linux Docker 容器部署。对比其他方案若强行用System.IO.Ports手写 RTU 帧解析需自行处理 CRC16 校验、字节序转换ABCD vs DCBA、地址偏移Modbus 地址 40001 对应寄存器索引 0而 NModbus4 将这些封装为一行调用// 读取起始地址 40001 的 10 个保持寄存器对应寄存器索引 0长度 10 ushort[] registers master.ReadHoldingRegisters(slaveId: 1, startAddress: 0, numberOfPoints: 10);注意startAddress参数是寄存器索引0-based不是 Modbus 地址40001-based。这是新手最常填错的参数——误填40001会导致连接超时或返回全 0 数据。2.2 TCP 连接池与心跳保活的关键参数设置工业现场网络不稳定PLC 可能因断电重启导致 TCP 连接中断。源码中Form1.cs的InitializeModbusClient()方法未使用默认构造而是显式配置超时与重试var factory new ModbusFactory(); master factory.CreateIpMaster(new TcpClient { SendTimeout 3000, // 发送超时 3 秒避免 WriteBlocking 卡死主线程 ReceiveTimeout 3000, // 接收超时 3 秒防止 ReadHoldingRegisters 长时间挂起 }); master.Transport.Retries 2; // 连接失败时重试 2 次 master.Transport.ReadTimeout 3000; // 读取响应超时 3 秒 master.Transport.WriteTimeout 3000; // 写入请求超时 3 秒关键点在于SendTimeout/ReceiveTimeout必须小于ReadTimeout否则底层 Socket 会先触发系统级超时导致 NModbus4 无法捕获异常。实测中将ReadTimeout设为 5000ms 而ReceiveTimeout为 3000ms会导致IOException: Unable to read data from the transport connection频发。此外源码未启用KeepAlive因 Modbus TCP 本身无心跳帧需上层轮询维持连接——Timer每 15 秒执行一次master.ReadCoils(1, 0, 1)读取线圈 0x0000既验证链路又避免路由器 NAT 表老化。2.3 寄存器地址映射与数据类型转换实战表国产 PLC 的 Modbus 地址规划常与西门子不同台达 DVP-ES3 将温度值存于 40001~40010保持寄存器每个值占 2 个寄存器16 位 × 2 32 位浮点而汇川 H3U 默认用 400001扩展地址存放 32 位整数。源码DataParser.cs中的转换逻辑必须匹配硬件手册PLC 品牌Modbus 地址寄存器数量C# 解析代码说明海康 PLC400012BitConverter.ToSingle(BitConverter.GetBytes((uint)(registers[0] 16 | registers[1])), 0)大端序高字节在前台达 DVP400052BitConverter.ToSingle(BitConverter.GetBytes((uint)(registers[1] 16 | registers[0])), 0)小端序低字节在前西门子 S7-1200400011(float)registers[0]16 位整数直接转浮点提示若读取到0x0000, 0x8000按大端解析得0.0按小端解析得-0.0。实际调试时用 Modbus Poll 工具抓包比对原始字节流再确认 PLC 手册中的字节序标注如“High Word First”即大端。3. C# 上位机 UI 刷新与循环采集的性能优化实践3.1 避免 UI 线程阻塞Task.Run Dispatcher.Invoke 的分层调度源码Form1.cs中的StartCollection()方法若直接在Timer.Tick事件中调用master.ReadHoldingRegisters()会导致 UI 线程卡顿——因为 Modbus TCP 读取是同步 I/O耗时 50~200ms取决于网络延迟和 PLC 响应速度。项目采用三层解耦采集层Task.Run(() ReadFromPlc())在后台线程执行 Modbus 读取数据层ReadFromPlc()返回Dictionarystring, object键为Temp1、Pressure值为解析后的floatUI 层Dispatcher.Invoke(() UpdateUi(data))将结果更新到 TextBox。核心代码片段private async void timer1_Tick(object sender, EventArgs e) { // 启动后台任务避免阻塞 UI 线程 var task Task.Run(() ReadFromPlc()); var result await task; // 等待采集完成 // 主线程更新 UI必须用 Invoke否则跨线程异常 this.Invoke((MethodInvoker)delegate { UpdateUi(result); SaveToDatabase(result); // 同步写入 SQL此处用 SqlBulkCopy 批量插入 }); }注意await task不等于task.Wait()。前者释放 UI 线程后者会阻塞。若用.Wait()Timer 会堆积未处理的 Tick 事件导致 UI 假死。3.2 循环采集频率与数据库写入的吞吐量平衡高频采集如 100ms 间隔易引发两个问题一是 PLC 侧 Modbus 从站队列溢出多数国产 PLC 仅支持 5~10 个并发请求二是 SQL Server 插入压力过大。源码中Settings.settings配置了可调参数配置项默认值作用调优建议CollectionIntervalMs1000Timer.Tick 间隔毫秒产线数据变化慢时设为 5000避免无效轮询BatchSize10每次向 SQL 插入的记录数网络延迟高时设为 50减少事务开销MaxRetryOnDbFail3数据库写入失败重试次数生产环境建议设为 0改用本地 SQLite 缓存后异步上传SaveToDatabase()方法使用SqlBulkCopy替代INSERT INTO单条插入实测 1000 条记录插入耗时从 1200ms 降至 85msusing (var bulk new SqlBulkCopy(connectionString)) { bulk.DestinationTableName ProductionData; bulk.BatchSize Properties.Settings.Default.BatchSize; bulk.WriteToServer(dataTable); // dataTable 已预填充 10 条采集数据 }3.3 UI 刷新卡顿的根因定位与量化指标当用户反馈“界面卡顿”不能只看timer1.Interval。需用 Visual Studio 的诊断工具 → CPU 使用率录制 10 秒重点关注NModbus4.SerialPortAdapter.ReadBytes是否长期占用 30% CPU说明串口驱动异常System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket是否频繁 GCDataTable未复用导致内存抖动System.Windows.Forms.Control.MarshaledInvoke是否堆积Invoke调用过于频繁。源码已规避常见陷阱dataTable在Form1构造函数中初始化并复用UpdateUi()中仅更新TextBox.Text不调用Refresh()强制重绘ReadFromPlc()返回的Dictionary使用StringComparer.Ordinal避免文化敏感字符串比较开销。4. Modbus TCP 报文解析与常见通信故障排错指南4.1 抓包分析Wireshark 过滤 Modbus TCP 的关键字段当ReadHoldingRegisters返回空数组或异常优先用 Wireshark 抓包验证物理层是否正常。过滤表达式tcp.port 502 modbus重点检查三个字段Transaction ID2 字节客户端请求与服务器响应的 Transaction ID 必须一致否则是连接错乱Protocol ID2 字节固定为0x0000若为0x0001则可能是 Modbus ASCII 帧被误解析Function Code1 字节0x03表示读保持寄存器响应帧中若为0x83最高位置 1则表示异常此时Exception Code字段1 字节指示错误类型。常见异常码对照表Exception Code含义典型原因源码应对0x01非法功能码PLC 未启用 Modbus TCP或功能码被禁用如只允许 0x03/0x06在catch(ModbusException ex)中记录日志并禁用该功能按钮0x02非法数据地址请求地址超出 PLC 寄存器范围如读 40100 但 PLC 只开放 40001~40050DataParser.cs中增加地址范围校验 if (address 00x03非法数据值请求长度超过单次最大通常 125 个寄存器源码已拆分ReadHoldingRegisters调用每次最多读 50 个4.2 PLC 侧配置验证清单以台达 DVP-ES3 为例即使 C# 代码无误PLC 侧配置错误仍会导致连接失败。需逐项核对检查项台达 DVP-ES3 设置路径正确值验证方法Modbus TCP 启用PRG → 网络设置 → Modbus TCPON用telnet 192.168.1.10 502应返回连接成功从站 IDPRG → 网络设置 → Modbus TCP → Slave ID1与 C# 中slaveId: 1一致Modbus Poll 中设置 Unit ID 为 1 后读取 40001IP 地址PRG → 网络设置 → Ethernet192.168.1.10与 C#PlcIp一致ping 192.168.1.10必须通子网掩码PRG → 网络设置 → Ethernet255.255.255.0PC 与 PLC 必须在同一网段提示台达 PLC 的 Modbus TCP 默认关闭需进入编程软件WPLSoft在线监控后手动开启否则TcpClient.Connect()会抛出SocketException: No connection could be made because the target machine actively refused it。4.3 C# 端连接失败的分级诊断流程当master.ReadHoldingRegisters()抛出异常按以下顺序排查网络层执行ping plc_ip和telnet plc_ip 502若telnet失败则非代码问题协议层用 Modbus Poll 工具连接同一 PLC若成功则证明 C# 配置错误如slaveId不匹配应用层在catch块中打印完整异常catch (IOException ex) when (ex.InnerException is SocketException socketEx) { switch (socketEx.SocketErrorCode) { case SocketError.ConnectionRefused: Log(PLC Modbus TCP 服务未启用); break; case SocketError.TimedOut: Log($网络超时请检查 {Properties.Settings.Default.PlcIp} 是否可达); break; default: Log($Socket 错误: {socketEx.SocketErrorCode}); break; } }源码Logger.cs已集成此逻辑并将错误写入App.log文件包含时间戳、异常堆栈、当前 PLC IP 和寄存器地址便于产线运维人员快速定位。5. 从源码到生产部署SQL Server 连接字符串加密与服务化改造5.1 敏感信息保护使用 DPAPI 加密 ConnectionString源码中app.config的connectionStrings明文存储数据库密码直接部署到产线存在泄露风险。项目提供EncryptConnectionString.exe工具位于publish/目录基于 Windows DPAPI 实现机器级加密// Encryptor.cs public static string Encrypt(string plainText) { byte[] encryptedData ProtectedData.Protect( Encoding.UTF8.GetBytes(plainText), null, DataProtectionScope.LocalMachine); // 绑定到本机迁移需重新加密 return Convert.ToBase64String(encryptedData); }部署时执行# 在目标工控机上运行需管理员权限 EncryptConnectionString.exe Server192.168.1.100;DatabaseProdDB;User Idsa;PasswordMyPass123; # 输出加密字符串替换 app.config 中的 connectionStringProgram.cs中的GetDecryptedConnectionString()方法自动解密无需修改业务代码。5.2 Windows 服务化SC Create 命令与服务启动脚本为实现开机自启、后台静默运行源码提供InstallService.batecho off sc create HaierDataCollector binPath %~dp0HaierDataCollecing.exe start auto obj LocalSystem sc description HaierDataCollector 海康PLC数据采集服务 sc failure HaierDataCollector reset 86400 actions restart/60000/restart/60000/restart/60000 net start HaierDataCollector关键参数说明obj LocalSystem以系统账户运行避免用户登录状态影响服务failure连续三次失败后等待 60 秒重启60 秒后再次失败则再等 60 秒最终 24 小时后重置计数器start auto开机自动启动无需人工干预。服务日志通过EventLog.SourceExists(HaierDataCollector)写入 Windows 事件查看器替代控制台输出方便集中监控。5.3 多 PLC 扩展基于配置文件的动态设备管理当前源码硬编码单台 PLC但产线常需采集多台设备。Devices.config文件定义设备列表devices device idPLC1 ip192.168.1.10 slaveId1 pollInterval1000 / device idPLC2 ip192.168.1.11 slaveId2 pollInterval2000 / /devicesDeviceManager.cs动态创建ModbusIpMaster实例池按pollInterval启动独立Timer避免单点故障影响全局。新增 PLC 时只需修改 XML无需重新编译——这正是源码设计为“实例”而非“Demo”的核心价值它预留了生产环境必需的可维护性接口。本文还有配套的精品资源点击获取