跳到主要内容

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 符号接口。

IEC 61131-3与C/C++混合调用工程结构

示例工程结构如下:

目录或文件作用
PROGRAM/IEC 程序、功能块和数据类型定义
MODULES/clib1/C/C++ 用户库,示例中提供 Cfb1 功能块
MODULES/clib2/C/C++ 用户库,示例中提供 FB_CALLER 功能块
MODULES/iec_interface.hIDE 自动生成的 IEC 符号映射头文件
CONFIG/tasks/任务配置,决定 ST 主程序的周期执行方式

推荐操作顺序

混合调用涉及 ST、C/C++ 用户库、全局变量和自动生成头文件。建议先把参与调用的 POU 和变量准备完整,再编写 ST 主程序调用语句。这样可以避免在程序中写完调用后,因为类型、接口或实例缺失而反复修改。

推荐顺序如下:

  1. 创建或确认 C/C++ 用户库。
  2. 在 C/C++ 用户库中创建需要导出的函数或功能块 POU,并选择实现语言。
  3. 创建 ST 侧需要被 C/C++ 回调的 IEC POU。
  4. 创建 GVL 全局变量或程序变量,用于保存要访问的 IEC 对象和功能块实例。
  5. 生成 C/C++ 用户库接口代码。
  6. 生成或刷新 iec_interface.h
  7. 在 ST 主程序中声明实例并编写调用语句。
  8. 在 C/C++ 实现代码中引用 iec_interface.h 并访问 IEC 符号。
  9. 编译 C/C++ 用户库和 PLC 工程。
  10. 连接目标 PLC 或模拟器进行调试验证。

示例中涉及的 POU 和变量如下,用户可以按自己的业务名称替换:

对象类型实现语言用途
DEMO_PLC_PRG1PROGRAMST周期任务入口,负责调用 C/C++ 功能块
FB_CALLERFUNCTION_BLOCKC++被 ST 调用,并在内部访问 IEC 符号
ST_POU1FUNCTION_BLOCKST被 C/C++ 通过 IEC 调用入口回调
Cfb1FUNCTION_BLOCKC 或 C++被另一个 C/C++ 功能块调用
GVL_var3全局变量IEC示例中类型为 ST_POU1,供 C/C++ 访问和调用
GVL_var5全局变量IEC示例中类型为 Cfb1,供 C/C++ 调用另一个 C/C++ FB
提示

先准备 POU 和全局变量,再写 ST 主程序调用,可以让编辑器补全、接口生成和编译检查更早发现问题。

调用链说明

典型执行链如下:

  1. 周期任务调度 ST 主程序。
  2. ST 主程序更新变量,并调用 C/C++ 功能块实例。
  3. C/C++ 功能块通过 iec_interface.h 访问 IEC 全局变量或结构体。
  4. C/C++ 功能块调用 IEC 功能块入口,或调用其他 C/C++ 用户库功能块。
  5. 本周期执行结束,等待下一次任务调度。

示例调用链:

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++ 功能块 POUFB_CALLER选择 C++ 或 C 实现,作为 ST 调用入口
C/C++ 功能块实例callerFB : FB_CALLER在 ST 主程序变量区声明,通过实例调用
IEC 功能块ST_POU1使用 ST 实现,可被 C/C++ 回调
IEC 全局变量GVL_var3GVL_var5C/C++ 可通过 iec_interface.h 访问
结构体或自定义类型STRUCT1C/C++ 会看到对应的 C 结构体布局

准备完成后,再在 ST 主程序中调用 C/C++ 功能块:

NewVar1 := NewVar1 + 1;
callerFB(var1 := NewVar1, var2 => NewVar2);

其中:

代码说明
callerFBC/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接口头文件

建议在以下场景中重新生成 IEC 接口头文件:

场景为什么需要重新生成
新增或删除 GVL 变量C/C++ 侧需要同步全局变量声明
修改结构体字段C/C++ 侧结构体布局需要同步
修改 IEC 功能块接口C/C++ 调用入口参数结构需要同步
新增或删除 IEC POUC/C++ 侧需要同步 POU 调用入口
新增或删除 C/C++ 用户库 POUIEC 符号镜像中可能需要同步用户库实例包装类型

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_var3C/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.hIEC 全局变量、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);
}

访问 IEC 符号时应注意:

注意项说明
类型要匹配C/C++ 侧字段类型由 IEC 类型生成,不应自行假设内存布局
修改接口后要重新生成ST 侧类型或变量变化后,需要更新 iec_interface.h
不要缓存失效指针功能块实例和全局变量应通过生成接口访问
注意周期执行C/C++ 对全局变量的修改会进入当前任务周期的执行链

步骤6:C/C++调用另一个C/C++功能块

IEC 功能块和 C/C++ 用户库功能块的调用原型不同,需要区分。理解这一点时,可以把内容分成两类:系统生成的内容用户需要编写的内容

系统生成的内容

当工程中存在 C/C++ 功能块 Cfb1,并且在 IEC 侧声明了该功能块类型的全局变量或实例后,iec_interface.h 会生成实例包装类型。该包装类型通常包含两部分:

typedef struct Cfb1_Inst {
void *inst_ptr;
Cfb1_Data data;
} Cfb1_Inst;

其中:

生成字段说明
inst_ptrC/C++ 功能块运行时实例指针,由系统维护
dataC/C++ 功能块的数据区,类型来自用户库生成接口

如果 IEC 侧声明了一个全局变量 GVL_var5,类型为 Cfb1,则 iec_interface.h 中会生成类似声明:

extern Cfb1_Inst GVL_var5;

同时,C/C++ 用户库自己的 .MODULE/cpp/wa_interface.h 中会生成 C/C++ 功能块调用入口:

void Cfb1(Cfb1_Data *self, void *instance);

这些类型、全局变量声明和函数原型都是生成内容,用户不需要手写。

用户需要编写的内容

用户需要做的是:

  1. 在 C/C++ 用户库中创建功能块 POU,例如 Cfb1
  2. 在 IEC 侧声明该功能块类型的实例或全局变量,例如 GVL_var5
  3. 重新生成接口代码和 iec_interface.h
  4. 在 C/C++ 代码中使用生成出来的实例包装对象进行调用。

调用示例:

GVL_var5.data.var1 = 1;
Cfb1(&GVL_var5.data, GVL_var5.inst_ptr);

其中:

用户代码含义
GVL_var5.data.var1 = 1给生成的数据区字段赋值
&GVL_var5.data传入 C/C++ 功能块数据区
GVL_var5.inst_ptr传入系统维护的 C/C++ 功能块实例指针
Cfb1(...)调用生成的 C/C++ 功能块入口函数

与IEC功能块调用的区别

IEC 功能块调用入口通常只需要传入功能块数据区:

extern void ST_POU1(ST_POU1_FUNCTION_BLOCK *self);

C/C++ 用户库功能块通常需要同时传入数据区和实例指针:

void Cfb1(Cfb1_Data *self, void *instance);
注意

不要手动定义 Cfb1_InstCfb1_Datainst_ptr。这些内容由系统生成和维护。用户只需要在 IEC 侧声明实例,并在 C/C++ 中使用生成的变量名进行访问和调用。

步骤7:构建和运行

完成 ST 接口、C/C++ 用户库和 IEC 符号接口更新后,建议按以下顺序执行:

  1. 保存 ST 程序、GVL、结构体和用户库接口。
  2. 重新生成 C/C++ 用户库接口代码。
  3. 重新生成或更新 iec_interface.h
  4. 编译 C/C++ 用户库模块。
  5. 编译整个 PLC 工程。
  6. 连接目标 PLC 或模拟器并运行。

如果只修改 C/C++ 实现代码,一般重新编译对应用户库即可。如果修改了 ST 侧类型、GVL 或 POU 接口,应重新生成 iec_interface.h 后再编译。

任务周期中的执行顺序

混合调用通常发生在周期任务中。示例工程中任务为 MainTask,模式为 cycle,周期为 1ms,挂载程序为 DEMO_PLC_PRG1

每个周期的执行顺序可以理解为:

MainTask 每 1ms 触发
└── DEMO_PLC_PRG1
├── NewVar1 := NewVar1 + 1
└── callerFB(var1 := NewVar1, var2 => NewVar2)
└── FB_CALLER::call
├── 读写 GVL_var3
├── ST_POU1(&GVL_var3)
└── Cfb1(&GVL_var5.data, GVL_var5.inst_ptr)
提示

如果 C/C++ 中读写的变量会被其他任务同时访问,需要结合任务调度关系评估数据一致性。单任务周期调用链通常最容易调试和验证。

交叉源码调试

连接目标 PLC 或模拟器后,可以同时在 ST 和 C/C++ 源码中设置断点,并启动源码调试。调试时建议在以下位置设置断点:

断点位置目的
ST 主程序调用 C/C++ 功能块处确认 ST 周期任务已经执行到调用点
C/C++ 功能块 call 函数确认 ST 已进入 C/C++ 用户库
C/C++ 调用 IEC 功能块前后确认 IEC 回调入口是否被执行
IEC 功能块内部确认 C/C++ 回调后进入 ST 实现
ST和C/C++交叉源码调试

典型调用栈从上到下可能包含:

层级说明
ST_POU1使用 ST 实现的 IEC 功能块
FB_CALLER::call用户编写的 C/C++ 功能块逻辑
FB_CALLER()自动生成的 C/C++ 功能块调用入口
DEMO_PLC_PRG1周期任务挂载的 ST 主程序

常见问题

问题常见原因处理建议
C/C++ 编译提示找不到 iec_interface.hIEC 接口头文件未生成或包含路径不正确重新生成 IEC 接口头文件,并确认文件位于 MODULES/iec_interface.h
C/C++ 编译提示找不到 GVL_xxxGVL 新增后未更新接口保存 GVL 后重新生成 iec_interface.h
C/C++ 编译提示结构体字段不存在ST 侧结构体或功能块接口已修改重新生成 iec_interface.h,并按新字段名修改 C/C++ 代码
调用 C/C++ 功能块运行异常未传入正确的实例指针使用生成的实例包装对象,调用时传入 datainst_ptr
ST 断点命中但 C/C++ 断点不命中用户库未重新编译、调用实例未执行或断点位置不正确重新编译用户库,确认 ST 调用语句和任务挂载
C/C++ 修改变量后 ST 侧看不到预期值访问的变量不是同一实例,或任务周期覆盖了值检查 GVL/实例对象,结合调用顺序单步调试

检查清单

完成混合调用配置后,可以按以下清单检查:

检查项说明
ST 主程序是否挂载到任务没有任务调度,调用链不会周期执行
C/C++ 功能块实例是否在 ST 中声明ST 侧需要通过实例调用功能块
用户库接口代码是否已生成修改函数或功能块接口后需要重新生成
iec_interface.h 是否已更新修改 IEC 类型、GVL 或 POU 后需要重新生成
C/C++ 是否包含 iec_interface.h访问 IEC 符号必须包含该头文件
功能块调用原型是否正确IEC FB 与 C/C++ FB 调用参数不同
是否完成用户库和工程编译混合调用需要 C/C++ 模块和 PLC 工程都编译通过