
简介面向C#与工控开发者的OPC通讯实例源码包由工控老马整理并亲测可用核心解决通过OPC服务器连接PLC进行数据读写的问题。压缩包采用Visual Studio解决方案组织包含完整可编译的C#工程、可执行程序、界面图片及Word使用说明便于直接运行、调试与二次开发。全包共30个文件以6个.cs源文件、3个.exe可执行程序、2个.png界面截图和1个.doc说明文档为主另附动态链接库、项目配置文件、编译缓存与辅助资源各类文件分工明确整体仅1.19MB轻量而完整。目前已有848人学习使用适合入门新手及有一定经验的开发人员。通过示例中的精美实用界面和分层清晰的项目结构可以快速掌握OPC客户端开发思路、服务器连接配置、PLC数据读写实现要点以及工控上位机软件的常见组织方式。1. C# OPC通讯实例这套源码让PLC数据在三分钟之内进上位机车间三台设备一台西门子PLC一台通过串口走Modbus一台只有以太网口数据要统一汇到计算机上的状态看板。一个一个设备写驱动工作量不算大麻烦的是后续每一台设备通讯参数变了都要改代码重编译。当时手上的方案就是用一个OPC Server软件做中间层把不同接口的设备统一成OPC数据项再用C#写一个WinForms客户端去读写。这套OPC通讯实例就是干这个事的作者工控老马把连接、读取、写入和界面都打包好了VS直接打开就能跑对新手来说比从零看OPC规范快太多对有经验的上位机开发人员也是一个很好的工程参照。2. OPC DA通讯与选型理由为什么C#客户端普遍走OPC DA而不是直接写PLC驱动2.1 OPC DA和OPC UA的取舍老项目DA仍是主流OPC全称是OLE for Process Control最早是基于Windows的COM/DCOM技术做出来的。它把PLC、DCS、仪表这些设备抽象成一个统一的数据空间上位机不需要关心底层走的是什么协议只要连上OPC服务器按Item ID读数据就行。OPC DAData Access是经典规范数据按组Group管理每个组里有若干Item读写都走COM接口调用。OPC UAUnified Architecture是新一代标准不再依赖DCOM跨平台能力更强也开始支持信息安全加密。但现实里大量老旧产线、国产组态软件和早期DCS系统只提供OPC DA接口有的PLC走专用协议设备厂商配套的OPC Server还是DA版本。如果直接把项目改成UA硬件端不一定接得上。做上位机选型时我的判断标准是设备端服务器固件如果只支持DA客户端这边优先用DA先保证连通后面再考虑统一升级到UA。这套源码就是典型的OPC DA实现代码里通过OPCAutomation接口和服务器交互结构清晰想改成UA也有改造路径可循。2.2 测试环境怎么搭Matrikon OPC Simulation 模拟器与 DCOM 配置跑这套源码之前建议先装一个OPC Server模拟器。Matrikon OPC Simulation是Free的装完自带一个叫Matrikon.OPC.Simulation的服务器里面内置了Random、Saw-tooth、Sine wave这类模拟数据项不需要真实PLC就能验证读写逻辑。服务器装好之后C#客户端要能和服务器通讯必须过DCOM这一关。OPC DA基于DCOM远程访问时两台机器的权限配置不对客户端会一直卡在“连接服务器”这一步。我一般会这样配运行dcomcnfg打开组件服务找到OpcEnum和对应的OPC Server组件把“启动和激活权限”里加上Everyone允许本地启动和远程启动。身份验证级别设置成“无”模拟环境里不要开加密否则调试期会反复被安全策略挡住。如果是本机测试可以把客户端和服务器装在同一台机器上先排除网络因素再逐步加远程访问条件。# 以管理员身份注册OPC自动化接口DLL cd C:\Windows\SysWOW64 regsvr32 opcdaauto.dll提示Windows 10/11 64位系统上如果代码以x86模式编译DLL路径用SysWOW64以x64模式编译用System32。注册完能看到“DllRegisterServer succeeded”才算成功。这套源码包里自带的说明文档对DCOM配置写得比较详细对照着做就行。环境准备好之后打开OPC_Client.sln第一步先看工程的引用第二步跑起来连接模拟器只要列表里出现Random、Saw-tooth这类Item说明整条链路已经通了。3. 源码包结构拆解从.sln到界面按钮都在哪里3.1 压缩包里都有什么sln、suo、doc文档和工程文件夹先把压缩包解开里面不是只有一个文件夹而是带说明文档和截图的完整交付包。OPC通讯实例(C#通过OPC连接PLC读写数据).zip解压后能看到OPC_Client.sln、OPC_Client.suo、OPC_Client工程目录外加Word版说明文档、软件界面截图和ResourceHome.png。OPC_Client.sln是Visual Studio解决方案入口双击它就能打开整个工程。OPC_Client.suo是Visual Studio的用户选项文件记录了你上次打开时窗口布局、断点位置这些个人信息这个文件删了不影响编译我一般不会把它提交到版本库。真正的源码都在OPC_Client文件夹下里面是WinForms项目的全部文件Form主窗体、Program.cs入口、OPC操作相关的封装类。Word版说明文档是这个包比较值钱的部分工控老马把OPC服务器的选择、DCOM配置步骤、代码在哪几个方法里改IP和ItemID都写清楚了。新手拿到包先看文档、再开工程基本能避免走弯路。3.2 主界面布局连接配置区、数据展示区、写入控制区从软件界面截图能看出这个客户端是实用的工业风格。窗体从上到下大体分三个区域顶部是服务器连接配置区包括服务器ProgID、主机名、连接和断开按钮外加一个刷新周期设置框中间是数据展示区用一个ListView或DataGridView列出ItemID、值、质量戳和时间戳模拟量的变化在这里能看到滚动效果底部是写入操作区输入ItemID和要写的值点写入按钮就能往服务器里写数据。// 界面刷新周期设置单位毫秒 private void numericUpDown1_ValueChanged(object sender, EventArgs e) { int rate (int)numericUpDown1.Value; if (opcGroup ! null) { // 更新组的回调周期 opcGroup.UpdateRate rate; } }这段代码是刷新周期调整的典型写法。OPC组有一个UpdateRate属性单位是毫秒它决定了服务器多久主动向客户端推送一次数据变化。数值设得太小比如50毫秒CPU占用会明显上升设得太大比如5000毫秒以上界面数据看起来像卡死一样。我一般先用500毫秒调试确认链路正常之后再按实际监控需求调到200~1000毫秒之间。界面上每个控件的命名都比较规范照着方法名就能看出哪个按钮触发连接、哪个按钮触发读取、哪个按钮触发写入。对有一定WinForms基础的开发者来说这个界面就是一套现成的上位机模板把OPC相关代码抽出去换成Modbus或TCP通讯逻辑也不难。4. 核心读写实现连接、回调、写入三条代码主线4.1 连接服务器OPCAutomation接口与OpcServerClass的使用C#要调OPC DA服务器最省事的路径是引用OPC Automation 2.0这个COM组件。因为OPC DA属于COM接口直接在C#里声明接口结构比较繁琐而OPCAutomation封装好了OpcServer、OpcGroup、OpcItem这几个类用起来和操作普通C#对象一样。using OPCAutomation; // 创建OPC服务器对象 OPCServer opcServer new OPCServer(); // 连接本机Matrikon模拟服务器 opcServer.Connect(Matrikon.OPC.Simulation, localhost);这里Connect方法的第一个参数是服务器的ProgID必须和实际安装的OPC服务器一致模拟器装好后是Matrikon.OPC.Simulation如果是Kepware通常写成Kepware.KEPServerEX.V6。第二个参数是主机名本机调试写localhost远程访问写成对方机器的IP或机器名。连接成功之后服务器对象上就能取到Groups集合。// 在服务器上创建一个组 OPCGroup opcGroup opcServer.OPCGroups.Add(Group1); // 设置回调刷新周期 opcGroup.UpdateRate 500; opcGroup.IsActive true; opcGroup.IsSubscribed true;Add方法返回一个OPCGroup对象Group是OPC DA里一个很关键的概念所有Item都要挂到组下面才能读写。UpdateRate在上面已经解释过是数据主动上报的周期。IsActive控制这个组是否参与读写IsSubscribed控制是否订阅服务器数据变化。如果IsSubscribed设成false后面的OnDataChange回调就不会触发只能靠手动Read去取值。很多新手把回调不触发归结为代码问题先检查这两个布尔值。4.2 数据读取OPCItemClass的Read与OnDataChange回调两种方式读取数据有两种思路一是主动去读适用于周期性扫描少量变量二是订阅服务器推送适用于需要实时响应数据变化的场景。这套源码里考虑到上位机监控的实际情况采用的是订阅方式也就是在组的OnDataChange事件里拿数据。// 添加Item到组 OPCItem item opcGroup.OPCItems.AddItem(Simulation.Random.Real8, 1); int serverHandle item.ServerHandle; // 订阅数据变化事件 opcGroup.OnDataChange OpcGroup_OnDataChange;AddItem的第一个参数是ItemID这个ID必须和OPC服务器里配置的一致。Matrikon模拟器的随机模拟量ItemID是Simulation.Random.Real8其中Real8表示8字节浮点数。第二个参数是客户端句柄是我们自己定义的编号可以用来在回调里区分是哪个Item的数据。AddItem返回的OPCItem对象上有ServerHandle属性服务器句柄是服务器端给这个Item分配的唯一编号回调里返回的就是这个值。private void OpcGroup_OnDataChange(int transactionID, int numItems, ref Array clientHandles, ref Array values, ref Array qualities, ref Array timeStamps) { // 数据变化时回调触发把新值更新到界面 for (int i 0; i numItems; i) { int handle (int)clientHandles.GetValue(i 1); object value values.GetValue(i 1); int quality (int)qualities.GetValue(i 1); // 根据句柄匹配到对应显示控件 if (handle 1) { txtValue.Text value.ToString(); } } }回调方法的参数里clientHandles、values、qualities、timeStamps都是数组而且数组下标从1开始不是从0开始这是OPC DA自动化接口的一个老传统无数人在这里栽过跟头。循环里第一件事就是取出客户端句柄再根据句柄更新对应的控件。quality是质量戳值是192表示Good非192表示数据不可信。接真实设备时界面除了显示数值最好把quality也显示出来否则设备断线时界面上可能还是上一个值。4.3 数据写入SyncWrite同步写入与质量戳校验写数据和读数据走的是同一个组但调用的是组的SyncWrite方法。同步写入的意思是调用之后一直等待服务器返回结果不会异步返回。// 写入前先根据ItemID找到Item对象 OPCItem writeItem opcGroup.OPCItems.AddItem(Simulation.Random.Real8, 2); int serverHandle writeItem.ServerHandle; int[] serverHandles new int[] { serverHandle }; object[] values new object[] { 23.5 }; int[] errors new int[] { 0 }; // 执行同步写入 opcGroup.SyncWrite(1, ref serverHandles, ref values, ref errors);SyncWrite的参数依次是写入个数、服务器句柄数组、值数组、错误数组。前面提到过OPC DA的自动化接口很多数组都是从下标1开始的这里serverHandles和values传进去的时候虽然用的是0下标数组但OPC内部会按这个数组顺序和句柄一一对应。errors数组在调用前先初始化成0调用后如果某个位置的值不为0就说明对应的Item写入失败。写入失败最常见的原因是数据类型不匹配。Matrikon模拟器的Simulation.Random.Real8期望的是double类型如果传入一个字符串或整数服务器会直接返回类型错误。真实PLC场景下要写整数型寄存器就传int写实数就传double写布尔就传bool。还有一类情况是Item本身是只读的比如模拟器里有些正弦波、三角波项服务器端就禁止写入写之前先确认这个Item能写。// 写入后检查质量戳和错误码 if (quality 192 errors[0] 0) { MessageBox.Show(写入成功); } else { MessageBox.Show(写入失败错误码 errors[0].ToString()); }这段判断比只看返回值可靠得多。质量戳是192Good说明服务器端数据状态正常错误码是0说明本次写入操作被接受。两者都满足才算真正写进去了。在调试真实设备时我习惯在写入按钮的点击事件里加一个日志输出把次数、ItemID、值、错误码全部追加到文本文件这样现场出了问题能按时间线回查。5. 避坑与常见问题排查DCOM权限、ItemID、界面卡顿5.1 引用不到OPC Automation组件或注册失败现象新建工程后引用列表里找不到“OPC Automation 2.0”或者regsvr32注册opcdaauto.dll时报模块加载失败。原因64位系统上DLL路径不对或者COM组件权限不够导致注册被拒绝。很多机器上opcdaauto.dll没有被Windows默认注册需要用管理员权限手动注册。解决先确认编译目标是x86还是x64然后到对应路径下注册。x86编译就进C:\Windows\SysWOW64x64编译就进C:\Windows\System32。注册命令必须是管理员权限的CMD里执行。注册成功后引用对话框COM选项卡里会出现OPC Automation 2.0勾选确认即可。如果还找不到重启一下Visual Studio再试。5.2 能连接上OPC服务器但永远读不到值界面上全是0或空白现象Connect成功组也建了Item也加了但数据不变也没有回调触发。原因ItemID写错了。上位机和OPC服务器通讯不是按变量名模糊匹配的必须写完整的ItemID路径。比如Matrikon模拟器的随机数项的ItemID是Simulation.Random.Real8少一个点、差一个大小写都匹配不到。解决先用OPC客户端工具Matrikon OPC Explorer或UaExpert连接到服务器展开服务器树找到实际ItemID再复制到程序里。这个坑在接真实PLC时更常见因为PLC程序里DB块的变量名和OPC服务器导出的名称不一定一样必须以OPC服务器里看到的为准。5.3 本机能连上模拟器远程访问连不上现象客户端装在A机器OPC服务器装在B机器A机器连接B机器时一直超时或报“拒绝访问”。原因DCOM的远程权限没有配置。OPC DA用DCOM通讯默认安全策略只允许本机访问远程机器的启动权限、访问权限都没开放。解决在B机器上运行dcomcnfg组件服务里找到OpcEnum和Matrikon OPC Simulation组件把权限里的“启动和激活权限”“访问权限”都加上Everyone或专门建一个opc用户身份验证级别设为“无”模拟环境可以先关闭防火墙测试通了以后再设置防火墙放行DCOM端口范围。这套源码的说明文档里对这一块有展开现场按步骤操作比我在这里列更细。5.4 程序运行几分钟后界面卡死拖动窗口都困难现象通讯正常数据在变化但界面卡得厉害CPU占有率某核长时间100%。原因OnDataChange回调里做了耗时的UI操作。WinForms的UI控件只能在UI线程更新而OPC回调从COM线程进来直接在回调里访问控件会触发频繁的跨线程操作再加上个别机器上COM线程和UI线程切换代价大界面就会慢慢冻住。解决回调里不要直接操作控件把数据放进队列或Buffer用System.Windows.Forms.Timer每200毫秒从UI线程取一次数据刷新界面。这也是这行里最常见的“玄学”模拟器数据量小看不出问题接真实PLC点位多了以后必现。我习惯在回调里只做内存赋值所有UI刷新都走Timer。5.5 写入返回成功但PLC那边实际没变化现象程序提示写入成功errors数组也是0但到PLC监控里看变量还是原值或者过一会儿被恢复了。原因一种是写入方向反了程序写的是服务器缓存但服务器没有把数据推到PLC通常是OPC服务器里Item的读写映射没配置好另一种是PLC程序扫描周期里把这个变量又重新赋值了外部写入被覆盖。解决先查OPC服务器端的Item属性确认客户端写入的Item和实际PLC地址绑定正确且该地址允许外部写入。再查PLC程序逻辑看有没有OB块在周期执行时强制赋值。真实项目里第二种情况更常见需要PLC工程师配合改程序或加互锁条件。6. 从读一个Item到批量管理配置驱动与OPC UA迁移思路源码包里的例子是固定ItemID写死在代码里。实际做车间看板或数据采集项目时一个OPC服务器上可能挂了上百个点位不可能每个点位都写一行AddItem。我在这套源码基础上改过一个版本把ItemID、客户端句柄、数据类型、显示列名全部放在一个DataTable里启动时循环AddItem关窗时循环RemoveItem。这样现场加点位只需要维护表格不上线改代码。DataTable dtItems GetItemConfig(); foreach (DataRow row in dtItems.Rows) { string itemId row[ItemID].ToString(); int clientHandle Convert.ToInt32(row[ClientHandle]); OPCItem item opcGroup.OPCItems.AddItem(itemId, clientHandle); _itemMap.Add(clientHandle, row[Alias].ToString()); }这段代码每次启动前先从配置表加载Item清单clientHandle在程序里起“序号”的作用。回调拿到的clientHandles数组来自服务器推送用它在_itemMap里反查别名把原始ItemID翻译成业务含义。比如ItemID是DB1.STATUS界面显示的是“1号线状态”调试时一眼就能看出来是哪台设备的数据。还有一个值得提醒的方向是OPC UA迁移。如果你现场遇到的服务器是OPC UA而不是OPC DA这套基于OPCAutomation的代码就不能直接用了。UA服务器走的是IP端口方式连接例如opc.tcp://192.168.1.10:4840不再依赖DCOM也没有Group概念。C#这边常见的做法是引用opcfoundation的Opc.Ua.Client库读取流程变成先创建Session再创建Subscription最后在DataNotification里处理数据。从DA往UA迁的时候原来的ItemID规则基本能保留但连接代码、权限校验和节点浏览方式全部要重写。我个人的习惯是新项目直接用UA老项目维护先用DA跑通再逐步迁移。这个源码包的边界在于它是一个DA客户端建议你拿到后先在Matrikon模拟器上跑通确认DCOM环境没问题再对接真实PLC。有一次我在现场被WinCC先占用了OPC服务器句柄排查了半天后来强制以管理员身份先运行OPC快速客户端确认句柄状态再启动自己的程序从那以后我每次到一个新环境都强制先走一遍“模拟器→快速客户端→自己程序”这个链路能省下大量现场的尴尬时间。希望帮到你。本文还有配套的精品资源点击获取