OPC UA从入门到落地:PLC与PC通信的完整实现指南
2026/9/15 3:00:05 网站建设 项目流程

简介:面向工业自动化与上位机通信开发场景,这套OPC UA通信实例源码帮助PLC与PC机实现标准化的数据交换,解决设备互联与系统集成难题。压缩包内共285个文件,大小约926KB,核心为111个C源文件与47个头文件,同时提供21个Python辅助脚本、CMake构建配置、说明文档与文本文件,目录结构清晰,便于查阅和二次开发。已有263人学习下载。实例覆盖服务器端与客户端的完整实现,包括信息模型构建、用户身份验证与授权、读写/订阅/浏览等标准服务调用,以及二进制编解码和TCP/IP网络传输细节,并配有配置文件和简单的示例应用;通过实际运行和阅读源码,可快速掌握从节点建模、数据访问到推送更新的全流程,为开发可靠的工业通信程序提供直接参考。适合有一定PLC或工业通信基础的开发者学习,也可直接作为项目原型。

1. OPC UA 不是 Modbus 的替代品,而是工厂数据层的操作系统

很多做 PLC 与 PC 机通信的工程师,第一反应是 Modbus TCP 或厂商私有协议。这两种方式在单机、单厂商场景下够用,但一旦涉及跨品牌设备、数据加密、或者要让 MES 和 SCADA 系统统一取数,就会变得非常繁琐。OPC UA 不是一个简单的数据传输通道,它是带信息模型、安全机制和服务框架的完整通信协议。这份实例源码包含了从零实现的 OPC UA 服务器和客户端,服务器可以运行在 PLC 侧或 PC 侧,客户端负责浏览、读写和订阅变量。适合想绕过复杂协议规范、直接掌握落地方法的自动化工程师和上位机开发者。文章会从信息模型开始,一直拆到源码结构、参数配置和抓包验证。

2. 先搞懂 OPC UA 的信息模型和会话模型:节点、地址空间与订阅

2.1 信息模型:节点和引用决定“数据长什么样”

OPC UA 中所有数据都以节点为载体,节点通过引用关系组成地址空间。理解这个层次最关键:你读的不是一个寄存器地址,而是一个带路径的节点。节点分变量、对象、方法等类型,每个节点都有属性,例如 NodeId、BrowseName、Value、DataType。例如,PLC 的 DB 块在服务端可以映射为ns=2;i=10的变量节点,客户端也可以通过浏览路径Root/Objects/PLC/DB1/Value1访问。因为地址空间是自描述的,客户端可以动态发现设备数据,不需要预先约定地址表。

服务端的地址空间可以由代码静态创建,也可以从 XML 或 JSON 加载。常见做法是直接用 open62541 或 OPC UA .NET Standard 库,把 PLC 数据模型镜像成 UA 节点。写代码时先定义 NodeId 和数据类型,再添加到地址空间。节点 ID 的选择会影响后续客户端访问方式,若使用数字 ID,访问性能更好,但可读性差;若使用字符串 ID,调试方便,但编码时要注意命名空间。

2.1.1 浏览路径与 NodeId 的映射关系

实际项目中,建议把 PLC 变量名直接编码成 BrowseName,这样上位机看到的名字是L1_Temperature而不是%DB10.DBD0。服务器端维护一张变量名到 NodeId 的映射表;客户端通过BrowseTranslateBrowsePathsToNodeIds服务解析路径。下面是一段用 C++ 和 open62541 创建变量节点并设置初始值的代码:

UA_VariableAttributes attr = UA_VariableAttributes_default; UA_Int32 initialValue = 42; UA_Variant_setScalar(&attr.value, &initialValue, &UA_TYPES[UA_TYPES_INT32]); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); UA_NodeId variableNodeId = UA_NODEID_NUMERIC(2, 1001); UA_QualifiedName browseName = UA_QUALIFIEDNAME(2, "PLC_DB1_Value1"); UA_NodeId serverNodeId; UA_Server_addVariableNode(server, variableNodeId, parentNodeId, parentReferenceNodeId, browseName, UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, &serverNodeId);

这里UA_NODEID_NUMERIC(2, 1001)表示应用命名空间编号为 2,节点编号 1001。parentNodeId指向 Objects 文件夹,引用类型选择Organizes,变量会出现在对象浏览路径下。UA_VariableAttributes_default会补全默认的读写属性和访问级别,实际工程中还要设置UserAccessLevelMinimumSamplingInterval。如果把变量挂在工程设备对象下,引用类型要改成HasComponent,否则浏览树与你预期不一致。

2.2 服务集与会话:读、写、订阅不是“裸命令”

OPC UA 定义了一整套服务集,所有操作都建立在会话之上。完整顺序是:创建安全通道(OpenSecureChannel)、创建会话(CreateSession)、激活会话(ActivateSession),之后才能调用 Read、Write、Browse、CreateSubscription 等。很多人第一次调试时直接把连接当成“能 ping 通就行”,结果卡在服务层。连接成功不一定会话激活成功,证书错误时连接线程返回正常,但第一个 Read 请求会得到 BadSecurityChecksFailed。

订阅是 OPC UA 的关键能力,客户端订阅变量后,服务端按发布间隔检查数据变化,一旦超过阈值就主动推送。轮询模式下,100 个变量每秒读一次会产生 100 个请求;订阅模式下,只要数据没变化,网络占用几乎为零。在实际工厂项目中,模拟量采集要配合死区设置,属性中的Deadband可以过滤微小波动,否则一个 10ms 发布周期内,PID 调节输出频繁抖动会让订阅队列瞬间填满。

下面是一段 OPC UA .NET Standard 客户端创建订阅的代码:

var subscription = new Subscription(parent: session, publishingInterval: 100, lifetimeCount: 20, keepAliveCount: 10); var monitorItem = new MonitoredItem(subscription.DefaultItem); monitorItem.StartNodeId = new NodeId("PLC_DB1_Value1", 2); monitorItem.AttributeId = Attributes.Value; monitorItem.Notification += OnDataChange; subscription.AddItem(monitorItem); subscription.Create();

publishingInterval决定服务端每隔多少毫秒推送一次数据,值太大会让趋势曲线呈阶梯状,太小会占用大量 CPU;lifetimeCount必须大于keepAliveCount,否则订阅会因为发送超时被服务端误杀。OnDataChange回调里能拿到变量的ValueStatusCode,可用于判断数据是否有效。注意,StartNodeId用的字符串是 BrowseName 而非 NodeId 的实际值,在很多库中这种写法会隐式解析,容易让人误会。

2.3 传输层:opc.tcp 二进制协议与安全通道

OPC UA 支持opc.tcp(二进制)、HTTPS 和 WebSocket 等传输方式,默认端口 4840。二进制协议在 PLC 与 PC 这种内网低时延场景下优先选择,因为消息头小,解析开销低。通信前客户端会发送 Hello 消息协商缓冲区大小,然后通过 OpenSecureChannel 建立安全通道,后续服务调用都在这个通道内进行。通道有超时时间,工厂里经常出现设备长时间不通、通道被防火墙回收,客户端卡死。

安全策略决定加密算法和强度。None 表示不加密,Basic256Sha256 表示用 AES 和 SHA-256。实例源码的安全配置通常在配置文件或服务端启动参数中指定。如果没有配置证书,建议先从 None 开始跑通功能,再升级到加密。服务端会校验客户端证书,如果没有互相信任,ActivateSession 会失败,报错BadCertificateUntrusted。很多现场问题都出在证书信任链上,而不是协议理解上。

3. 实例源码结构拆解:服务器端怎么建,客户端怎么连

3.1 源码目录与关键文件划分

拿到实例源码,先看目录结构,不要急着编译。一般会分成server/client/config/certs/。服务器代码负责把 PLC 的数据暴露成 OPC UA 节点,客户端代码负责连接、监控和写入。下表列出常见文件及职责:

文件/目录作用
server.c / server.cpp创建 UA_Server,注册变量节点,维护 PLC 数据读写回调
client.c / client.cpp连接服务端,浏览节点,读取、写入、订阅变量
config.xml 或 conf.ini端点 URL、安全策略、证书路径、发布间隔等运行参数
certs/客户端/服务端证书及信任列表
plc_bridge.c可选协议桥接模块,连西门子或三菱 PLC 并同步到 UA 节点

理解这个结构后,你会发现 OPC UA 核心和具体 PLC 品牌解耦了。服务器端负责把 PLC 的数据模型变成 UA 地址空间,具体走 S7 协议还是 MC 协议,都在plc_bridge.c的线程里做轮询或驱动回调。这样做的好处是以后换 PLC 型号只改桥接层,上位机无需改动。

编译方面,基于 open62541 的工程通常用 CMake。库有单文件版和源码版,单文件版open62541.copen62541.h用起来最省事,适合初学。如果要上生产,建议编译动态库,并开启UA_ENABLE_SUBSCRIPTIONSUA_ENABLE_ENCRYPTION选项。实例源码里如果没有 CMakeLists,可以用下面的最小 CMake 工程跑通:

cmake_minimum_required(VERSION 3.10) project(opcua_plc_demo) find_package(open62541 REQUIRED) add_executable(server server.c) target_link_libraries(server open62541) add_executable(client client.c) target_link_libraries(client open62541)

这里假设 open62541 已经安装。如果没有安装,把源码文件直接加入add_executable也可以。注意 Windows 下需要额外链接ws2_32crypt32,否则会出现unresolved external symbol。Linux 下一般不需要。

3.2 服务器端实现:地址空间、数据回调和生命周期

服务端启动后,会注册一个周期性回调,用来刷新 PLC 数据到 UA 节点。用 open62541 时,先设置默认配置,然后添加变量节点,最后UA_Server_run循环运行。常见做法是单独线程读 PLC 的 DB 区,把值写入 UA 节点属性;写操作则通过回调函数反向同步到 PLC。如果只有一个线程,读 PLC 的耗时操作会阻塞 UA 服务,导致所有客户端卡住。

下面代码展示了初始化服务器并启动:

UA_ServerConfig *config = UA_ServerConfig_new_default(); UA_Server *server = UA_Server_new(config); addPldVariable(server); UA_Server_run(server, &shutdownMarker);

UA_ServerConfig_new_default()会创建所有监听端点,包括opc.tcp://0.0.0.0:4840。如果要启用证书,需要手动 loadCertificate 并使用允许的证书链。匿名访问在内网快速验证时很方便,但跨网段部署要关闭,然后配置用户名密码或证书认证。UA_Server_run是阻塞式的,所以通常在独立线程中运行,主线程负责 PLC 数据采集。

数据写回 PLC 的逻辑,需要在节点写入回调里实现。容易忽略的点是,服务端默认允许匿名读写,但很多开发者没有设置最小采样间隔,导致订阅模式下 CPU 占用很高。正确的做法是对慢变量设置MinimumSamplingInterval,比如温度类 1 秒采样一次,位置类 10ms 采样一次。另外,回调函数里不要直接调用 PLC 驱动,因为 PLC 驱动可能有自己的超时逻辑,建议把写请求放入队列,由采集线程消费。

3.3 客户端实现:连接、浏览、订阅和写入

客户端的典型步骤是:解析端点 URL,创建客户端,选择安全策略,建立会话,激活会话,然后 Browse 或直接读节点。PLC 与 PC 通信的主要场景是大量读取实时数据和偶尔写入控制字,所以客户端通常会创建一个订阅和多个 MonitoredItem,而不是轮询 Read。轮询有延迟且浪费带宽,现场有几百个变量时就体现出来了。

下面是一段 open62541 客户端读取节点值的代码:

UA_Client *client = UA_Client_new(); UA_ClientConfig_setDefault(UA_Client_getConfig(client)); UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://192.168.1.10:4840"); UA_ReadRequest req; UA_ReadRequest_init(&req); req.nodesToReadSize = 1; req.nodesToRead = UA_ReadValueId_new(); req.nodesToRead[0].nodeId = UA_NODEID_NUMERIC(2, 1001); req.nodesToRead[0].attributeId = UA_ATTRIBUTEID_VALUE; UA_ReadResponse resp = UA_Client_Service_read(client, &req); UA_Variant *value = &resp.results[0].value; UA_Int32 intValue = *(UA_Int32*)value->data; UA_ReadRequest_clear(&req); UA_ReadResponse_clear(&resp); UA_Client_disconnect(client); UA_Client_delete(client);

UA_Client_connect内部会自动完成创建会话和激活会话,但不会创建订阅。如果要订阅,需要调用UA_Client_Subscriptions_newUA_Client_MonitoredItems_createDataChange,并传入回调。读取结果时要检查resp.results[0].hasValuestatusCode,否则你会拿到空值或旧值。实际源码里往往把读操作封装成ReadVariable(nodeId)函数,方便上层调用。

我一般还会在客户端里加一个“地址空间浏览”函数,连接到一台未知服务器时,先递归浏览节点树,把节点和数据类型打印出来。这个习惯能立刻发现服务器端是否漏配了节点,也能直观看到命名空间编号。如果浏览出来的节点类型和文档不一致,优先检查服务器端数据类型映射。

4. 让 PLC 和 PC 真正跑起来:配置、参数与踩坑记录

4.1 端点配置与安全策略选择

实例源码里的服务器配置是第一个要动的文件。如果 PLC 和 PC 在同一个局域网,客户端访问服务端时,端点地址不要写成localhost,要写本机 IP。服务端监听地址可以设置为 0.0.0.0,这样网关和 PC 客户端都能连上。很多开发者在这里栽跟头:明明服务端和客户端跑在同一台电脑上测试正常,部署到现场后怎么都连不上,原因就是监听地址绑定了 127.0.0.1,只允许本机访问。

安全策略选择上,没有域环境的车间建议先用 None,或 Basic256Sha256。选择加密后必须处理证书信任关系:把服务端证书添加到客户端的受信任列表中。下表给出了常见组合和错误现象:

安全策略客户端要求常见错误
None无(明文传输)
Basic256Sha256信任服务端证书BadCertificateUntrusted
客户端证书服务端信任所有客户端BadSecurityChecksFailed

调试时优先把配置文件里的日志级别改为 debug,open62541 会打印证书验证过程。如果看到error: fetching the certificate from the server failed,说明安全通道握手时证书格式有问题或证书链不完整。现场没有域环境时,可以用自签名证书,关键是把证书的 SAN(Subject Alternative Name)配置成正确的 IP,否则即使导入信任列表也可能报错。

4.2 节点 ID 规划和数据类型映射

PLC 与 OPC UA 的类型映射是另一个高发错误点。PLC 里REAL要映射成 UA 的FloatDoubleBOOL映射成Boolean,INT/DINT 映射成 Int16/Int32。如果映射不一致,UA 服务端读到的数据会是空值。常见做法是在服务器端变量定义时显式指定UA_TYPES中的对应类型,而不是让库自动推断。

除了类型,节点 ID 也要按命名空间隔离。namespace 1 放设备模型,namespace 2 放 PLC 数据点,namespace 3 放统计信息。不要让所有变量漂在 namespace 0 里,否则会和 OPC UA 内置节点混在一起,客户端浏览时不直观。节点编号可以按 PLC 数据块地址编码,比如 DB1 的第 0 变量对应ns=2;i=1000+blockIndex*100+offset。这种规则在源码注释里写清楚,后期维护会省很多时间。

变量命名也要注意。我见过不少工程把变量名起成Value1Value2,结果到现场调试根本分不清哪个是温度哪个是压力。正确做法是用设备的 tagname,比如Line1.Temperature.PV,这样客户端浏览时一目了然。如果嫌字符串太长,可以在DisplayName里写简短名称,BrowseName 保持完整路径。

4.3 常见坑:证书、订阅丢失、数据时间戳

  • 证书过期:工业现场设备常年不重启,证书到期后客户端突然连不上。需要在日历里设置维护周期,更新前把新证书提前下发。
  • 订阅静默丢失:如果lifetimeCount太小,或者网络闪断,订阅会进入BadTimeout,但客户端不一定马上报错。解决办法是周期检查订阅状态,不健康时重建订阅。
  • 时间戳问题:OPC UA 变量默认用 UTC 时间,PC 端显示时要转换成北京时间。如果服务器和 PLC 时间不同步,上报的历史数据会有负的时间差,建议对 PLC 统一做 NTP 同步。
  • 数据回写权限:很多实例只实现了服务端刷数据,写回调是空实现。测试写功能前,先确认节点属性的 AccessLevel 包含 CurrentWrite,否则客户端返回 BadNotWritable。

下面 C 代码演示设置节点可读可写并绑定写回调:

UA_NodeAttributes attr = UA_NodeAttributes_default; attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; attr.writeMask = UA_WRITEMASK_VALUE; UA_Server_setVariableNode_valueCallback(server, nodeId, writeCallback, NULL);

writeCallback收到的是 UA_Server、UA_NodeId 和 UA_Variant。常见错误是在回调里直接修改 PLC 数据,而回调线程可能与 PLC 轮询线程竞争同一地址,需要加锁或使用原子变量,否则会出现写值被轮询覆盖的情况。轮询线程每 10ms 刷新一次节点值,写回调设置的 50ms 可能被下一次刷新覆盖。

5. 一个技巧:用 UAExpert 和 Wireshark 验证通信并定位故障

5.1 用 UAExpert 快速验证服务端

UAExpert 是调试 OPC UA 最常用的客户端工具。打开后点击 Add Server,手动输入opc.tcp://PLC_IP:4840,双击服务器名称连接。如果安全策略用的是 Basic256Sha256,首次连接会弹出证书信任提示,勾选信任后才能在地址空间看到节点。地址空间树中能直接浏览变量节点,双击 Value 可以读取,修改 Write Value 并点击 Write 可以写入。

这一步能快速区分问题在服务器端还是客户端。如果 UAExpert 都连不上,说明问题在服务端监听或证书配置;如果 UAExpert 能读写,而你的自研客户端连不上,问题大概率在客户端的安全策略或节点 ID 初始化上。我一般会在现场带一个免安装版 UAExpert,配合抓包工具,排查速度会快很多。

5.2 Wireshark 抓包看握手流程

Wireshark 抓 OPC UA 特别适合定位握手层问题。启动 Wireshark 后,过滤器用tcp.port == 4840,再触发客户端连接,就能看到 HEL、OpenSecureChannel、CreateSession、ActivateSession 等消息。如果服务端没有响应,先看 TCP 端是否有 SYN/ACK;如果 TLS 握手过程出现在 OPC UA 之前,说明安全策略配置成了某个加密组合。使用 None 策略时,消息体可以二进制解析;使用加密时,只能看到消息头和密文,所以调试初期建议先用 None 跑通再升加密。

最后一个小技巧:把过滤器保存为opcua_diagnostic,定义成tcp.port == 4840 || opcua,以后每次现场排查直接套用。抓包结束后,重点看 ActivateSession 返回值,如果是 BadSecurityChecksFailed,就检查证书;如果是 BadTooManySessions,就检查连接数上限;如果是 BadTimeout,多半是防火墙或网络抖动。这样就有了一个可观测的最小通信链路。

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

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

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

立即咨询