
简介一套面向工控软件开发者的C# OPC通讯实例源码演示通过OPC服务器连接PLC并读写数据的完整实现附带精美实用界面由“工控老马”出品亲测可用适合从新手到有经验的技术人员对照学习。资源以微软VS解决方案工程为主共30个文件涵盖cs源代码、sln与csproj工程文件、可直接运行的exe程序、debug调试记录、dll依赖库、界面资源与截图并附有一份操作说明文档。整套包大小仅1.19MB轻量易读目前已有848人学习浏览。源码工程模块划分清晰既可直接编译运行观察OPC通信效果也能按需修改界面与读写逻辑移植到实际项目中。配套文档对OPC服务器连接PLC的数据读写流程做了说明可帮助理解从连接建立、标签绑定到值读写的关键步骤是快速上手OPC开发的实用参考。1. 这是给上位机工程师的OPC通讯实例能跑通、能改、能落地车间里那台西门子PLCModbus TCP死活不通上位机又急等着看几个DB块的实时值这时候最稳的路子就是走OPCPLC侧装OPC服务器C#上位机当客户端按Item地址去读写数据。我最近拆的这套《OPC通讯实例(C#通过OPC连接PLC读写数据)程序源码》是一个能直接编译的VS工程界面是成品通讯层完整连上OPC服务器就能读能写。配套的doc文档把界面操作和代码位置标得很清楚新手照着手抄也能跑起来。适合第一次接OPC的C#开发者也适合想把这套通讯代码抽出来塞进自己做工装的老手。后面所有步骤我都按“能复现”的标准写包括环境配置、代码逻辑、地址命名以及最容易翻车的DCOM坑。2. OPC DA原理与运行环境为什么选OPCAutomation.dll而不是自己写驱动2.1 OPC DA在工控链路里的位置以及它和OPC UA的区别先把这个资源的定位讲清楚。它实现的是OPC DAData Access也就是COM/DCOM时代的经典协议链路分三层PLC作为数据源OPC服务器作为中间层C#程序作为客户端。PLC品牌繁杂三菱、西门子、AB各自私有协议每个都自己写驱动不现实。OPC DA的价值就是把协议差异吃掉统一成“Item地址 Value”的模型SCADA、MES往上挂数都是走这一层原理完全一样。这就是为什么我不建议自写S7或MC协议驱动私有协议本身有版权和版本变动的风险调试周期长而且现场还得有人持续维护。需要说明的是这不是OPC UA。UA是基于TCP/IP的跨平台协议有加密、证书、命名空间模型现在新项目都在聊UA但存量车间里大量还在运行的服务器是DA。这套代码的价值就在这里它能直接对接老服务器把数据先拿回来。以后对接新型网关再把通讯层升级成UA技术改造就行界面和业务逻辑都可以保留。2.2 运行环境与依赖OPCAutomation.dll、OPCEnum和Matrikon模拟器这个源码是.NET Framework WinForms工程核心依赖是OPCAutomation.dll这个COM组件VS引用后会自动生成Interop.OPCAutomation.dll。在64位Win10/Win11上跑有几个环境点需要先确认我列一个核对清单。组件作用关键点OPCEnumOPC服务器注册与枚举服务Win2000之后系统自带连不上时先看这个服务是否运行OPCAutomation.dllOPC DA官方自动化接口封装OPCServer/OPCGroup/OPCItems工程引用后输出目录会出现Interop文件Matrikon.OPC.Simulation免费的OPC模拟服务器没有PLC时用它验证全部读、写、回调流程打开工程里的OPC_Client.sln编译前先确认项目平台目标。OPCAutomation这个COM组件体系是32位时代的产物在64位系统上最稳妥的做法是把平台目标设置为x86具体操作在VS里项目属性 → 生成 → 平台目标 → 选x86。这一步不做后面很容易出现“Windows下能装但连不上服务器”的怪问题具体现象我在第5章展开。2.3 为什么先用模拟服务器跑通我的习惯是不插PLC先上Matrikon。原因很实际直接对PLC调时间和精力容易消耗在“到底是Item地址写错了还是网络不通”的猜测上。Matrikon装完自带一批仿真变量浮点、整数、布尔都有客户端跟它通了说明DCOM、OPCEnum、通讯链路都没问题这时再去接真实PLC就只剩地址映射和权限两件事。具体步骤安装Matrikon后启动打开它的OPC Explorer能看到挂在服务器底部的Simulation Items分支把里面几个变量的完整Item路径记下来比如Simulation Items.Real.Real_Data_1。然后启动OPCEnum服务在服务管理器里找到OPC Enumeration确认状态是“正在运行”一般默认是自动启动被禁用的话要到组件服务里把它恢复。提示Matrikon模拟服务器免费常用于OPC客户端开发调试。真实西门子项目里PLC侧需要用对应的OPC服务器软件把PLC点位映射成OPC服务器后再连接。3. 连接PLC四步走从OPCServer实例化到Item注册3.1 实例化OPCServer并ConnectProgID和主机名是关键第一步在通讯核心类里实例化OPCServer然后连接。ProgID是OPC服务器在系统中的注册名Matrikon模拟器填Matrikon.OPC.Simulation西门子侧要根据实际装的服务器程序填常见是Siemens.OPC.DA开头的一组名称具体到型号会略有差异以安装程序提供的为准。private OPCServer opcServer; private string lastError ; public bool Connect(string progId, string host) { try { opcServer new OPCServer(); // progId 是OPC服务器在系统中的注册名 // host 是服务器所在机器名本机调试可传 Environment.MachineName opcServer.Connect(progId, host); return true; } catch (Exception ex) { lastError ex.Message; return false; } }Connect失败时lastError里会带HRESULT错误码常见是0x80040154类未注册或0x80070005权限拒绝。host参数在本机调试时直接取机器名就行跨机器访问才填IP或计算机名。这里的常见误用是拿“localhost”去连在部分服务器程序上会绕过身份验证导致DCOM判定异常我习惯统一用Environment.MachineName。3.2 建立OPCGroupUpdateRate和DeadBand决定采集节奏OPCServer连上后下一步添加OPCGroup。OPCGroup是Item的集合读写、订阅全部挂在组上相当于一个采集中转站。private OPCGroup opcGroup; public bool AddGroup(int updateRateMs) { opcGroup opcServer.OPCGroups.Add(MyGroup); // 更新周期单位毫秒一般取200~500 opcGroup.UpdateRate updateRateMs; // 死区0~100百分比只对模拟量有意义 // 变化量小于死区的值不会触发DataChange事件 opcGroup.DeadBand 0; opcGroup.IsSubscribed true; return true; }UpdateRate不要追求越小越好。现场常见的错误是设成10ms以为越快越好结果OPC服务器和网络被大量无效报文打满CPU占用翻倍界面数据反而刷不动。做转速、压力这类快速变化的量100到200ms足够做温度、液位这类慢变量500ms即可。DeadBand也是同样的逻辑模拟量现场波动大时设成1到5能挡掉大量无意义的推送开关量不受它影响。3.3 注册OPCItem地址命名是读不读得到数的分水岭组建好之后添加Item。Item地址是PLC变量在OPC服务器侧的映射名差一个字符都读不到。private Dictionarystring, OPCItem items new Dictionarystring, OPCItem(); public bool AddItem(string itemId, int clientHandle) { // clientHandle 是自定义编号回调时靠它区分是哪个Item OPCItem item opcGroup.OPCItems.AddItem(itemId, clientHandle); items[itemId] item; return true; }调用示例// Matrikon模拟服务器内置的实数变量 AddItem(Simulation Items.Real.Real_Data_1, 1); // 西门子OPC服务器里DB1的REAL0具体命名以服务器点位表为准 AddItem(S7:[DB1]REAL0, 2);itemId在Matrikon里可以通过OPC Explorer直接复制在西门子那边一般由PLC程序里的DB块地址映射。这里最容易翻车的是凭记忆写Item名正确做法是先到OPC服务器的浏览树里把正式名字复制出来再填进代码。clientHandle是程序自定义的标记异步回调时靠它识别数据来源我习惯让它直接对应业务数据字典的索引这样回调里拿到handle就能定位到控件。3.4 读质量戳判断数据可不可信的标配读值和写值之外OPC DA还规范了每个Item的质量戳Quality字段。数据源在不同状态下会返回Good、Bad、Uncertain等结果很多新手只拿Value而忽略Quality结果把PLC停机态下的残留值当成真实数据用。我的习惯是在采集结构体里把Quality一并存下来界面显示数值的同时标记当前质量状态。public class OpcValue { public object Value; public short Quality; public DateTime TimeStamp; }质量戳为Bad时业务逻辑直接跳过或触发报警不做计算。这一步在调试阶段可能觉得多余但接到MES、SCADA对接需求时质量戳往往是对方验收的硬指标提前做了反而省事。4. 读与写同步、异步和界面联动4.1 同步读适合一键刷新和批量快照同步读是阻塞式读取调用后等待OPC服务器返回适合“点一下刷新按钮拿当前值”这类场景。public object SyncReadItem(string itemId) { try { Array itemIDs new[] { itemId }; Array values, errors, qualities; // OPCDevice直读设备OPCCache读服务器缓存 opcGroup.SyncRead( (short)OPCDataSource.OPCDevice, 1, itemIDs, out values, out errors, out qualities); return values.GetValue(0); } catch (Exception ex) { return null; } }SyncRead第一个参数是数据源OPCDevice代表直读设备OPCCache读服务器缓存。需要最新现场值时用OPCDevice但读取频率会被设备和服务器响应时间限制住高频调用的代价是阻塞UI线程一般配合BackgroundWorker使用。返回值是object装箱类型布尔、整数、浮点在底层都是COM的VARIANT取出后要做类型转换。4.2 异步读与DataChange实时刷新的核心机制实时界面不推荐轮询式同步读OPC DA原生支持订阅模式服务器数据变化会主动推给客户端。opcGroup.DataChange OpcGroup_DataChange; private void OpcGroup_DataChange( int transactionID, int numItems, ref Array clientHandles, ref Array values, ref Array errors, ref Array qualities, ref Array masterErrors) { for (int i 0; i numItems; i) { int handle (int)clientHandles.GetValue(i); object v values.GetValue(i); short q (short)qualities.GetValue(i); // 回调在COM线程池更新界面要Invoke回UI线程 this.BeginInvoke(new Action(() { UpdateUIByHandle(handle, v, q); })); } }提示DataChange回调线程不是UI线程。直接在里面改TextBox会报跨线程异常或者界面卡死。回调里只收集数据用BeginInvoke再更新控件。这里有个隐蔽点DataChange不是每个PLC扫描周期都推一条而是按UpdateRate周期合并变化后推送值没变时默认不推送。如果业务要求记录全部变化轨迹需要在客户端自己维护缓存并打时间戳。开启IsSubscribed并确认Item注册成功后DataChange才能生效这也是第5章那个“回调不触发”坑的根源。4.3 写操作类型对不上是写不进去的最大原因写操作在OPC DA里走SyncWrite或AsyncWrite这里以同步写为例。public bool WriteItem(string itemId, object value) { Array itemIDs new[] { itemId }; Array values new[] { value }; Array errors; opcGroup.SyncWrite(1, itemIDs, values, out errors); short err (short)errors.GetValue(0); return err 0; }调用示例bool ok1 WriteItem(S7:[DB1]REAL0, 12.5f); // 写实数 bool ok2 WriteItem(Simulation Items.Bool.Bool_Data_1, true); // 写布尔写失败九成原因是类型对不上PLC里是REAL传了doublePLC里是BOOL传了true字符串。COM互操作层对类型很敏感我写之前先看读回来的Value实际是什么Type再按对应类型写。写布尔用bool写整数用int写实数用float字符串只用于服务器自定义的String变量。errors数组返回非零值时到OPC规范里查对应错误码比如0x8004000A开头的错误通常和Item句柄失效有关。4.4 大批量读写批量操作比逐个调用快得多现场一个界面上几十个变量是常态逐个调SyncRead会带来大量COM调用开销正确做法是批量读。public object[] SyncReadItems(string[] itemIds) { Array itemIDs itemIds; Array values, errors, qualities; opcGroup.SyncRead( (short)OPCDataSource.OPCDevice, itemIds.Length, itemIDs, out values, out errors, out qualities); return values.Castobject().ToArray(); }批量读时values、errors、qualities三个数组的下标顺序和传入的itemIDs顺序一一对应。我遇到过有同事把返回值顺序当成不可控其实COM层按请求顺序原样返回几个数组对应位置就是同一个Item的结果。大批量读时注意给组设置合适的UpdateRate读太快服务器处理不过来错误码会开始出现超时。5. 常见问题排查DCOM、64位和断线重连的五个坑这章是血泪总结都是实用排查记录每一条都按实际现象、原因、解决方法来写。5.1 坑连接报“0x80040154 类未注册”现象Connect那一步直接抛COMException错误码0x80040154程序根本进不了下一步。原因OPCAutomation.dll未注册或注册失效也可能是OPCEnum服务没有运行ProgID在注册表里找不到对应CLSID。重装系统或清理注册表插件后最容易出现。解决第一步确认ProgID对不对Matrikon模拟器就是Matrikon.OPC.Simulation在“运行”里输入dcomcnfg到组件服务里能看到已注册的OPC服务器列表。第二步检查OPCEnum服务服务管理器里找到OPC Enumeration状态要是“正在运行”。第三步用管理员身份运行“regsvr32 OPCAutomation.dll”把COM组件重新注册一遍。5.2 坑本机能连跨机器连不上现象客户端和OPC服务器在同一台机器上一切正常把客户端换到另一台电脑Connect超时或返回权限错误。原因OPC DA远程通讯走DCOMDCOM有自己的一套启动、激活、访问权限模型默认只放行本机用户。防火墙也没放行DCOM端口。解决两端都做配置。先跑dcomcnfg依次找“组件服务 → 计算机 → 我的电脑 → DCOM配置”在列表里找到OPCEnum和对应的OPC服务器程序把它们的启动和激活权限、访问权限改为自定义加入运行客户端程序的Windows用户。然后在防火墙高级设置里放行TCP 135端口如果OPC服务器程序有固定监听端口也一并放行。DCOM这块配置完必须重启OPC服务器进程才会生效我每次改完都要强调一遍不然白改。注意DCOM权限配到最小可用范围不要图省事直接给Everyone开完全控制那是给车间网络埋雷。5.3 坑64位系统下连上了服务器但枚举不到Item现象系统是Win10/11 64位程序编译默认AnyCPU运行正常连接也不报错但添加Item时总失败或者Item列表读出来是空的。原因OPCAutomation.dll及其底层的COM服务器是32位体系64位进程通过COM互操作访问32位对象时容易出现注册表视图不一致系统里存在两套注册表程序找到的不一定是服务器注册的那个类。解决项目属性 → 生成 → 平台目标改为x86重新编译。另外确认工程引用的Interop.OPCAutomation.dll是VS从COM组件自动生成的不要从网上随意拷贝来源不明的dll版本不一致时64位下会出现读取异常。这一条说是玄学其实就是架构错位。5.4 坑异步Read和DataChange偶尔不触发现象程序跑起来界面数据偶尔不动等几十秒又恢复或者干脆一直不动重启程序后正常。原因事件订阅时机不对。DataChange回调只在Item成功注册到组之后才有效AddItem参数里写错一个字符COM层会返回错误但代码没检查返回值结果订阅还挂着数据根本没进来。另一个常见原因是回调里抛异常被静默吞掉。解决第一订阅事件放在连接成功、建组完成之后立即挂不要再等Item注册完。第二AddItem调用后检查返回错误码非零就中断流程并提示地址错误。第三回调方法里全程try-catch异常写日志不静默。我用这个方法定位过两次“不触发”最终都是Item名错了。5.5 坑PLC重启或网线断开后客户端假死现象PLC断电再上电或者网线松动OPC客户端不报错、界面也不刷新看起来像死了一样。原因PLC侧断连后OPC服务器进入半断开状态客户端既收不到新数据也不会主动报错。没有重连逻辑的话程序就一直在那空等。解决监听OPCServer的ServerShutdown事件同时定时检查质量戳Quality连续几次为Bad时触发自动重连按Connect → AddGroup → AddItem顺序恢复。重连时保持clientHandle不变避免UI绑定的字典索引失效。private void opcServer_ServerShutdown(string reason) { // reason 可入库记录断连原因 Reconnect(); } private void Reconnect() { Disconnect(); Connect(progId, host); AddGroup(200); // 按点位表重新AddItemclientHandle用与原来相同的值 }6. 进阶把Item读写封装成泛型方法减少一半类型代码6.1 泛型读写封装读多写多之后我发现每次都要做object转具体类型的强制转换写值也要考虑类型匹配代码里大量重复。后来我把读写统一改成泛型方法业务层调用时干净很多。public T ReadValueT(string itemId) { object raw SyncReadItem(itemId); if (raw null || raw DBNull.Value) return default(T); // Convert.ChangeType 支持 bool/int/float/string 的互转 return (T)Convert.ChangeType(raw, typeof(T)); } public bool WriteValueT(string itemId, T val) { return WriteItem(itemId, val); }调用示例float temp ReadValuefloat(Simulation Items.Real.Real_Data_1); bool start ReadValuebool(Simulation Items.Bool.Bool_Data_1); WriteValuefloat(S7:[DB1]REAL0, 12.5f);泛型封装后业务层不用再关心COM层返回的VARIANT类型代码量直接砍掉三分之一。注意Convert.ChangeType处理float和int互转没问题但如果PLC变量本身类型定义变了比如把实数改成整数读出来的底层类型变了转换时会报错需要按点位表核对。6.2 用Stopwatch验证采集性能封装完了建议顺手做一次性能验证连上Matrikon开着界面对着Simulation Items.Real.Real_Data_1高频读用Stopwatch统计单次同步读的耗时和实际触发频率确认UpdateRate和DataChange的推送节奏是否符合预期。UpdateRate设200ms时统计结果应该在5次每秒附近偏差过大就要检查是不是有别的客户端也在连同一台OPC服务器抢占了服务进程资源。这套工程我拆过之后最大的感受是OPC DA的坑不在C#语法上全在环境配置和地址命名上。从那以后我每接手一个OPC项目都强制先花半小时在Matrikon模拟器上把读、写、回调、断线重连完整跑一遍再上真机调参数省下来的远不止半小时。希望帮到你。本文还有配套的精品资源点击获取