简介:面向工业自动化中PLC与上位机之间的数据交换,提供一套完整的OPC UA通信实例源码,适合PLC工程师、工控软件开发人员及需要深入理解OPC UA协议栈的进阶学习者,可借此快速搭建服务器与客户端的通信框架,从而降低工业通信项目的开发门槛。资源共285个文件,压缩包大小仅926KB,源码以C语言和C++程序文件为主,另含Python辅助脚本、CMake构建配置、Shell部署脚本、Markdown说明文档等,目录分明,便于定位服务器端、客户端及安全配置模块,兼顾可读性与工程实用性。已有263人学习下载,代码覆盖OPC UA的核心机制,包括信息模型节点组织、身份验证与加密、读写与订阅服务、二进制编码传输等,并配套示例应用展示完整的交互流程,帮助开发者快速理解关键实现。通过学习,可理解从服务器建模、监听请求到客户端浏览读写、订阅推送的完整通信链路,为实际项目集成或二次开发提供直接可用的工程参考。
1. OPC UA 通信在 PLC 与 PC 机之间到底解决什么问题
车间里最不缺的就是各种协议。老式 PLC 要往 PC 机传数据,常见做法是组态软件做变量映射,或者写底层 Socket 去解析私有协议;换一台 PLC 型号,解析代码就要重来一遍。OPC UA 的出现把设备与上位机之间的数据通道标准化成以太网服务:PLC 侧要么内置服务器,要么通过网关把地址空间暴露出来;PC 机侧只用统一的 OPC UA 客户端库,就能读状态、写指令、订阅变化。标题里“实例源码”多数是指 PC 机客户端这部分,以及与之对应的 PLC 服务器配置。对做设备数据采集、MES 对接、上位机开发的工程师来说,这个标题实际能拆成三件事:连接怎么建立、节点怎么定、读写和订阅怎么落地。
2. PLC 与 PC 机通信前先准备好 OPC UA 服务器端点与网络连接
2.1 PLC 侧内置 OPC UA 服务器已经是最省事的接法
现在的 PLC 侧支持 OPC UA 通信的路径有好几条。最省事的是使用 PLC 内置的 OPC UA 服务器,比如西门子 S7-1200/1500 在博途项目中可以勾选激活 OPC UA 服务器,CPU 固件本身就会监听 4840 端口;三菱、汇川等品牌对 OPC UA 的支持也在逐步覆盖,不支持时通常借助协议转换网关。内置服务器的好处是少一台中间硬件,PLC 的 DB 块、I/Q 地址会被映射成 OPC UA 服务器地址空间中的节点,PC 机客户端不需要安装 PLC 私有驱动,直接走标准协议。
无论内置还是网关,PC 机和 PLC 之间第一步是网络层能通。PLC 一般固定占用一个工程网口,IP 通常为 192.168.1.x,PC 机网卡要设在同一个网段,例如 PLC 为 192.168.1.10,PC 机为 192.168.1.20,掩码统一 255.255.255.0。如果用虚拟机跑客户端,网络连接模式建议选桥接而不是 NAT,桥接能让 PLC 的回包直接发到宿主机的物理网卡,NAT 模式在部分工业网段里会出现 ARP 不同、PLC 端回包错路的现象。这里说的网络连接模式,指 VMware 或 VirtualBox 给虚拟机网卡分配 IP 的方式,桥接模式下 PC 机虚拟机可以拿到和 PLC 同一子网的 IP。
2.2 OPC UA 客户端连接地址和最小连接代码
PC 机侧 OPC UA 客户端以库的形式接入。C# 常见做法是拿 OPC Foundation 的 UA-.NETStandard 库,C++ 用 open62541,Python 可以用 asyncua。连接地址统一写成opc.tcp://192.168.1.10:4840,这是 UA 的标准格式,不需要额外指定协议厂商标识。很多 OPC UA 客户端库还会要求一个 ApplicationName,服务器会拿这个名称来区分是哪台 PC 发起的连接。
C# 的最小连接代码如下,省掉证书细节,先把会话建起来:
using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; var config = new ApplicationConfiguration { ApplicationName = "PCClient", ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { // 演示环境先自动接受不受信任的证书,生产环境必须删除这一行 AutoAcceptUntrustedCertificates = true, RejectSha1SignedCertificates = false }, TransportQuotas = new TransportQuotas { OperationTimeout = 15000 } }; // useSecurity=false 表示先用无加密策略把连接跑通 var endpoint = CoreClientUtils.SelectEndpoint( config, "opc.tcp://192.168.1.10:4840", useSecurity: false); using (var session = Session.Create(config, endpoint, true, true, "pc-session").Result) { Console.WriteLine("连接成功: " + session.Endpoint.EndpointUrl); }逻辑说明:ApplicationConfiguration是每个 OPC UA 客户端启动前都要准备的配置文件,包含应用名、证书、传输配额。CoreClientUtils.SelectEndpoint会先尝试连接服务器并读取它声明的支持的安全策略列表,useSecurity: false表示选择SecurityPolicy.None这一条,适合现场调试。Session.Create的参数里,第三个updateBeforeConnect为 true 时会在创建会话前重新获取端点,第四个checkDomain为 true 时跳过证书域名校验,IP 直连 PLC 时这句很关键,否则可能出现域名不匹配的异常。OperationTimeout是单个请求的等待时间,PLC 侧响应慢时调到 30000 毫秒更稳妥。
连接参数可以直接做成配置项,建一个表格方便对接时快速核对。
| 参数 | 常见值 | 说明 |
|---|---|---|
| EndpointUrl | opc.tcp://192.168.1.10:4840 | PLC 侧 OPC UA 服务器地址,内置服务器默认 4840 |
| SecurityPolicy | None / Basic256Sha256 | 演示先用 None,正式环境使用加密策略 |
| OperationTimeout | 15000 ms | PC 机客户端请求超时,PLC 响应慢要调大 |
| ApplicationName | PCClient | 会出现在 PLC 服务器的会话列表里 |
| checkDomain | true | IP 直连时跳过证书域名校验 |
2.3 节点地址不是猜出来的
连接建立后,读写的是 PLC 地址空间里的节点。节点地址由命名空间和标识符组成,例如ns=2;s=DB1_Temp。ns=2是命名空间索引 2,不同 PLC 品牌把 DB 块字段放在不同的命名空间后面;s=DB1_Temp是字符串标识符,表示 PLC 程序里的变量。节点 ID 的准确写法必须通过 OPC UA 客户端工具浏览地址空间得到,常见工具是 UA Expert,输入上面的连接地址,连上后就能看到 PLC 暴露出来的所有 DB、I/Q 地址和它们的类型。不要凭记忆拼节点 ID,尤其是西门子 DB 块,在 PLC 侧还需要把对应 DB 块的“OPC UA 访问权限”打开,否则节点在服务器侧不会公布。
3. PC 机侧 OPC UA 实例源码:读实时值、写点与订阅数据变化
3.1 批量读取多个节点,一次往返拿到一整组数据
PC 机与 PLC 通信最常用的是读操作。逐点调用ReadValue会要求每个点做一次请求,PLC 地址空间大的时候 PC 机 CPU 占用率很高。常见做法是用ReadValues把多个 NodeId 打包在一个请求里,OPC UA 服务器端也会尽量顺序执行,整体延迟比逐点读低不少。
var nodeIds = new NodeIdCollection { new NodeId("ns=2;s=DB1_Temp"), new NodeId("ns=2;s=DB1_Pressure"), new NodeId("ns=2;s=DB1_Speed") }; var readValues = session.ReadValues(nodeIds, 0, TimestampsToReturn.Both); for (int i = 0; i < readValues.Count; i++) { Console.WriteLine($"{nodeIds[i]} => {readValues[i].Value} | {readValues[i].StatusCode}"); }逻辑说明:ReadValues第一个参数是要读的节点集合,第二个参数 0 表示取最近的值,第三个参数TimestampsToReturn.Both表示同时返回来源时间戳和服务器时间戳,方便 PC 机侧判断数据是否长时间未更新。返回列表和请求节点顺序一一对应,循环时按索引取数据。StatusCode用于判断单个节点读取失败,比如某个节点地址写错,不会影响同批次其它节点成功返回。
3.2 写操作:布尔点、整数点、浮点点的类型对齐
PC 机向 PLC 写入启动命令、设定值,是 PLC 控制逻辑要求最严的一部分。写操作必须保证 NodeId 准确,并且值类型和 PLC 侧变量类型一致;常见坑是 PLC 侧是REAL,PC 侧写了个int,服务器会直接返回BadTypeMismatch。
var writeCollection = new WriteValueCollection { new WriteValue { NodeId = new NodeId("ns=2;s=DB1_StartBtn"), AttributeId = Attributes.Value, Value = new DataValue(true) { SourceTimestamp = DateTime.UtcNow, ServerTimestamp = DateTime.UtcNow } }, new WriteValue { NodeId = new NodeId("ns=2;s=DB1_SpeedSet"), AttributeId = Attributes.Value, Value = new DataValue(30.0f) } }; session.Write(writeCollection, out var results); foreach (var r in results) { Console.WriteLine(r.Code); }逻辑说明:WriteValue的AttributeId固定是Attributes.Value,DataValue里不光放值,还可以带时间戳。写入布尔点时 PLC 侧一般映射为 BIT 或 BOOL,用true/false;浮点最好显式写成30.0f,不要写成30,避免 C# 编译成 double 后 OPC UA 栈把类型识别成双精度,而 PLC 变量是单精度 REAL。写的结果列表和请求集合顺序一致,拿到Good才算成功;如果返回BadNotWritable,先检查 PLC 服务器账户的写权限,而不是怀疑代码。
3.3 订阅数据变化:按周期推送,比轮询更省网络带宽
OPC UA 相对传统 Socket 通信的明显优势是订阅。PC 机不需要每秒向 PLC 轮询,而是告诉服务器“我关注这几个节点,有变化再推给我”。订阅由Subscription和MonitoredItem组成,一个订阅可以挂多个监控项,服务器按发布周期批量推送。
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100, KeepAliveCount = 10, LifetimeCount = 100 }; session.AddSubscription(subscription); subscription.Create(); var monitoredItem = new MonitoredItem { StartNodeId = new NodeId("ns=2;s=DB1_Temp"), AttributeId = Attributes.Value, SamplingInterval = 100, QueueSize = 10, DiscardOldest = true }; monitoredItem.Notification += (MonitoredItem sender, MonitoredItemNotificationEventArgs e) => { var dataValue = sender.LastValue; Console.WriteLine($"温度值: {dataValue.WrappedValue.Value}"); }; subscription.AddItem(monitoredItem); subscription.ApplyChanges();逻辑说明:PublishingInterval是服务器向 PC 机客户端推送数据的周期,单位毫秒,100 毫秒意味着最多每秒推送 10 次;SamplingInterval是 PLC 侧采样节点的间隔,它决定了服务器多久检查一次节点变化。两者不相等的时候,数据是否真的推送还取决于节点值变化和发布周期。QueueSize=10表示服务器在客户端处理不及时的时候缓存最多 10 个变化值,DiscardOldest=true表示趁旧保新,适合温度、压力这种只关心当前值的过程量。Notification事件在客户端收到推送时触发,事件处理器里直接从sender.LastValue取最新值,不需要再主动读一次 PLC。
表格总结三个操作的关键 API:
| 操作 | API | 关键参数 |
|---|---|---|
| 批量读 | session.ReadValues(ids, 0, ts) | 第三个参数设置时间戳返回策略 |
| 写 | session.Write(items, out results) | Value必须和 PLC 变量类型一致 |
| 订阅 | Subscription+MonitoredItem | PublishingInterval 与 SamplingInterval 分开设 |
4. 实例源码连通性排查与参数调优:端点、证书、采样间隔
4.1 先分清网络不通还是协议不通
写完了连接、读写、订阅代码,剩下的工作就是排坑。最常见的第一道坑是 PC 机 ping 不通 PLC。在 PC 机上先做端口探测,确认 4840 真的在监听:
# Windows PowerShell Test-NetConnection 192.168.1.10 -Port 4840如果显示TcpTestSucceeded: False,问题在网络层,检查 PLC 是否启用了 OPC UA 服务器、防火墙是否放行,Windows 还得注意专用网络与公用网络的文件共享策略。如果端口能通但客户端报BadTimeout或握手错误,才进入协议层排查。用 UA Expert 工具直接连同一个地址,能连上就说明 PC 机代码库版本和服务器不兼容的概率很小,问题出在客户端配置。
排查表可以覆盖绝大多数现场情况:
| 现象 | 原因 | 处理办法 |
|---|---|---|
| 端口测试失败 | PLC 侧服务器未启动 / 防火墙 | 检查 PLC 项目是否激活 OPC UA;放行 4840 |
BadSecurityChecksFailed | 客户端证书不被服务器信任 | 把客户端证书发到 PLC 服务器信任列表,或临时 AutoAccept |
BadNodeIdUnknown | 节点 ID 拼错 | 用 UA Expert 浏览地址空间复制节点 |
BadSessionIdInvalid | 会话被服务器关闭 | 增大 KeepAliveCount,或客户端做了长连接重连 |
| 数据读到 null | 节点类型映射错误 | 用 UA Expert 查看节点 DataType,再改 C# 类型 |
4.2 证书与安全策略的正确配置顺序
OPC UA 服务器在 PLC 侧只监听一个端口,但访问控制由安全策略加用户认证两层决定。PC 机客户端第一次连接时,库会生成本机证书;服务器默认不接受陌生证书,所以会出现BadSecurityChecksFailed。测试期最省事的做法是把AutoAcceptUntrustedCertificates设为 true,让它自动信任服务器端证书。这个开关只能用于内网演示,一旦接入产线,PLC 侧的安全审计就会把它拒掉,因此生产环境要把客户端证书导出后添加到 PLC 服务器的信任列表。不同 PLC 的添加界面不一样,但原理都是“服务器端信任客户端,客户端信任服务器”。
安全策略的选择顺序:老式 PLC 或网关只支持Basic256,新设备基本支持Basic256Sha256,更强的是Aes128_Sha256_RsaOaep。能支持时优先用Basic256Sha256。如果客户端代码里设了useSecurity: true但服务器不支持对应策略,SelectEndpoint会返回一个不支持的 endpoint 然后握手失败;反过来把安全策略设成 None 就可以通。这说明不是代码逻辑错,而是两端策略不匹配。
4.3 采样间隔和发布间隔不是越小越好
订阅参数调优是 OPC UA 通信实例源码里最容易被人忽略的部分。把PublishingInterval调到 10 毫秒,PC 机端每秒要处理 100 个包,PLC 侧 CPU 也承受不住。实际参数至少参考三个因素:PLC 扫描周期、数据变化速度、PC 机数据显示需求。
给出一个经验参考表:
| 场景 | SamplingInterval | PublishingInterval | QueueSize |
|---|---|---|---|
| 电机转速监控 | 50 ms | 100 ms | 5 |
| 模拟量温度采集 | 500 ms | 1000 ms | 10 |
| 状态机变化 | 100 ms | 100 ms | 10 |
| 打开调试日志 | 20 ms | 50 ms | 50 |
还有一个容易踩的坑:SamplingInterval设得比 PLC 实际扫描周期还短,服务器会按自己能力返回值,不会报错,但 PC 机端可能以为数据是每 10 毫秒刷新一次,实际上 PLC 扫描一遍就要几十毫秒。调试时可以打上数据的时间戳看真实间隔。订阅对象要长期持有,方法退出就被 GC 回收的话,订阅会静默断开,等下一次发布超时才会发现。
5. 把 OPC UA 实例源码扩展成控制场景的高级用法
5.1 用结构化节点一次读回整块 DB 数据
PLC 侧程序里一旦有几十个温度、压力、状态信号,逐个读写节点会让代码变得又长又难维护。常见做法是在 PLC 内把这些信号放进同一个 DB 块,然后让 OPC UA 服务器把这个 DB 块暴露成一个结构化节点。PC 机端只需要读这一个 NodeId,就能拿回一个带有多个字段的结构体,网络往返次数从几十次降低到一次。
var dbNode = new NodeId("ns=2;s=DB1_DataBlock"); DataValue dv = session.ReadValue(dbNode); if (dv.Value is ExtensionObject ext && ext.Body != null) { Console.WriteLine(ext.Body); }说明:ExtensionObject是 OPC UA 表示复杂结构体的一种数据类型,具体字段顺序要在客户端侧按 PLC 定义的结构体反序列化。西门子 PLC 的 OPC UA 服务器会把整个 DB 的结构暴露出来,如果发现读取结果是 null,多数是因为服务器没有把这个 DB 设置为可访问。
5.2 用方法调用代替裸写,把互锁放到 PLC 侧
面向控制场景时,PC 机直接写“启动”位是不推荐的做法。更可靠的方法是 PLC 里定义好 OPC UA 方法,例如StartPump、StopAlarm,PC 机调用方法而不是写变量。调用时所有互锁条件在 PLC 内判断,返回一个结果码,PC 机再根据结果码显示成败原因。这样即使 PC 机键盘误触发,PLC 扫描周期内也会首先执行安全条件。实例源码里这一块往往只体现客户端调用,真正的实现细节在 PLC 侧方法块里。
5.3 用 Reconnect 保证断线后自动恢复
长连的 OPC UA 会话会因 PLC 重启、网线松脱而断开,PC 机程序需要监听SessionReconnect事件并重新建立订阅。常见做法是维护一个已订阅节点集合,断线重连成功后把这些监控项重新加回去。注意重新订阅后的第一帧数据通常需要等一个发布周期,控制程序里要做超时判断,不要误以为订阅已经失效。
本文还有配套的精品资源,点击获取