文章目录

ST结构体实例化:TYPE...END_TYPE 定义与多实例数据隔离

发布于 2026-03-19 20:02:55 · 浏览 2 次 · 评论 0 条

在结构化文本(ST)编程中,TYPE...END_TYPE 是定义自定义数据类型的核心语法。它不创建数据,只声明模板;真正生成独立、可操作的数据实体,必须通过实例化完成。许多初学者混淆“类型定义”与“变量声明”,导致多个设备共用同一组内存地址,引发状态错乱、数据覆盖、调试困难等问题。本文直击痛点,用纯文字讲清 TYPE...END_TYPE 如何定义结构体、为何必须实例化、如何实现多实例间完全数据隔离,并给出可直接复用的完整示例。


一、先看一个典型故障场景:为什么“写一次,到处用”会出事?

假设你为三台电机编写统一控制逻辑,使用如下代码:

TYPE MotorCtrl:
STRUCT
    bStart: BOOL;
    bStop: BOOL;
    nSpeed: INT;
    bRunning: BOOL;
END_STRUCT
END_TYPE

VAR_GLOBAL
    g_Motor1: MotorCtrl;
    g_Motor2: MotorCtrl;
    g_Motor3: MotorCtrl;
END_VAR

表面看,g_Motor1g_Motor2g_Motor3 各自独立——但若你在程序中错误地这样写:

g_Motor1.bStart := TRUE;
g_Motor2.bStart := FALSE;
// 后续逻辑却统一读取 g_Motor1.bStart 判断所有电机启停
IF g_Motor1.bStart THEN
    // 启动所有电机
END_IF

问题就暴露了:你本意是分别控制,却因逻辑耦合,让 g_Motor1.bStart 成为全局开关。更隐蔽的错误是——未显式声明实例,而用指针或数组别名间接访问同一块内存,例如:

VAR
    aMotors: ARRAY[1..3] OF MotorCtrl;
    pActive: POINTER TO MotorCtrl;
END_VAR
pActive := ADR(aMotors[1]); // 指向第一个元素
// 后续所有操作都通过 pActive,彻底丢失实例边界

这类问题无法靠编译器报错发现,运行时才显现:按下电机2启动按钮,电机1也转;修改电机3速度,电机1速度同步跳变。根本原因只有一个:没有为每个物理对象分配专属、互不重叠的内存空间


二、TYPE...END_TYPE 的本质:它是模具,不是零件

TYPE...END_TYPE 块仅做两件事:

  1. 描述数据布局:规定字段名称、类型、顺序及对齐方式;
  2. 注册类型标识符:让编译器认识 MotorCtrl 是一个合法类型名。

不分配任何内存,也不生成变量。这就像设计一张电路板图纸(TYPE),图纸本身不会发光发热;只有把图纸交给工厂,打样出三块实物PCB(实例),它们才能各自焊接芯片、通电工作。

关键规则:

  • TYPE 必须成对出现,中间只能是 STRUCT...END_STRUCTUNION...END_UNION 或嵌套类型;
  • 类型名区分大小写,且不能与系统保留字(如 BOOLINTTIME)重复;
  • 结构体内字段名在同一层级不可重复,但不同结构体间可重名(MotorCtrl.nSpeedValveCtrl.nSpeed 互不影响)。

合法定义示例:

TYPE PumpCtrl:
STRUCT
    bEnable: BOOL;          // 使能标志
    rSetPressure: REAL;     // 设定压力(bar)
    rActPressure: REAL;     // 实际压力(bar)
    tTimeout: TIME;         // 超时时间
    sStatus: STRING[16];    // 状态描述,最大16字符
END_STRUCT
END_TYPE

注意:STRING[16] 表示固定长度字符串,占17字节(16字符+1字节终止符),这是确定内存占用的关键——结构体总大小 = 所有字段大小之和 + 编译器填充字节(padding)。不同PLC平台填充策略略有差异,但只要不手动指定 __PACKED 等属性,均按自然对齐(如 REAL 通常4字节对齐)。


三、实例化的唯一正确方式:VAR / VAR_GLOBAL / VAR_TEMP 声明

真正创建数据实体,必须通过变量声明区完成。每一条声明语句,都会触发编译器在内存中划分一块与类型定义完全匹配的新区域。

3.1 三种声明位置对比

声明区域 内存位置 生命周期 多实例适用性 典型用途
VAR FB/FC 局部栈 功能块执行期间有效 ✅ 每次调用新建一份 算法中间变量、临时状态缓存
VAR_GLOBAL 全局数据区 PLC上电至断电全程 ✅ 全局唯一命名 设备主控变量、HMI通信接口
VAR_TEMP 临时栈(无保持) 单次扫描周期内有效 ❌ 不保留上次值 快速计算、条件中间结果

重点:VARVAR_GLOBAL 支持多实例;VAR_TEMP 不支持,因其不保持数据,谈不上“隔离”

3.2 正确实例化写法(以 PumpCtrl 为例)

// 方式1:全局变量 —— 推荐用于设备级主控
VAR_GLOBAL
    g_PumpA: PumpCtrl;
    g_PumpB: PumpCtrl;
    g_PumpC: PumpCtrl;
END_VAR

// 方式2:功能块内部变量 —— 推荐用于模块化复用
FUNCTION_BLOCK FB_PumpController
VAR
    stLocal: PumpCtrl;           // 每个FB实例独享一份
    stBackup: PumpCtrl;          // 同一FB内多个变量,各自独立
END_VAR

// 方式3:数组批量声明 —— 推荐用于同构设备群组
VAR
    aPumps: ARRAY[0..2] OF PumpCtrl; // aPumps[0], aPumps[1], aPumps[2] 互不干扰
END_VAR

验证数据隔离是否生效?只需检查地址偏移:

  • g_PumpA 占用地址 DB1.DBX0.0DB1.DBX26.7(假设总长27字节);
  • g_PumpB 自动分配下一可用地址 DB1.DBX27.0DB1.DBX53.7
  • aPumps[0] 起始地址 M100.0aPumps[1] 紧跟其后 M127.0(27字节对齐后)。

只要声明语法正确,编译器自动保证物理地址不重叠。这是数据隔离的硬件基础。


四、高危陷阱:这些“看似实例化”的写法,实际不隔离!

以下写法在语法上可能通过编译,但完全破坏数据隔离,务必规避:

❌ 陷阱1:用 REFPOINTER 替代实例声明

VAR
    pPump: REF TO PumpCtrl; // 错误!这只是指针变量,未指向任何有效实例
END_VAR
pPump := ADR(g_PumpA); // 此时才指向,但pPump本身不拥有数据
// 若后续 pPump := ADR(g_PumpB),原指向丢失,且无内存分配

REFPOINTER 是地址容器,不是数据实体。它们必须明确指向已声明的实例,否则解引用将导致未定义行为(如读取随机内存、PLC宕机)。

❌ 陷阱2:在 VAR_IN_OUT 中传递结构体(非地址)

FUNCTION FB_ProcessPump
VAR_IN_OUT
    stPump: PumpCtrl; // 危险!这是值传递,每次调用复制全部27字节
END_VAR
// 调用方:FB_ProcessPump(stInput); 
// stInput 内容被复制进FB内部,FB修改stPump不影响stInput原始值
// 但若FB内又将stPump赋给全局变量,则产生隐式覆盖

值传递(pass-by-value)会触发深层拷贝,消耗CPU与带宽;若结构体含大数组(如 ARRAY[1..1000] OF REAL),极易超限。正确做法是传地址:

VAR_IN_OUT
    pstPump: REF TO PumpCtrl; // 传指针,零拷贝,且可双向修改
END_VAR

❌ 陷阱3:滥用 UNION 导致字段内存重叠

TYPE FaultUnion:
UNION
    wCode: WORD;
    bDetails: ARRAY[0..1] OF BYTE;
END_UNION
END_TYPE

TYPE PumpCtrl_V2:
STRUCT
    uFault: FaultUnion; // uFault.wCode 与 uFault.bDetails[0..1] 共享同一块2字节内存
    // 修改wCode会同时改bDetails[0]和bDetails[1]
END_STRUCT
END_TYPE

UNION 的设计初衷是节省内存(多字段复用同一地址),而非隔离。若需隔离,请坚持用 STRUCT


五、实战:一个可直接部署的双泵冗余控制实例

下面给出完整、可编译的ST代码,包含类型定义、三处实例化(1全局+2局部)、以及体现数据隔离的核心逻辑:

// 1. 类型定义:泵控制器模板
TYPE PumpCtrl:
STRUCT
    bAuto: BOOL;              // 自动模式使能
    bManual: BOOL;            // 手动模式使能
    bRunCmd: BOOL;            // 运行命令(手动模式下有效)
    bFault: BOOL;             // 故障标志
    rSetFlow: REAL;           // 设定流量(m³/h)
    rActFlow: REAL;           // 实际流量(m³/h)
    nRunHours: DINT;          // 累计运行小时数
    sModel: STRING[20];       // 型号(如 "CPA-150")
END_STRUCT
END_TYPE

// 2. 全局实例:主泵(物理设备1)
VAR_GLOBAL
    g_MainPump: PumpCtrl;
END_VAR

// 3. 全局实例:备用泵(物理设备2)
VAR_GLOBAL
    g_StandbyPump: PumpCtrl;
END_VAR

// 4. 功能块:冗余管理器(内部含局部实例)
FUNCTION_BLOCK FB_RedundancyMgr
VAR
    // 局部结构体实例:存储切换决策状态
    stDecision: STRUCT
        bSwitchRequested: BOOL;
        tLastSwitch: TIME;
        nSwitchCount: UDINT;
    END_STRUCT

    // 局部结构体实例:缓存上一周期泵状态,用于变化检测
    stPrevMain: PumpCtrl;
    stPrevStandby: PumpCtrl;
END_VAR

// 主逻辑:仅当主泵故障且备用泵就绪时,发出切换请求
IF g_MainPump.bFault AND NOT g_StandbyPump.bFault THEN
    stDecision.bSwitchRequested := TRUE;
    stDecision.tLastSwitch := T#100MS; // 示例:记录时间戳
    stDecision.nSwitchCount := stDecision.nSwitchCount + 1;
END_IF

// 数据隔离验证:此处修改 stDecision 不影响任何全局泵变量
// 修改 g_MainPump.bRunCmd 也不会改变 stPrevMain.bRunCmd(除非显式赋值)

// 周期性更新缓存(体现实例独立性)
stPrevMain := g_MainPump;
stPrevStandby := g_StandbyPump;

// 5. 数组实例:用于趋势记录(最后10次故障信息)
VAR
    aFaultLog: ARRAY[0..9] OF STRUCT
        sPumpName: STRING[10];
        tTime: DATE_AND_TIME;
        wCode: WORD;
    END_STRUCT;
END_VAR

// 当主泵故障时,记录日志(操作 aFaultLog[0] 不会影响 g_MainPump 或 stDecision)
IF g_MainPump.bFault THEN
    aFaultLog[0].sPumpName := 'MAIN';
    aFaultLog[0].tTime := LOCALTIME();
    aFaultLog[0].wCode := 16#8001;
    // 自动滚动:后续条目前移(逻辑略,不展开)
END_IF

关键验证点

  • g_MainPumpg_StandbyPump 地址不重叠 → 物理隔离;
  • FB_RedundancyMgr.stDecision 在每次FB调用时重新初始化 → 逻辑隔离;
  • aFaultLog 是独立数组,其元素 sPumpName 字段与 g_MainPump.sModel 完全无关 → 存储隔离。

六、高级技巧:利用实例化实现配置驱动

当项目需支持多种泵型号(如离心泵、隔膜泵、螺杆泵),可定义通用基类 + 派生实例:

// 基础结构体(所有泵共有的字段)
TYPE BasePump:
STRUCT
    bPowerOn: BOOL;
    bReady: BOOL;
    rVoltage: REAL;
END_STRUCT
END_TYPE

// 派生结构体(继承+扩展)
TYPE CentrifugalPump:
STRUCT
    EXTENDS BasePump;           // 继承基础字段
    rMaxRPM: REAL;             // 离心泵特有:最高转速
    rEfficiency: REAL;         // 效率系数
END_STRUCT
END_TYPE

TYPE DiaphragmPump:
STRUCT
    EXTENDS BasePump;           // 同样继承
    nMaxCycles: UDINT;         // 隔膜泵特有:最大往复次数
    bPulseMode: BOOL;          // 是否脉冲输出
END_STRUCT
END_TYPE

// 实例化:不同设备使用不同派生类型
VAR_GLOBAL
    g_CentrifugalA: CentrifugalPump;
    g_DiaphragmB: DiaphragmPump;
END_VAR

此时 g_CentrifugalA.bPowerOng_DiaphragmB.bPowerOn 虽字段名相同,但因所属类型不同、内存布局不同,绝对隔离。编译器能严格校验 g_CentrifugalA.rMaxRPM 合法,而 g_DiaphragmB.rMaxRPM 则编译报错——这正是类型安全带来的强隔离保障。


七、总结:守住三条铁律,永保数据纯净

  1. 定义归定义,实例归实例TYPE...END_TYPE 只管画图,VAR 声明才真正盖楼。二者缺一不可,且不可混用。
  2. 每个物理对象,对应一个显式声明的实例:三台泵 → 三个 VAR 行;十个阀门 → 十个变量或一个10元数组。拒绝“一个变量走天下”。
  3. 禁用值传递大结构体,优先用 REF TO:既避免内存爆炸,又确保操作直达目标实例,不产生意外副本。

只要严格执行这三点,无论项目规模多大、设备数量多少,ST程序的数据边界始终清晰如刀切。你看到的每一个 . 操作符(如 g_PumpA.bRunCmd),背后都是独立内存地址的精准访问——这才是工业自动化可靠性的根基。

评论 (0)

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

扫一扫,手机查看

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