本文档记录从零开始使用 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
- 访问 https://store.codesys.com/codesys-control/ 下载CODESYS Control Win V3(免费版含 SL 许可)
- 安装时勾选
CODESYS Control Win V3 x64(64 位软 PLC 运行时) - 同时会安装CODESYS Development System(IDE,用于编写 PLC 程序)
CODESYS Development System 既是编程 IDE,也用于下载/在线调试 PLC 程序。
2.2 安装 UaExpert
- 访问 https://www.unified-automation.com/downloads/opc-ua-clients.html
- 下载UaExpert(免费,需注册账号)
2.3 Qt + open62541 环境
Qt 客户端依赖 open62541 开源 OPC UA 协议栈。环境搭建可参考同目录下的OPCUA_Demo_完整指南.md,关键步骤:
| 组件 | 说明 |
|---|---|
| Qt 5.15.2 | GUI 框架 |
| open62541 v1.5.x | OPC UA C 协议栈,编译为libopen62541.so |
.pro文件 | 需添加LIBS += -lopen62541 |
三、CODESYS PLC 程序开发
3.1 新建工程
- 打开 CODESYS Development System
- File → New Project
- 选择Standard project
- 设备选择CODESYS Control Win V3 x64
- 编程语言选择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
- Build → Compile(编译检查语法)
- 在线登录:Online → Login
- 点击Start(F5)让 PLC 进入运行状态
- 确认 PLC 状态指示灯为绿色(RUNNING)
四、启用 OPC UA Server
停止并登出后进行配置修改,按照如图所示操作打开Opc UA服务。
允许匿名登录,否则需要配置证书,会比较麻烦。
配置完成后重新编译,并登录启动。
五、用 UaExpert 连接测试
UaExpert 是开发 Qt 客户端前的关键一步——先用它确认 NodeId 和数据类型,再写代码。
5.1 添加服务器
- 打开 UaExpert
- 点击工具栏Add Server(➕)
- 参考下图所示进行配置
5.2 连接并浏览地址空间
- 在左侧 Server 列表中双击刚添加的服务器
- 连接成功后,展开Address Space面板
- 沿路径
Objects → DeviceSet → CODESYS Control Win V3 x64 → ...逐级展开 - 找到
main下的 6 个变量节点
5.3 拖拽变量到 Data Access View
- 打开下方Data Access View标签页
- 将 6 个变量从 Address Space 拖拽到 Data Access View
- 即可实时看到变量值变化
5.4 写入变量测试
- 在 Data Access View 中选中
TargetTemp或DeviceMode - 右键 →Write…
- 输入新值,确认写入
- 观察 CODESYS 中变量是否更新
通过 UaExpert 验证 NodeId 和数据类型无误后,即可开始编写 Qt 客户端。
补充:
CODESYS 暴露的变量 NodeId 采用字符串格式,命名规则为:
|var|<设备名>.Application.<POU名>.<变量名>例如本项目的变量 NodeId 为:
| 变量 | NodeId |
|---|---|
| Counter | ns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Counter |
| Temperature | ns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Temperature |
| Pressure | ns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Pressure |
| Humidity | ns=4;s=|var|CODESYS Control Win V3 x64.Application.main.Humidity |
| TargetTemp | ns=4;s=|var|CODESYS Control Win V3 x64.Application.main.TargetTemp |
| DeviceMode | ns=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 += -lopen625416.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 按钮(浏览服务器节点结构)
- 日志区
运行流程:
- 输入 URL
opc.tcp://<PLC所在IP>:4840 - 设置 NS = 4
- 设置 BasePath =
|var|CODESYS Control Win V3 x64.Application.main. - 点击 Connect
- 点击各 Read 按钮读取,或 Read All 批量读取
- 在 Set 输入框填值,点 Write 写入 TargetTemp / DeviceMode
七、踩坑经历
以下是实际开发中遇到的问题,按出现顺序记录。
坑 1:数据类型不匹配 — 读取 ERR、写入 BadTypeMismatch
现象:Temperature、Pressure、Humidity、TargetTemp(REAL 类型)读写正常,但Counter、DeviceMode(INT 类型)读取显示 ERR,写入报BadTypeMismatch。
原因:CODESYS 的INT是IEC 61131-3 标准的 16 位整数(对应 OPC UAInt16),而 Qt 客户端代码按Int32读写。REAL 是 32 位 Float,恰好匹配,所以没问题。
解决:
- 读取:写一个
variantToInt64辅助函数,兼容 SByte / Byte / Int16 / UInt16 / Int32 / UInt32 / Int64 / UInt64 所有整数类型,统一转为qint64返回。 - 写入:写入前先读取目标节点的当前值,获取其实际 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 后缀 | 初始值 | 读/写 |
|---|---|---|---|---|---|
| Counter | INT | Int16 | Counter | 0 | 只读 |
| Temperature | REAL | Float | Temperature | 25.0 | 只读 |
| Pressure | REAL | Float | Pressure | 100.0 | 只读 |
| Humidity | REAL | Float | Humidity | 60.0 | 只读 |
| TargetTemp | REAL | Float | TargetTemp | 22.0 | 读/写 |
| DeviceMode | INT | Int16 | DeviceMode | 0 | 读/写 |
8.2 连接配置速查
| 参数 | 值 |
|---|---|
| URL | opc.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 UA | C/C++ (Qt) |
|---|---|---|---|
BOOL | 1 | Boolean | bool |
SINT | 8 有符号 | SByte | int8_t |
USINT | 8 无符号 | Byte | uint8_t |
INT | 16 有符号 | Int16 | int16_t |
UINT | 16 无符号 | UInt16 | uint16_t |
DINT | 32 有符号 | Int32 | int32_t |
UDINT | 32 无符号 | UInt32 | uint32_t |
REAL | 32 浮点 | Float | float |
LREAL | 64 浮点 | Double | double |
易错:PLC 的
INT= 16 位,不是 C 的int(通常 32 位)!