ARTICLE DETAIL

资讯详情

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

C#上位机使用S7.NET读写西门子PLC数据实战指南

C#上位机使用S7.NET读写西门子PLC数据实战指南 简介面向C#上位机开发者的西门子PLC通信示例基于S7.NET协议实现与S7-200smart、S7-1200、S7-1500系列PLC的网口通信。程序由作者在工业现场长期运行验证实测稳定可靠代码结构清晰、注释完整不仅适合入门学习也方便在此基础上扩展自己的业务逻辑。资源压缩包共包含34个文件以C#源码、配置文件、可执行程序、DLL依赖库和PDF说明文档为主同时带有运行截图、工程解决方案等辅助材料整体大小约1008KB非常轻量。目前已有204人学习下载社区反馈实用性强。通过该资源读者可获得可直接运行的示例程序、完整工程源码、通信实现思路以及现场调试经验对于正在搭建PLC上位机监控系统的开发者有直接参考价值能够显著缩短S7.NET协议对接的摸索时间。1. 用C#上位机连西门子PLC先搞清楚S7.NET能干什么做设备数据采集或者产线监控只要对面是西门子S7-1200/1500甚至老一点的S7-300/400S7.NET几乎是C#开发者捡起来就能用的协议库。它不依赖Simatic Net软件授权直接在TCP层实现了西门子的S7通信协议用NuGet拉包就能连PLC的网口做读写。相比OPC UAS7.NET的优点是轻、延迟低、API直白适合做MES工位数据上报、参数下发这类上位机开发场景。这篇文章按我做项目的顺序来写连接参数怎么配、DB块和M区怎么读写、循环数据采集和UI刷新卡顿怎么解决、断线怎么自动重连最后给一套可以直接复制的字节解析工具。2. S7.NET连接前的准备CPU型号、TSAP和IP怎么配2.1 TSAP决定连接层能否握手S7-1200/1500和300/400的差异S7.NET在底层做的事情是先用TCP连到PLC的102端口然后通过TPKT/COTP协议发起一次S7会话。这个过程中TSAPTransport Service Access Point传输服务访问点的作用是告诉PLC“我是上位机/编程器”同时指定我要访问CPU的哪个槽位。很多第一次用S7.NET的人报错Unable to connect实际不是IP不通而是TSAP没配对。S7-1200和S7-1500在默认组态下CPU的TSAP是0x0100对应S7.NET里的写法就是CpuType.S71200或CpuType.S71500配合rack0, slot0即可。S7-300和S7-400则要根据硬件组态来决定因为CPU在机架里的物理槽位不同比如S7-300的CPU通常在2号槽。这里有个常见误用有人拿S7-300的代码去连1200把slot填成2连接就会一直超时。CpuType枚举典型Rack典型Slot适用场景S7120000S7-1200全系列固件V4.x以上S7150000S7-1500全系列软PLC也常见S730002S7-300CPU在2号槽S740003S7-400部分机架为4号槽提示S7.NET的构造参数顺序是(cpuType, ip, rack, slot)中间没有端口号因为S7协议固定走102端口。2.2 最小连接代码NuGet引入S7.Net并Open在Visual Studio 2019里建一个WPF或WinForm项目NuGet搜索S7.Net装稳定版即可。注意区分S7.Net和Sharp7前者API更贴近C#开发习惯后者更底层、速度更快但需要自己管理更多缓冲区细节。我一般用S7.Net做快速落地。using S7.Net; public class PlcConnection { private Plc _plc; private readonly object _lock new object(); public bool Connect(string ip, CpuType cpuType, short rack, short slot) { lock (_lock) { _plc new Plc(cpuType, ip, rack, slot); _plc.Open(); return _plc.IsConnected; } } public void Disconnect() { lock (_lock) { _plc?.Close(); _plc null; } } }逻辑说明lock锁是必要的因为S7.NET的Plc实例不是完全线程安全的。如果采集线程和界面线程共用同一个_plc同时调用Read和Write可能出现TCP数据包交错导致返回的字节数组错位。我见过有人把这个问题归结为“PLC数据不稳定”其实是连接实例被多线程并发使用。另外Open()内部是同步TCP连接默认超时时间在几秒到十几秒之间如果PLC不在线调用线程会卡住所以最好在调用前先Ping或者做超时控制。2.3 多台PLC并存时的配置文件与路由检查产线上通常不止一台西门子PLC多工位、多机台各自独立。我不会在代码里硬编码IP和CPU型号而是维护一个JSON配置文件程序启动时加载并逐个建立连接。[ { name: PLC_Station1, ip: 192.168.1.10, cpu: S71200, rack: 0, slot: 0 }, { name: PLC_Station2, ip: 192.168.1.11, cpu: S71500, rack: 0, slot: 0 } ]对应的加载逻辑就是遍历这个数组反射枚举值后new Plc(...)再Open()。这种做法在替换PLC或者改IP时只动配置文件不用重新编译上位机。还有一点容易被忽略如果一台工控机有多个网卡分别连着办公网和工控网到PLC的流量必须走工控网那张网卡。S7.NET没有暴露绑定本地IP的选项最终走哪条路由是由操作系统路由表决定的。排查手段是route print看目标网段的路由优先级或者在工控机上把办公网卡的网关去掉。3. S7.NET读写PLC数据的实操DB块、M区与字节序解析3.1 ReadBytes拉取DB块为什么用它替代ReadS7.NET最核心的读取API是ReadBytes(DataType.DataBlock, dbNumber, startByteAdr, count)它按字节偏移从指定的DB块里取一段连续区域。举个例子读DB10前100个字节var bytes _plc.ReadBytes(DataType.DataBlock, 10, 0, 100);Read方法虽然也能读单个变量但一次只能读一个当变量一多代码就变成了一长串Read(DB10.0)、Read(DB10.2)每次都是一次完整的S7请求-响应。而ReadBytes一次把整块区域拉回来再由本地代码做类型解析S7通信次数从N次降成1次。对于100ms采集周期来说这差距非常明显尤其是DB块里几十个变量的时候。用ReadBytes时还有个技巧PLC端的数据布局要事先规划好把相同刷新频率的变量放在相邻的字节区域这样一次能读全。比如温度、压力、速度模拟量放在DB10的0到199字节设备状态字放在200到219字节互不混排上位机读起来就很有规律。byte[] raw _plc.ReadBytes(DataType.DataBlock, 10, 0, 200); // 此时已经拿到DB10的0~199字节可以整体解析参数说明DataType.DataBlock表示访问DB块第二个参数10是DB块号必须和PLC里实际存在的块号一致第三个参数是起始字节偏移从0开始第四个参数是要读取的字节数最大长度受S7协议PDU大小限制一般一次不超过240字节超过就需要分多次读。3.2 Write方法写M区和Q区类型与位地址的对应写入用Write方法签名和Read类似。下面是三个典型操作// 向MW20写入整数50对应S7的Int类型 _plc.Write(DataType.Memory, 20, 0, (short)50); // 向M22.0写入True注意bitAdr0 _plc.Write(DataType.Memory, 22, 0, true); // 向QB0写入一个字节控制输出模块 _plc.Write(DataType.Output, 0, 0, (byte)0xFF);第一行代码里DataType.Memory是M区20是起始字节地址0是位偏移值用short类型是因为S7的Int是16位整数。如果误传了C#的intS7.NET的转换逻辑可能会占用4个字节把隔壁的MW22给覆盖掉。这个坑我在现场排查过现象是电机速度写进去旁边的阀门开度跟着变了。DataType.Output对应Q区也就是输出映像区。写入Q区的值会直接影响设备输出所以现场调试时要格外小心。我一般会在写Q区之前加一层软保护只有当前设备处于手动模式且未报警时才允许上位机写输出。M区虽然不像Q区那样直接驱动设备但很多PLC程序用M区做模式切换或启动停止的中间变量写错了同样会引起逻辑错乱。3.3 字节序坑为什么读上来的数值不对西门子PLC的数据在内存里是Big-Endian也就是高位字节在前。而C#的BitConverter默认按Little-Endian解析。直接拿BitConverter.ToInt32去转换读到的字节数值大概率不对。// 假设raw里存的是从DB读出的4个字节 byte[] raw new byte[] { 0x41, 0xA0, 0x00, 0x00 }; if (BitConverter.IsLittleEndian) Array.Reverse(raw); float temperature BitConverter.ToSingle(raw, 0); // 20.0f这段代码先判断当前运行时是不是小端环境是的话就把字节反转回来再做解析。Array.Reverse是原地反转反转后raw的顺序就符合西门子的原始字节序了。同理读16位Int的时候要反转2字节读32位DInt要反转4字节。为了不让业务代码到处写反转逻辑我习惯把解析方法集中到一个静态工具类里传byte[]和偏移量返回解析好的数值业务层就不用关心字节序了。4. 循环采集不卡UIS7.NET的异步调用与断线重连4.1 卡顿根因同步Read阻塞在UI线程用C#写上位机最常见的界面卡顿原因不是CPU计算量大而是把阻塞的PLC通信放到了UI线程里。很多人刚开始写的采集循环是这样的while (true) { var data _plc.ReadBytes(DataType.DataBlock, 10, 0, 100); textBox1.Text data[0].ToString(); // 直接在UI线程操作 Thread.Sleep(100); }这段代码的问题在于ReadBytes是一个同步阻塞调用PLC一个扫描周期是10到50毫秒加上网络传输和等待响应总耗时可能到几十甚至上百毫秒。在UI线程里执行这个循环时界面上的按钮、拖拽、输入框全部得不到响应用户直观感受就是“窗口卡住了”。更糟的是当PLC已经断线但尚未超时ReadBytes会一直阻塞到TCP超时界面可能卡十几秒才恢复在产线上这就是事故了。提示凡是涉及网络通信的循环无论S7.NET还是Modbus TCP采集逻辑都应该放在后台线程UI只负责订阅数据。4.2 用Task.Run和async/await改造采集循环推荐的改造方案是利用Task.Run把阻塞调用丢到线程池再配合async/await保持代码的同步风格。下面是一个带取消令牌的后台采集示例private async Task PollLoopAsync(Channelbyte[] channel, CancellationToken ct) { while (!ct.IsCancellationRequested) { try { byte[] data await Task.Run(() _plc.ReadBytes(DataType.DataBlock, 10, 0, 200), ct); await channel.Writer.WriteAsync(data, ct); } catch (OperationCanceledException) { break; // 程序退出时正常终止 } catch (Exception ex) { _logger.LogError(ex, 读取PLC数据失败); } await Task.Delay(100, ct); } }这个循环每100毫秒采集一次DB10的前200字节读回的数据通过Channel传给UI层。Task.Run里包的是之前那个同步的ReadBytes调用这样UI线程永远不会被PLC的响应时间拖住。Channel是.NET内置的生产者消费者集合比直接Invoke跨线程更新控件更解耦UI层只需要单独开一个异步消费者去接收数据并刷新控件。参数说明CancellationToken可以来自CancellationTokenSource在程序退出或用户点击“停止采集”时调用Cancel()循环内的Task.Delay(100, ct)也会立即抛异常退出避免线程泄漏。4.3 心跳任务与重连状态机工厂环境里网络闪断、PLC重启是常态。S7.NET连接一旦断开旧的Plc实例基本上是废的直接再调Open()虽然也能连上但在某些固件版本下会出现句柄复用问题。所以我写了一个独立的心跳任务每10秒检查一次连接状态断线就新建Plc实例重连public async Task KeepAliveAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { if (_plc null || !_plc.IsConnected) { try { _plc?.Close(); _plc new Plc(_cpuType, _ip, _rack, _slot); _plc.Open(); Console.WriteLine(${DateTime.Now}: PLC重连成功); } catch { Console.WriteLine(${DateTime.Now}: PLC重连失败稍后重试); } } await Task.Delay(10_000, ct); } }注意这里每次重连都是new Plc()不是在旧实例上反复Open因为旧的socket状态可能已经残留了半开连接的数据。plc.Read()在连接断开后会抛异常所以我们把这个异常视作触发重连的信号。5. S7.NET实战技巧多PLC并行采集与统一字节解析5.1 并行采集时每个PLC用独立连接实例项目里如果有多台西门子PLC需要同时采集不要在一个Plc实例上轮询多台设备的地址那样根本没有这个APIS7.NET一个实例只能连一台PLC的CPU。正确做法是每台PLC建一个独立的PlcConnection然后用Task.WhenAll并行处理var tasks plcConnections.Select(async conn { byte[] data await Task.Run(() conn.ReadBytes(DataType.DataBlock, 20, 0, 100)); return (conn.Name, data); }); var results await Task.WhenAll(tasks);每台PLC有自己的socket连接互不阻塞。这样当一台PLC断电时其他工位的采集不受影响。5.2 把Real和String解析提成工具方法最后说一个实用的工具类。S7.NET虽然自带Types类能做Class映射但那种方式需要定义与DB块结构一一对应的C#类遇到字段增删就要重新编译。我更倾向于用偏移量手动解析灵活性和可维护性都好一些public static class S7Converter { public static float ReadReal(byte[] buffer, int offset) { var bytes new byte[4]; Array.Copy(buffer, offset, bytes, 0, 4); if (BitConverter.IsLittleEndian) Array.Reverse(bytes); return BitConverter.ToSingle(bytes, 0); } public static int ReadDInt(byte[] buffer, int offset) { var bytes new byte[4]; Array.Copy(buffer, offset, bytes, 0, 4); if (BitConverter.IsLittleEndian) Array.Reverse(bytes); return BitConverter.ToInt32(bytes, 0); } public static string ReadString(byte[] buffer, int offset) { int currentLength buffer[offset 1]; return Encoding.ASCII.GetString(buffer, offset 2, currentLength); } }ReadString里第一个字节是字符串声明的最大长度第二个字节是当前实际长度数据从第三个字节开始。这个格式和博途中定义的String变量完全一致直接按偏移取就行不需要先读长度再读内容。项目中我通常会在PLC组态里把所有字符串统一用S7 String类型声明并且规定最大长度上位机这边的解析代码就可以保持稳定。本文还有配套的精品资源点击获取
返回列表