
简介OPCUA通信实例源码聚焦工业自动化场景面向需要实现PLC与PC机数据交换的嵌入式工程师、上位机开发者和系统集成商。源码基于OPC UA统一通信框架围绕服务器与客户端两大核心展开完整演示了设备数据接入、访问控制及安全连接的全过程。压缩包共285个文件主体为111个C源码与47个头文件用以实现OPCUA协议栈和业务逻辑21个Python脚本可用于测试与数据模拟另有CMake构建文件、Shell脚本、配置文件和说明文档便于跨平台编译与快速理解工程结构。整个包仅926KB轻量但内容充实。已有263人学习/下载是工控通信领域值得参考的入门实例。价值层面该实例提供完整的服务器端与客户端实现覆盖身份验证、加密通信、信息模型构建、节点浏览与读写、订阅推送等OPCUA关键技术点。开发者可借助源码梳理协议流程借配置文件掌握安全配置方法并直接将可复用模块移植到自有项目中显著降低开发调试成本。1. 从一台 PLC 到一台 PCOPC UA 把通信协议从“私有”变成了“数据字典”先还原一个现场产线上有西门子 S7-1500旁边工位是三菱 FX5U改造时又加了一台做视觉的工控机。过去要取数据S7 用 S7 协议三菱用 MC 协议PC 端每种写一套驱动换品牌等于重新开发。OPC UAOPC Unified ArchitectureIEC 62541解决的是这件事PLC 端把变量、报警、方法按统一信息模型暴露出来PC 端用一套客户端代码访问所有品牌的 PLC。它不再是传统 OPC DA 那种基于 Windows COM/DCOM 的重量级方案而是走 TCP 端口的跨平台协议。这篇内容围绕“OPC UA 通信用于 PLC 与 PC 机通信实例源码”展开先讲清 UA 的信息模型和订阅机制再给出西门子 S7-1200/1500 的服务器配置方法最后是一份可抄的 C 客户端源码覆盖连接、读写、订阅和断线重连。适合正在做设备数据采集、MES 对接或跨品牌 PLC 集成的工程师。2. OPC UA 的节点与地址PC 机理解 PLC 数据的核心模型2.1 从 OPC DA 到 OPC UA为什么 PLC 通信要换协议底座传统 OPC DA 使用 COM/DCOM 做进程间通信服务器和客户端必须都跑在 Windows 上DCOM 还要配置一堆安全策略。更麻烦的是OPC DA 只传输数据不描述数据含义读一个 DB 块里的温度客户端看到的只是一个 Float 值得靠上位机软件里手动映射“这个值对应 1 号炉膛温度”。OPC UA 把“数据”和“数据的语义”一起打包。服务器端用节点的形式组织数据每个节点有 NodeId、BrowseName、DataType 和属性。PLC 的一个 DB 变量、一个模拟量输入、一个报警事件在 UA 服务器里都是一个节点。PC 客户端通过浏览节点树就能知道这个点叫什么、什么类型、单位是什么不必提前给每个地址写死注释。这是它和 S7 协议、Modbus TCP 这类纯地址型协议最本质的区别。UA 还支持两种数据获取方式客户端主动轮询以及服务器主动推送。推送由 Subscription 机制实现PLC 侧数据变化后在客户端设定的发布周期内把变更包发过来网络开销远小于周期轮询。2.2 节点与引用的类型PLC 的 DB 块、I/O 区、报警如何映射UA 服务器内部是一棵节点树节点之间通过 Reference 连接。对象节点相当于文件夹变量节点相当于数据点方法节点相当于可调用的函数。常用 NodeClass含义PLC 侧对应物Object逻辑对象、分组PLC 程序块、结构化 DBVariable变量数据点DB 变量、I/O 地址、M 区地址Method可被调用的方法PLC 里的 FC/FB 或者功能块方式封装的操作View视图节点子集按工艺划分的数据子集地址的表达方式最关键。UA 的节点 ID 有两种常见写法一种数值型例如ns3;i1001另一种字符串型例如ns3;sDB1.Temperature。ns 是 Namespace Index即命名空间索引西门子 PLC 通常会把自己的变量节点放在一个特定的命名空间里具体索引值不固定需要用 UA Expert 这类客户端浏览后确认。字符串地址则可以直接对应 PLC 里的符号名。提示编写跨品牌采集程序时不要在代码里硬编码 ns 值。启动时先调用UA_Client_readNamespaceArray读取服务器支持的命名空间数组找到 URI 中包含品牌标识或项目名的索引再拼接节点 ID。2.3 通道、会话、订阅三者的生命周期必须先理清一个客户端连接 PLC 的 UA 服务器逻辑上分三层。最底层是 SecureChannel负责加密和身份认证往上一个 SecureChannel 可以建立多个 SessionSession 是服务器与客户端之间的逻辑会话每个 Session 下又可以创建多个 SubscriptionSubscription 里放若干个 MonitoredItem被监控项。参数上最容易混淆的是采样间隔SamplingInterval和发布间隔PublishingInterval。采样间隔是服务器读取底层数据源的频率比如每 100ms 读一次 DB 变量的当前值发布间隔是服务器把数据变化打包发给客户端的频率。两者不相等时服务器以 Max 和 Min 限制共同决定实际生效值。应用的典型配置参数典型值说明RequestedSamplingInterval100ms越短越占 CPU适合变化缓慢的温度、压力RequestedPublishingInterval500ms网络延时不敏感时可放宽MaxNotificationsPerPublish100单包最多通知条数控制消息体大小KeepAliveCount10超过 N 次心跳未收到算断线订阅机制解决的核心问题是“如何减少 PC 端无意义轮询”。比如 PLC 里有 200 个监控变量轮询周期 500ms 就是每秒 400 次请求而订阅模式中服务器只在值变化超过死区时才推送数据不变时网络几乎零流量。3. 跑通最小链路TIA 里开服务器PC 上用 UaExpert 验证3.1 在 TIA Portal 中启用 S7-1200/1500 的 OPC UA 服务器优先在西门子 S7-1200/1500 上做验证两块 CPU 从固件版本开始已经内置 UA 服务器。打开 TIA Portal选中 CPU在属性中找到“OPC UA”选项卡勾选“激活 OPC UA 服务器”。然后到“防护与安全”中设置安全策略测试阶段可以选“无安全策略”生产环境建议至少选择 Basic256Sha256 并配置证书。紧接着给 DB 块开访问权限。默认新建的 DB 不一定暴露给 UA 服务器。右键 DB 块进入属性在“OPC UA”相关设置里勾选“允许从 OPC UA 访问”对需要只读的置为“可读”对需要上位机下发的变量置为“可写”。编译下载后PLC 会在默认端口 4840 上监听等号地址为opc.tcp://PLC的IP:4840。注意S7-1200 早期固件只支持较少并发连接。部署前确认 CPU 型号和固件版本的 UA 连接数上限常见上限为 5 个会话。超过限制时后续客户端连接会被直接拒绝。3.2 用 UaExpert 浏览节点树确认变量地址Unified Automation 的 UaExpert 是验证 UA 服务器最常用的免费客户端。安装后打开左侧 Server 列表点 Add输入opc.tcp://192.168.0.10:4840双击后连接。连接成功后右侧 Address Space 窗口会显示 PLC 暴露出来的对象树找到目标 DB 块的变量右键可以 Read 查看当前值也可以 Write 测试写入。这一步做两件事第一确认 PLC 侧变量已经映射成 UA 节点并可见第二记录真正的节点地址和命名空间索引。UA Expert 的 Attributes 窗口会显示 NodeId例如ns3;sDB1_Temperature或者ns3;i101。这些地址就是后续 C 客户端里UA_Client_readValueAttribute要使用的节本。如果连接时报 BadCertificate 错误是因为客户端证书不受信任。解决路径从 UaExpert 的证书管理里导出 der 证书把它导入到 TIA 中 PLC 的“OPC UA 信任列表”里再重新下载到设备。3.3 用 open62541 在 PC 上写最小连接程序open62541 是开源的 UA 协议栈同时支持客户端和服务器编译成单文件库后接入很方便。二进制包可以在官方 GitHub 发布页获取源码编译则是常规做法。mkdir build cd build cmake -DUA_ENABLE_AMALGAMATIONON .. make编译完成后代码目录下会生成open62541.h和open62541.c直接把这两个文件加入你的 C 工程。#include open62541/client.h #include open62541/client_config_default.h #include open62541/client_highlevel.h #include open62541/client_subscriptions.h UA_Client *client UA_Client_new(); UA_ClientConfig_setDefault(UA_Client_getConfig(client)); // 连接 PLC 的 UA 服务器不启用证书加密 UA_StatusCode status UA_Client_connect(client, opc.tcp://192.168.0.10:4840); if (status ! UA_STATUSCODE_GOOD) { UA_Client_delete(client); return status; } // 读取变量节点 UA_Variant value; UA_NodeId nodeId UA_NODEID_STRING(3, DB1_Temperature); // ns3;s status UA_Client_readValueAttribute(client, nodeId, value); if (status UA_STATUSCODE_GOOD UA_Variant_hasScalarType(value, UA_TYPES[UA_TYPES_DOUBLE])) { UA_Double temp *(UA_Double *)value.data; printf(Temperature%.2f\n, temp); } UA_Variant_clear(value); UA_Client_disconnect(client); UA_Client_delete(client);逻辑说明UA_ClientConfig_setDefault负责填入默认网络参数包括超时时间、心跳间隔和安全配置UA_Client_connect内部完成 SecureChannel 和 Session 的建立读取后用UA_Variant_hasScalarType检查数据类型防止服务器返回的类型和预期不一致。提示测试阶段先用UA_NODEID_STRING(3, DB1_Temperature)这种字符串 ID因为它在 UaExpert 里一眼能看到。生产环境再考虑改为整型节点 ID整型 ID 的查询效率更快但需要维护一张“变量名到 NodeId”的映射表。如果 PC 端是 C#/.NET 环境可以用 OPC Foundation 的 UA-.NETStandard 库UAClient的接口风格不同但节点模型和订阅参数完全一致。open62541 这份代码的价值在于讲清楚 UA 协议栈内部的工作方式看完能平滑迁移到任何语言。4. 实例源码解析读、写、订阅与断线重连的 C 实现4.1 客户端配置结构体与连接参数完整实例不会只读一个点。先定义一个配置结构体把服务器地址、用户名、密码、超时时间、发布间隔集中管理。struct UAConfig { std::string endpoint opc.tcp://192.168.0.10:4840; std::string username; std::string password; int timeoutSec 10; uint32_t publishingIntervalMs 500; uint32_t maxReconnectAttempts 10; }; static UA_Client *connectWithRetry(const UAConfig cfg) { UA_Client *client UA_Client_new(); UA_ClientConfig *cc UA_Client_getConfig(client); UA_ClientConfig_setDefault(cc); cc-timeout cfg.timeoutSec * 1000; cc-connectTimeout cfg.timeoutSec * 1000; // 如果 PLC 开启了用户名密码认证 if (!cfg.username.empty()) { UA_ClientConfig_setAuthenticationUsername(cc, cfg.username.c_str(), cfg.password.c_str()); } for (int i 0; i cfg.maxReconnectAttempts; i) { UA_StatusCode ret UA_Client_connect(client, cfg.endpoint.c_str()); if (ret UA_STATUSCODE_GOOD) { return client; } // 指数退避重连 int sleepMs std::min(1000 * (1 i), 15000); std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); } UA_Client_delete(client); return nullptr; }参数说明connectTimeout控制 TCP 握手阶段的等待时间PLC 不在线时不会卡住主流程UA_ClientConfig_setAuthenticationUsername设置用户名密码时协议栈会自动选择对应的安全策略。指数退避重连在车间网络抖动时非常有用避免每 1 秒暴力重连给 PLC 造成额外负载。4.2 读取 PLC 变量整型节点 ID 与字符串节点 ID 的取舍上一章用字符串 ID 演示生产代码通常做成一个函数统一从配置表里取节点。以下代码演示如何读取整型 ID 节点并将结果转换为int16_t。UA_StatusCode readInt16(UA_Client *client, UA_UInt32 nsIdx, UA_Int32 numericId, int16_t *out) { UA_NodeId nodeId UA_NODEID_NUMERIC(nsIdx, numericId); UA_Variant value; UA_StatusCode ret UA_Client_readValueAttribute(client, nodeId, value); if (ret ! UA_STATUSCODE_GOOD) { return ret; } if (UA_Variant_hasScalarType(value, UA_TYPES[UA_TYPES_INT16])) { *out *(UA_Int16 *)value.data; } else { ret UA_STATUSCODE_BADTYPEMISMATCH; } UA_Variant_clear(value); return ret; }这里有一个容易踩的坑PLC 里声明的是 INT16 位UA 服务器暴露的 DataType 是 Int16如果用UA_TYPES_INT32去读UA_Variant_hasScalarType会返回 false。跨品牌 PLC 对接时建议前端先浏览节点把参数类型存到配置表里再用对应类型去读而不是在代码里做盲转。4.3 写入与调用方法PC 控制 PLC 启停的根本手法写变量是控制 PLC 的基础操作。以下代码把布尔变量置为 true作为电机启动信号。UA_StatusCode writeBool(UA_Client *client, const UA_NodeId nodeId, bool value) { UA_Variant val; UA_Bool b value ? UA_TRUE : UA_FALSE; UA_Variant_setScalar(val, b, UA_TYPES[UA_TYPES_BOOLEAN]); UA_StatusCode ret UA_Client_writeValueAttribute(client, nodeId, val); return ret; }UA_Variant_setScalar把本地布尔值包装成 UA 的数据结构之后UA_Client_writeValueAttribute才认得。很多新手直接传value给写接口编译不报错但运行时报 BadTypeMismatch原因就是UA_Variant的 data 指针必须指向 UA 已知类型的存储。除写变量外UA 还支持调用 PLC 内的方法节点。用UA_Client_call传入方法节点 ID 和对象节点 ID参数以UA_Variant数组形式传递。缺点是多品牌 PLC 对 Method 的支持程度不统一跨品牌移植性不如写变量。4.4 订阅机制搞清采样间隔、发布间隔和回调线程订阅是这章里最重要的一节。以下代码创建一个订阅并监控一个温度节点。static void onDataChange(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) { if (value-hasValue UA_Variant_hasScalarType(value-value, UA_TYPES[UA_TYPES_DOUBLE])) { UA_Double temp *(UA_Double *)value-value.data; printf(sub%u mon%u temp%.2f\n, subId, monId, temp); } } void setupSubscription(UA_Client *client, const UA_NodeId nodeId, uint32_t samplingMs, uint32_t publishMs) { // 1. 创建订阅设置发布间隔 UA_CreateSubscriptionRequest req UA_CreateSubscriptionRequest_default(); req.requestedPublishingInterval publishMs; req.requestedLifetimeCount 3000; req.requestedMaxKeepAliveCount 10; UA_UInt32 subId 0; UA_Client_createSubscription(client, req, onDataChange, NULL, subId); // 2. 创建监控项 UA_MonitoredItemCreateRequest itemReq UA_MonitoredItemCreateRequest_default(nodeId); itemReq.requestedParameters.samplingInterval samplingMs; itemReq.requestedParameters.queueSize 1; itemReq.requestedParameters.discardOldest UA_TRUE; UA_UInt32 monId 0; UA_Client_MonitoredItemCreate(client, subId, itemReq, NULL, NULL, monId); }参数说明requestedLifetimeCount是订阅在没有发布消息时的最大心跳周期数超过后服务器会判定订阅过期requestedMaxKeepAliveCount是心跳判定参数。采样 100ms、发布 500ms 的组合适合温度、压力这类变化缓慢的数据高速振动信号建议采样 1ms、发布 10ms但要注意 PC 端回调线程的处理速度。注意onDataChange回调运行在 open62541 内部的网络线程里。回调里只做数据拷贝和队列投递绝对不要直接调用 PLC 写接口或者长时间阻塞操作。4.5 断线重连状态机比盲目重试更可靠PLC 会重启、网线会被踢掉、产线切换可能导致暂短断网。生产级客户端必须有重连状态机。常见做法是维护三个状态已连接、尝试重连、重连成功。enum class LinkState { CONNECTED, RECONNECTING }; void runLoop(UA_Client *client, const UAConfig cfg) { LinkState state LinkState::CONNECTED; int backoffMs 1000; while (true) { // 周期性发送心跳检测连接是否健在 UA_Client_run_iterate(client, 100); if (state LinkState::CONNECTED) { if (UA_Client_getState(client) UA_CLIENTSTATE_DISCONNECTED) { state LinkState::RECONNECTING; backoffMs 1000; } } else { UA_StatusCode ret UA_Client_connect(client, cfg.endpoint.c_str()); if (ret UA_STATUSCODE_GOOD) { // 重连成功后需要重新创建订阅 // 创建订阅和监控项的代码在这里再执行一次 state LinkState::CONNECTED; } else { std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs)); backoffMs std::min(backoffMs * 2, 15000); } } } }重连成功后最容易遗漏的是订阅读取。UA 的 Session 重建后原有的 Subscription 和 MonitoredItem 全部失效必须在UA_Client_connect成功后重新走一遍 4.4 节的订阅创建流程。否则 PC 端显示已连接但数据流是断的。5. OPC UA 生产部署证书、性能参数与三条常见报错5.1 证书配置连接被拒绝时先检查信任列表无安全策略连接只适合实验室。生产环境启用 Basic256Sha256 后UA 服务器和客户端都要持有证书并互加信任列表。常见做法是 PC 端用 open62541 的UA_ClientConfig_setDefault时自动生成自签名证书然后把这个证书导出到 PLC 的信任列表。S7-1500 的 TIA 项目里在 CPU 属性 OPC UA 安全 受信任的客户端中导入 der 证书。一个容易被忽略的坑UA 证书验证会检查证书有效期和客户端系统时间。PC 主板电池没电导致时间偏差超过 5 分钟连接就会报 BadCertificateTimeInvalid。遇到这类错误先同步 NTP 时间再查证书。5.2 性能参数的合理上限参数建议上限超限后果MaxNodesPerRead100单次请求数据包过大PLC 端拒答MaxMonitoredItemsPerSubscription500订阅建立失败MaxSubscriptionsPerSession50新订阅被拒绝PublishingInterval不低于 50ms服务器 CPU 占用过高大批量采集时优先用UA_Client_readValueAttribute的数组版本UA_Client_readValues一次带多个节点 ID而不是循环调用单点读取。5.3 错误码速查表错误码含义排查方向BadNoMatch节点找不到用 UaExpert 重新浏览节点确认 ns 和 ID 正确BadSessionIdInvalid会话失效服务器重启过检查重连逻辑是否重建 SessionBadCertificateUntrusted证书不受信任把 PC 端证书导出导入 PLC 的受信任列表BadTimeout请求超时检查网络延迟调大客户端 timeout 参数确认 Subscription 是否卡死排错的最后手段是打开 open62541 的日志功能。实例化客户端后执行UA_ClientConfig_setLogger(cc, UA_Log_Stdout, UA_LOGLEVEL_DEBUG)所有节点浏览、连接握手、数据收发都会有日志输出到标准输出。注意生产环境把日志级别调回 WARNING否则调试日志会产生较大磁盘 I/O。本文还有配套的精品资源点击获取