CODESYS + UaExpert + Qt OPC UA 入门指南

CODESYS + UaExpert + Qt OPC UA 入门指南

本文档记录从零开始使用 CODESYS 编写 PLC 程序、通过 OPC UA 协议与 Qt 客户端通信的完整过程,包含环境配置、程序开发、连接测试以及实际踩坑经历。
主要难点在于免证书进行匿名登录(CODESYS关于这个功能藏得太深了,搞了很久T^T)

环境信息

  • PLC 软件:CODESYS Control Win V3 x64(软 PLC,运行于 Windows)
  • OPC UA 客户端工具:UaExpert(Unified Automation)
  • 开发环境:Qt 5.15.2 + open62541(CentOS 7 虚拟机 / Windows 均可)
  • OPC UA 端口:4840

目录

  • 一、整体架构
  • 二、环境准备
  • 三、CODESYS PLC 程序开发
  • 四、启用 OPC UA Server
  • 五、用 UaExpert 连接测试
  • 六、Qt 客户端开发
  • 七、踩坑经历
  • 八、附录

一、整体架构

┌─────────────────────┐ OPC UA/TCP ┌─────────────────────┐ │ CODESYS Control │ ◄──────────────► │ Qt OPC UA Client │ │ Win V3 x64 │ port 4840 │ (open62541) │ │ (PLC + UA Server) │ │ │ └─────────────────────┘ └─────────────────────┘ ▲ ▲ │ OPC UA/TCP │ ▼ │ ┌─────────────────────┐ │ │ UaExpert │ ◄──────────────────────────┘ │ (调试/验证用) │ (可选,用于前期验证 NodeId) └─────────────────────┘
  • CODESYS:既是 PLC 运行时(执行梯形图逻辑),又内置 OPC UA Server(对外暴露变量)
  • UaExpert:通用 OPC UA 客户端,用于浏览节点、验证变量 NodeId,是开发 Qt 客户端前的必备调试工具
  • Qt 客户端:基于 open62541 库开发的 GUI 程序,连接 CODESYS 读写变量

二、环境准备

2.1 安装 CODESYS

  1. 访问 https://store.codesys.com/codesys-control/ 下载CODESYS Control Win V3(免费版含 SL 许可)
  2. 安装时勾选CODESYS Control Win V3 x64(64 位软 PLC 运行时)
  3. 同时会安装CODESYS Development System(IDE,用于编写 PLC 程序)

CODESYS Development System 既是编程 IDE,也用于下载/在线调试 PLC 程序。

2.2 安装 UaExpert

  1. 访问 https://www.unified-automation.com/downloads/opc-ua-clients.html
  2. 下载UaExpert(免费,需注册账号)

2.3 Qt + open62541 环境

Qt 客户端依赖 open62541 开源 OPC UA 协议栈。环境搭建可参考同目录下的OPCUA_Demo_完整指南.md,关键步骤:

组件说明
Qt 5.15.2GUI 框架
open62541 v1.5.xOPC UA C 协议栈,编译为libopen62541.so
.pro文件需添加LIBS += -lopen62541

三、CODESYS PLC 程序开发

3.1 新建工程

  1. 打开 CODESYS Development System
  2. File → New Project
  3. 选择Standard project
  4. 设备选择CODESYS Control Win V3 x64
  5. 编程语言选择Ladder Diagram (LD)(或其他熟悉的语言)

3.2 定义变量

main程序(PRG)中声明 6 个变量:

PROGRAM main VAR Counter : INT := 0; // 计数器(每周期 +1) Temperature : REAL := 25.0; // 温度(°C) Pressure : REAL := 100.0; // 压力(kPa) Humidity : REAL := 60.0; // 湿度(%) TargetTemp : REAL := 22.0; // 目标温度(可写入) DeviceMode : INT := 0; // 设备模式(可写入) END_VAR

⚠️注意 IEC 61131-3 数据类型与 OPC UA 类型的对应关系(这是后续踩坑的根源):

IEC 61131-3 类型位宽对应 OPC UA 类型
INT16 位有符号Int16
DINT32 位有符号Int32
REAL32 位浮点Float
LREAL64 位浮点Double

INT在 IEC 61131-3 中是 16 位,不是 C 语言的 32 位!这点极易搞混。

3.3 编写梯形图逻辑

用梯形图(LD)实现控制逻辑:Counter 每个扫描周期递增,Temperature/Pressure/Humidity 模拟波动,根据 DeviceMode 和 TargetTemp 进行简单控制。

3.4 编译并下载到 PLC

  1. Build → Compile(编译检查语法)
  2. 在线登录:Online → Login
  3. 点击Start(F5)让 PLC 进入运行状态
  4. 确认 PLC 状态指示灯为绿色(RUNNING)


四、启用 OPC UA Server

停止并登出后进行配置修改,按照如图所示操作打开Opc UA服务。

允许匿名登录,否则需要配置证书,会比较麻烦。

配置完成后重新编译,并登录启动。


五、用 UaExpert 连接测试

UaExpert 是开发 Qt 客户端前的关键一步——先用它确认 NodeId 和数据类型,再写代码

5.1 添加服务器

  1. 打开 UaExpert
  2. 点击工具栏Add Server(➕)
  3. 参考下图所示进行配置

5.2 连接并浏览地址空间

  1. 在左侧 Server 列表中双击刚添加的服务器
  2. 连接成功后,展开Address Space面板
  3. 沿路径Objects → DeviceSet → CODESYS Control Win V3 x64 → ...逐级展开
  4. 找到main下的 6 个变量节点

5.3 拖拽变量到 Data Access View

  1. 打开下方Data Access View标签页
  2. 将 6 个变量从 Address Space 拖拽到 Data Access View
  3. 即可实时看到变量值变化

5.4 写入变量测试

  1. 在 Data Access View 中选中TargetTempDeviceMode
  2. 右键 →Write…
  3. 输入新值,确认写入
  4. 观察 CODESYS 中变量是否更新

通过 UaExpert 验证 NodeId 和数据类型无误后,即可开始编写 Qt 客户端。

补充:

CODESYS 暴露的变量 NodeId 采用字符串格式,命名规则为:

|var|<设备名>.Application.<POU名>.<变量名>

例如本项目的变量 NodeId 为:

变量NodeId
Counterns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Counter
Temperaturens=4;s=|var|CODESYS Control Win V3 x64.Application.main.Temperature
Pressurens=4;s=|var|CODESYS Control Win V3 x64.Application.main.Pressure
Humidityns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Humidity
TargetTempns=4;s=|var|CODESYS Control Win V3 x64.Application.main.TargetTemp
DeviceModens=4;s=|var|CODESYS Control Win V3 x64.Application.main.DeviceMode
  • ns=4是 CODESYS 变量命名空间
  • 公共前缀(BasePath)为|var|CODESYS Control Win V3 x64.Application.main.

六、Qt 客户端开发

6.1 项目配置

.pro文件关键配置:

QT += core gui widgets CONFIG += c++11 LIBS += -lopen62541

6.2 连接服务器

m_client=UA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(m_client));QByteArray urlBa=url.toUtf8();UA_StatusCode ret=UA_Client_connect(m_client,urlBa.constData());// ret == UA_STATUSCODE_GOOD 表示连接成功

6.3 读取变量(字符串 NodeId)

CODESYS 变量用字符串 NodeId,需用UA_NODEID_STRING

QByteArray ba=nodeIdStr.toUtf8();UA_NodeId nodeId=UA_NODEID_STRING(ns,ba.data());UA_Variant value;UA_Variant_init(&value);UA_StatusCode ret=UA_Client_readValueAttribute(m_client,nodeId,&value);if(ret==UA_STATUSCODE_GOOD&&UA_Variant_hasScalarType(&value,&UA_TYPES[UA_TYPES_FLOAT])){UA_Float v=*(UA_Float*)value.data;// 使用 v ...}UA_Variant_clear(&value);

6.4 写入变量

写入前需确认目标数据类型,避免类型不匹配:

// 先读取确定类型,再按实际类型写入UA_Variant cur;UA_Client_readValueAttribute(m_client,nodeId,&cur);// 根据 cur.type 选择对应的 setScalarCopy ...UA_Variant_clear(&cur);

6.5 界面与运行

客户端界面包含:

  • 连接配置区(URL、命名空间、BasePath)
  • 自动变量读取区(Counter / Temperature / Pressure / Humidity)
  • 手动变量读写区(TargetTemp / DeviceMode)
  • Browse Address Space 按钮(浏览服务器节点结构)
  • 日志区

运行流程:

  1. 输入 URLopc.tcp://<PLC所在IP>:4840
  2. 设置 NS = 4
  3. 设置 BasePath =|var|CODESYS Control Win V3 x64.Application.main.
  4. 点击 Connect
  5. 点击各 Read 按钮读取,或 Read All 批量读取
  6. 在 Set 输入框填值,点 Write 写入 TargetTemp / DeviceMode

七、踩坑经历

以下是实际开发中遇到的问题,按出现顺序记录。

坑 1:数据类型不匹配 — 读取 ERR、写入 BadTypeMismatch

现象TemperaturePressureHumidityTargetTemp(REAL 类型)读写正常,但CounterDeviceMode(INT 类型)读取显示 ERR,写入报BadTypeMismatch

原因:CODESYS 的INTIEC 61131-3 标准的 16 位整数(对应 OPC UAInt16),而 Qt 客户端代码按Int32读写。REAL 是 32 位 Float,恰好匹配,所以没问题。

解决

  1. 读取:写一个variantToInt64辅助函数,兼容 SByte / Byte / Int16 / UInt16 / Int32 / UInt32 / Int64 / UInt64 所有整数类型,统一转为qint64返回。
  2. 写入:写入前先读取目标节点的当前值,获取其实际 OPC UA 数据类型,再用UA_Variant_setScalarCopy按实际类型写入。
// 读取时兼容所有整数类型staticboolvariantToInt64(constUA_Variant*value,qint64*out){constUA_DataType*t=value->type;if(t==&UA_TYPES[UA_TYPES_INT16]){*out=*(UA_Int16*)value->data;returntrue;}if(t==&UA_TYPES[UA_TYPES_UINT16]){*out=*(UA_UInt16*)value->data;returntrue;}if(t==&UA_TYPES[UA_TYPES_INT32]){*out=*(UA_Int32*)value->data;returntrue;}// ... 其他类型returnfalse;}

教训永远不要假设 PLC 变量的数据类型。IEC 61131-3 的INT≠ C 的int。用 UaExpert 先确认每个变量的实际 OPC UA 类型。


坑 2:浏览地址空间崩溃 —free(): invalid pointer

现象:点击 Browse Address Space 后,浏览到第一个字符串 NodeId 的节点时崩溃,控制台报:

*** Error in '...': free(): invalid pointer: 0x0000000002359828 ***

原因UA_NODEID_STRING宏是浅拷贝nodeId.identifier.string.data指向QByteArray的内部缓冲区(Qt 管理的内存)。当UA_BrowseRequest_clear清理请求时,调用UA_free()试图释放这个指针——但它是 Qt 分配的内存,不是 UA 分配器分配的,导致free()崩溃。

关键点:前 6 个被浏览的节点都是数字 NodeId(如ns=0;i=85),不涉及字符串指针,所以不崩溃。第 7 个节点ns=4;s=|plc|CODESYS Control Win V3 x64是第一个字符串 NodeId,一清理就炸。

解决:字符串 NodeId 必须用UA_STRING_ALLOC分配 UA 堆内存,让UA_BrowseRequest_clear能正确释放:

// ❌ 错误:指向 Qt 内存,clear 时崩溃QByteArray strBa=cur.strId.toUtf8();nodeId=UA_NODEID_STRING(cur.ns,strBa.data());// ✅ 正确:用 UA 分配器复制字符串QByteArray strBa=cur.strId.toUtf8();UA_String uaStr=UA_STRING_ALLOC(strBa.constData());nodeId=UA_NODEID_STRING(cur.ns,(char*)uaStr.data);// uaStr 所有权交给 nodeId,由 UA_BrowseRequest_clear 统一释放

教训:open62541 中凡是涉及字符串/字节串的 NodeId,要注意内存所有权。UA_NODEID_STRING是浅拷贝,UA_*_clear会尝试free字符串指针——必须确保该指针指向 UA 分配器分配的内存。


坑 3:编译错误 —const char*char*的转换

现象:编译报错:

error: invalid conversion from 'const char*' to 'char*' [-fpermissive]

原因UA_NODEID_STRING宏的第二个参数类型是char*,而QByteArray::constData()返回const char*,类型不匹配。

解决:改用QByteArray::data(),它返回非 const 的char*

// ❌ constData() 返回 const char*nodeId=UA_NODEID_STRING(ns,strBa.constData());// ✅ data() 返回 char*nodeId=UA_NODEID_STRING(ns,strBa.data());

教训:Qt 的QByteArray提供constData()(const)和data()(非 const)两个版本,传给要求char*的 C 接口时用data()


坑 4:程序崩溃后 UI 日志丢失

现象:程序崩溃后,界面上的日志区被清空,无法分析崩溃原因。

原因:日志只写到了QTextEdit控件(内存中),崩溃时进程异常终止,控件内容来不及保存。

解决:调试阶段用宏同时输出到 UI 日志和控制台qDebug(),控制台输出在崩溃后依然保留:

#defineBROWSE_LOG(msg)do{log(msg);qDebug().noquote()<<msg;}while(0)

正式调试完成后,已移除 qDebug 双输出,恢复为简洁的log()调用。

教训:调试崩溃问题时,日志必须输出到进程外(控制台/文件),不能只依赖进程内的 UI 控件。


坑 5:递归浏览导致栈溢出风险

现象:早期版本用递归实现地址空间浏览,节点层级深时可能栈溢出崩溃。

解决:改用BFS 迭代QQueue队列),并设置最大深度(MAX_DEPTH=3)和最大节点数(MAX_NODES=300)双保险,避免无限递归。

教训:地址空间浏览优先用迭代而非递归,PLC 服务器的节点树可能很深。


八、附录

8.1 变量清单

变量名IEC 类型OPC UA 类型NodeId 后缀初始值读/写
CounterINTInt16Counter0只读
TemperatureREALFloatTemperature25.0只读
PressureREALFloatPressure100.0只读
HumidityREALFloatHumidity60.0只读
TargetTempREALFloatTargetTemp22.0读/写
DeviceModeINTInt16DeviceMode0读/写

8.2 连接配置速查

参数
URLopc.tcp://<PLC_IP>:4840
Namespace (NS)4
BasePath|var|CODESYS Control Win V3 x64.Application.main.

8.3 常用 OPC UA 状态码

状态码含义常见原因
Good成功
BadTypeMismatch类型不匹配写入的数据类型与节点定义不符
BadNodeIdUnknown节点不存在NodeId 字符串拼写错误
BadConnectionClosed连接已关闭服务器断开或网络中断
BadTimeout超时网络不通或服务器无响应

8.4 IEC 61131-3 与 OPC UA / C 类型对照

IEC 61131-3位宽OPC UAC/C++ (Qt)
BOOL1Booleanbool
SINT8 有符号SByteint8_t
USINT8 无符号Byteuint8_t
INT16 有符号Int16int16_t
UINT16 无符号UInt16uint16_t
DINT32 有符号Int32int32_t
UDINT32 无符号UInt32uint32_t
REAL32 浮点Floatfloat
LREAL64 浮点Doubledouble

易错:PLC 的INT= 16 位,不是 C 的int(通常 32 位)!