OPC UA 通信实战:从 PLC 到 PC 的 C++ 客户端实例与源码解析
2026/9/15 18:46:03 网站建设 项目流程

简介:OPCUA通信实例源码聚焦工业自动化场景,面向需要实现PLC与PC机数据交换的嵌入式工程师、上位机开发者和系统集成商。源码基于OPC UA统一通信框架,围绕服务器与客户端两大核心展开,完整演示了设备数据接入、访问控制及安全连接的全过程。压缩包共285个文件,主体为111个C源码与47个头文件,用以实现OPCUA协议栈和业务逻辑;21个Python脚本可用于测试与数据模拟;另有CMake构建文件、Shell脚本、配置文件和说明文档,便于跨平台编译与快速理解工程结构。整个包仅926KB,轻量但内容充实。已有263人学习/下载,是工控通信领域值得参考的入门实例。价值层面,该实例提供完整的服务器端与客户端实现,覆盖身份验证、加密通信、信息模型构建、节点浏览与读写、订阅推送等OPCUA关键技术点。开发者可借助源码梳理协议流程,借配置文件掌握安全配置方法,并直接将可复用模块移植到自有项目中,显著降低开发调试成本。

1. 从一台 PLC 到一台 PC:OPC UA 把通信协议从“私有”变成了“数据字典”

先还原一个现场:产线上有西门子 S7-1500,旁边工位是三菱 FX5U,改造时又加了一台做视觉的工控机。过去要取数据,S7 用 S7 协议,三菱用 MC 协议,PC 端每种写一套驱动,换品牌等于重新开发。OPC UA(OPC Unified Architecture,IEC 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 程序块、结构化 DB
Variable变量数据点DB 变量、I/O 地址、M 区地址
Method可被调用的方法PLC 里的 FC/FB 或者功能块方式封装的操作
View视图,节点子集按工艺划分的数据子集

地址的表达方式最关键。UA 的节点 ID 有两种常见写法,一种数值型,例如ns=3;i=1001,另一种字符串型,例如ns=3;s=DB1.Temperature。ns 是 Namespace Index,即命名空间索引,西门子 PLC 通常会把自己的变量节点放在一个特定的命名空间里,具体索引值不固定,需要用 UA Expert 这类客户端浏览后确认。字符串地址则可以直接对应 PLC 里的符号名。

提示:编写跨品牌采集程序时,不要在代码里硬编码 ns 值。启动时先调用UA_Client_readNamespaceArray读取服务器支持的命名空间数组,找到 URI 中包含品牌标识或项目名的索引,再拼接节点 ID。

2.3 通道、会话、订阅:三者的生命周期必须先理清

一个客户端连接 PLC 的 UA 服务器,逻辑上分三层。最底层是 SecureChannel,负责加密和身份认证;往上一个 SecureChannel 可以建立多个 Session,Session 是服务器与客户端之间的逻辑会话;每个 Session 下又可以创建多个 Subscription,Subscription 里放若干个 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,例如ns=3;s=DB1_Temperature,或者ns=3;i=101。这些地址就是后续 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_AMALGAMATION=ON .. make

编译完成后,代码目录下会生成open62541.hopen62541.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"); // ns=3;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 里声明的是 INT(16 位),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会话失效服务器重启过,检查重连逻辑是否重建 Session
BadCertificateUntrusted证书不受信任把 PC 端证书导出,导入 PLC 的受信任列表
BadTimeout请求超时检查网络延迟,调大客户端 timeout 参数,确认 Subscription 是否卡死

排错的最后手段是打开 open62541 的日志功能。实例化客户端后执行UA_ClientConfig_setLogger(cc, UA_Log_Stdout, UA_LOGLEVEL_DEBUG),所有节点浏览、连接握手、数据收发都会有日志输出到标准输出。注意生产环境把日志级别调回 WARNING,否则调试日志会产生较大磁盘 I/O。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询