
本文档记录从零开始使用 CODESYS 编写 PLC 程序、通过 OPC UA 协议与 Qt 客户端通信的完整过程包含环境配置、程序开发、连接测试以及实际踩坑经历。主要难点在于免证书进行匿名登录CODESYS关于这个功能藏得太深了搞了很久T^T环境信息PLC 软件CODESYS Control Win V3 x64软 PLC运行于 WindowsOPC UA 客户端工具UaExpertUnified Automation开发环境Qt 5.15.2 open62541CentOS 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 x6464 位软 PLC 运行时同时会安装CODESYS Development SystemIDE用于编写 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.2GUI 框架open62541 v1.5.xOPC UA C 协议栈编译为libopen62541.so.pro文件需添加LIBS -lopen62541三、CODESYS PLC 程序开发3.1 新建工程打开 CODESYS Development SystemFile → 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 位有符号Int16DINT32 位有符号Int32REAL32 位浮点FloatLREAL64 位浮点DoubleINT在 IEC 61131-3 中是 16 位不是 C 语言的 32 位这点极易搞混。3.3 编写梯形图逻辑用梯形图LD实现控制逻辑Counter 每个扫描周期递增Temperature/Pressure/Humidity 模拟波动根据 DeviceMode 和 TargetTemp 进行简单控制。3.4 编译并下载到 PLCBuild → Compile编译检查语法在线登录Online → Login点击StartF5让 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 为变量NodeIdCounterns4;s|var|CODESYS Control Win V3 x64.Application.main.CounterTemperaturens4;s|var|CODESYS Control Win V3 x64.Application.main.TemperaturePressurens4;s|var|CODESYS Control Win V3 x64.Application.main.PressureHumidityns4;s|var|CODESYS Control Win V3 x64.Application.main.HumidityTargetTempns4;s|var|CODESYS Control Win V3 x64.Application.main.TargetTempDeviceModens4;s|var|CODESYS Control Win V3 x64.Application.main.DeviceModens4是 CODESYS 变量命名空间公共前缀BasePath为|var|CODESYS Control Win V3 x64.Application.main.六、Qt 客户端开发6.1 项目配置.pro文件关键配置QT core gui widgets CONFIG c11 LIBS -lopen625416.2 连接服务器m_clientUA_Client_new();UA_ClientConfig_setDefault(UA_Client_getConfig(m_client));QByteArray urlBaurl.toUtf8();UA_StatusCode retUA_Client_connect(m_client,urlBa.constData());// ret UA_STATUSCODE_GOOD 表示连接成功6.3 读取变量字符串 NodeIdCODESYS 变量用字符串 NodeId需用UA_NODEID_STRINGQByteArray banodeIdStr.toUtf8();UA_NodeId nodeIdUA_NODEID_STRING(ns,ba.data());UA_Variant value;UA_Variant_init(value);UA_StatusCode retUA_Client_readValueAttribute(m_client,nodeId,value);if(retUA_STATUSCODE_GOODUA_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 / DeviceModeBrowse Address Space 按钮浏览服务器节点结构日志区运行流程输入 URLopc.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、TargetTempREAL 类型读写正常但Counter、DeviceModeINT 类型读取显示 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*tvalue-type;if(tUA_TYPES[UA_TYPES_INT16]){*out*(UA_Int16*)value-data;returntrue;}if(tUA_TYPES[UA_TYPES_UINT16]){*out*(UA_UInt16*)value-data;returntrue;}if(tUA_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如ns0;i85不涉及字符串指针所以不崩溃。第 7 个节点ns4;s|plc|CODESYS Control Win V3 x64是第一个字符串 NodeId一清理就炸。解决字符串 NodeId 必须用UA_STRING_ALLOC分配 UA 堆内存让UA_BrowseRequest_clear能正确释放// ❌ 错误指向 Qt 内存clear 时崩溃QByteArray strBacur.strId.toUtf8();nodeIdUA_NODEID_STRING(cur.ns,strBa.data());// ✅ 正确用 UA 分配器复制字符串QByteArray strBacur.strId.toUtf8();UA_String uaStrUA_STRING_ALLOC(strBa.constData());nodeIdUA_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*nodeIdUA_NODEID_STRING(ns,strBa.constData());// ✅ data() 返回 char*nodeIdUA_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_DEPTH3和最大节点数MAX_NODES300双保险避免无限递归。教训地址空间浏览优先用迭代而非递归PLC 服务器的节点树可能很深。八、附录8.1 变量清单变量名IEC 类型OPC UA 类型NodeId 后缀初始值读/写CounterINTInt16Counter0只读TemperatureREALFloatTemperature25.0只读PressureREALFloatPressure100.0只读HumidityREALFloatHumidity60.0只读TargetTempREALFloatTargetTemp22.0读/写DeviceModeINTInt16DeviceMode0读/写8.2 连接配置速查参数值URLopc.tcp://PLC_IP:4840Namespace (NS)4BasePath|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)BOOL1BooleanboolSINT8 有符号SByteint8_tUSINT8 无符号Byteuint8_tINT16 有符号Int16int16_tUINT16 无符号UInt16uint16_tDINT32 有符号Int32int32_tUDINT32 无符号UInt32uint32_tREAL32 浮点FloatfloatLREAL64 浮点Doubledouble易错PLC 的INT 16 位不是 C 的int通常 32 位