IEC 61131-3(ST)与C/C++混合调用
IEC 61131-3(ST)程序可以调用 C/C++ 用户库中的函数或功能块,C/C++ 用户库也可以通过 IEC 符号接口访问 ST 侧定义的全局变量、结构体、程序实例和功能块实例。
本文以一个混合调用工程为例,说明如何搭建 ST -> C/C++ 功能块 -> IEC 符号/IEC 功能块 的调用链,并说明接口更新、构建、调试和常见问题处理方式。
适用场景
| 场景 | 说明 |
|---|---|
| ST 调用 C/C++ 算法 | 在 ST 主程序中调用 C/C++ 用户库函数或功能块 |
| C/C++ 读取 PLC 数据 | 在 C/C++ 中读取 GVL、结构体或程序实例数据 |
| C/C++ 回调 IEC POU | 在 C/C++ 中调用 ST 实现的功能块或程序入口 |
| 跨语言调试 | 在 ST 与 C/C++ 源码之间设置断点并查看调用栈 |
混合调用适合封装清晰的算法或底层逻辑。建议将跨语言访问集中在少量 C/C++ 功能块中,避免在多个模块中分散读写同一批 IEC 符号。
工程组成
混合调用工程通常包含 IEC 程序、IEC 类型定义、C/C++ 用户库和自动生成的 IEC 符号接口。
示例工程结构如下:
| 目录或文件 | 作用 |
|---|---|
PROGRAM/ | IEC 程序、功能块和数据类型定义 |
MODULES/clib1/ | C/C++ 用户库,示例中提供 Cfb1 功能块 |
MODULES/clib2/ | C/C++ 用户库,示例中提供 FB_CALLER 功能块 |
MODULES/iec_interface.h | IDE 自动生成的 IEC 符号映射头文件 |
CONFIG/tasks/ | 任务配置,决定 ST 主程序的周期执行方式 |
推荐操作顺序
混合调用涉及 ST、C/C++ 用户库、全局变量和自动生成头文件。建议先把参与调用的 POU 和变量准备完整,再编写 ST 主程序调用语句。这样可以避免在程序中写完调用后,因为类型、接口或实例缺失而反复修改。
推荐顺序如下:
- 创建或确认 C/C++ 用户库。
- 在 C/C++ 用户库中创建需要导出的函数或功能块 POU,并选择实现语言。
- 创建 ST 侧需要被 C/C++ 回调的 IEC POU。
- 创建 GVL 全局变量或程序变量,用于保 存要访问的 IEC 对象和功能块实例。
- 生成 C/C++ 用户库接口代码。
- 生成或刷新
iec_interface.h。 - 在 ST 主程序中声明实例并编写调用语句。
- 在 C/C++ 实现代码中引用
iec_interface.h并访问 IEC 符号。 - 编译 C/C++ 用户库和 PLC 工程。
- 连接目标 PLC 或模拟器进行调试验证。
示例中涉及的 POU 和变量如下,用户可以按自己的业务名称替换:
| 对象 | 类型 | 实现语言 | 用途 |
|---|---|---|---|
DEMO_PLC_PRG1 | PROGRAM | ST | 周期任务入口,负责调用 C/C++ 功能块 |
FB_CALLER | FUNCTION_BLOCK | C++ | 被 ST 调用,并在内部访问 IEC 符号 |
ST_POU1 | FUNCTION_BLOCK | ST | 被 C/C++ 通过 IEC 调用入口回调 |
Cfb1 | FUNCTION_BLOCK | C 或 C++ | 被另一个 C/C++ 功能块调用 |
GVL_var3 | 全局变量 | IEC | 示例中类型为 ST_POU1,供 C/C++ 访问和调用 |
GVL_var5 | 全局变量 | IEC | 示例中类型为 Cfb1,供 C/C++ 调用另一个 C/C++ FB |
先准备 POU 和全局变量,再写 ST 主程序调用,可以让编辑器补全、接口生成和编译检查更早发现问题。
调用链说明
典型执行链如下:
- 周期任务调度 ST 主程序。
- ST 主程序更新变量,并调用 C/C++ 功能块实例。
- C/C++ 功能块通过
iec_interface.h访问 IEC 全局变量或结构体。 - C/C++ 功能块调用 IEC 功能块入口,或调用其他 C/C++ 用户库功能块。
- 本周期执行结束,等待下一次任务调度。
示例调用链:
MainTask
└── DEMO_PLC_PRG1 ST 主程序
└── callerFB(...) C/C++ 功能块 FB_CALLER
├── 读写 GVL_var3
├── 调用 ST_POU1(&GVL_var3)
└── 调用 Cfb1(&GVL_var5.data, GVL_var5.inst_ptr)
步骤1:准备POU和变量
在编写 ST 主程序调用语句之前,建议先准备以下对象:
| 对象 | 示例 | 说明 |
|---|---|---|
| 主程序 | DEMO_PLC_PRG1 | 挂载到周期任务中执行 |
| C/C++ 功能块 POU | FB_CALLER | 选择 C++ 或 C 实现,作为 ST 调用入口 |
| C/C++ 功能块实例 | callerFB : FB_CALLER | 在 ST 主程序变量区声明,通过实例调用 |
| IEC 功能块 | ST_POU1 | 使用 ST 实现,可被 C/C++ 回调 |
| IEC 全局变量 | GVL_var3、GVL_var5 | C/C++ 可通过 iec_interface.h 访问 |
| 结构体或自定义类型 | STRUCT1 | C/C++ 会看到对应的 C 结构体布局 |
准备完成后,再在 ST 主程序中调用 C/C++ 功能块:
NewVar1 := NewVar1 + 1;
callerFB(var1 := NewVar1, var2 => NewVar2);
其中:
| 代码 | 说明 |
|---|---|
callerFB | C/C++ 功能块实例 |
var1 := NewVar1 | 输入参数传入 C/C++ 功能块 |
var2 => NewVar2 | 输出参数从 C/C++ 功能块返回到 ST 变量 |
步骤2:准备C/C++用户库接口
在 C/C++ 用户库中创建供 ST 调用的函数或功能块。示例中 clib2 提供 FB_CALLER 功能块,ST 主程序通过 callerFB(...) 调用它。
C/C++ 用户库生成后,通常会包含以 下文件:
| 文件 | 说明 |
|---|---|
.MODULE/cpp/wa_interface.h | 当前 C/C++ 用户库自动生成的接口声明 |
.MODULE/cpp/wa_interface.cpp | 当前 C/C++ 用户库自动生成的入口桥接实现 |
implements/cpp/*.cpp | 用户编写的 C/C++ 实现代码 |
implements/cpp/*.h | 用户编写的 C/C++ 类或函数声明 |
如果修改了 C/C++ 用户库的函数或功能块接口,需要执行 生成接口代码,再重新编译用户库。
步骤3:生成IEC符号接口
iec_interface.h 是 C/C++ 访问 IEC 对象的关键文件,通常位于 MODULES/iec_interface.h。它由 WasomeCodeX_AI 根据当前工程中的 IEC 程序、功能块、类型和全局变量自动生成。
建议在以下场景中重新生成 IEC 接口头文件:
| 场景 | 为什么需要重新生成 |
|---|---|
| 新增或删除 GVL 变量 | C/C++ 侧需要同步全局变量声明 |
| 修改结构体字段 | C/C++ 侧结构体布局需要同步 |
| 修改 IEC 功能块接口 | C/C++ 调用入口参数结构需要同步 |
| 新增或删除 IEC POU | C/C++ 侧需要同步 POU 调用入口 |
| 新增或删除 C/C++ 用户库 POU | IEC 符号镜像中可能需要同步用户库实例包装类型 |
iec_interface.h 中常见内容如下:
| 内容 | 示例 | 说明 |
|---|---|---|
| IEC 程序布局 | DEMO_PLC_PRG1_PROGRAM | 映射 ST 主程序的数据区 |
| IEC 功能块布局 | ST_POU1_FUNCTION_BLOCK | 映射 ST 功能块实例数据 |
| 用户库实例包装 | Cfb1_Inst | 包含 inst_ptr 和用户库数据区 |
| 全局变量声明 | extern ST_POU1_FUNCTION_BLOCK GVL_var3 | C/C++ 可直接引用的 IEC 全局变量 |
| POU 调用入口 | extern void ST_POU1(...) | C/C++ 调用 IEC POU 的函数原型 |
iec_interface.h 是自动生成文件,不建议手动修改。手动修改在重新生成后可能丢失,也容易造成 C/C++ 侧结构体布局与 IEC 侧不一致。
步骤4:在C/C++中包含接口头文件
在需要访问 IEC 符号的 C/C++ 实现文件中,需要同时包含当前用户库接口和 IEC 符号接口:
#include ".MODULE/cpp/wa_interface.h"
#include "iec_interface.h"
#include "FB_CALLER.h"
包含关系说明:
| 头文件 | 用途 |
|---|---|
.MODULE/cpp/wa_interface.h | 当前 C/C++ 用户库的函数、功能块数据结构和基类声明 |
iec_interface.h | IEC 全局变量、IEC 类型、IEC POU 调用入口声明 |
| 用户头文件 | 当前功能块类或函数实现所需的用户声明 |
如果编译 时报找不到 iec_interface.h,先确认该文件已经生成,再确认当前 C/C++ 用户库构建包含了工程级 MODULES 目录的头文件搜索路径。
步骤5:C/C++访问IEC全局变量和功能块
C/C++ 可以通过 iec_interface.h 中的声明直接访问 IEC 全局变量,也可以调用 IEC 功能块执行入口。
访问全局变量
在 C/C++ 编辑器中访问 IEC 全局变量时,语法通常为:
全局变量名.成员名
例如输入全局变量名 GVL_var3 后,再输入 .,编辑器可根据 iec_interface.h 中的结构体定义提示成员:
GVL_var3.NewVar = true;
如果访问的是 IEC 程序实例,可以在编辑器中输入 IEC_PROGRAM 触发补全,选择对应的程序实例,再通过 . 访问程序变量:
IEC_PROGRAM_DEMO_PLC_PRG1.NewVar1 = 1;
代码含义:
| 代码 | 说明 |
|---|---|
GVL_var3.NewVar | 访问 IEC 全局变量中的字段 |
IEC_PROGRAM_DEMO_PLC_PRG1.NewVar1 | 访问 IEC 程序实例中的变量 |
调用IEC功能块
调用 IEC 功能块时,使用 iec_interface.h 中生成的 POU 调用入口,并传入对应的数据区。例如 GVL_var3 的类型为 ST_POU1_FUNCTION_BLOCK,可以作为 ST_POU1 的调用参数:
ST_POU1(&GVL_var3);
在 C/C++ 功能块中组合访问和调用的示例:
void FB_CALLER::call(FB_CALLER_Data *data) {
GVL_var3.NewVar = !GVL_var3.NewVar;
ST_POU1(&GVL_var3);
}