ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#与S7协议实现西门子PLC数据交互:从环境搭建到实战封装

C#与S7协议实现西门子PLC数据交互:从环境搭建到实战封装 简介这是一份面向工业自动化初学者与C#开发者的西门子PLC通信入门实践资源聚焦零基础对接S7系列PLC的数据读写需求解决上位机与PLC间基于以太网的TCP/IP通信难题。资源包共30个文件含9个核心C#源码文件如Form1.cs、Program.cs、1个Visual Studio解决方案.sln与项目文件.csproj、2个可执行程序.exe及配置文件.config、资源文件.resx和调试支持文件.pdb/.cache整体仅42KB轻量易部署。已有4444人学习下载说明其在工控入门场景中具备较强实用性与传播度。读者可直接运行示例工程快速掌握S7协议下对S7-200smart、S7-300、S7-1200、S7-1500等主流PLC的高性能读写逻辑所有代码完全开源、不依赖第三方组件后台线程调用设计避免UI卡顿配套使用说明.txt清晰指引连接配置与数据类型映射是理解工业通信底层实现的理想参考样本。1. 从零开始为什么选择C#与S7协议进行PLC数据交互如果你是一名上位机开发的新手或者是一位自动化工程师想自己动手写个程序来监控产线上的西门子PLC数据那么你大概率会听到两个词C#和S7协议。这几乎成了这个领域的一个“黄金组合”。我刚开始接触时也纳闷为什么不是Python、Java或者C后来在项目里摸爬滚打几年才明白这背后是生态、效率和稳定性的综合考量。C#特别是配合Visual Studio和.NET Framework/.NET Core现在是.NET 5/6/7/8在工业上位机开发领域有着得天独厚的优势。它的WinForms和WPF框架能快速构建出稳定、美观的桌面监控界面而它的异步编程模型async/await非常适合处理PLC通讯这种高并发、需等待硬件响应的I/O密集型任务。更重要的是整个工业软件生态包括大量的第三方控件如图表、报表、数据网格和专业的工业通讯库对C#的支持都非常成熟。你很少需要从零造轮子大部分需求都有现成的、经过产线验证的解决方案。而S7协议是西门子为其S7系列PLC从老款的S7-200/300/400到现在的S7-1200/1500以及精简型的S7-200 SMART设计的“方言”。它运行在TCP/IP协议之上是西门子PLC与上位机之间最高效、最底层的原生数据交换方式之一。相比于通过OPC UA服务器中转S7协议直连的延迟更低对网络配置要求更简单通常只需在同一网段并且能直接访问PLC的DB块、M区、I区、Q区等所有存储区控制粒度更细。对于200 SMART、1200、1500这些新一代PLCS7协议更是标配的开放式通讯功能。所以当你用C#写一个程序通过S7协议去读写一台西门子PLC时你实际上是在用这个生态里最主流、最直接的工具去完成最核心的数据采集与控制任务。这条路可能不是唯一的但绝对是踩坑最少、资料最全、最容易成功的一条。接下来我就带你从最基础的环境搭建开始一步步实现这个目标。2. 环境与工具准备搭建你的开发与测试战场工欲善其事必先利其器。在写第一行代码之前我们需要把“战场”布置好。这里分为软件开发环境和硬件/模拟测试环境两部分。2.1 软件开发环境搭建首先你需要安装集成开发环境IDE。对于C#开发首推Visual Studio 2022。社区版Community是免费的功能对于学习和大多数工业应用开发已经完全足够。安装时记得勾选“.NET桌面开发”工作负载这会包含WinForms、WPF等必要的组件。接下来是最关键的一步选择S7通讯库。这是连接C#和PLC的桥梁。市面上有几个主流选择S7NetPlus这是一个非常流行、完全托管纯C#的开源库。它实现了S7协议的核心不依赖任何西门子DLL跨平台性好支持.NET Core/.NET 5文档和社区活跃。对于大多数读写需求它是首选。Sharp7/S7.Net这两个也是比较知名的开源库。Sharp7是S7Net的一个衍生版本性能有所优化。不过目前社区的焦点更多在S7NetPlus上。西门子官方库S7.NET.NETStandard西门子自己也提供了一些.NET版本的库但通常集成在大型软件包中单独使用和文档支持不如开源库友好。对于零基础入门我强烈推荐S7NetPlus。你可以通过Visual Studio的NuGet包管理器轻松安装。打开你的项目在“解决方案资源管理器”中右键点击项目名选择“管理NuGet程序包”在浏览标签页搜索“S7NetPlus”然后安装它。这是最省心、最不容易出问题的方式。2.2 硬件与模拟测试环境准备你不可能一开始就连接产线上的真实PLC。我们需要一个安全的测试环境。真实PLC如果你手头有一台西门子PLC如200 SMART、1200、一根网线和一个交换机那当然最好。确保你的电脑和PLC在同一个局域网网段例如电脑IP192.168.0.100 PLC IP192.168.0.1。PLC模拟器这是更经济便捷的选择。对于S7-1200/1500可以使用西门子TIA Portal软件自带的PLCSIM Advanced。它可以在你的电脑上虚拟出一个PLC并拥有真实的IP地址完美支持S7协议通讯。对于S7-200 SMART可以使用S7-200 SMART PC Access软件中的模拟功能或者寻找第三方的模拟软件但兼容性需要测试。虚拟网络如果你的模拟器如PLCSIM Advanced需要与你的C#程序在同一台电脑上通讯可能需要配置虚拟网卡或使用特定的网络设置确保两者能ping通。在开始编程前请务必确认你的C#程序所在的电脑能与PLC真实或模拟进行网络通信。打开命令提示符输入ping [PLC的IP地址]如果能收到回复说明物理/虚拟链路是通的。同时你需要知道PLC的机架号Rack和槽位号Slot。对于S7-1200/1500通常是Rack0 Slot1。对于S7-300/400需要根据硬件组态查看。对于200 SMART它使用基于连接的协议通常槽位号固定为1。这些信息在后续连接时会用到。注意许多初学者的第一个“坑”就是网络不通或防火墙拦截。请务必关闭电脑和PLC端的防火墙进行测试或者为相关端口S7协议默认TCP 102端口添加防火墙入站规则。3. 核心连接与基础读写第一个“Hello World”环境准备好后我们来写第一个真正的通讯程序。这个程序的目标很简单连接PLC读取一个布尔量比如M0.0再写入一个布尔量比如M0.1。3.1 建立连接首先在C#中引用S7NetPlus的命名空间并创建一个PLC连接对象。using S7.Net; // 定义PLC类型、IP地址、机架号、槽位号 PlcType plcType PlcType.S71200; // 根据你的PLC型号选择如S7200, S7300, S71200, S71500 string ipAddress 192.168.0.1; short rack 0; short slot 1; // 创建Plc实例 using (Plc plc new Plc(plcType, ipAddress, rack, slot)) { // 打开连接 plc.Open(); if (plc.IsConnected) { Console.WriteLine(成功连接到PLC); // --- 在这里进行读写操作 --- } else { Console.WriteLine(连接PLC失败); } // 使用using语句离开作用域时会自动调用Close()确保连接关闭 }这段代码有几个关键点PlcType必须选对不同型号PLC的协议细节有差异。S7200Smart对应200 SMARTS71200对应1200/1500因为它们协议兼容S7300对应300/400。using语句这是最佳实践。Plc对象实现了IDisposable接口using能确保即使发生异常连接也能被正确关闭防止资源泄漏。plc.Open()这是一个同步阻塞方法。在实际的上位机程序中为了避免界面卡死你应该使用await plc.OpenAsync()在异步方法中调用。3.2 读取数据假设我们要读取M存储区第0个字节的第0位即M0.0。// 读取一个布尔值M0.0 bool m0_0_Value (bool)plc.Read(DataType.DataBlock, 0, 0, VarType.Bit, 0, 1); Console.WriteLine($M0.0 的状态是{m0_0_Value}); // 读取一个16位整数MW2即M2.0和M3.0组成的字 short mw2_Value (short)plc.Read(DataType.DataBlock, 0, 2, VarType.Word, 1); Console.WriteLine($MW2 的值是{mw2_Value}); // 读取一个32位浮点数MD4即M4.0开始的4个字节组成的双字 float md4_Value (float)plc.Read(DataType.DataBlock, 0, 4, VarType.Real, 1); Console.WriteLine($MD4 的值是{md4_Value});Read方法的参数需要解释一下DataType数据块类型。DataBlock对应M区或DB块对于M区DB号填0Memory是旧版API的用法Input对应I区Output对应Q区。db数据块编号。对于M区填0。startByteAdr起始字节地址。M0.0的字节地址是0位地址是0。MW2的字节地址是2。varType变量类型。Bit位、Byte字节、Word字、Int16位整数、DWord双字、DInt32位整数、Real32位浮点等。bitAdr仅当varType为Bit时有效指定位在字节中的位置0-7。对于M0.0bitAdr是0对于M0.5bitAdr是5。varCount读取的数量。读取单个变量填1如果要批量读取多个连续的同类变量可以增加此值。3.3 写入数据写入操作与读取类似。// 写入一个布尔值将M0.1设置为True plc.Write(DataType.DataBlock, 0, 0, VarType.Bit, 1, true); // 注意bitAdr1 对应 M0.1 // 写入一个整数到MW10 plc.Write(DataType.DataBlock, 0, 10, VarType.Word, 1, 12345); // 写入一个浮点数到MD20 plc.Write(DataType.DataBlock, 0, 20, VarType.Real, 1, 123.456f);至此你已经完成了一个最基本的PLC读写程序。编译运行如果一切顺利你将在控制台看到读取的值并且PLC对应的地址值会被改变。这标志着你的C#程序已经成功跨越了网络与西门子PLC“握手”成功。4. 深入DB块读写与类型映射处理复杂数据结构在实际项目中我们很少直接读写零散的M区地址。规范的做法是将数据组织在数据块DB中。DB块就像PLC中的结构化数据库可以定义各种数据类型的变量如Bool、Int、Real、String、Array甚至自定义结构Struct。通过S7协议读写DB块是上位机通讯的核心。4.1 读写DB块中的基本变量假设PLC中有一个DB1里面定义了几个变量Start(Bool) 在 DB1.DBX0.0Speed(Int) 在 DB1.DBW2Temperature(Real) 在 DB1.DBD4在C#中读写它们// 读取DB1.DBX0.0 (Start信号) bool startSignal (bool)plc.Read(DataType.DataBlock, 1, 0, VarType.Bit, 0, 1); // 读取DB1.DBW2 (Speed) int speed (int)plc.Read(DataType.DataBlock, 1, 2, VarType.Int, 1); // 读取DB1.DBD4 (Temperature) float temperature (float)plc.Read(DataType.DataBlock, 1, 4, VarType.Real, 1); // 写入DB1.DBD4 plc.Write(DataType.DataBlock, 1, 4, VarType.Real, 1, 78.5f);关键变化在于db参数从0M区变成了具体的数据块编号这里是1。4.2 处理复杂类型数组与字符串数组如果要读取DB1中从DBB10开始的5个字节的数组Byte数组。// 方法1使用ReadBytes方法S7NetPlus提供 byte[] byteArray plc.ReadBytes(DataType.DataBlock, 1, 10, 5); // 方法2使用通用的Read方法但需要自己转换 var result plc.Read(DataType.DataBlock, 1, 10, VarType.Byte, 5); // 读取5个字节 if (result is byte[] bytes) { // 使用bytes数组 }字符串西门子PLC中的字符串比较特殊它有特定的格式。例如String[20]在DB中实际占用22个字节前2个字节是最大长度和当前长度后面20个字节是字符内容ASCII或UTF-16取决于PLC设置。S7NetPlus提供了ReadString和WriteString方法来简化。// 假设DB1.DBB30开始是一个String[20] string myString plc.ReadString(DataType.DataBlock, 1, 30, 20); // 从字节30开始最大长度20 Console.WriteLine($读取的字符串{myString}); // 写入字符串 plc.WriteString(DataType.DataBlock, 1, 30, 20, Hello PLC);注意字符串读写最容易出错的地方在于长度和编码。务必确保C#中指定的最大长度与PLC中定义的一致并且编码要匹配西门子默认通常是ASCII。如果PLC中字符串是UTF-16WString则需要使用专门处理WString的方法或手动处理字节数组。4.3 批量读写与性能优化频繁地调用Read/Write进行单个变量的读写效率很低因为每次调用都意味着一次网络往返。正确的做法是批量读写。S7NetPlus提供了ReadMultipleVars和WriteMultipleVars方法但更高效的方式是使用ReadBytes和WriteBytes一次性读写一大片连续的字节区域然后在C#端进行解析。例如DB1中从DBB0到DBB99定义了100个字节的各种变量。与其逐个读取不如// 一次性读取DB1中前100个字节 byte[] db1Data plc.ReadBytes(DataType.DataBlock, 1, 0, 100); // 现在在内存中快速解析这些字节 // 假设我们知道数据结构 // DB1.DBX0.0 (bool) - db1Data[0] 的第0位 bool start (db1Data[0] 0x01) ! 0; // DB1.DBW2 (Int) - 取db1Data[2], db1Data[3] 组成一个short (注意字节序) short speed (short)((db1Data[3] 8) | db1Data[2]); // S7协议是Big-Endian大端序 // DB1.DBD4 (float) - 取db1Data[4], db1Data[5], db1Data[6], db1Data[7] float temperature BitConverter.ToSingle(new byte[] { db1Data[7], db1Data[6], db1Data[5], db1Data[4] }, 0); // 注意大端序转换这里有一个至关重要的细节字节序Endianness。西门子S7协议使用大端序Big-Endian而Intel架构的PC包括你的C#程序通常使用小端序Little-Endian。这意味着多字节数据类型如Int, Word, DInt, Real在传输时字节顺序是反的。上面的代码片段中手动组合speed和转换temperature时都进行了字节序的调整。S7NetPlus库在内部已经帮我们处理了大部分常用类型的字节序转换但当你直接操作字节数组时必须自己处理。批量写入也是类似先在内存中构建好符合S7大端序格式的字节数组然后一次性写入。byte[] dataToWrite new byte[100]; // ... 填充dataToWrite数组注意将C#中的值转换为大端序字节 ... plc.WriteBytes(DataType.DataBlock, 1, 0, dataToWrite);这种批量读写的方式将成百上千次网络请求减少到一次通讯效率提升是数量级的对于需要高速刷新的监控画面至关重要。5. 实战封装与异常处理构建健壮的通讯层直接在主程序里到处写plc.Read/Write会让代码难以维护和调试。一个良好的实践是将PLC通讯操作封装到一个独立的类或服务中。这有助于集中管理连接状态、处理异常、实现重连机制并提供更友好的API给业务层调用。5.1 封装一个简单的PLC服务类public class PlcService : IDisposable { private Plc _plc; private readonly string _ip; private readonly PlcType _type; private readonly short _rack; private readonly short _slot; private readonly ILogger _logger; // 可选用于记录日志 private bool _isConnected false; private readonly object _lockObject new object(); // 用于线程同步 public bool IsConnected _isConnected; public PlcService(string ip, PlcType type, short rack 0, short slot 1) { _ip ip; _type type; _rack rack; _slot slot; } public async Taskbool ConnectAsync(CancellationToken cancellationToken default) { if (_isConnected) return true; lock (_lockObject) { if (_isConnected) return true; // 双重检查锁定 _plc new Plc(_type, _ip, _rack, _slot); } try { await _plc.OpenAsync(cancellationToken).ConfigureAwait(false); _isConnected _plc.IsConnected; if (_isConnected) { _logger?.LogInformation($PLC连接成功: {_ip}); } else { _logger?.LogError($PLC连接失败: {_ip}); } return _isConnected; } catch (Exception ex) { _logger?.LogError(ex, $连接PLC时发生异常: {_ip}); _isConnected false; DisposePlc(); return false; } } public async TaskT ReadAsyncT(DataType dataType, int db, int startByteAdr, VarType varType, int bitAdr 0, int varCount 1) { if (!_isConnected || _plc null) { throw new InvalidOperationException(PLC未连接。); } try { var result await Task.Run(() _plc.Read(dataType, db, startByteAdr, varType, bitAdr, varCount)).ConfigureAwait(false); return (T)result; } catch (Exception ex) { _logger?.LogError(ex, $读取PLC数据失败。地址: DB{db}.DB{varType}{startByteAdr}.{bitAdr}); // 可以根据异常类型决定是否标记连接断开 if (ex is System.Net.Sockets.SocketException) { _isConnected false; } throw; // 重新抛出让调用者处理 } } public async Task WriteAsync(DataType dataType, int db, int startByteAdr, VarType varType, object value, int bitAdr 0, int varCount 1) { // ... 类似的实现包含异常处理和连接状态检查 ... } public void Disconnect() { _isConnected false; DisposePlc(); _logger?.LogInformation($PLC连接已断开: {_ip}); } private void DisposePlc() { if (_plc ! null) { try { _plc.Close(); } catch { /* 忽略关闭时的异常 */ } _plc.Dispose(); _plc null; } } public void Dispose() { Disconnect(); } }这个封装类提供了几个关键改进异步支持使用async/await避免阻塞UI线程。线程安全使用lock防止多线程同时操作连接。集中式异常处理与日志所有通讯异常都在这里捕获和记录便于排查。连接状态管理自动检测网络异常如SocketException并更新连接状态。资源管理实现了IDisposable模式确保连接被正确关闭。5.2 实现自动重连机制工业现场网络不稳定是常态。一个健壮的上位机必须能处理短暂的网络中断并自动恢复。可以在PlcService类中添加一个后台任务或定时器定期检查连接状态并在断开时尝试重连。public class PlcServiceWithReconnect : PlcService { private Timer _reconnectTimer; private readonly int _reconnectIntervalMs 5000; // 5秒重试一次 public PlcServiceWithReconnect(string ip, PlcType type, short rack 0, short slot 1) : base(ip, type, rack, slot) { // 可以在这里启动定时器或者提供一个StartAutoReconnect方法 } public void StartAutoReconnect() { _reconnectTimer new Timer(ReconnectCallback, null, 0, _reconnectIntervalMs); } private async void ReconnectCallback(object state) { if (!IsConnected) { _logger?.LogInformation(尝试重新连接PLC...); try { await ConnectAsync().ConfigureAwait(false); } catch { // 重连失败等待下次定时器触发 } } } // 在Disconnect和Dispose中记得停止定时器 }在实际业务层调用时你可以这样使用private PlcService _plcService; private CancellationTokenSource _cts; private async void StartMonitoring() { _plcService new PlcServiceWithReconnect(192.168.0.1, PlcType.S71200); _plcService.StartAutoReconnect(); if (await _plcService.ConnectAsync()) { _cts new CancellationTokenSource(); // 启动一个后台任务循环读取数据 _ Task.Run(async () await ReadDataLoop(_cts.Token)); } } private async Task ReadDataLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { float temp await _plcService.ReadAsyncfloat(DataType.DataBlock, 1, 4, VarType.Real); // 更新UI注意需要Invoke到UI线程 // ... await Task.Delay(100, token); // 每100ms读取一次 } catch (InvalidOperationException ex) // 连接断开异常 { // 等待重连机制恢复这里可以暂停读取 await Task.Delay(1000, token); } catch (OperationCanceledException) { break; } } }通过这样的封装你的业务逻辑代码将变得非常清晰和健壮能够从容应对工业现场的各种网络波动。6. 高级话题与性能调优应对大规模数据与高并发当你的应用从读取几个变量发展到需要监控成百上千个点或者需要同时与多台PLC通讯时基础的读写方式就会遇到瓶颈。这时就需要考虑更高级的策略。6.1 分块与异步并发读取即使使用ReadBytes批量读取如果一次读取的字节数过多例如超过8000字节可能会超出PLC单次处理的能力或导致通讯超时。合理的做法是分块。将需要读取的DB地址划分为多个合理的块例如每块200-500字节然后并发地读取这些块。public async TaskDictionarystring, object ReadMultipleBlocksAsync(Dictionarystring, (int db, int startByte, int length) addressBlocks) { var results new Dictionarystring, object(); var tasks new ListTask(); foreach (var block in addressBlocks) { string blockName block.Key; var (db, startByte, length) block.Value; tasks.Add(Task.Run(async () { try { byte[] data await Task.Run(() _plc.ReadBytes(DataType.DataBlock, db, startByte, length)).ConfigureAwait(false); lock (results) // 注意线程安全 { results[blockName] data; } } catch (Exception ex) { _logger?.LogError(ex, $读取块 {blockName} 失败); } })); } await Task.WhenAll(tasks).ConfigureAwait(false); return results; }然后你可以根据返回的字节数组并行地解析出各个变量的值。这种“并行读取 并行解析”的模式能极大提升数据刷新率。6.2 使用PLC的“优化块访问”功能对于S7-1200/1500可以在TIA Portal中勾选数据块的“优化块访问”属性。这会改变数据在块中的存储方式从绝对地址变为符号地址类似C结构体能提升PLC自身的访问效率。但是这会影响S7协议的访问当DB块被优化后你无法再使用固定的字节偏移地址如DB1.DBD4来访问变量。因为编译器会重新排列变量内存布局以优化空间。此时你有两种选择关闭优化在PLC编程时取消勾选DB块的“优化的块访问”。这样地址固定上位机编程简单但PLC性能略有损失。使用符号名访问S7协议本身不支持通过符号名访问。你需要使用其他方式如OPC UA这是西门子主推的下一代通讯协议天然支持符号名访问。西门子.NET库如Siemens.Simatic.S7.NET.S7Plus某些高级库可能支持。在C#端复制数据结构在PLC和C#中定义一模一样的数据结构结构体或类通过批量读取整个DB块然后使用序列化/反序列化如BinarySerializer或手动内存拷贝来映射。这要求你对两端的内存布局有精确控制复杂度高。对于入门和大多数传统应用建议关闭DB块的优化访问使用固定地址这是最稳妥、最通用的方法。6.3 连接池与资源管理如果你的上位机需要同时连接数十台甚至上百台PLC为每台PLC创建一个Plc对象并保持长连接可能会消耗大量系统资源Socket句柄、内存。这时可以考虑使用连接池但S7协议是面向连接的通常连接池管理的是“通讯会话”而非物理TCP连接。更常见的模式是按需连接或分时复用。例如一个数据采集服务轮询100台PLC它可以创建100个PlcService实例但并非所有实例都同时保持连接。可以设计一个调度器每次只同时与其中10-20台PLC通讯通讯完成后断开再连接下一批。这需要精细的超时和重试控制。另一种思路是使用异步I/O和多路复用。.NET的异步Socket本身效率很高维持上百个空闲连接的资源消耗是可以接受的。关键在于你的读写操作必须是完全异步的ReadAsync/WriteAsync并且妥善处理回调避免阻塞线程。6.4 诊断与调试技巧当通讯出现问题时不要盲目修改代码。系统化的排查步骤是Ping测试ping PLC_IP。这是最基本的网络层测试。端口测试使用telnet PLC_IP 102或Test-NetConnection PLC_IP -Port 102(PowerShell) 测试102端口是否开放。如果失败检查PLC侧是否启用了“允许PUT/GET通讯访问”对于1200/1500在“防护与安全”-“连接机制”中勾选。使用第三方工具验证使用Wireshark抓包过滤tcp.port 102查看S7协议报文。或者使用西门子的Simatic Net中的 “Communication Setup” 或 “PG/PC Interface” 进行测试。这能帮你确定问题是出在PLC配置、网络还是你的代码。简化测试写一个最简单的控制台程序只做一次连接和读写排除业务逻辑的干扰。查看异常信息仔细阅读Plc库抛出的异常信息。常见的错误码如0x832可能表示资源不足或协议错误0xD209可能表示地址错误。提示对于S7-200 SMART需要注意其PUT/GET通讯有连接数量限制通常最多8个且需要在其编程软件STEP 7-Micro/WIN SMART中设置正确的通信端口和地址。确保你没有超过连接数限制。7. 从控制台到工业界面集成到WinForms/WPF应用最后我们将通讯模块集成到一个有实际界面的上位机程序中。这里以WinForms为例展示如何安全地在UI线程中更新数据。7.1 设计一个简单的监控界面创建一个WinForms项目添加几个Label和Button用来显示数据和触发操作。7.2 在窗体中集成PlcServicepublic partial class MainForm : Form { private PlcServiceWithReconnect _plcService; private CancellationTokenSource _monitoringCts; public MainForm() { InitializeComponent(); // 初始化PLC服务可以来自配置文件 _plcService new PlcServiceWithReconnect(Properties.Settings.Default.PlcIp, PlcType.S71200); _plcService.StartAutoReconnect(); } private async void BtnConnect_Click(object sender, EventArgs e) { BtnConnect.Enabled false; try { if (await _plcService.ConnectAsync()) { LblStatus.Text 已连接; LblStatus.BackColor Color.LightGreen; StartDataMonitoring(); } else { LblStatus.Text 连接失败; LblStatus.BackColor Color.LightCoral; } } catch (Exception ex) { MessageBox.Show($连接时发生错误{ex.Message}); } finally { BtnConnect.Enabled true; } } private void StartDataMonitoring() { _monitoringCts?.Cancel(); // 停止之前的监控任务 _monitoringCts new CancellationTokenSource(); // 启动一个不阻塞UI线程的监控任务 _ Task.Run(async () await MonitorDataAsync(_monitoringCts.Token)); } private async Task MonitorDataAsync(CancellationToken token) { while (!token.IsCancellationRequested _plcService.IsConnected) { try { // 读取数据 float temperature await _plcService.ReadAsyncfloat(DataType.DataBlock, 1, 4, VarType.Real); bool motorRunning await _plcService.ReadAsyncbool(DataType.DataBlock, 1, 0, VarType.Bit, 0); // 更新UI控件必须在UI线程上执行 this.Invoke((MethodInvoker)delegate { LblTemperature.Text ${temperature:F1} °C; LblMotorStatus.Text motorRunning ? 运行 : 停止; LblMotorStatus.BackColor motorRunning ? Color.LightGreen : Color.LightGray; }); await Task.Delay(200, token); // 控制刷新频率 } catch (InvalidOperationException) // 连接断开 { // 等待重连更新UI状态为断开 this.Invoke((MethodInvoker)delegate { LblStatus.Text 连接断开; LblStatus.BackColor Color.LightCoral; }); break; } catch (OperationCanceledException) { break; } catch (Exception ex) { // 记录其他异常但继续尝试 Debug.WriteLine($监控循环异常{ex.Message}); await Task.Delay(1000, token); } } } private async void BtnStartMotor_Click(object sender, EventArgs e) { if (!_plcService.IsConnected) return; try { await _plcService.WriteAsync(DataType.DataBlock, 1, 0, VarType.Bit, true, 2); // 假设DB1.DBX0.2是启动命令 } catch (Exception ex) { MessageBox.Show($写入命令失败{ex.Message}); } } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { // 清理资源 _monitoringCts?.Cancel(); _plcService?.Dispose(); } }关键点UI线程安全所有对UI控件如Label.Text的更新都必须通过Control.Invoke方法切换到UI线程执行。否则会引发跨线程访问异常。异步与取消使用async/await避免UI卡顿。使用CancellationToken来优雅地停止后台监控任务特别是在窗体关闭时。异常处理在后台循环中妥善处理各种异常将连接断开与其他临时错误区分开并更新UI状态。资源释放在窗体关闭时确保取消监控任务并断开PLC连接。通过以上步骤你已经拥有了一个具备基本连接管理、数据监控、手动控制功能且相对健壮的工业上位机雏形。你可以在此基础上添加图表显示、报警记录、数据存储数据库、配方管理等功能逐步构建出一个完整的SCADA监控与数据采集系统。记住稳定性、可维护性和对异常情况的处理能力是工业软件区别于普通软件的核心。本文还有配套的精品资源点击获取
返回列表