ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#上位机与S7-1200通信:基于S7.net的线程循环读取实践指南

C#上位机与S7-1200通信:基于S7.net的线程循环读取实践指南 简介面向C#开发及工控自动化人员内容以西门子S7-1200 PLC为例系统讲解基于S7.net库从建立连接、单次读取到线程循环批量读取的完整通信方案。围绕VS2019 WinForm项目展开覆盖PLC侧PUT/GET访问开启与取消优化的块访问、S7netplus与thinger库的安装引用、CPU类型/IP/机架/插槽参数配置以及利用ReadBytes每100毫秒刷新20字节数据的循环读取方式。同时给出数据解析思路通过自定义Variable类标记起始地址、偏移量与数据类型借助thinger库转换Word、Int、Real、String等格式并解决跨线程Invoke更新UI和DataType命名冲突等实际坑点。包内仅含1个docx文档约4.56MB步骤完整可对照操作已有4197人学习适合需要快速落地西门子S7-1200实时数据采集与监控的工程师参考。1. C#上位机和S7-1200通信S7.net是绕不开的一个选项写过C#上位机、接过西门子S7-1200 PLC的工程师基本都经历过同一个纠结自己照着S7协议规范写一套通信还是直接用现成的库。S7.net是当前社区使用面最广的S7通信库支持从S7-200到S7-1500全系列对S7-1200和S7-1500的适配尤其成熟。它不需要安装Simatic Net不需要额外授权一个DLL加几行代码就能完成连接、读写DB块和I/O区对做设备数据采集、产线监控类的上位机场景非常合适。整篇文章围绕“用线程循环读取”这个具体方式来展开从环境准备到连接建立从循环线程的骨架到数据解析再把断线重连、字节顺序这些容易踩坑的地方单独挑出来讲目标是让新手拿着能跑通完整链路让熟手在参数细节和异常处理上有可以对照的参照。2. 环境与连接准备S7netplus安装与PLC侧三处关键设置2.1 在Visual Studio里引入S7.net包名是S7netplus第一次用这个库的人最容易在第一步就卡住去NuGet搜索“S7.net”能搜到结果但装上后发现命名空间对不上或者版本很老。正确做法是搜索“S7netplus”这是S7.net社区活跃维护分支使用的包名包内部命名空间仍然是S7.Net支持.NET Framework 4.6.1以及.NET 6.0以上的目标框架。老工程如果还在用.NET Framework 4.5需要先升级目标框架再安装否则编译时会报一堆程序集引用错误。在Visual Studio里可以用“管理NuGet程序包”界面搜索安装也可以在包管理器控制台执行Install-Package S7netplus安装完成后在代码文件头部引入命名空间using S7.Net;逻辑说明using S7.Net引入之后Plc、CpuType、VarType、DataItem这些核心类型才能直接使用。如果你写的是WinForm或WPF项目建议把通信相关代码单独放在一个类库里界面工程只订阅数据更新事件这样后面扩展读取变量时不用动界面代码。参数说明NuGet上存在“S7net”和“S7netplus”两个相近包名前者是早期版本后者是社区继续维护的分支。API大体兼容但S7netplus修复了不少连接稳定性和异步方法上的问题新项目直接选S7netplus。安装后看一眼项目引用确认S7.Net程序集已经在列表里再往下写代码。装完包我习惯先写一个最小连接测试确认链路通再继续往上加线程和循环。这个测试代码量不超过二十行能省下后面排查问题的大量时间。2.2 PLC侧要做的三处设置IP、PUT/GET授权、非优化DB块S7-1200和S7-300/400不一样它默认只允许TIA Portal访问。上位机要连上去必须在PLC侧把三个开关都打开少一个都连不通或者读到错乱数据。第一处是IP地址。在TIA Portal的设备组态里点击CPU上的以太网口给PLC分配一个和上位机同网段的固定IP。比如PLC是192.168.0.10上位机是192.168.0.50子网掩码都用255.255.255.0。S7-1200有的型号带两个网口编程口和额外的PROFINET口是独立的上位机要连的是和编程下载同一个网口否则路由不通。第二处是PUT/GET授权。在TIA Portal项目树里双击CPU选中“设备组态”在下方属性窗口切到“防护与安全”找到“连接机制”勾选“允许来自远程对象的PUT/GET通信访问”。不同固件版本这个选项的位置略有差异有的在“防护与安全”下有的在“组态”里找不到的话直接用关键词搜索TIA的帮助。改完配置要重新下载到PLC并且PLC会要求停机一次这对现场来说要安排合适的时间窗口。第三处是非优化DB块访问。S7.net按地址读写DB比如“DB1.DBD0”里的DB1是块号DBD0是偏移0字节的32位数据区。但S7-1200新建DB块时默认勾选“优化的块访问”勾选后变量地址由系统内部管理外部看不到实际偏移S7.net会读到错位的字节。需要右键目标DB块进入属性取消“优化的块访问”勾选然后重新下载块。下载时TIA提示块被重新生成确认即可。提示非优化块里每个变量都有明确的“偏移”列S7.net能读什么地址完全由这个偏移决定。拿不到TIA工程时至少要让PLC程序员截图给你各个变量的起始偏移。2.3 建立连接的最小代码CpuType、Rack、Slot和OpenC#里建立连接核心代码其实很短Plc plc new Plc(CpuType.S71200, 192.168.0.10, 0, 1); plc.Open(); if (plc.IsConnected) { Console.WriteLine(连接成功); } else { Console.WriteLine(连接失败); }逻辑说明构造函数创建了一个面向S7-1200的通信实例IP指向PLCplc.Open()执行TCP连接和S7协议握手这一步是同步的本地局域网内通常几百毫秒IsConnected返回当前连接状态。连接失败时Open()可能直接抛异常也可能是IsConnected为false取决于失败发生在Socket层还是S7握手层。参数说明构造函数里0是机架号Rack1是插槽号SlotS7-1200常见组合是0和1。S7-300/400常用0和2换PLC型号时这两个值要跟着变。这里有个容易忽视的细节Open()内部会先连TCP再走S7握手如果PLC拒绝连接或网络不通这里可能卡住几秒才返回所以连接动作一定不要放到UI线程里直接执行。连接建立后用一个最简单的地址验证链路比如读DB1的第一个字节object result plc.Read(DB1.DBB0); Console.WriteLine(result);这个能读通说明IP、授权、DB块访问模式都没问题。也顺便验证了地址字符串的写法——S7.net的地址格式是“块号数据类型偏移”比如DB1.DBB0是1号DB块的0偏移字节DB1.DBW2是2偏移开始的字DB1.DBD4是4偏移开始的双字。3. 线程循环读取为什么用Thread而不是Timer或async3.1 三种定时读取方案的取舍上位机读PLC定时循环是常规操作。实现方式常见有三种System.Timers.Timer、async/await加Task.Delay、独立Thread加while循环。三种我都用过各自的适用边界不太一样。System.Timers.Timer的问题在于回调在线程池里执行如果一次读取没完成下一次回调又触发读取间隔会漂移甚至堆积。特别当PLC响应变慢时回调重入会让S7.net内部状态错乱。System.Windows.Forms.Timer更不适合它在UI线程执行一旦读操作阻塞界面直接就卡死。async/await的方式写法简洁配合CancellationToken能优雅退出循环。但S7.net的读方法底层是同步Socketawait并不会把阻塞的读取变成真正的非阻塞只是把阻塞挪到了后台线程读一个慢PLC时依然会占住一个线程资源。而且S7.net的ReadAsync在老版本里实现并不完善异常处理容易遗漏。独立线程加while循环是我比较推荐的做法。一个专门的后台线程里跑读取循环读操作天然和UI隔离循环内做连接检查、重连、数据分发都很顺手关键是生命周期完全可控——线程什么时候停、停之前要不要关连接都看得明明白白。它的缺点是自己要处理取消和资源释放但这些学会一次就能复用很久。方案执行线程连接管理停启控制适合场景System.Timers.Timer线程池弱容易重入一般高频小数据量async/await循环调用方线程一般异常易漏可取消低频读取、逻辑简单Thread while独立后台线程强集中管理完全可控持续循环读取推荐3.2 线程循环的标准骨架CancellationToken加WaitOne一个能直接用的循环读取线程至少要包含四部分取消令牌、循环条件、读取动作、间隔控制。下面是完整骨架private CancellationTokenSource _cts; private Plc _plc; public void StartReading() { _cts new CancellationTokenSource(); Thread readThread new Thread(() ReadLoop(_cts.Token)); readThread.IsBackground true; readThread.Name PlcReadThread; readThread.Start(); } public void StopReading() { _cts.Cancel(); _cts.Dispose(); } private void ReadLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { object value _plc.Read(DB1.DBD0); Console.WriteLine($实时值: {value}); token.WaitHandle.WaitOne(100); } catch (Exception ex) { Console.WriteLine($读取异常: {ex.Message}); token.WaitHandle.WaitOne(1000); } } }逻辑说明StartReading创建后台线程并传入取消令牌ReadLoop里的while循环在令牌未生效前反复读取token.WaitHandle.WaitOne(100)让线程暂停100毫秒后继续下一轮。和Thread.Sleep(100)相比WaitOne的好处是取消时能立即唤醒退出不用干等Sleep结束。catch块里的WaitOne也一样目的是异常时不至于让循环空转刷屏。参数说明100毫秒对应10Hz的读取频率设备监控场景足够用。采集温度、液位这类缓变量200到500毫秒都合适做伺服或机器人状态跟随可以改成10到20毫秒但这时要缩小读取范围只在循环里读关键的几个变量别把整块DB都拖回来。PLC端通信负载也要考虑S7-1200对单连接的吞吐是有限的频率太高反而会让响应变慢。线程的IsBackground true也值得说一句。后台线程在主程序退出时会自动终止不会让进程关不掉。但这也意味着你需要在关闭前主动调用StopReading否则可能留下一个半开的PLC连接等进程完全退出才释放。3.3 用DataItem数组批量读取别一条条读循环里逐条执行Read每条命令都是一次TCP往返。读10个变量就是10次往返延迟大还会增加S7连接被PLC判定为超时的风险。更好的做法是用DataItem数组一次读取多个变量。DataItem[] items new DataItem[] { new DataItem { Db 1, StartByteAdr 0, VarType VarType.Real }, new DataItem { Db 1, StartByteAdr 4, VarType VarType.Int }, new DataItem { Db 1, StartByteAdr 6, VarType VarType.Int }, new DataItem { Db 1, StartByteAdr 8, VarType VarType.Bit, BitAdr 0 } }; _plc.Read(items); foreach (DataItem item in items) { Console.WriteLine(${item.VarType} at {item.Db}.DBD{item.StartByteAdr}: {item.Value}); }逻辑说明每个DataItem描述一个读取点Db是块号StartByteAdr是起始字节偏移VarType是数据种类BitAdr只在读取位变量时使用。_plc.Read(items)把多个读取请求合并成一条S7协议指令发出去PLC返回后自动把值写入每个item.Value。这样读5个变量只需要一次通信往返。参数说明VarType必须要和PLC侧变量类型严格一致类型错了读出来的值就是乱的。PLC里的Int对应VarType.Int读出来是short类型Real对应VarType.Real读出来是float类型。不同的VarType可以混在同一个数组里S7.net会按顺序解析。要注意如果数组里读取的地址分散在DB的不同区域S7.net内部会拆成多次请求性能提升会打折扣所以把物理地址邻近的变量尽量放在一起。批读取还有个额外好处异常处理集中到一个try/catch里一次失败要么整批失败要么整批成功不会出现同一轮循环里部分变量更新了、部分还是旧值的情况。对数据一致性要求高的场景这一点比逐条读取可靠得多。4. 数据类型与字节顺序读对了数据才真正落地4.1 S7-1200常用类型和S7.net的映射表S7-1200的数据类型和C#不是一一对应最容易混淆的是S7的Int是16位对应C#的short而不是intS7的Real是32位浮点对应C#的float而不是doubleS7的DInt是32位整数才是C#的int。写上位机时心里要时刻绷着这根弦。S7-1200类型位宽S7.net的VarTypeC#读出类型地址后缀示例Bool1位BitboolDB1.DBX0.0Byte8位BytebyteDB1.DBB0Char8位BytebyteDB1.DBB1Int16位IntshortDB1.DBW2Word16位WordushortDB1.DBW2DInt32位DIntintDB1.DBD4DWord32位DWorduintDB1.DBD4Real32位RealfloatDB1.DBD8LReal64位LRealdoubleDB1.DBD12String[n]变长StringstringDB1.DBB16这张表建议打印出来贴在工位旁边。实际开发里因为类型不匹配导致读值错乱的情况远比你想象的常见。尤其注意S7的Int对应C#的short这会让很多人觉得“奇怪”但S7协议规范就是这么定的。4.2 大端字节序Big-Endian和偏移对齐的两个坑S7协议在网络传输时使用大端字节序也就是高字节在前。S7.net在读取Real、Int、DInt时已经自动完成了字节序转换正常情况下你不需要手动处理。但有两个场景躲不开字节顺序问题。第一个场景是用ReadBytes读取原始字节后自己解析。比如读取一个Real变量直接拿BitConverter.ToSingle解析会得到错误的结果byte[] raw _plc.ReadBytes(DataType.DataBlock, 1, 0, 4); if (BitConverter.IsLittleEndian) { Array.Reverse(raw); } float value BitConverter.ToSingle(raw, 0);逻辑说明S7发送过来的4个字节是大端序而BitConverter.ToSingle是按当前平台的字节序解析的x86和ARM处理器几乎都是小端。Array.Reverse把字节数组反转成小端序再交给BitConverter才能得到正确的float。BitConverter.IsLittleEndian判断当前平台是否需要反转这样代码在大小端平台上都安全。第二个场景是偏移对齐。S7-1200的非优化DB块虽然取消了系统的符号寻址但变量依然遵循内存对齐规则。比如一个Int变量占2字节后面紧跟一个Real变量Real不会从偏移2开始而是从偏移4开始中间空出2字节填充。所以照着PLC程序里变量的声明顺序去推偏移算出来的地址往往对不上。正确做法是打开TIA Portal在非优化DB块的表格视图里直接看“偏移”列照着抄。如果PLC工程是别人维护的你拿不到TIA在线权限只能通过ReadBytes把DB块整块抓下来按S7的字节长度和对齐规则手动解析。先用两个已知值验证解析是否正确再逐步扩大范围。这个过程比较费时间但也是唯一可靠的办法。4.3 写入操作的类型转换细节写操作和读操作一样要严格匹配。S7.net的Write方法重载很多但传入的C#值类型必须和PLC侧变量类型一致否则轻则抛异常重则把错误的数据写入设备这在现场是安全事故级别的错误。_plc.Write(DB1.DBD4, 25.5f); // Real - float _plc.Write(DB1.DBW0, (short)100); // Int - short _plc.Write(DB1.DBX0.0, true); // Bool - bool逻辑说明第一行往一个Real变量写入25.5C#必须传float。如果写25.5d那是double类型S7.net会找不到匹配的重载或写入错误的字节长度。第二行写Int直接写100是int32位需要显式转成short16位否则可能写入到DInt的格式里把相邻变量一并冲掉。第三行写位变量传true或false。参数说明Write方法在失败时抛出的异常类型不固定可能是SocketException也可能是PlcException调用点附近必须有try/catch防止一个写入失败导致整个线程循环退出。写入频率也要控制没有操作员动作时不要反复写同一个值。除了增加PLC负载还可能被PLC程序误判为外部强制信号引发安全逻辑误动作。5. 通信避坑实录连接掉线、数值错乱、UI卡死5.1 连接跑一段时间就断开Read抛异常现象线程循环正常运行过了一两个小时突然抛出一个“远程主机强迫关闭了一个现有连接”之类的异常之后即便等待很久也无法自动恢复。原因S7-1200对同时连接数有上限常见型号默认允许2到8个连接具体数值和固件版本有关。如果上位机程序调试时反复连、不主动释放或者TIA Portal、HMI也在同时连接达到上限后PLC侧会把最旧的连接踢掉。另一个原因是PLC侧有闲置超时机制长时间没有读写请求时主动断开连接。解决第一读取周期不能太长保证在PLC闲置超时时间内有请求发出比如每100到500毫秒读一次就不会触发闲置断开。第二循环里对IsConnected做主动检查读取出错时先Close()再重新Open()不能只在程序启动时连一次。第三调试时确保每次运行结束进程完全退出避免残留连接占着PLC名额。5.2 读出来的Real值完全不对变成天文数字或NaN现象DB块地址确认没写错VarType也用的Real但读出来的浮点数明显不对有时候是几百万、几亿有时候是NaN。原因绝大多数情况是目标DB块开了“优化的块访问”S7.net按固定偏移读到的是错位后的字节序列。也有一部分情况是VarType填错了比如用Int去读Real变量4个字节被按2个字节解析出来的数自然不对。解决先在TIA里确认目标DB是“非优化访问”再核对VarType和PLC变量类型一致。如果两个条件都满足还是不对用ReadBytes把原始字节打出来和TIA里的实际数对比。这一步能最快判断是起始偏移错了、字节顺序错了还是类型解析错了别在原地瞎猜。5.3 UI界面卡死点击按钮没有响应现象程序启动后界面能显示但点击按钮时明显卡顿Windows提示“无响应”过很久才恢复严重时只能强制结束进程。原因典型的把S7.net的Read或Open直接放在按钮点击事件里。这两个操作都是同步阻塞的如果PLC网络不通或响应迟钝UI线程被卡住界面就完全失去响应。尤其是Open()在PLC不回应时可能阻塞几十秒。解决所有PLC读写都从UI线程挪到后台线程。按钮事件里只做一件事把写入请求丢给后台线程去执行或者置一个标志位让读取线程去处理。如果要在UI线程展示结果通过事件派发或者Control.Invoke来更新界面。用async/await包装同步Read也能不卡界面但底层依然阻塞在后台线程里所以根源还是要保证连接健康和数据读取不过于频繁。5.4 读取线程和写入线程同时运行数据出现错乱现象单独读没问题单独写也没问题读写同时发生时偶尔读到的值是上一次的结果或者直接抛出异常。原因S7.net内部是基于单TCP连接的协议类没有为多线程并发读写做完整保护。两个线程同时操作同一个Socket流请求和响应的对应关系会混乱读到的数据自然是错的。解决最直接的办法是让所有读写调用共用一把锁保证同一时刻只有一个线程进入通信方法。private readonly object _lockObj new object(); private object ReadValue(string address) { lock (_lockObj) { return _plc.Read(address); } } private void WriteValue(string address, object value) { lock (_lockObj) { _plc.Write(address, value); } }逻辑说明lock (_lockObj)确保同一时刻只有一个收发序列。读取线程循环和写按钮回调都统一走这两个方法数据就不会交错。这个方案牺牲了一定并发性但在单连接的上位机里完全够用也是我实际项目里一直在用的方式。如果你的场景对写入实时性要求高可以在锁外先缓存写入值由读取线程在每轮循环末尾统一执行写入。5.5 PLC断电重启后程序再也连不上现象PLC断电重启或者网线拔掉再插上程序界面上连接状态显示还是已连接但读取不到数据程序重启后连接也一直失败。原因PLC重启期间旧的TCP连接已经失效但S7.net只有在发出请求收到异常时才会发现这一点。而IsConnected属性在TCP Socket断开前可能仍是true程序不会主动去重连所以看起来像“连接还在”但就是没数据。解决在循环里加入连接状态检测发现IsConnected为false或读取异常时主动Close()再重新Open()。连续失败时要加上退避逻辑等待时间从1秒逐步增加到30秒避免一次次快速重连把PLC有限的连接列表占满。更稳妥的做法是把重连和读取拆成两个方法状态机清晰一点private bool EnsureConnected(CancellationToken token) { if (_plc.IsConnected) { return true; } try { _plc.Close(); _plc.Open(); return true; } catch { token.WaitHandle.WaitOne(TimeSpan.FromSeconds(3)); return false; } }逻辑说明EnsureConnected在读取前被调用连接正常就立即返回true连接断开时先Close清理旧的Socket状态再Open建立新连接。重试期间等待3秒不给PLC制造不必要的连接压力。除了这五个高频问题还有两个日常容易忽略的细节程序退出前务必调用plc.Close()主动断开连接否则PLC侧连接不释放S7.net的Plc对象实现了IDisposable程序退出时释放资源能避免下次调试时连不上PLC。6. 把循环读取封装成服务断线重连、日志与验证6.1 一个最小可用的通信服务类把前面几章的代码合并成一个服务类是落地到实际项目最省事的方式。下面这个类可以直接搬进你的上位机工程里public class PlcReadService : IDisposable { private readonly Plc _plc; private readonly object _lock new object(); private readonly int _readIntervalMs; private CancellationTokenSource _cts; private Thread _thread; public PlcReadService(string ip, int readIntervalMs 100) { _plc new Plc(CpuType.S71200, ip, 0, 1); _readIntervalMs readIntervalMs; } public void Start() { _cts new CancellationTokenSource(); _thread new Thread(() Loop(_cts.Token)); _thread.IsBackground true; _thread.Start(); Console.WriteLine(PLC读取服务已启动); } private void Loop(CancellationToken token) { while (!token.IsCancellationRequested) { if (!EnsureConnected(token)) { continue; } try { lock (_lock) { object value _plc.Read(DB1.DBD0); Console.WriteLine($实时值: {value:0.00}); } token.WaitHandle.WaitOne(_readIntervalMs); } catch (Exception ex) { Console.WriteLine($读取异常: {ex.Message}); token.WaitHandle.WaitOne(1000); } } } private bool EnsureConnected(CancellationToken token) { if (_plc.IsConnected) { return true; } try { _plc.Close(); _plc.Open(); Console.WriteLine(重新连接成功); return true; } catch (Exception ex) { Console.WriteLine($重连失败: {ex.Message}); token.WaitHandle.WaitOne(3000); return false; } } public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); _plc.Close(); _plc.Dispose(); } }逻辑说明Start创建后台线程并启动循环Loop每轮先确保连接可用再执行读取成功后等待设定的间隔失败则等1秒再试Dispose里取消线程并释放PLC资源。EnsureConnected做了断线重连连续失败时每次等待3秒避免高频重连。这套结构完成了一个基本可用的上位机读取服务。参数说明readIntervalMs默认100毫秒实际项目里根据设备响应速度调整。WaitOne(1000)这个异常分支的值也可以做成字段比如快速失败时只等200毫秒长期故障时逐步拉长到5秒防止日志刷屏。6.2 用心跳计数和数值范围检查验证链路代码跑通了不够还要验证通信链路是真的“活”的而不是读到缓存里的旧值。一个我常用的方法是在PLC里写一个每秒自增1的计数器放在DB1.DBD20上位机每次读取后检查这个数值有没有增加。如果两次读数相同说明数据链路可能已经停滞需要强制重连并记录日志。另一个习惯是给关键模拟量加上数值范围检查。温度、压力、流量这些物理量都有合理的上下限读回来的float如果超出量程直接标记为无效数据不送进业务逻辑。这样即使PLC侧数据异常上位机也不会把错误数据写进数据库或触发报警。数据校验在自动化项目里是底线不能省。6.3 最后一句经验话我习惯把读取间隔、重连延时、超时秒数全部做成服务类的公开属性随时可调不写死在代码里。每次改完参数后跑一个至少两小时的老化测试重点观察线程是否稳定、日志会不会堆积、连接有没有意外掉线确认没问题再交付现场。这套流程虽然朴素但能过滤掉大部分偶发问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表