ST(Structured Text)是IEC 61131-3标准定义的高级文本编程语言,广泛用于PLC(可编程逻辑控制器)开发。在电气自动化系统中,程序的可靠性、可维护性与安全性直接取决于代码结构是否清晰、模块边界是否明确。而“私有变量保护”并非ST语言原生支持的概念(如C++的private或Python的_命名约定),而是通过严格遵循作用域规则、合理组织程序组织单元(POU)和规避全局暴露实现的工程实践。
本文不依赖语法糖或扩展指令,只使用标准ST语法,手把手教你如何用最基础的语言机制,在真实工程项目中达成等效于“私有变量”的效果:让内部计算逻辑、中间状态、配置参数对外不可见、不可访问、不可误改。
一、认清ST的作用域本质:三个层级,一个铁律
ST中变量可见性由声明位置和所属POU类型共同决定。不存在“类内私有”概念,只有“本POU内可见”或“跨POU可见”。必须掌握以下三层作用域:
- 局部作用域(Local):在
FUNCTION、FUNCTION_BLOCK或PROGRAM内部VAR段声明的变量,仅在该POU内部语句中可读写。 - 输入/输出作用域(Input/Output):在
FUNCTION_BLOCK的VAR_INPUT/VAR_OUTPUT段声明的变量,对调用者可见(即外部可读/可写),但不是局部变量。 - 全局作用域(Global):在
VAR_GLOBAL或VAR_EXTERNAL段声明的变量,全项目可见——这是私有性最大的敌人。
✅ 铁律:所有不必要暴露的变量,必须声明为局部变量(
VAR),且绝不能出现在VAR_GLOBAL、VAR_EXTERNAL、VAR_INPUT或VAR_OUTPUT中。
例如,某温度控制功能块需计算PID偏差积分项,中间变量integral_sum绝不能作为VAR_INPUT传入,也不应放在全局DB中:
FUNCTION_BLOCK FB_TempPID
VAR
// ✅ 正确:仅本FB内部使用,完全隐藏
integral_sum : REAL := 0.0;
last_error : REAL := 0.0;
dt_ms : TIME := T#10ms;
END_VAR
VAR_INPUT
setpoint : REAL; // 必须由外部设定 → 暴露合理
process_value : REAL; // 必须由传感器提供 → 暴露合理
END_VAR
VAR_OUTPUT
output : REAL; // 必须反馈给执行器 → 暴露合理
END_VAR
integral_sum、last_error、dt_ms三者均未出现在任何输入/输出段,外部POU无法通过myPID.integral_sum方式访问,也无法在HMI或上位机变量表中看到它们——这就是最朴素、最可靠的“私有化”。
二、禁止全局变量:从源头掐断泄露路径
全局变量是电气自动化项目中最常见的“私有性破坏源”。一旦某个中间计算值被声明为VAR_GLOBAL,它就自动获得以下危险属性:
- 可被任意POU读写(包括无意修改的调试函数);
- 可被HMI组态软件直接绑定显示或修改;
- 可被OPC UA服务器发布为节点,暴露给IT网络;
- 版本升级时极易因命名冲突引发静默错误。
正确做法:用局部变量 + 显式接口替代全局共享。
| 场景 | ❌ 危险做法 | ✅ 安全做法 |
|---|---|---|
| 多个FB需共用采样周期 | VAR_GLOBAL sample_time : TIME := T#20ms; |
将sample_time作为VAR_INPUT传入每个FB:<br>myFB(sample_time := T#20ms); |
系统运行标志(如is_auto_mode) |
全局BOOL变量,各处直接读写 | 声明为PROGRAM局部变量;通过FUNCTION_BLOCK封装状态机,仅暴露GetMode()函数读取 |
| 设备校准偏移量 | VAR_GLOBAL sensor_offset : REAL; |
在设备初始化FB中声明为VAR,校准后存入VAR_OUTPUT calibrated_offset : REAL;,下游仅通过该输出获取 |
关键原则:全局区只存放真正需要跨POU一致访问的、只读或受控写入的顶层配置,例如:
// ✅ 合理的全局常量(只读,不可修改)
VAR_GLOBAL CONSTANT
MAX_VOLTAGE : REAL := 400.0;
MIN_TEMP : REAL := -20.0;
SYSTEM_ID : STRING[16] := 'LINE_A_01';
END_VAR
所有带下划线_CONSTANT后缀的全局声明,编译器禁止赋值,天然具备“只读私有”属性。
三、利用FUNCTION_BLOCK封装状态:私有性的终极载体
FUNCTION_BLOCK(FB)是ST中唯一能保持内部状态的POU类型。其VAR段变量在每次调用时保留上次值,这使其成为隐藏实现细节的理想容器。
下面以一个典型应用——“电机启停防抖动控制器”为例,展示如何将延时逻辑、计数器、去抖状态全部封装为私有:
FUNCTION_BLOCK FB_MotorDebounce
VAR
// ✅ 全部私有:外部不可见、不可访问
debounce_timer : TON;
press_counter : INT := 0;
last_state : BOOL := FALSE;
stable_state : BOOL := FALSE;
timer_active : BOOL := FALSE;
END_VAR
VAR_INPUT
raw_button : BOOL; // 原始信号(含抖动)
debounce_time : TIME := T#50ms; // 参数化配置,非硬编码
END_VAR
VAR_OUTPUT
debounced_signal : BOOL; // 干净输出,唯一对外接口
END_VAR
// --- 内部逻辑(完全隐藏)---
debounce_timer(IN := raw_button, PT := debounce_time);
IF raw_button AND NOT last_state THEN
press_counter := press_counter + 1;
END_IF;
last_state := raw_button;
IF debounce_timer.Q THEN
stable_state := raw_button;
timer_active := FALSE;
ELSIF raw_button THEN
debounce_timer(IN := TRUE); // 重启定时器
timer_active := TRUE;
ELSE
IF timer_active THEN
stable_state := last_state; // 保持上一稳定值
END_IF;
END_IF;
debounced_signal := stable_state;
调用方只需写:
PROGRAM PLC_PRG
VAR
motor_start_btn : BOOL;
clean_start : BOOL;
END_VAR
fb_debounce(raw_button := motor_start_btn,
debounce_time := T#40ms,
debounced_signal => clean_start);
外部永远看不到press_counter、debounce_timer等任何内部变量。即使打开在线监控,也只能看到fb_debounce.debounced_signal,其余均为灰色不可见项。这种封装使该FB可复用于10台不同设备,无需担心状态污染或误操作。
四、禁止隐式全局:警惕“未声明即全局”的陷阱
部分PLC平台(如早期Codesys版本)存在历史遗留行为:当变量在VAR段未显式声明类型时,编译器可能将其视为隐式全局变量。更隐蔽的是:数组索引越界、指针解引用失败、未初始化的REF变量,可能导致内存区域被意外覆盖,间接暴露内部数据。
防御手段只有两条:
- 始终开启编译器严格模式:启用
"Treat undeclared identifiers as error"选项,杜绝未声明变量; - 禁用所有不安全类型:在项目设置中关闭
ALLOW_POINTER_ARITHMETIC和ALLOW_UNSAFE_CASTS。
此外,对REF(引用)类型必须零容忍地检查:
// ❌ 危险:ref未初始化,指向随机地址
VAR
p_data : REF TO INT;
END_VAR
// 若后续执行 `p_data^ := 100;`,将写入未知内存
// ✅ 安全:声明即初始化,且目标为已知局部变量
VAR
local_val : INT := 0;
p_data : REF TO INT := ADR(local_val);
END_VAR
ADR()取地址操作必须作用于明确声明的局部变量,不可用于临时表达式或函数返回值。
五、面向维护的私有设计:让“看不见”等于“不需要懂”
真正的私有性不仅是技术隔离,更是认知减负。一个设计良好的FB,应做到:
- 调用者无需理解内部算法:比如
FB_PID无需知道它用的是位置式还是增量式; - 参数精简到最少必要集:
FB_MotorDebounce只暴露raw_button和debounce_time,不暴露“是否启用计数”、“是否保持最后状态”等开关; - 错误不向外传播:内部异常(如除零、溢出)必须捕获并转为安全默认值,绝不让
NaN或INF流出VAR_OUTPUT。
示例:安全的浮点除法封装
FUNCTION SafeDivide : REAL
VAR_INPUT
numerator : REAL;
denominator : REAL;
default_val : REAL := 0.0;
END_VAR
IF ABS(denominator) > 1E-9 THEN
SafeDivide := numerator / denominator;
ELSE
SafeDivide := default_val; // ✅ 主动设默认,不抛异常(ST无异常机制)
END_IF
调用方写ratio := SafeDivide(10.0, my_var),永远不用担心my_var为零导致失控——这个保护逻辑完全私有,外部既看不到也无需关心。
六、工具链协同:让私有性在全生命周期生效
私有变量保护不能只靠编码规范,还需工具链配合:
- HMI组态:在变量管理器中,勾选“仅显示
VAR_INPUT/VAR_OUTPUT”,隐藏所有VAR段变量; - OPC UA服务器:配置节点权限,对
FB_xxx.前缀变量默认设为Read-Only,对VAR段变量不发布任何节点; - 版本控制(Git):在
.gitignore中加入*.tmp、*.cache,防止编译器生成的符号映射文件意外提交,暴露内部变量名; - 代码审查清单:每轮CR必须检查:
- 是否存在
VAR_GLOBAL非常量声明? - 是否所有FB的
VAR段变量均无对应VAR_INPUT/VAR_OUTPUT别名? - 是否有
ADR()指向非局部变量?
- 是否存在
七、常见误区与反模式(务必规避)
| 误区 | 后果 | 修正方案 |
|---|---|---|
把VAR_IN_OUT当作“私有输入” |
VAR_IN_OUT仍对外可见,且允许外部写入,破坏封装 |
改用VAR_INPUT+内部复制,或彻底用VAR+初始化逻辑 |
在PROGRAM中大量使用VAR模拟FB状态 |
PROGRAM无实例化能力,无法复用;且重启后VAR重置,状态丢失 |
将逻辑提取为FUNCTION_BLOCK,用多个实例管理多设备 |
用STRING拼接变量名实现“动态访问” |
如'motor_' + INT_TO_STRING(i) + '_speed',绕过编译检查,运行时报错难定位 |
ST不支持反射;改用数组或结构体索引:motor[i].speed |
| 认为注释“// PRIVATE”就能保护变量 | 注释不影响编译器行为,变量依然全局可见 | 删除注释,改为真正的作用域隔离 |
八、进阶实践:用结构体(STRUCT)强化逻辑分组
当一组私有变量语义强相关时,用STRUCT封装可进一步提升可维护性,并天然强化私有边界:
TYPE MotorState :
STRUCT
rpm_target : REAL := 0.0;
current_limit : REAL := 120.0;
thermal_acc : REAL := 0.0;
last_update : DATE_AND_TIME;
END_STRUCT
END_TYPE
FUNCTION_BLOCK FB_MotorCtrl
VAR
// ✅ 一个结构体 = 一组私有变量,整体不可拆分访问
state : MotorState;
END_VAR
VAR_INPUT
cmd_rpm : REAL;
END_VAR
// ... 内部使用 state.rpm_target 等,外部无法访问 state 结构体内部字段
结构体本身是值类型,state作为局部变量,其全部成员自动继承私有性。且state无法被外部POU通过.操作符访问成员(因state不在输入/输出段),彻底阻断穿透路径。
所有措施最终服务于一个目标:让每个POU像一台黑盒子——你只关心它接收什么、输出什么、响应多快,其余一切细节,包括内存布局、算法选择、中间状态,都沉在水面之下。这不是为了炫技,而是让产线停机排查时间缩短70%,让新工程师三天内能安全修改控制逻辑,让安全审计报告中“变量越权访问”项为零。

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