简介这份资源面向具备一定C#基础、希望打通上位机与工业控制器数据链路的开发者聚焦C#与汇川PLC之间的通信实现覆盖网络编程、Modbus协议应用与异常处理等关键环节适合自动化系统集成入门与工程实践参考。压缩包共624个文件、约214.54MB包含12个cs与12个vb源码文件、6个sln解决方案、多个csproj与vcxproj工程文件以及dll、lib、exe等编译产物和xml、config配置、docx说明文档另有大量tlog、cache、pdb等构建中间文件完整保留了示例工程的原始开发痕迹。已有582人学习下载。读者可从中获得可直接运行的通信示例工程理解Socket建立TCP连接、构造与解析Modbus报文、读写线圈与保持寄存器、CRC校验及超时重试等排错思路并参考汇川PLC地址映射与参数配置细节为界面集成与性能优化提供可复用的代码骨架。1. 从一条产线调试的深夜说起C# 与汇川 PLC 通信到底在解决什么凌晨两点产线还在跑上位机界面上的轴位置突然不刷新了。现场工程师第一反应是网线松了第二反应是 PLC 程序改了第三反应才想到——上位机那套 C# 通信代码可能压根没处理断线重连。这个场景在工控上位机开发里太常见了设备端是汇川 PLC上位机是 C# 写的 WinForm 或 WPF 程序中间靠以太网通信把寄存器、轴状态、报警码读上来再下发启动、停止、参数修改指令。这份「C# 与汇川 PLC 通信 示例」资源做的就是把这层通信链路讲清楚、跑起来、能改。它适合两类人一类是刚接手上位机项目、手里有汇川 PLC 但不知道怎么用 C# 连上的新手另一类是已经能连上、但代码写得零散、断线就崩、想整理成可复用通信模块的熟手。通信本身不玄学玄学的是现场网络抖动和寄存器地址对不上后面几章会把这些血泪经验拆开讲。2. 通信架构拆解从 TCP 连接到寄存器读写的一条完整链路2.1 为什么汇川 PLC 上位机通信大多走 Modbus TCP汇川的 PLC 产品线里H3U、H5U、AM400、AM600 这些型号在上位机通信场景下最常见的落地方式是 Modbus TCP。原因不复杂它跑在标准以太网上不需要额外装厂商驱动C# 侧用现成的 Socket 或者第三方 Modbus 库就能发请求调试时还能用 Modbus Poll 这类工具先验证寄存器地址对不对。相比串口通信TCP 少了波特率、校验位这些参数现场布线也简单一根网线进交换机就行。但要注意Modbus TCP 只是协议层汇川 PLC 内部还有自己的软元件地址体系比如 M 区、D 区、X 区、Y 区。上位机读的时候发出去的是 Modbus 的功能码加起始地址加数量PLC 那边再映射到自己的软元件上。这个映射关系如果搞错现象就是「连上了但读出来全是 0」或者「读出来的值和触摸屏上对不上」。常见做法是先在 PLC 编程软件里确认软元件地址再对照通信手册换算成 Modbus 寄存器地址不要凭感觉填。2.2 示例项目的分层结构连接层、协议层、业务层一份能二次开发的通信示例代码结构不能是一坨 Form 里塞满 Socket 调用。合理的拆法是三层连接层管 TCP 连接的建立、保持、断开和重连协议层管 Modbus 报文的组包和解析业务层管具体读哪个地址、写什么值、怎么转成工程单位。下面这个目录结构是这类示例项目里比较典型的组织方式实际资源里文件命名可能略有差异但分层思路一致。PlcCommDemo/ ├── Comm/ │ ├── TcpConnection.cs # TCP 连接管理含重连 │ ├── ModbusTcpClient.cs # Modbus 报文组包解析 │ └── PlcAddress.cs # 软元件地址换算 ├── Models/ │ └── PlcTag.cs # 点位定义地址、类型、系数 ├── Services/ │ └── PlcService.cs # 业务层读轴状态、写参数 ├── Forms/ │ └── MainForm.cs # 界面绑定与定时刷新 └── Config/ └── tags.json # 点位配置文件这样拆的好处是换一个 PLC 型号或者换一种协议只需要动 Comm 层业务层和界面层不用大改。新手容易犯的错是把 Socket 直接写在按钮点击事件里读一次建一次连接现场跑几天就出现端口耗尽。连接层单独抽出来用长连接加心跳才是能上产线的写法。2.3 一次读寄存器的完整调用链把链路串起来看界面定时器触发 → 业务层调用 PlcService.ReadTags → 协议层按点位列表组 Modbus 请求帧 → 连接层通过 Socket 发送 → 收到响应后协议层解析字节 → 业务层按系数换算成工程量 → 界面更新。这条链上任何一环出问题现象都不一样连接层断了是超时异常协议层错了是返回异常码业务层错了是数值不对但通信正常。排查时按链路分段定位比盲目改代码快得多。3. 核心代码拆解TCP 连接、Modbus 组包与点位读写3.1 TCP 长连接与断线重连的实现先看连接层。工控现场的网络不像机房那么稳交换机重启、网线被叉车压断、PLC 断电都会导致连接断开。如果代码里没有重连逻辑上位机就只能重启。下面这段是连接管理的核心写法用后台线程维持连接状态断线后按间隔重试。public class TcpConnection { private TcpClient _client; private NetworkStream _stream; private readonly string _ip; private readonly int _port; private volatile bool _running; private readonly object _lock new object(); public TcpConnection(string ip, int port) { _ip ip; _port port; } public void Start() { _running true; Task.Run(KeepAliveLoop); // 后台维持连接 } private async Task KeepAliveLoop() { while (_running) { try { if (_client null || !_client.Connected) { Connect(); } await Task.Delay(1000); // 每秒检查一次连接状态 } catch { Close(); await Task.Delay(3000); // 断线后等 3 秒再重试 } } } private void Connect() { lock (_lock) { _client new TcpClient(); _client.ReceiveTimeout 2000; // 读超时 2 秒 _client.SendTimeout 2000; // 写超时 2 秒 _client.Connect(_ip, _port); _stream _client.GetStream(); } } public byte[] SendAndReceive(byte[] request, int expectLen) { lock (_lock) { if (_stream null) throw new IOException(连接未建立); _stream.Write(request, 0, request.Length); var buffer new byte[expectLen]; int read 0; while (read expectLen) { int n _stream.Read(buffer, read, expectLen - read); if (n 0) throw new IOException(连接被关闭); read n; } return buffer; } } }这段代码有几个参数值得说清楚。ReceiveTimeout和SendTimeout设成 2000 毫秒是因为现场 PLC 响应一般在几十毫秒内超过 2 秒基本可以判定链路异常设太长会让界面卡住。重连间隔 3 秒是经验值太短会在 PLC 重启期间疯狂重试刷日志太长又恢复慢。lock保证同一时刻只有一个线程在收发避免多线程同时写同一个流导致报文错位。注意_running用了volatile是为了让后台线程能及时看到停止标志。3.2 Modbus TCP 请求帧的组包与解析协议层的关键是报文格式。Modbus TCP 的请求帧由 MBAP 头和 PDU 组成MBAP 头 7 个字节PDU 里功能码 1 个字节加数据。读保持寄存器用功能码 0x03写单个寄存器用 0x06写多个用 0x10。下面以读为例。public static byte[] BuildReadRequest(ushort transactionId, ushort startAddr, ushort count) { var frame new byte[12]; // MBAP 头 frame[0] (byte)(transactionId 8); // 事务标识高字节 frame[1] (byte)(transactionId 0xFF); // 事务标识低字节 frame[2] 0; // 协议标识固定 0 frame[3] 0; frame[4] 0; // 长度高字节 frame[5] 6; // 长度低字节后续 6 字节 frame[6] 1; // 单元标识一般填 1 // PDU frame[7] 0x03; // 功能码读保持寄存器 frame[8] (byte)(startAddr 8); // 起始地址高字节 frame[9] (byte)(startAddr 0xFF); // 起始地址低字节 frame[10] (byte)(count 8); // 寄存器数量高字节 frame[11] (byte)(count 0xFF); // 寄存器数量低字节 return frame; }transactionId每发一次请求递增用来匹配响应虽然很多简单示例不校验它但现场多线程读写时校验一下能避免响应错配。startAddr是 Modbus 寄存器地址注意它和汇川软元件地址之间通常有一个偏移常见做法是 D 区地址直接对应M 区需要按手册换算具体偏移以所用型号的通信手册为准不要照搬别的型号。count一次读的数量不要超过 125 个寄存器这是 Modbus 协议限制超了 PLC 会返回异常码。解析响应时前 7 字节是 MBAP 头第 8 字节是功能码第 9 字节是字节数后面才是数据。每个寄存器占 2 字节高字节在前。如果功能码最高位是 1说明是异常响应第 9 字节是异常码常见 0x02 是地址非法0x03 是数量非法看到异常码先查地址和数量不要怀疑网络。3.3 点位配置与工程量换算业务层不要硬编码地址用配置文件把点位定义出来改地址不用重新编译。下面是一个点位定义的模型和读取后的换算示例。public class PlcTag { public string Name { get; set; } // 点位名如 Axis1Pos public ushort Address { get; set; } // Modbus 寄存器地址 public string DataType { get; set; } // Int16 / UInt16 / Int32 public double Scale { get; set; } 1; // 换算系数 public string Unit { get; set; } // 工程单位 } // 读取后换算32 位整数占两个寄存器需要合并 public static int ToInt32(byte[] data, int offset) { return (data[offset] 24) | (data[offset 1] 16) | (data[offset 2] 8) | data[offset 3]; } // 工程量 原始值 * 系数 double engineering rawValue * tag.Scale;Scale这个系数在现场很关键。PLC 里存的可能是放大 10 倍或 100 倍的整数比如温度存 253 表示 25.3 度上位机不除系数显示出来就是错的。32 位数据要占两个连续寄存器合并时注意字节序汇川 PLC 一般是高字在前但不同型号可能有差异调试时用一个已知值验证一下最稳。DataType字段决定解析方式有符号和无符号在负值场景下结果完全不同这一点新手很容易翻车。4. 跑起来环境准备、连接配置与第一个读取测试4.1 开发环境与依赖准备跑这个示例需要的东西不多Visual Studio 2019 或以上.NET Framework 4.7.2 或 .NET 6 都可以示例代码本身不依赖特殊 NuGet 包Socket 和字节操作都是基础库。如果资源里用了第三方 Modbus 库按项目里的 packages 配置还原即可。PLC 侧需要确认三件事PLC 的 IP 地址、Modbus TCP 端口一般是 502、以及 PLC 是否开启了 Modbus TCP 从站功能。有些型号默认不开启需要在编程软件里配置以太网参数并下载。网络连通性先用 ping 验证再用 telnet 测端口两步都通了再跑代码。现场常见的情况是 ping 通但端口不通那多半是 PLC 侧没开从站或者防火墙拦了。上位机电脑如果有安全软件也可能拦掉出站连接调试阶段可以先放行。4.2 连接参数配置与首次读取把 PLC 的 IP 和端口填到配置里先只配一个点位做测试。下面是一段最小可运行的调用代码读一个保持寄存器并打印。var conn new TcpConnection(192.168.1.10, 502); conn.Start(); Thread.Sleep(500); // 等连接建立 var request ModbusTcpClient.BuildReadRequest(1, 0, 1); // 读地址 0 的 1 个寄存器 var response conn.SendAndReceive(request, 11); // 读 1 个寄存器响应共 11 字节 int value (response[9] 8) | response[10]; // 第 9、10 字节为数据 Console.WriteLine($读到值{value});响应长度 11 是这样算的MBAP 头 7 字节 功能码 1 字节 字节数 1 字节 数据 2 字节。如果实际收到的长度不对先检查expectLen有没有算错长度算错会导致Read一直等或者读多。第一次测试建议读一个已知值的寄存器比如在 PLC 里给 D0 写个 123上位机读出来也是 123说明地址映射和解析都对。读出来是 0 或者乱值先换地址试再查字节序。4.3 用定时器做周期刷新与界面绑定实际项目里不会手动点一次读一次而是定时刷新。WinForm 里用System.Windows.Forms.Timer间隔设 200 到 500 毫秒比较合适太快会增加 PLC 通信负担太慢界面看起来卡顿。刷新逻辑放在后台线程读读完再Invoke回 UI 线程更新控件不要在 UI 线程里直接做阻塞的 Socket 读否则界面会假死。批量读的时候把连续地址合并成一次请求比如 10 个连续寄存器一次读完比读 10 次效率高得多这也是现场调优的一个基本点。5. 避坑与排查现场通信最容易翻车的五个地方5.1 连上了但读出来全是 0现象是 Socket 连接正常请求也发出去了返回的数据全是 0。原因通常是寄存器地址映射错了或者读的是未使用的区域。解决方法是先在 PLC 编程软件里确认软元件当前值再对照通信手册换算 Modbus 地址用一个已知非零的地址做对照测试。不要拿触摸屏上显示的地址直接填两者可能差一个偏移。5.2 偶发超时但网络看着正常现象是大部分时间正常偶尔报超时重连后又恢复。原因可能是交换机负载高、网线质量差也可能是上位机同时开了太多连接。解决方法是检查是否有多个线程同时收发确保连接层加了锁把超时时间适当放宽到 3 秒观察是否还频繁出现现场有条件的话换一根网线或者换个交换机端口对比。血泪经验是这类问题不要先改代码先排除物理链路。5.3 32 位数据高低字颠倒现象是读 32 位数值时结果和预期差了一个数量级或者符号不对。原因是高低字顺序或者字节序理解反了。解决方法是写一个已知值到 PLC比如 0x00010002上位机读出来看是 0x00010002 还是 0x00020001据此调整合并顺序。不同型号的 PLC 可能不一样不要假设所有汇川型号都一致。5.4 写值不生效但读正常现象是读没问题写指令发出去 PLC 没反应。原因可能是功能码用错写单个寄存器应该用 0x06写多个用 0x10也可能是写的地址是只读区。解决方法是先用 Modbus 调试工具手动写一次确认地址可写再对照代码里的功能码和报文长度。写多个寄存器时数量字段和字节数字段都要对少一个字节 PLC 就返回异常。5.5 程序跑几天后连接数暴涨现象是任务管理器里看到大量连接处于等待状态最后连不上。原因是每次操作都 new 一个 TcpClient 且没有关闭或者异常路径里没释放。解决方法是连接层用单例长连接所有异常路径都走统一的 Close 方法TcpClient用完必须Dispose。这个问题在调试阶段看不出来上产线跑一周才暴露属于典型的后悔药场景。6. 二次开发改造把示例变成可复用的通信组件示例代码能跑通只是起点真正要落地到项目里得把它改造成能复用的组件。我一般会做三件事。第一把点位配置从代码里彻底抽出来用 JSON 或数据库存界面上做一个点位管理页现场工程师自己能改地址和系数不用找开发重新发版。第二给通信层加一个状态事件连接断开、重连成功、读写异常都往外抛事件界面顶部做一个连接状态灯出问题一眼能看到而不是等数据不刷新了才去查日志。第三加一个简单的通信日志记录每次请求的地址、数量、耗时和异常码日志按天切分现场排查时直接看日志比远程猜快得多。改造后的验证方法也有讲究。不要只在办公室用一台 PLC 测要模拟断线拔网线看是否自动重连、PLC 断电再上电看数据是否恢复、连续跑 24 小时看内存和连接数是否稳定。下面这个表格是我常用的验证清单每次改完通信层都过一遍。验证项操作预期结果断线重连拔掉网线 10 秒再插回3 秒内自动重连数据恢复刷新PLC 重启给 PLC 断电再上电上位机不崩溃重连后数据正常长时间运行连续运行 24 小时连接数稳定内存无明显增长异常地址配置一个非法寄存器地址返回异常码并记录日志不影响其他点位并发读写同时触发读和写报文不错位读写结果正确还有一个技巧值得单独说把通信层做成接口比如IPlcClient读、写、连接状态各定义方法业务层只依赖接口。这样以后换别的协议或者别的品牌 PLC只要新写一个实现类业务代码一行不用改。这个设计在项目初期多花半天后期省下的返工时间远不止半天。从那以后我每次接新的上位机项目都强制先把连接层和协议层单独跑通、单独压测再往上叠业务逻辑绝不在界面代码里直接写 Socket。通信这层看着简单现场一跑起来问题全在细节里早拆早省心。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?