文章目录

ST泛型编程:利用ANY类型实现ST通用算法函数

发布于 2026-03-19 00:13:31 · 浏览 4 次 · 评论 0 条

ST泛型编程的核心目标,是避免为每种数据类型重复编写逻辑相同、仅数据类型不同的函数。在IEC 61131-3标准的结构化文本(ST)中,原生不支持C++或Python式的模板语法,但通过ANY类型及其派生类型(如ANY_DERIVEDANY_ELEMENTARYANY_NUM等),可实现高度复用的通用算法函数。该机制不是语法糖,而是基于运行时类型检查与地址操作的底层能力,需严格遵循类型安全规则,否则将导致PLC运行时错误或不可预测行为。


一、理解ANY类型:ST中的“类型占位符”

ANY本身不是可声明的变量类型,而是一组类型通配符声明符,仅用于函数块(FB)、功能块(FC)或方法(METHOD)的输入/输出参数声明。其本质是告诉编译器:“此处接受任意兼容类型,具体类型由调用时传入的实际参数决定”。

IEC 61131-3标准定义了以下关键ANY类型族:

类型声明 兼容的实际类型示例 典型用途
ANY 所有类型(含数组、结构体、功能块实例) 极少直接使用,多用于底层系统函数
ANY_DERIVED 用户自定义结构体(STRUCT)、枚举(ENUM)、数组(ARRAY) 实现结构体拷贝、比较、序列化
ANY_ELEMENTARY BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, LREAL, STRING 基础数据操作(如交换、取绝对值)
ANY_NUM INT, DINT, REAL, LREAL, USINT, UINT, UDINT 数值计算(加减乘除、求最大值)
ANY_REAL REAL, LREAL 浮点专用运算(如三角函数、指数)
ANY_INT SINT, INT, DINT, USINT, UINT, UDINT 整数位运算、模运算、整除

关键约束:

  • ANY类参数不能直接读写值。例如,声明val : ANY_NUM;后,val := 5;是非法的——必须通过类型转换或地址操作访问其内容。
  • 所有ANY参数在函数内部,必须先通过ADR()获取其内存地址,再用__GET_TYPE_INFO()__GET_SIZE()获取类型元信息,最后用__MOVE()或类型强制转换(TO_XXX())进行安全访问。

二、实战:编写通用交换函数 SWAP<T>

目标:一个函数,能交换两个同类型的变量,支持INTREALSTRING[20]甚至自定义结构体MOTOR_CONFIG

步骤1:声明函数接口(FC)

FUNCTION SWAP : BOOL
VAR_INPUT
    p1 : ANY;
    p2 : ANY;
END_VAR
VAR
    size : DINT;
    tempBuffer : ARRAY[0..255] OF BYTE; (* 最大缓冲区,覆盖常见类型 *)
END_VAR

p1p2声明为ANY,允许传入任意类型实参。
❌ 不可声明为ANY_NUM——因STRINGSTRUCT不兼容ANY_NUM,会限制通用性。

步骤2:获取实际类型大小并校验一致性

// 获取p1和p2的类型大小(字节)
size := __GET_SIZE(ADR(p1));
IF size <> __GET_SIZE(ADR(p2)) THEN
    SWAP := FALSE; // 类型大小不同,拒绝执行
    EXIT;
END_IF;

// 获取p1和p2的类型代码(确保同族)
IF __GET_TYPE_INFO(ADR(p1)).dwDataType <> __GET_TYPE_INFO(ADR(p2)).dwDataType THEN
    SWAP := FALSE;
    EXIT;
END_IF;

__GET_SIZE()返回变量实际占用字节数(如INT=2,REAL=4,STRING[20]=21)。
__GET_TYPE_INFO()返回TYPE_INFO结构体,其中dwDataType是唯一类型ID(由PLC厂商分配,同一厂商下相同类型ID一致)。

步骤3:安全交换(使用__MOVE避免类型误判)

// 将p1内容复制到tempBuffer
__MOVE(
    srcAddr := ADR(p1),
    dstAddr := ADR(tempBuffer),
    size := size
);

// 将p2内容复制到p1位置
__MOVE(
    srcAddr := ADR(p2),
    dstAddr := ADR(p1),
    size := size
);

// 将tempBuffer内容复制到p2位置
__MOVE(
    srcAddr := ADR(tempBuffer),
    dstAddr := ADR(p2),
    size := size
);

SWAP := TRUE;

__MOVE是IEC 61131-3标准提供的无类型内存复制函数,等效于C语言memcpy()。它不关心数据含义,只按字节数搬运,因此完全绕过类型系统限制,是ANY编程的安全基石。

步骤4:调用示例(完全类型安全)

PROGRAM PLC_PRG
VAR
    a, b : INT := 10, 20;
    x, y : REAL := 3.14, 2.71;
    s1, s2 : STRING[10] := 'ABC', 'XYZ';
    m1, m2 : MOTOR_CONFIG; // 假设已定义STRUCT
END_VAR

// 所有调用均通过编译,且运行时零开销
SWAP(ADR(a), ADR(b));   // 交换INT
SWAP(ADR(x), ADR(y));   // 交换REAL
SWAP(ADR(s1), ADR(s2)); // 交换STRING
SWAP(ADR(m1), ADR(m2)); // 交换STRUCT

⚠️ 注意:实参必须用ADR()传递地址。直接传值(如SWAP(a, b))会导致编译错误——ANY参数只接受地址。


三、进阶:通用最大值函数 MAX<T>

目标:返回两个同类型数值中的较大者,支持INTDINTREALLREAL

关键挑战:

  • ANY_NUM无法直接比较(p1 > p2非法);
  • 必须根据运行时类型,跳转到对应比较逻辑。

解决方案:类型分发表 + 函数指针

TYPE E_DATA_TYPE : (
    TYPE_UNKNOWN := 0,
    TYPE_INT := 1,
    TYPE_DINT := 2,
    TYPE_REAL := 3,
    TYPE_LREAL := 4,
    TYPE_UINT := 5,
    TYPE_UDINT := 6
);
END_TYPE

FUNCTION MAX_ANY : ANY_NUM
VAR_INPUT
    p1 : ANY_NUM;
    p2 : ANY_NUM;
END_VAR
VAR
    typeID : DINT;
    pInfo : TYPE_INFO;
END_VAR

pInfo := __GET_TYPE_INFO(ADR(p1));
typeID := pInfo.dwDataType;

CASE typeID OF
    1: // INT → 调用MAX_INT
        MAX_ANY := MAX_INT(ADR(p1), ADR(p2));
    2: // DINT → 调用MAX_DINT
        MAX_ANY := MAX_DINT(ADR(p1), ADR(p2));
    3: // REAL → 调用MAX_REAL
        MAX_ANY := MAX_REAL(ADR(p1), ADR(p2));
    4: // LREAL → 调用MAX_LREAL
        MAX_ANY := MAX_LREAL(ADR(p1), ADR(p2));
    ELSE
        MAX_ANY := 0; // 或触发错误处理
END_CASE

每个子函数(如MAX_INT)是独立FC,参数为INT类型,内部可直接使用>比较:

FUNCTION MAX_INT : INT
VAR_INPUT
    p1, p2 : REF TO INT;
END_VAR
IF p1^ > p2^ THEN
    MAX_INT := p1^;
ELSE
    MAX_INT := p2^;
END_IF

REF TO INT是安全的类型化引用,p1^解引用后即可参与运算。
🔁 此模式可无限扩展:新增MAX_TIME只需增加CASE分支+对应FC。


四、高阶技巧:ANY_DERIVED实现结构体深拷贝

场景:将设备配置结构体从DB块复制到本地变量,要求自动处理嵌套结构、数组、字符串。

FUNCTION STRUCT_COPY : BOOL
VAR_INPUT
    src : ANY_DERIVED;
    dst : ANY_DERIVED;
END_VAR
VAR
    size : DINT;
    info : TYPE_INFO;
END_VAR

info := __GET_TYPE_INFO(ADR(src));
size := info.dwSize;

// 单次内存复制即完成整个结构体拷贝(包括所有嵌套成员)
__MOVE(
    srcAddr := ADR(src),
    dstAddr := ADR(dst),
    size := size
);
STRUCT_COPY := TRUE;

原理:结构体在内存中是连续布局(IEC 61131-3保证),__MOVE按总字节数搬运,天然支持任意深度嵌套。无需递归遍历字段——这是ANY_DERIVED相比手动逐字段赋值的最大优势。


五、避坑指南:90%开发者踩过的5个错误

  1. 误用ANY代替具体类型
    VAR myVar : ANY; → 编译失败。ANY只能用于参数。
    FUNCTION FOO : BOOL VAR_INPUT x : ANY; END_VAR

  2. 忘记ADR()导致地址错误
    SWAP(a, b); → 编译错误。
    SWAP(ADR(a), ADR(b));

  3. ANY变量直接赋值
    p1 := 100; → 编译错误。
    __MOVE(ADR(100), ADR(p1), 4);(需知p1DINT

  4. 跨厂商类型ID不兼容
    __GET_TYPE_INFO().dwDataType在不同PLC品牌间无意义,不可用于跨平台序列化。仅限同一项目内类型一致性校验。

  5. 缓冲区溢出
    tempBuffer大小必须≥所有可能类型的__GET_SIZE()结果。建议设为256字节(覆盖99%工业场景),超长结构体需动态分配(需厂商扩展支持)。


六、性能与安全边界

  • 时间开销__GET_SIZE()__GET_TYPE_INFO()为常量时间O(1),__MOVE()为O(n)(n=字节数),与手写类型函数无差异。
  • 内存安全__MOVE无边界检查。若size计算错误,将导致内存越界——务必用__GET_SIZE()而非硬编码。
  • 实时性:所有操作均为确定性执行,无动态内存分配,符合PLC硬实时要求。
  • 调试支持:主流IDE(TIA Portal、Codesys、Unity Pro)均支持ANY参数的在线监控,显示实际类型与值。

七、何时不该用ANY编程?

  • 当仅需支持2~3种固定类型时,手写重载函数更清晰;
  • 当算法依赖类型特有行为(如REALNaN判断、STRINGFIND)时,应拆分为专用函数;
  • 在资源极度受限的微型PLC(<16KB RAM)上,避免大缓冲区(如256字节tempBuffer)。

真正的泛型价值,在于降低维护成本:当新增一种传感器数据结构TEMP_SENSOR_T,只需1次调用STRUCT_COPY(ADR(db.temp), ADR(local.temp));,无需修改任何底层代码。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文