在电气自动化编程中,实现“三选一”逻辑(即从三个输入信号 A、B、C 中任一为真时,使输出 Output 为真)看似简单,但实际需结合控制需求、可读性、可维护性、诊断能力及标准规范综合判断。ST(Structured Text,结构化文本)是IEC 61131-3标准定义的高级编程语言,广泛用于PLC(可编程逻辑控制器)开发。它支持布尔运算、条件分支、循环和结构化数据操作。本文将手把手说明如何用ST编写真正可靠、可验证、符合工业实践的三选一逻辑,涵盖三种主流写法:布尔表达式直写、IF-ELSE链、CASE语句,并逐项对比其适用场景、隐患与优化技巧。
一、明确“三选一”的真实含义:先定义需求,再写代码
工业现场中,“三选一”常被误解为纯布尔或运算。但实际需求往往隐含以下约束:
-
互斥性要求?
若A、B、C代表三台泵的启动请求,系统可能要求“同一时刻仅允许一台运行”(即互斥),此时Output := A OR B OR C仅表示“有任一请求”,但未阻止多台同时运行——这不是真正的“三选一”,而是“三选多”。 -
优先级要求?
若A是手动急停信号,B是自动运行信号,C是远程启停信号,需明确:A有效时是否必须屏蔽B/C?此时逻辑不再是简单OR,而需嵌入优先级判定。 -
诊断与复位要求?
当Output为TRUE时,操作员需快速知道“是哪个输入触发的”。若仅用A OR B OR C,无法追溯源头,故障排查耗时翻倍。 -
信号有效性验证?
工业现场信号易受干扰。A、B、C是否已做滤波(如上升沿检测)、去抖(debounce)、状态确认(如反馈回读)?未预处理的原始信号直接参与逻辑,会导致误动作。
因此,第一步永远不是写代码,而是用文字锁定需求。例如:
“当本地按钮A、触摸屏按钮B、远程DCS指令C中任意一个发出‘有效启动脉冲’(上升沿且持续≥50ms),且无急停信号E=FALSE、无故障信号F=FALSE,则置位输出Q_Start。输出为TRUE期间,记录并保持触发源(A/B/C)。复位由停止按钮G或故障信号F上升沿触发。”
这个描述已包含:边沿检测、时间滤波、安全条件、源记录、复位机制——远超 OR 表达式能力。
二、写法一:布尔表达式直写 —— 何时可用?如何加固?
最简写法:
Output := A OR B OR C;
✅ 适用场景:
- 纯状态汇总(如“报警汇总灯”:任一子系统报警则总灯亮);
- 三信号物理上不可能同时为真(如三路独立安全光幕,每路只对应一个门区);
- 项目处于原型验证阶段,且后续会重构。
❌ 严禁场景:
- 需要区分触发源;
- A/B/C存在竞争或时序重叠风险;
- 信号未经滤波(如按钮未消抖,导致Output频繁抖动)。
🔧 加固措施(必须添加):
- 信号预处理:为每个输入增加50ms去抖(以CODESYS为例):
// 声明FB实例(需在VAR_GLOBAL或FB内部声明) fbDebounce_A(IN := A, PT := T#50ms); fbDebounce_B(IN := B, PT := T#50ms); fbDebounce_C(IN := C, PT := T#50ms);
// 使用滤波后信号
Output := fbDebounce_A.Q OR fbDebounce_B.Q OR fbDebounce_C.Q;
2. **增加安全使能条件**(防止误启):
```pascal
Output := (fbDebounce_A.Q OR fbDebounce_B.Q OR fbDebounce_C.Q)
AND NOT EmergencyStop
AND NOT SystemFault;
⚠️ 注意:OR 运算符在ST中从左到右短路求值(IEC 61131-3 §3.4.2),即若 fbDebounce_A.Q = TRUE,则B、C不再计算。这可提升效率,但若B/C含副作用(如调用函数块),结果不可预测——故禁止在OR右侧放置有副作用的表达式。
三、写法二:IF-ELSE链 —— 清晰表达优先级与源记录
当需要定义执行顺序或记录触发源时,IF-ELSE链最直接:
// 声明源记录变量(类型为枚举或整数)
TYPE tTriggerSource :
(
NONE : 0,
BUTTON_A : 1,
BUTTON_B : 2,
BUTTON_C : 3
);
END_TYPE
VAR
TriggerSource : tTriggerSource := NONE;
Output : BOOL := FALSE;
END_VAR
// 主逻辑
IF fbDebounce_A.Q AND NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
TriggerSource := BUTTON_A;
ELSIF fbDebounce_B.Q AND NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
TriggerSource := BUTTON_B;
ELSIF fbDebounce_C.Q AND NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
TriggerSource := BUTTON_C;
ELSE
Output := FALSE;
// 可选:保持TriggerSource不变,或清零
END_IF;
✅ 优势:
- 优先级一目了然(A > B > C);
TriggerSource变量可直接用于HMI显示或历史记录;- 每个分支可独立添加安全条件(如B还需校验权限等级)。
⚠️ 关键细节:
ELSIF而非ELSE IF:ST中必须连写,否则语法错误;- 所有分支必须覆盖
Output的赋值,避免锁存旧值; - 安全条件(
NOT EmergencyStop)重复出现,易出错——应提取为中间变量:SafeToStart : BOOL := NOT EmergencyStop AND NOT SystemFault; ... IF fbDebounce_A.Q AND SafeToStart THEN ...
四、写法三:CASE语句 —— 结构化、易扩展、适合多状态
当“三选一”未来可能扩展为“五选一”或需映射到设备ID时,CASE是首选:
// 假设A/B/C对应设备编号1/2/3,输入为UINT
VAR
RequestSource : UINT := 0; // 外部传入:1=A, 2=B, 3=C
Output : BOOL := FALSE;
ActiveDeviceID : UINT := 0;
END_VAR
// 将离散信号转换为源编码(需在主循环前执行)
IF fbDebounce_A.Q THEN RequestSource := 1; END_IF;
IF fbDebounce_B.Q THEN RequestSource := 2; END_IF;
IF fbDebounce_C.Q THEN RequestSource := 3; END_IF;
// 主CASE逻辑
CASE RequestSource OF
1:
IF NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
ActiveDeviceID := 1;
END_IF;
2:
IF NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
ActiveDeviceID := 2;
END_IF;
3:
IF NOT EmergencyStop AND NOT SystemFault THEN
Output := TRUE;
ActiveDeviceID := 3;
END_IF;
ELSE
Output := FALSE;
ActiveDeviceID := 0;
END_CASE;
✅ 优势:
- 新增选项只需增加一个
X:分支,不破坏原有结构; ActiveDeviceID可直接驱动设备选择器或通信地址;- 符合模块化设计思想,便于单元测试(对每个数字输入单独验证)。
⚠️ 陷阱规避:
- 避免漏写 ELSE:若
RequestSource因干扰变为0或4,无ELSE将导致Output锁存旧值——必须显式置FALSE; - 禁止在CASE内修改判别变量:
CASE RequestSource OF ... RequestSource := 0; ... END_CASE;是未定义行为; - 数值型判别需确保范围可控:若
RequestSource来自模拟量AD转换,必须加限幅:RequestSource := LIMIT(1, 3, RequestSource); // 限定1~3
五、终极方案:封装为可复用函数块(FB)
将逻辑抽象为FB,实现一次开发、多处调用:
// FB名称:FB_ThreeChoiceStarter
FUNCTION_BLOCK FB_ThreeChoiceStarter
VAR_INPUT
Start_A : BOOL;
Start_B : BOOL;
Start_C : BOOL;
Enable : BOOL := TRUE; // 总使能
Reset : BOOL := FALSE; // 复位边沿
END_VAR
VAR_OUTPUT
Output : BOOL;
SourceID : BYTE; // 1=A, 2=B, 3=C, 0=None
END_VAR
VAR
fbDeb_A, fbDeb_B, fbDeb_C : R_TRIG; // 上升沿检测
Deb_A, Deb_B, Deb_C : BOOL;
LastOutput : BOOL;
END_VAR
// 去抖与边沿检测
fbDeb_A(CLK := Start_A);
fbDeb_B(CLK := Start_B);
fbDeb_C(CLK := Start_C);
Deb_A := fbDeb_A.Q;
Deb_B := fbDeb_B.Q;
Deb_C := fbDeb_C.Q;
// 核心逻辑(带优先级与复位)
IF Reset THEN
Output := FALSE;
SourceID := 0;
ELSIF Enable THEN
IF Deb_A THEN
Output := TRUE;
SourceID := 1;
ELSIF Deb_B THEN
Output := TRUE;
SourceID := 2;
ELSIF Deb_C THEN
Output := TRUE;
SourceID := 3;
END_IF;
ELSE
Output := FALSE;
SourceID := 0;
END_IF;
调用示例:
fbStarter1(Start_A := LocalBtn, Start_B := HMI_Btn, Start_C := DCS_Cmd,
Enable := NOT E_Stop, Reset := StopBtn);
MotorRun := fbStarter1.Output;
LED_Source := fbStarter1.SourceID;
✅ 价值:
- 每个实例独立维护状态(
LastOutput,SourceID); - 输入/输出接口标准化,HMI、SCADA、其他FB均可统一接入;
- 修改内部逻辑不影响调用端——符合IEC 61131-3封装原则。
六、调试与验证:3步实操检查清单
写完代码后,必须通过以下验证:
-
信号时序仿真:
在PLC仿真环境(如Codesys Simulator)中,手动设置A/B/C的上升沿时间差为1ms、10ms、100ms,观察Output是否在首个有效信号到达后1个扫描周期内置位,且SourceID始终正确。 -
安全条件穿透测试:
强制EmergencyStop := TRUE,再触发A/B/C——Output必须保持FALSE,且SourceID不更新。 -
复位可靠性测试:
使Output := TRUE,然后给Reset := TRUE一个单周期脉冲——Output必须在下一周期变为FALSE,SourceID归零。
若任一测试失败,立即回溯:检查变量声明域(是否误用GLOBAL)、边沿检测时钟源(是否与主任务同步)、复位逻辑位置(是否在CASE/IF之后执行)。
七、避坑指南:90%工程师踩过的5个错误
| 错误现象 | 根因 | 修正方案 |
|---|---|---|
Output 偶发不动作 |
信号未去抖,PLC扫描周期内电平不稳定 | 必用 R_TRIG 或 TON 滤波,禁用原始BOOL |
SourceID 显示错误 |
多个输入同时有效,IF-ELSE优先级未覆盖全部组合 | 改用CASE,或明确声明“仅首个有效信号生效” |
代码编译报错 Syntax error near 'OR' |
OR 前后缺少空格,或使用中文字符 |
检查所有空格为ASCII 32,OR 前后各一个空格 |
| 下载后PLC报运行时错误 | CASE 中 RequestSource 为INT但分支用UINT字面量 |
统一类型:CASE RequestSource OF 1: ... END_CASE;(1自动转为INT) |
| HMI显示源ID延迟1秒 | SourceID 未在每次扫描都赋值,依赖锁存 |
确保每个分支(含ELSE)均对 SourceID 赋值 |
八、工程建议:按项目阶段选择写法
- 样机/快速验证:用布尔表达式
Output := A OR B OR C,但必须同步添加滤波和使能; - 正式投产项目:强制使用IF-ELSE链,优先级写死,源变量命名清晰(如
g_bStartSource_A); - 平台化/长期运维项目:封装为FB,接口增加
DiagnosticText : STRING输出当前状态描述,供HMI直接显示。
记住:自动化代码不是写给自己看的,而是写给三年后的维护工程师、写给审计人员、写给安全评估机构的。每一行ST代码,都要经得起问:“为什么这样写?不这样写的后果是什么?”

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