在结构化文本(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_Motor1、g_Motor2、g_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 块仅做两件事:
- 描述数据布局:规定字段名称、类型、顺序及对齐方式;
- 注册类型标识符:让编译器认识
MotorCtrl是一个合法类型名。
它不分配任何内存,也不生成变量。这就像设计一张电路板图纸(TYPE),图纸本身不会发光发热;只有把图纸交给工厂,打样出三块实物PCB(实例),它们才能各自焊接芯片、通电工作。
关键规则:
TYPE必须成对出现,中间只能是STRUCT...END_STRUCT、UNION...END_UNION或嵌套类型;- 类型名区分大小写,且不能与系统保留字(如
BOOL、INT、TIME)重复; - 结构体内字段名在同一层级不可重复,但不同结构体间可重名(
MotorCtrl.nSpeed与ValveCtrl.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 |
临时栈(无保持) | 单次扫描周期内有效 | ❌ 不保留上次值 | 快速计算、条件中间结果 |
重点:VAR 和 VAR_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.0至DB1.DBX26.7(假设总长27字节);g_PumpB自动分配下一可用地址DB1.DBX27.0至DB1.DBX53.7;aPumps[0]起始地址M100.0,aPumps[1]紧跟其后M127.0(27字节对齐后)。
只要声明语法正确,编译器自动保证物理地址不重叠。这是数据隔离的硬件基础。
四、高危陷阱:这些“看似实例化”的写法,实际不隔离!
以下写法在语法上可能通过编译,但完全破坏数据隔离,务必规避:
❌ 陷阱1:用 REF 或 POINTER 替代实例声明
VAR
pPump: REF TO PumpCtrl; // 错误!这只是指针变量,未指向任何有效实例
END_VAR
pPump := ADR(g_PumpA); // 此时才指向,但pPump本身不拥有数据
// 若后续 pPump := ADR(g_PumpB),原指向丢失,且无内存分配
REF 和 POINTER 是地址容器,不是数据实体。它们必须明确指向已声明的实例,否则解引用将导致未定义行为(如读取随机内存、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_MainPump与g_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.bPowerOn 和 g_DiaphragmB.bPowerOn 虽字段名相同,但因所属类型不同、内存布局不同,绝对隔离。编译器能严格校验 g_CentrifugalA.rMaxRPM 合法,而 g_DiaphragmB.rMaxRPM 则编译报错——这正是类型安全带来的强隔离保障。
七、总结:守住三条铁律,永保数据纯净
- 定义归定义,实例归实例:
TYPE...END_TYPE只管画图,VAR声明才真正盖楼。二者缺一不可,且不可混用。 - 每个物理对象,对应一个显式声明的实例:三台泵 → 三个
VAR行;十个阀门 → 十个变量或一个10元数组。拒绝“一个变量走天下”。 - 禁用值传递大结构体,优先用
REF TO:既避免内存爆炸,又确保操作直达目标实例,不产生意外副本。
只要严格执行这三点,无论项目规模多大、设备数量多少,ST程序的数据边界始终清晰如刀切。你看到的每一个 . 操作符(如 g_PumpA.bRunCmd),背后都是独立内存地址的精准访问——这才是工业自动化可靠性的根基。

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