搞工业自动化的朋友应该都体会过这种场景:车间里十几台西门子S7-1200分布在不同工位上,每天要么跑过去看触摸屏,要么等操作工用对讲机报故障。其实用C#写一个上位机APP,把这些PLC的数据集中拉上来,实时监控状态、记录趋势、推送报警,并不需要多高深的技术。这篇文章就把我从零搭建一套C#上位机监控S7-1200的完整过程拆开讲,从通信选型、PLC侧配置、代码实现到界面设计,再到我实际调试过程中踩过的坑,一次性说清楚。
这套方案适合谁看?如果你是有一定编程基础、准备入工控上位机开发的工程师,或者已经在用WinCC/组态王但想自己掌控整套软件逻辑的朋友,这篇文章能帮你省掉不少摸索时间。我会尽量把关键参数和决策理由讲透,方便你直接照着做。
1. 项目整体设计与通信方案选型
1.1 为什么选C#做上位机监控
先说结论:C#不是唯一选择,但对大多数工控场景来说,它是性价比最高的选择之一。
工控上位机常用语言无非三种:C++、C#、LabVIEW。C++性能强但开发效率低,光是一个界面布局就能磨掉你两三天;LabVIEW在仪器控制领域很强,但做复杂报表、对接数据库、写业务逻辑时,你会发现它的编程范式很别扭。C#是典型的托管语言,开发快、内存安全、有强大的WinForm/WPF UI框架,再加上Visual Studio这整套IDE,从拖控件到断点调试都极其顺手。工业现场那些设备数据量又不是高频交易,通信周期100ms到500ms完全够用,C#的性能根本没有瓶颈。
另外一个关键点是生态。C#社区里现成的PLC通信库非常成熟,不管是走底层S7协议的开源库,还是通过OPC UA中转的标准方案,都有大量开源代码和文档可以参考。这意味着你不用从零开始啃西门子通信协议栈。
我选择C#还有一个现实原因:后期维护。车间的设备要不断加测点、改报警逻辑,用C#写的上位机程序,改起来就是改几行代码重新编译的事,发布也简单,拷个exe加依赖文件就能跑。而且C#开发人员比C++好找得多,这对项目长期维护很重要。
1.2 三种主流通信方案对比:S7.NET、Snap7、OPC UA
连接S7-1200做上位机监控,业界常见的路子有三条,我把它们放在一起对比:
| 方案 | 底层协议 | 是否需要额外软件授权 | 开发难度 | 适用场景 |
|---|---|---|---|---|
| S7.NET(S7.Net Plus) | S7协议(ISO-on-TCP,端口102) | 不需要 | 低,API封装很好 | 中小型项目、单机或少量PLC |
| Snap7 | S7协议 | 不需要 | 中,需处理DLL调用 | linux/windows跨平台、开源生态 |
| OPC UA客户端(如Opc.Ua) | OPC UA | PLC侧需配UA server,部分固件功能受限 | 较高,信息模型复杂 | 大型系统、多品牌设备统一接入 |
先说OPC UA。它是西门子主推的未来方向,优点在于跨平台、跨厂商、信息安全机制完善,但如果你的项目就是监控几台S7-1200,用OPC UA有点杀鸡用牛刀。配置UA server要额外折腾证书、端口、用户认证,PLC侧还需要支持UA功能(S7-1200从Firmware 4.0以上支持,但实际用起来还是要小心固件版本差异)。如果后续要对接MES、ERP这类IT系统,OPC UA的优势才会真正体现出来。
再对比S7.NET和Snap7。这两者底层都是直接走S7协议和PLC通信,都不需要安装西门子任何软件,只要有IP地址和通信权限就能读写。区别在于S7.NET是纯C#封装,用起来最顺手,比如plc.Read("DB1.DBD0")一行代码就能读一个浮点数,对新手极其友好;Snap7则是一个跨平台的C++动态库,提供C接口,要在C#里通过P/Invoke调用,同时还要管理好原生DLL的生命周期,稍不注意就会出现内存访问异常(后面我会专门讲这个问题)。Snap7的优势是性能天花板更高、跨平台能力更强(Linux工控机也能用),而且它的核心库由C++实现,通信效率理论上比纯托管的S7.NET略好。
1.3 我的最终选择:S7.NET + 备用Snap7
在这个项目里,我最终选了S7.NET作为主力通信库,理由很直白:开发效率最高,API设计贴合工业语义,官方文档和GitHub示例都够用。具体用的是社区维护的S7.Net Plus(NuGet包名是S7.Net,当前版本支持S7-200/300/400/1200/1500)。
不过我也把Snap7的DLL放在了项目里作为备选。什么时候需要它?当S7.NET遇到它不支持的奇数数据块读取时,或者当你需要同时读取PLC里上百个变量、对通信性能到了极致要求时,Snap7提供的底层报文拼接方式更灵活。两套方案都写好了封装接口,切换成本不高。后面在复盘优化性能时,我会提到实际数据。
2. S7-1200侧的通信准备(不配置好,代码写得再好也白搭)
2.1 开启PLC的PUT/GET通信权限
很多第一次做S7-1200上位机的人都会卡在第一步:程序连不上PLC。IP地址能ping通,但创建连接时就是报错。绝大多数情况下,是因为PLC侧没有开放远程通信权限。
在TIA Portal(博途)里,打开CPU的"设备组态",选中CPU,在属性窗口中依次找到"防护与安全"->"连接机制",勾选"允许来自远程伙伴(PLC、HMI、PC)的PUT/GET通信访问"。这一步做了之后,外部上位机才能通过S7协议读写PLC数据。
还有一个细节:S7-1200的通信权限其实分成"完全访问"和"只读访问"两种,如果你只是做监控需求,勾选只读就够了。但如果后续要写参数、下发控制指令,就需要完全访问权限。我通常直接勾选完全访问,因为后面调试时总免不了写几个变量测试,省得来回改配置。
2.2 DB块优化访问的问题
S7-1200从固件V4.0开始,新建的DB块默认开启了"优化的块访问"。这个功能对PLC内部程序有好处,它让PLC对DB块的寻址变得更灵活,但对外部上位机来说是个大坑——优化块访问的DB没有固定偏移地址,外部S7协议无法按地址读取。
解决方法是选中对应的DB块,在属性里取消勾选"优化的块访问"。注意这一步必须在DB块变量还没有被程序大量引用之前做,改完之后DB块内变量的偏移地址会被重新计算,程序里的符号引用不受影响,所以实际改动风险不大。
但生产设备上已经跑着的PLC不能随便改配置怎么办?还有一个办法:在PLC程序里新建一个专门的"通信数据DB",用于上位机集中读写。这个DB取消优化访问,里面按固定偏移放好上位机需要的所有变量。这样做既不动原程序的数据结构,也方便上位机端做地址映射。
2.3 数据块的起始地址确定
取消了优化访问的DB块,可以在博途的DB编辑界面看到每个变量的"偏移地址",格式通常是DB块号.DBX字节地址.位地址或者DB块号.DBD字节地址。比如"数据DB"编号是3,第一个浮点数放在偏移地址4到7字节,那在S7.NET里读它的地址就是"DB3.DBD4"。
这里一定要理解S7地址的构成规则,我总结成一张表:
| 数据类型 | 地址格式 | 占用字节 | 说明 |
|---|---|---|---|
| BOOL | DB1.DBX0.0 | 1位 | 点号后是位索引0-7 |
| BYTE | DB1.DBB0 | 1 | |
| WORD | DB1.DBW0 | 2 | 起始地址必须偶数 |
| DWORD | DB1.DBD0 | 4 | 起始地址必须是4的倍数 |
| INT | DB1.DBW0 | 2 | 有符号16位 |
| DINT | DB1.DBD0 | 4 | 有符号32位 |
| REAL | DB1.DBD0 | 4 | 32位浮点数 |
| STRING | DB1.DBB0 | 视定义长度 | 前两字节存最大长度和当前长度 |
这里面最容易出错的地方是地址对齐。S7协议对WORD、DWORD这类多字节数据有对齐要求,你如果在DB里把几个BYTE和一个REAL排在一起,博途会自动插入填充字节来对齐。所以上位机读地址时,不要自己脑补偏移量,一定要照着博途里显示的实际偏移来写。我在项目里干脆写了一个Excel表,把PLC侧变量、DB偏移、数据类型、上位机侧变量名做了一一映射,调试时对照着填代码,基本不会出错。
2.4 S7-1200的机架号与槽号
连接S7-1200时最让人困惑的是连接参数里的机架号(Rack)和槽号(Slot)。S7-300/400时代,这些参数有明确意义,Rack是机架编号,Slot是CPU在机架上的位置。而S7-1200是紧凑型PLC,CPU、电源、通信口都集成为一个整体,所以它的Rack固定为0,Slot固定为1。
S7.NET连接代码如下:
using S7.Net; // PLC IP地址:192.168.0.1,机架0,槽1 var plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1);这里要注意CpuType要选S71200,不要选成S7300,两种型号的CPU在S7协议握手细节上有差异。选错了虽然也能连上PLC,但读出来的数据类型解析可能有偏差。
3. C#上位机核心通信实现
3.1 项目架构与线程模型
上位机程序不能简单地把通信放在UI线程里。一个最基本的错误就是点按钮读取数据时用Thread.Sleep等待PLC响应,界面立刻卡死,客户在旁边看着就像程序崩溃了。
我的项目结构是标准的"UI线程 + 通信工作线程"分离:
UI层(WinForm) └── 主窗体:数据展示、曲线、报警 数据服务层 └── PlcService:连接管理、读写调度、重连机制 └── PlcDataModel:缓存变量快照 第三方库 └── S7.Net(S7协议通信)通信线程是一个后台Task,每100ms执行一次轮询任务,把PLC里的关键变量读上来放到一个共享的数据对象里。UI层通过System.Windows.Forms.Timer定时刷新数据显示控件。这样即使PLC响应慢一点,或者网络有波动,界面也不会冻结,最多是数据不更新而已。
共享数据的线程安全要注意,我用的方式是定义一个ConcurrentDictionary<string, object>存变量名和值的键值对,通信线程往里写,UI线程读。如果遇到需要强一致性的数据(比如报警信号),用lock包裹整个快照更新过程。
3.2 连接与重连机制
PLC连接不是一劳永逸的。S7-1200在CPU停机、断电重启、网络闪断的情况下,上位机的TCP连接会断开,程序必须有自动重连能力。
我的实现思路是这样:
public class PlcService { private readonly Plc _plc; private readonly Timer _reconnectTimer; private bool _manualStopped; public PlcService() { _plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1); _reconnectTimer = new Timer(5000); _reconnectTimer.Elapsed += TryReconnect; _reconnectTimer.Start(); } public bool Connect() { try { if (!_plc.IsConnected) { _plc.Open(); } return _plc.IsConnected; } catch (Exception ex) { LogHelper.Error($"连接PLC失败: {ex.Message}"); return false; } } private void TryReconnect(object sender, ElapsedEventArgs e) { if (_manualStopped) return; if (!_plc.IsConnected) { Connect(); } } }核心点有两个:一是Open()在前一次连接未断开时调用会抛异常,所以先判断IsConnected;二是重连间隔不能太短,我实测5秒是个不错的值,太短会把PLC的通信口打满,太长又影响故障恢复速度。
3.3 读写各类数据的API实践
S7.NET最爽的地方在于API设计。读数据只需两个参数:数据类型和地址。
// 读布尔 bool isRunning = plc.Read<bool>("DB3.DBX0.0"); // 读整数 short currentSpeed = plc.Read<short>("DB3.DBW2"); // 读浮点数(REAL对应C#的float) float temperature = plc.Read<float>("DB3.DBD4"); // 读运行时间(32位无符号整数) uint runTime = plc.Read<uint>("DB3.DBD8");写数据也类似:
plc.Write("DB3.DBW2", (short)1200); plc.Write("DB3.DBD4", 24.5f); plc.Write("DB3.DBX0.1", true);这里的注意事项是类型匹配。PLC的INT对应C#的short(16位有符号整数),PLC的DINT对应C#的int(32位有符号),PLC的REAL对应C#的float。如果把short错写成int去读WORD,拿到的数值会完全不对。
如果一次要读多个连续DB地址,S7.NET也支持读取字节数组:
byte[] buffer = plc.ReadBytes(DataType.DataBlock, 3, 0, 20); // DB3.DBB0开始,连续读20个字节读到原始字节后,用BitConverter.ToInt16、BitConverter.ToSingle等方法按偏移解析即可。这种方式效率远比多次调用Read高,适合通信量较大的场景。我自己实测,一次读20字节再手动解析,比单变量逐条读快接近一个数量级。
3.4 信号与数据正确性校验
工业现场通信环境并不干净,电磁干扰、网线松动、交换机丢包都可能造成数据的瞬间错误。上位机不能盲信每一次读到的数值。
我在项目里加了三层校验:
- 连续性检查:如果监控的压力值上一轮是2.5MPa,这一轮突然跳到12MPa,大概率是读错了。把变化率超过阈值的点丢弃并标记异常。
- 心跳变量:在PLC通信DB里放一个每秒自增的计数器,上位机每隔N秒检查这个值是否在变化。如果心跳停了,说明通信链路虽然还连着但数据已经不可信。
- 周期校验和:通信DB末尾放一个字节用于存放前面所有字节的异或和,上位机每轮读完比对,不匹配就整体丢弃这一帧。这个方案比CRC简单,够用就好。
4. 监控界面与数据可视化
4.1 界面布局方案
监控界面不需要花哨,但必须信息层级清晰。我的主界面分了四个区域:
- 顶部:设备状态总览条,显示各台PLC在线/离线状态,以及关键设备的运行/停止/故障三色指示灯。
- 中间左侧:实时数据表格,按工艺段分行,每行显示变量名、当前值、单位、正常范围。
- 中间右侧:实时趋势曲线,显示最近60秒到30分钟的关键工艺参数。
- 底部:报警信息列表,按时间顺序记录报警发生和恢复事件。
实时数据表格使用DataGridView绑定变量快照集合,用Timer每200ms刷新一次。注意刷新时不要每次重建整个列表,让BindingSource复位并重新绑定,否则会出现列表闪烁。更优的做法是手动逐格更新Rows[i].Cells[j].Value,数据量不大时这个方案足够流畅。
4.2 实时趋势曲线的轻量实现
趋势曲线是监控系统的灵魂。很多工控人第一反应是用第三方图表控件,比如ZedGraph、LiveCharts。但这类控件往往带出一堆依赖,渲染性能还要再调。我这里的做法是直接用WinForm自带的Chart控件。
关键设置很简单:
chart1.Series[0].ChartType = SeriesChartType.Line; chart1.Series[0].BorderWidth = 2; chart1.Series[0].Color = Color.SteelBlue; chart1.ChartAreas[0].AxisY.Minimum = 0; chart1.ChartAreas[0].AxisY.Maximum = 100;数据更新时:
if (chart1.Series[0].Points.Count > 3600) chart1.Series[0].Points.RemoveAt(0); var point = new DataPoint(xTime, yValue); chart1.Series[0].Points.Add(point);采集到新的点后不断追加,超出窗口范围就把最老的移除,这就实现了一个滚动窗口曲线。实际操作中要注意Points.Add操作要放在UI线程,如果放在通信线程里执行,跨线程操作会引起资源竞争。我通常让通信线程只更新数据模型,UI曲线由Timer在UI线程里去数据模型取最新点。
另一个细节是图表性能。默认的Chart控件在点数达到几千个之后,重绘会明显变卡。我的窗口限制在3600个点(30分钟、每秒采集两次),同时开启chart1.Series[0].XValueIndexed = true,索引模式比时间轴模式轻量得多。
4.3 报警监控与事件记录
报警监控是现场最看重的一部分。方案不是让上位机轮询所有变量然后自己判断,而是先把报警逻辑写在PLC里(PLC内部用OB组织块或定时中断做判断),PLC里用一个报警字来映射所有报警状态。上位机只需要读这一个WORD,解析每个bit即可。
举个例子,报警字MW100的Bit0代表"1号泵过载",Bit1代表"2号泵过载",Bit2代表"冷却水压力低":
int alarmWord = plc.Read<int>("MW100"); // 读报警字 var alarms = new List<string>(); if ((alarmWord & 0x0001) != 0) alarms.Add("1号泵过载"); if ((alarmWord & 0x0002) != 0) alarms.Add("2号泵过载"); if ((alarmWord & 0x0004) != 0) alarms.Add("冷却水压力低");这样做的原因是:PLC内部判断报警的实时性远好于上位机。上位机通信周期是100ms,漏掉一个时间很短的瞬态报警信号是常有的事;而PLCOB1扫描周期只有几毫秒,用上升沿捕获报警位,然后保持报警状态,上位机轮询时就能稳定读到。
报警事件要落到本地。简单方案是直接写文本文件,每行一条报警记录加上时间戳。要更进一步,我会用SQLite数据库,建一张alarm_log表,字段包括时间、报警代码、报警文本、状态(触发/恢复)。SQLite嵌入式部署成本低,一个dll即可,不需要额外安装服务。查询报警历史时用SQL就能按时间范围、报警级别筛选,和WinCC的报警归档效果不相上下。
5. 常见问题与排查经验集
5.1 连接失败的十大根因
开发期间我整理了连接失败排查清单,按出现频率排序如下:
| 序号 | 可能原因 | 排查方法 |
|---|---|---|
| 1 | PLC未开启PUT/GET访问 | 检查博途CPU属性,"防护与安全"->"连接机制" |
| 2 | IP网段不一致 | 上位机网卡IP和PLC必须在同一网段,Ping一下试试 |
| 3 | 防火墙拦截 | 关闭Windows防火墙,或放行TCP 102端口入站 |
| 4 | 机架号/槽号错误 | S7-1200固定Rack=0,Slot=1 |
| 5 | CpuType选错 | S7-1200对应CpuType.S71200 |
| 6 | PLC处于STOP状态 | 把PLC切到RUN状态,或者检查CPU指示灯 |
| 7 | 通信DB块是优化访问模式 | 检查DB块属性,取消"优化的块访问" |
| 8 | 通信被PLC别的连接占满 | S7-1200同时支持的最大连接数有限,断开其他调试连接 |
| 9 | 交换机/网线故障 | 替换测试或者用笔记本直连PLC测试 |
| 10 | PLC固件不兼容 | S7.NET要求S7-1200固件v3.0以上,检查博途里固件版本 |
第8条是最隐蔽的。S7-1200的CPU在标准型号上,同时并发的PUT/GET连接数只有3到8个(具体看CPU型号和固件版本)。我一台PLC上分别开了博途下载、WinCC画面、还有我自己的上位机调试窗口,当第三个上位机连进来时,就会莫名奇妙地连接失败。这是PLC侧的连接资源限制,代码改不到,只能规划好同时连接的数量。
5.2 读取地址常见错误
读出来的数值完全不对,绝大多数都是地址写错了。我的经验是分三类排查:
- 地址越界:读DBD120,但DB块实际只有100字节,访问会异常或者返回0。
- 数据类型不匹配:PLC里是INT,用
Read<string>去读,解析直接炸掉。用Read<short>就正常。 - 地址对齐错误:WORD/DWORD必须偶数字节起,REAL必须4字节对齐。手工计算偏移时漏掉了博途自动插入的填充字节。
调试这类问题时,我通常先在博途的"监视表格"里确认这个地址确实有值,再用第三方工具(比如S7.NET自带的调试窗口)去读,逐步缩小范围。
5.3 通信周期与性能调优
刚开始写的时候,我每轮读全部50个变量(每个变量调一次Read),结果一轮要200多毫秒。虽然也够用,但波动大的时候数据刷新不够流畅。优化手段有三个:
第一,把连续地址拼成大块,一次ReadBytes读整个区间。这个我在前面提过,收益最大。第二,调整轮询分组:把100ms必须刷新的高速变量(比如电机电流、速度)放在第一组,把每2秒刷新的低速变量(如累计产量)放在第二组,分开轮询周期。第三,关闭PLC通信DB里不用的变量,删除DB中不做监控的保留字段,减小每轮传输的数据量。
经过这三步优化后,一轮完整轮询从200ms降到了50ms以内,CPU占用率没明显变化。其实对于这种数据量,S7-1200本身不是瓶颈,协议握手的次数才是大头。减少通信往返次数,比提高单次传输速度有效得多。
5.4 UI线程卡顿与跨线程访问异常
上位机界面卡顿是新手最容易犯的错。跨线程访问UI控件会抛InvalidOperationException,这是因为Windows Forms的UI控件不是线程安全的,必须在创建它的线程上操作。解决方式是用Control.BeginInvoke:
private void UpdateUi(Action action) { if (this.IsHandleCreated) { this.BeginInvoke(action); } } // 通信线程里更新曲线 UpdateUi(() => { chart1.Series[0].Points.Add(point); });BeginInvoke是异步的,调用后立即返回,通信线程不会等UI刷新完才继续干活。不过要注意疯狂调BeginInvoke也可能让UI线程消息队列刷爆,稳妥的方法是UI更新放进Timer里,通信线程只写一个标志位和最新值,UI的Timer来取。
5.5 AccessViolationException(C0000005)的排查
很多人在C#上位机里调用C++类库时会突然碰到AccessViolationException,这个异常在热词里也出现过(C#调用C++出现access violation c0000005)。这个C0000005是Windows的"非法内存访问"错误码,根本原因是程序试图读写一个没有权限的内存地址。在Snap7这类需要P/Invoke调用原生DLL的通信方案里尤其常见。
产生原因无非这几类:
- 原生DLL的位数不匹配。C#项目是x64编译,却加载了x86版本的snap7.dll,调用时内存布局完全不对。
- 在通信线程中连续调用原生接口,PLC返回的数据长度超过了你分配的缓冲区长度,原生代码访问越界。
- 垃圾回收把托管对象的内存移动了位置,但传入原生接口的指针还指向旧位置,又没有用
fixed固定住。
解决方案也很明确:统一DLL位数、增大缓冲区并每次根据返回值校验收到的字节数、在P/Invoke声明中给byte[]加[MarshalAs(UnmanagedType.LPArray)]并配合分配固定大小的非托管内存。尽量避免直接在C#里带着托管字节数组去调用Snap7这种高度C风格接口的库,有必要的话可以包一层C++/CLI中转。用S7.NET时基本没碰到过这类问题,因为它内部用安全的托管代码做缓冲区管理。
6. 数据持久化与远程访问扩展
6.1 历史数据存储到SQLite
监控系统不能只有实时画面,客户早晚会问"今天下午三点那个压力突变,具体是多少?"。要回答这种问题,历史数据必须落库。
我的方案是每5秒存一条关键变量快照到SQLite。表结构很简洁:
CREATE TABLE IF NOT EXISTS plc_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, temperature REAL NOT NULL, pressure REAL NOT NULL, speed INTEGER NOT NULL, status_word INTEGER NOT NULL );写入使用参数化SQL语句,在独立线程中执行,避免阻塞通信线程。SQLite单文件机制对工控机特别友好,拷个数据库文件出来就能在办公室的电脑上用任何SQLite工具查看,不需要部署数据库服务。
关于存储量:一条记录大概50字节,5秒一条,一天8640条,一个月不到30万条。SQLite对这种规模的数据毫无压力,文件也就几十MB。如果项目运行两三年,我会再启动一个数据归档任务,把超过三个月的旧数据导出成CSV后从主库删除。
6.2 上位机数据如何变成手机APP可看
标题里的"APP"如果指的是手机APP,这里有一个常见的误区:手机APP不应该直接去连PLC。S7协议是基于TCP 102端口的工业协议,在公网或办公网传播存在安全风险,而且S7-1200的连接数限制也很容易被打满。正确的做法是中间加一个"物联中间层"。
我的落地架构是:C#上位机程序扮演数据采集终端的角色,把PLC数据经过处理后,通过MQTT协议上传到本地的MQTT Broker(如EMQX或Mosquitto)。手机APP作为MQTT客户端订阅这些主题,负责展示和报警通知。这样手机端不需要关心S7协议,PLC侧也不需要为移动端开放额外的端口。
如果不想引入MQTT这种组件,还有一个最小方案:上位机程序内置一个极简的HTTP API(用ASP.NET Core WebAPI),手机APP通过RESTful接口拉数据。这个方案适合只需要少数人看数据的小规模场景,开发和维护成本都极低。我在一个老客户现场就采用这种方式,因为他们的IT基础薄弱,多部署一个MQTT broker反而增加了出问题的概率。工业项目选型,永远是越简单越可靠。
7. 项目跑起来后的几件小事
最后分享几个项目上线后我总结的点。
一是日志。整个上位机程序必须有一套完整的日志系统,不光是Debug阶段,运行阶段更要记录。我用的log4net打到本地文件,每个操作员的每一步操作、每次通信异常、每次报警触发都记录在案。出了问题,翻日志比打电话问现场人员更快。
二是PLC程序版本管理。上位机和PLC程序往往是配套联调的,PLC侧改了DB变量布局,上位机不更新就会读错地址。我的做法是构建一个"通信变量版本号",PLC和上位机各有一个,连接握手时先比较版本,不一致就报警提示,避免排故时的鸡同鸭讲。
三是断线自动恢复的联调测试。模拟过几次PLC断电重启,S7.NET的连接句柄确实能自动重连,但有一个细节要注意:PLC重启后,上位机对旧连接的数据收发会一直报超时,必须先关闭旧连接再重新Open()。我在重连逻辑里显式先执行plc.Close(),这比直接判断IsConnected更可靠。
四是界面和程序稳定性的平衡。工控上位机不是互联网产品,不追求花里胡哨,稳定的数据更新比酷炫的动画重要得多。尽量让程序每天开机自启,运行几个月不重启也不出毛病,这才是好上位机。这背后的关键是避免内存泄漏、及时释放套接字和数据库连接、不要让Timer无限制增长。
这套系统在我负责的车间已经稳定跑了快一年,三台S7-1200的实时监控、报警推送、历史曲线都正常工作。设备维护人员反映最强烈的一点是:以前去现场看一遍设备要花20分钟,现在坐在中控室,电脑上五个标签页就能把所有关键参数扫完。这大概就是上位机监控系统最实在的价值。
如果你也准备做类似的C#上位机项目,我建议先花一天时间把PLC侧的通信配置和地址映射理清楚,这决定了后面的开发是否顺畅。通信库选S7.NET,界面用WinForm,架构上把通信和UI分线程,历史数据落SQLite。按这个思路走,两周内搭出一套能上线的监控系统是完全可以做到的。
提示:文中实现的代码结构、通信机制和界面方案,均基于实际调试通过的项目整理,关键参数(如通信周期、重连间隔、连接资源限制)请根据现场设备和网络环境适当调整。