简介:面向工业自动化软件开发人员与初学者,这份实例源码完整演示了使用C#语言与西门子可编程逻辑控制器进行S7协议通讯的实现过程,尤其适合需要掌握上位机与PLC数据交换、变量读写及状态监控的开发者参考。压缩包内含39个文件,主要包含C#源码文件、工程配置文件、可直接运行的可执行程序、动态链接库以及窗体界面资源,构成一套可打开即用的Visual Studio解决方案,便于对照学习和二次修改。包体仅244KB,结构紧凑,没有冗余内容。目前已有487人学习下载。源码中覆盖了通讯库的引入、网络连接的建立、读写特定地址区域的调用方法、异常捕获与多线程后台通讯等关键环节;同时窗体资源与后台逻辑的衔接方式,能够帮助读者理解如何为工业通讯功能配上友好的操作界面,整体可作为同类项目开发的实用模板。
1. 通过C#实现与西门子PLC的S7通讯:为什么我不推荐用OPC而直接撸S7协议
做上位机开发的这几年,我见过太多项目在“C#怎么和西门子PLC通信”这一步卡住。网上搜“C# 西门子PLC”,出来的教程一半是OPC UA,另一半是LibNoDave的远古封装,真正能直接拿来改的S7通讯实例源码反而少得可怜。如果你只是想在WinForm里读几个DB块、写几个M点,用S7协议直连是最省事的方案——不需要在工控机上装OPC服务器,不需要配DCOM,甚至不需要西门子的博途软件参与运行。这篇文章我会把S7通讯从协议选型到源码落地讲透,重点放在Sharp7这个开源库上,最后把那些让你半夜想砸电脑的坑挨个排一遍。
如果你是刚接触C#上位机的新手,或者已经在用OPC但觉得太重、延迟太高,这篇文章正好对症。我会给出一套可以抄作业的代码结构,连防火墙规则和PLC侧的DB块偏移怎么算都写清楚。
2. 先搞懂S7通讯协议:你连的是S7-300还是S7-1500,代码写法完全不一样
2.1 S7协议家族的三个变体:S7-300/400的Profinet与S7-1200/1500的PUT/GET
西门子PLC的S7通讯不是一个单一协议,而是一族协议。早期S7-300/400走MPI或者Profinet时,用的是一种基于ISO-on-TCP的封装,数据在PLC侧由CPU的通信功能块(FB15 "PUT"、FB14 "GET")来处理。而S7-1200/1500系列,除了兼容老的S7通信,还支持一种更简洁的PUT/GET直接访问方式——不需要在PLC程序里调用任何功能块,只要在PLC组态里勾选“允许PUT/GET通信访问”就行。
这里有个容易搞混的点:S7协议和Profinet协议是两回事。Profinet是实时以太网协议,用于IO设备与PLC之间的周期数据交换;而S7协议是应用层协议,跑在TCP/IP之上,用于编程软件、HMI或上位机与PLC的通信。我们用C#写的就是这个S7协议,它的默认端口是102。无论是S7-300还是S7-1500,只要配置好,C#这边看到的都是一个TCP连接加一套请求响应帧。
但不同PLC系列在数据访问上确实有差异。S7-300/400的DB块有上限范围,DB编号不能超过65535,且访问时需要精确指定DB号、起始字节和长度。S7-1200/1500除了DB,还支持直接用符号名访问(比如“MyDB”.StaticVar),但符号访问在Sharp7里支持有限,我一般建议用绝对地址,省去符号解析的麻烦。
2.2 为什么选Sharp7:对比S7netplus和自写协议栈
C#这边能用的S7通讯库主要有三个流派:Sharp7、S7netplus、还有自己用Socket撸协议。我最早用过S7netplus,它对S7-1200/1500支持还行,但对旧版S7-300的某些数据类型处理有bug,而且项目停更过一段时间。Sharp7是C++版Snap7的C#封装,Snap7在工控圈用了十几年,稳定性经过了大量现场验证,Sharp7也继承了这套底子。
自写协议栈我不建议,除非你只是想学习。S7协议的PDU(协议数据单元)有协商、读、写、读多点、写多点等多种服务,帧格式里包含TRAILER、HEADER、PARAM、DATA四个部分,每个部分又有自己的子结构,光是把错误码表对齐就要折腾一周。用Sharp7的话,核心调用就几个方法:Connect、ReadArea、WriteArea,内部帧格式和重连逻辑都已经处理好了。
还有一个选择是LibNoDave,它是个C库,对老式S5 PLC支持好,但C#调用要自己PInvoke,而且文档匮乏,遇到问题连谷歌都救不了你。所以我的结论是:新项目一律Sharp7,除非你有老设备兼容的硬需求。
2.3 Sharp7的安装与连接最小示例:从NuGet到TCP握手的完整代码
先建一个.NET Framework 4.7.2的WinForm项目(.NET 5以上也行,但工控机上老系统多,Framework兼容性更好)。在NuGet里搜索Sharp7,安装最新稳定版即可。安装完在代码顶部加上命名空间引用。
using Sharp7;接下来是最小连接示例。S7-1200的IP是192.168.0.1,机架号0,插槽号1,这些参数在PLC的以太网设置或博途的硬件组态里能看到。S7-1500默认机架0插槽0,S7-300经常是机架0插槽2。连接超时设3000毫秒,读超时设2000毫秒,第一次连接时这个值别设太小,PLC进行TCP握手加上TSAP协商需要一点时间。
// 创建S7客户端实例,参数依次为:IP、机架号、槽号 S7Client client = new S7Client(); int result = client.Connect("192.168.0.1", 0, 1); if (result == 0) { // 连接成功,0表示无错误 Console.WriteLine("连接成功,PDU请求长度: " + client.GetPDURequestLength()); } else { // S7库的错误码可以在S7Client.ErrorText()里转成可读信息 Console.WriteLine("连接失败: " + client.ErrorText(result)); }这段代码的关键点在于Connect方法内部的TSAP计算。S7-1200和S7-1500的TSAP通常是0x0100加上机架和槽位计算出来的,Sharp7在Connect内部已经封装了这个逻辑,只要机架号和槽号对,就能通。连接成功后,GetPDURequestLength能看到协商后的PDU长度,一般S7-1200是240字节左右,S7-1500是480字节左右,这个值决定了你要不要开启多PDU读写(后面讲大块数据时再说)。
2.4 PLC侧的准备工作:博途组态必勾选项与防火墙规则
PLC侧如果不放行,写再多C#代码也是白搭。在博途(TIA Portal)里打开CPU属性,找到“防护与安全”->“连接机制”,勾选“允许来自远程对象的PUT/GET通信访问”。注意S7-1200和S7-1500这一步是必须的,但S7-300/400没有这个选项——它们天生就允许S7通信,前提是CPU在运行状态。如果你的PLC在博途里做了安全设置,比如启用了“仅允许安全通信”,那普通S7连接会被拒,这时只能走OPC UA或者调整安全等级。
防火墙是另一个高频坑。笔记本直连PLC时Windows防火墙默认拦截102端口,在管理员命令行里执行下面的命令放行。如果你的上位机在后台常驻服务,建议放行TCP 102端口而不是临时关防火墙——关防火墙在工控环境里是安全大忌。
netsh advfirewall firewall add rule name="S7端口" dir=in action=allow protocol=TCP localport=102执行完可以用netsh advfirewall firewall show rule name="S7端口"确认规则存在。注意如果是跨网段访问,还要确保路由和PLC侧的子网掩码一致。PLC的IP、子网掩码在博途的“设备与网络”里设置,默认子网192.168.0.0/24,如果你的电脑是192.168.1.x,不在同一网段是通不了的。
3. C#上位机读写PLC数据:DB块、M区、I/Q区一次说透
3.1 用ReadArea读DB块的字节数组:地址偏移用16进制才不会算错
最常见的需求是读DB1里的数据。假设PLC里DB1的起始偏移是0,前4个字节是一个REAL(浮点数),后面2个字节是一个INT(整数),那么在C#里一次性读6个字节,然后用Sharp7的转换方法解析。这一步不要逐个字节读,S7通讯的最小PDU也有几十个字节,一次多读点能显著降低通信频次,提高吞吐。
// 读DB1,从第0字节开始读,共6个字节 byte[] buffer = new byte[6]; int result = client.ReadArea(S7Area.DB, 1, 0, 6, buffer); if (result == 0) { float tempValue = S7.GetRealAt(buffer, 0); // 解析第0字节开始4字节为浮点数 short speedValue = S7.GetIntAt(buffer, 4); // 解析第4字节开始2字节为短整型 }ReadArea的参数含义依次是:存储区类型、DB块编号、起始字节地址、读取长度、目标字节数组。存储区类型有S7Area.DB(数据块)、S7Area.M(M区)、S7Area.I(输入映像区)、S7Area.Q(输出映像区)。S7.GetRealAt和S7.GetIntAt是Sharp7的静态转换方法,从缓冲区里按字节序解析数据。
碰到底层的坑:西门子PLC的数据存储是大端序(高字节在前),而C#的BitConverter是小数端序。Sharp7内部已经处理了大端转换,所以你用S7.GetRealAt拿到的float,和PLC里看到的数值一致。如果你自己写转换逻辑,比如BitConverter.ToSingle(buffer, 0),拿到的数值会错得离谱。这是C#和PLC通信时最容易忽略的暗坑。
3.2 写DB块和M区数据:WriteArea与数值转换函数配套
写数据比读数据多一步:先把C#的数值转换成字节数组,再写入PLC。比如往DB2偏移8的位置写一个值为25.8的REAL,再往M10.0写一个BOOL(ON)。M区的地址写法要注意,M10.0是整个M区的第80位(10*8+0),但按字节来算,就是M区字节10的位0,所以偏移填10。
// 准备写入DB2的数据 byte[] writeBuffer = new byte[4]; S7.SetRealAt(writeBuffer, 0, 25.8f); // 把25.8转成S7字节序 int result = client.WriteArea(S7Area.DB, 2, 8, 4, writeBuffer); // 写M10.0,M区整体按字节偏移算,M10.0 = 字节10的位0 byte[] mWriteBuffer = new byte[1]; S7.SetBitAt(ref mWriteBuffer, 0, 0, 1); // 第0字节的第0位设为1 int mResult = client.WriteArea(S7Area.M, 0, 10, 1, mWriteBuffer);注意WriteArea的第五个参数长度单位和PLC侧的寻址单位要一致。S7协议里长度单位是字节,所以你写一个BOOL传到PLC侧要占1个字节,但这个字节的其他位不会被动。如果PLC侧用原生的S7通信功能块(比如AG_SEND/AG_RECV)和你对接,对方收到的数据块长度会比你写的长度多截断或补齐,这个后面在“和LAD编程的差异”那一章详细讲。
还有个坑:M区的起飞偏移不是0。对S7-300/400,M区在CPU的组态里可以设置起始地址(一般是0),但S7-1200/1500的M区上限是4096字节。如果写入的字节偏移超出范围,PLC会返回错误0x81000000(对象不存在),这时检查你的M区地址换算。
3.3 读I/Q输入输出映像区:为什么实际值和你看到的不一样
I/Q区是PLC的物理输入输出映像,读I区能拿到现场按钮、传感器的实时状态,写Q区能直接控制输出模块(继电器、伺服使能等)。但这里有个玄学问题:CPU的扫描周期可能早于外部信号的变化。
byte[] inputImage = new byte[1]; // 读I0.0-I0.7,I区按字节偏移,I0.0就是第0字节 client.ReadArea(S7Area.I, 0, 0, 1, inputImage); bool bit0 = S7.GetBitAt(inputImage, 0, 0); // I0.0的值 bool bit1 = S7.GetBitAt(inputImage, 0, 1); // I0.1的值读I/Q区时,如果发现数据看起来“卡住”或变化慢,先检查PLC程序里是不是用了“读取过程映像输入”而不是“立即读取”。S7-1200/1500的I/Q区默认按过程映像刷新,刷新速率在CPU组态的“循环周期”里设置,默认是扫描周期的固定倍数。上位机读到的值反映的是上一个扫描周期结束时的状态,不是物理信号的实时电平。对高速计数或编码器这类信号,建议在PLC程序里用“读时立即更新”(PIP)特性,或者把数据搬到DB里再让上位机读DB。
3.4 位读写操作的两种方式:整字节掩码与直接位寻址
Bit的读写有两种路径。第一种是用ReadArea读整个字节,用S7.GetBitAt取目标位,改完后用WriteArea写回整个字节。这种方式适合批量处理同一个字节里的多个位,比如控制字里的8个BOOL指令。
第二种是直接构造单字节缓冲区做位操作。前面的M区写入例子就是这种方式,但要注意SetBitAt的偏移参数是相对当前缓冲区的,不是PLC里的绝对地址。有些新手会直接写S7.SetBitAt(ref buffer, 10, 0)想写M10.0,结果写到了缓冲区的偏移第10字节的位0,然后WriteArea还把长度设为1,这就完全错位了。正确做法是缓冲区长度要覆盖你指定的字节偏移,即缓冲区至少要11个字节。
byte[] bitBuffer = new byte[11]; // 长度必须超过偏移所在字节 client.ReadArea(S7Area.M, 0, 0, 11, bitBuffer); // 先读出来 S7.SetBitAt(ref bitBuffer, 10, 0, 0); // 改M10.0为OFF client.WriteArea(S7Area.M, 0, 0, 11, bitBuffer);// 整个写回先读后改再写是安全做法,不然会覆盖同字节里其他位的数据。如果你在PLC侧用M区的位做互锁或报警,上位机写回整个字节时可能瞬间改变PLC程序正在使用的其他位,造成偶发性误动。我一般建议:能不用M区位做关键控制就不不要用,尽量用DB里的位,这样上位机读写的粒度更可控。
4. 实例源码:做一个WinForm的PLC读写面板,含多线程防卡死
4.1 界面布局与线程模型:为什么UI线程不能直接连PLC
WinForm里直接在主线程调用client.ReadArea,一旦PLC断网或响应慢,界面立即假死。正确做法是创建一个后台通信线程,UI通过BeginInvoke或Task把请求丢给后台线程执行,结果再通过回调更新界面。
下面是一个最小可跑的面板设计:上面一个读取按钮、一个写入按钮、一个IP地址输入框、一个状态标签,中间一个DataGridView显示DB块的实时值。通信线程循环每秒读一次DB1的前8字节(两个REAL),数据变化时更新UI显示。
// 通信线程主循环 private void CommThreadLoop() { while (!_isStop) { Thread.Sleep(500); // 500ms一个周期 byte[] buffer = new byte[8]; int result = _client.ReadArea(S7Area.DB, 1, 0, 8, buffer); if (result == 0) { float value1 = S7.GetRealAt(buffer, 0); float value2 = S7.GetRealAt(buffer, 4); // 用BeginInvoke避免跨线程操作UI this.BeginInvoke(new Action(() => { textBoxValue1.Text = value1.ToString("F2"); textBoxValue2.Text = value2.ToString("F2"); })); } else { // 掉线处理:置状态,尝试重连 this.BeginInvoke(new Action(() => labelStatus.Text = "连接断开: " + _client.ErrorText(result))); TryReconnect(); } } } // 用BeginInvoke避免跨线程操作UITryReconnect里做指数退避,比如第一次断开等1秒重连,第二次等2秒,最多等5秒。不要用固定间隔疯狂重连,PLC的CPU会记录连接错误次数,连太频繁可能触发它的通信防护机制。
4.2 连接与重连策略:断线后不要立即重连,先判断PLC是否可达
断线重连是上位机稳定性的关键。我踩过的坑是:PLC断电重启期间,C#客户端不会自动重新握手,ReadArea会一直返回0x00000103(TCP连接断开)。这时要做的不是直接重连,而是先Ping一下PLC的IP,判断是网络断了还是PLC程序还在启动中。
private void TryReconnect() { if (_isReconnecting) return; _isReconnecting = true; try { int retryCount = 0; while (!_isStop && retryCount < 5) { Ping ping = new Ping(); PingReply reply = ping.Send(_plcIp, 1000); if (reply.Status == IPStatus.Success) { int result = _client.Connect(_plcIp, 0, 1); if (result == 0) { this.BeginInvoke(new Action(() => labelStatus.Text = "已重新连接")); break; } } Thread.Sleep(2000 * (retryCount + 1)); // 退避等待 retryCount++; } } finally { _isReconnecting = false; } }注意PLC重启后,S7服务可能需要几秒到十几秒才能监听102端口,Ping通不代表能连上。这里的重试次数和延时是经验值,如果你的PLC启动很慢(比如带了很多从站),重试次数可以加到10次,每次间隔可以调到5秒。
4.3 多DB块批量读取:为什么循环读慢但可靠,多PDU拼接怎么处理
当你要读的数据分布在多个DB时,最简单的做法是一个DB一个ReadArea调用,这样代码直观,出错时定位快。但如果你追求扫描周期上的数据一致性,比如多个DB里的数据是一组联动配方,需要一次性快照,就得使用“多读”功能。
Sharp7提供ReadMultiVars方法,可以在一个PDU里封装多个读写请求。用这种方式要小心PDU长度的限制:每个请求需要约40字节的协议开销,S7-1200的PDU是240字节,意味着单次最多装4-5个读请求;S7-1500的PDU是480字节,可以装到8-9个。超过的话协议栈会报错,只能拆包。
// 定义一个包含多个读取项的结构 S7DataItem[] items = new S7DataItem[3]; byte[] db1Buf = new byte[4]; byte[] mBuf = new byte[1]; byte[] db2Buf = new byte[8]; items[0] = new S7DataItem { Area = S7Area.DB, DBNumber = 1, Start = 0, WordLen = S7WordLen.Byte, Amount = 4, Data = db1Buf }; items[1] = new S7DataItem { Area = S7Area.M, DBNumber = 0, Start = 10, WordLen = S7WordLen.Byte, Amount = 1, Data = mBuf }; items[2] = new S7DataItem { Area = S7Area.DB, DBNumber = 2, Start = 0, WordLen = S7WordLen.Byte, Amount = 8, Data = db2Buf }; int result = client.ReadMultiVars(items, 3); if (result == 0) { // items 数组里的Data缓冲区现在已有数据 float v1 = S7.GetRealAt(items[0].Data, 0); bool mBit = S7.GetBitAt(items[1].Data, 0, 0); }多读的坑在于起始地址和长度必须对齐字节,不同数据类型的长度校验很严格。比如Amount=2且WordLen=Bit时,某些PLC固件会报参数错误,所以多读里我一般只放字节型数据,位数据单独用ReadArea读。
4.4 日志与数据存库:读取异常别只写日志,要做报警联动
现场排障时,最宝贵的就是通信异常前后的数据上下文。我会把每次通信请求的返回码、耗时、PLC的CPU类型记录下来写到文本日志,同时存一份到SQLite或者SQL Server里。这里有个实用技巧:日志文件按天空分,超过一定大小自动切割,不然工控机硬盘撑不住。
日志至少记录这几项:时间戳、请求类型(读/写)、目标地址、返回码、耗时毫秒数。如果发现某段时间返回码频繁为0x81000004(数据长度不一致),那八成是你在C#侧定义了错误的长度,或PLC侧的DB被程序在线修改了。
存储到数据库时要注意通信线程和写库线程的隔离,别在通信线程里做数据库操作,否则数据库卡顿会反向拖累通信。常见做法是开一个队列,通信线程把记录丢到队列里,一个后台线程慢慢写库。
5. S7通讯实例避坑指南:5个让人崩溃的常见问题与排查方法
5.1 连接超时:从0x00000101错误码开始排查
现象:Connect方法返回0x00000101(连接超时),或ReadArea在通信一段时间后突然超时。原因通常有三个层次:网络不通、PLC不监听102端口、防火墙拦截。
排查步骤:先用Ping测IP通不通;再用telnet 192.168.0.1 102命令确认S7端口是否开放,如果telnet卡住或拒绝,说明PLC侧的PUT/GET没放开或PLC未运行。最后检查Windows防火墙规则,或者干脆临时关闭防火墙做个对照测试。经验里还有一种:PLC的以太网口接到了交换机的某个VLAN,你的电脑在另一个VLAN,交换机没做路由。
5.2 连接成功但读写返回0x81000004:DB块长度不匹配或数据类型搞错
现象:Connect成功,ReadArea调用返回0x81000004。原因是请求的长度和PLC侧的实际数据结构长度不匹配。比如你想读DB1的16个字节,但DB1实际只有8个字节,或者DB号不存在。
解决:在博途里打开DB块,看“偏移”和“声明”的实际属性,对照着修改C#侧参数。还有一个隐蔽情况:PLC程序里用了“优化的块访问”,此时DB的偏移地址不再按声明顺序排列,而是编译器重排了。这时要么在DB属性中取消“优化的块访问”,要么用符号名寻址(Sharp7的S7Client.ReadArea支持“DB名.变量名”吗?不支持,需用其他库)。
5.3 PDU长度协商失败:S7-1200连接成功但一读就断
现象:Connect成功,但第一次ReadArea后连接立即断开。原因多半是PDU协商出的缓冲长度小于你请求的数据长度。S7-1200的PDU默认约240字节,你一次读200字节数据加上协议头就超了。
解决:把单次读取长度限制在200字节以内,或者开启“多PDU读写”模式。Sharp7里没有直接设置多PDU的开关,常见做法是读取前先判断client.GetPDURequestLength(),根据这个值拆分读取。比如PDU是240,那就每190字节分一次,留足协议头空间。
5.4 DB号超过65535或地址越界:S7-1500的扩展寻址怎么处理
现象:S7-1500的DB号如果超过65535,Sharp7的DBNumber参数就不够用了。原因是S7协议的DB编号字段是16位的,最大65535。S7-1500支持更大的DB号,但需要S7协议的扩展寻址特性,Sharp7对这部分支持有限。
解决:要么在PLC侧把大DB拆成几个小DB,要么把数据复制到固定的全局DB里。我遇到几十个配方DB的情况,通常让PLC程序在启动时把当前配方数据复制到DB1(固定公共区),上位机只读写DB1,简单可靠。
5.5 时间戳漂移和数据抖动:高速采集时为什么读到的值是乱的
现象:以10ms周期读PLC数据,发现某些值偶发跳变。原因有几个可能:上位机定时器不精确(Windows计时器精度约15ms)、PLC扫描周期和上位机读取周期不对齐、多个线程同时操作同一个S7Client实例导致PDU交错。
解决:用一个高精度的Stopwatch驱动循环,确保周期稳定性;给S7Client的读写调用加锁。注意S7Client不是线程安全的,多个线程不能用同一个实例。实在要并发,就为每个线程创建一个独立连接(PLC侧注意最大连接数,S7-1200一般支持32个)。
private readonly object _lockObject = new object(); private int SafeReadArea(...) { lock (_lockObject) { return _client.ReadArea(...); } }5.6 与PLC程序内部变量的映射关系:DB块偏移怎么手动算出来
如果你在PLC里新建了DB,里面有两个REAL和一个BOOL,你想让上位机读这个BOOL。在“优化的块访问”关闭的情况下,第一个REAL偏移0,第二个REAL偏移4,BOOL在偏移8的字节第0位。如果勾选了“优化的块访问”,这些偏移会被编译器重排,你看到的不连续,实际访问就错位。
所以我的建议是:给上位机通信用途的DB,一律取消“优化的块访问”。然后在DB里用注释标注变量用途、数据类型和偏移,这样将来其他工程师接手也能快速核对。还有一招:在PLC里用“PLC变量”表定义带符号名的绝对地址,上位机侧维护一个从符号到地址的映射表,程序启动时加载,这样改PLC组态时不用重新编译上位机。
6. 进阶玩法:用异步轮询与队列缓冲实现高吞吐数据采集
如果你要采集的数据点很多,比如100个DB块、每个块若干字节,直接循环读太慢,且IO请求会在PLC侧排队。我的做法是引入一个采集任务队列,每个任务封装一个“读请求”,工作线程池并发处理,但每个S7Client实例只能被一个线程独占。
先建一个消息队列,用于生产消费数据。通信线程周期性的把所有需要读的地址打包成一个批次,然后按照PDU限制拆包,逐个执行多读。读回来的数据放进一个ConcurrentDictionary,UI线程或存储线程从这里取数据。
// 异步采集队列 ConcurrentQueue<byte[]> _dataQueue = new ConcurrentQueue<byte[]>(); // 通信线程循环 while (!_isStop) { Stopwatch sw = Stopwatch.StartNew(); // 假设要读DB1:0, DB2:0, M20:0 三个区域 byte[] data = new byte[4 + 8 + 2]; // 按照前面的多读方法填充 data _dataQueue.Enqueue(data); sw.Stop(); // 动态调整周期,保持总采集周期稳定 int delay = (int)(_targetCycleMs - sw.ElapsedMilliseconds); if (delay > 0) Thread.Sleep(delay); }这里有个关键参数:目标采集周期。PLC的扫描周期一般在1-10ms,上位机采集周期设为5-20ms已经覆盖大多数场景。如果设为1ms,会频繁产生S7请求,导致PLC的通信处理器CPU占用升高,影响PLC程序的实时性。我曾在一个设备上把周期设为2ms,结果PLC的循环周期从5ms被拉到8ms,设备报警,把上位机改成10ms后恢复正常。
队列消费端是一个独立线程,从_queue里取数据,做二次计算(比如温度换算、状态字拆解),然后写入SQLite或实时数据库。用队列的好处是消费端处理慢时不阻塞采集,如果积压严重,可以丢旧数据保新数据,我会在入队前检查队列长度,超过阈值就清空重建。
最后说一个我养成的检查习惯:每周会拉一次通信日志,统计返回码分布和时间戳间隔,看趋势提前发现隐患。有一次发现S7请求平均耗时从1.5ms涨到6ms,一周后PLC报通信负载过高,检查发现是有个同事在博途里开了在线监控,占用了大量通信资源。这种“玄学”故障,日志统计比临场抓包靠谱得多。
这套C#和S7的通信方案,从我第一版用S7netplus踩坑不断,到换Sharp7后稳定运行三年,过程里最大的教训就是:不要相信某个库能完美适配所有PLC型号,连接参数和PDU限制要根据实际设备现场调;也不要相信一次连接永不掉线,重连机制做得越稳,你在现场被叫去“重启软件”的次数越少。希望这份实例源码和避坑清单能帮你少走我当初的弯路,祝你的上位机和PLC一次握手成功。
本文还有配套的精品资源,点击获取