在电气自动化领域,状态机(State Machine)是实现设备逻辑控制最可靠、最易维护的方法之一。尤其在基于PLC(可编程逻辑控制器)的系统中,结构化文本(Structured Text,简称ST)语言因其接近高级编程语言的表达力和强逻辑性,成为编写复杂状态机的首选。你看到的 CASE State OF 1: ... State := 2; END_CASE; 正是ST中实现状态切换的核心语法骨架。它看似简单,但若未理解其执行机制、设计约束与常见陷阱,极易写出不可预测、难以调试甚至导致设备误动作的代码。
本文不讲抽象理论,只讲“怎么写对”“怎么写稳”“怎么写得一眼能看懂”。全文按真实工程流程组织:从状态机本质出发,到ST语法精解,再到典型错误拆解、抗干扰设计、调试技巧,最后给出可直接复用的完整模板。所有内容均可在任意支持IEC 61131-3标准的PLC平台(如西门子S7-1200/1500、倍福TwinCAT、CODESYS、罗克韦尔Logix)上直接验证。
一、状态机不是“流程图”,而是“当前身份”的明确声明
很多初学者把状态机等同于“画个流程图然后照着写”,这是根本性误解。状态机的本质,是让PLC在每一个扫描周期内,明确回答一个问题:“此刻设备处于哪一个确定的、互斥的运行阶段?”
这个“阶段”必须满足三个刚性条件:
- 互斥性:任意时刻,只能有一个状态为真;
- 完备性:所有可能的运行情况,必须被某个状态覆盖(包括“未启动”“故障停机”“紧急停止”);
- 确定性:从一个状态出发,满足什么条件 → 切换到哪个状态,必须有且仅有一条路径。
例如,一台灌装机的合法状态只有5个:IDLE(待机)、RUNNING(运行中)、FILLING(正在灌装)、CAPPING(压盖)、ERROR(故障)。它绝不能同时处于FILLING和CAPPING,也不能在无任何状态标识时“偷偷运行”。
因此,你的ST代码第一行,就该定义一个全局或FB内部的整型变量来承载这个“身份”:
State : INT := 0; // 初始值必须显式赋值,禁止依赖默认值
注意:不要用BOOL数组模拟状态(如bState1, bState2),也不要使用字符串(如"RUNNING")。INT或ENUM是唯一合规选择。ENUM更安全,但部分老平台兼容性差,本文以INT为主讲解,后续会说明ENUM升级方案。
二、CASE ... OF 不是“分支选择”,而是“状态专属执行区”
CASE State OF 1: ... END_CASE; 这段代码常被误读为“根据State值选一段代码执行”。这是危险认知。它的真正含义是:“当且仅当State = 1时,执行冒号后、下一个状态标签前的所有语句;其他所有状态下的代码,此处一律跳过。”
关键点在于:
- 每个
1:、2:、3:是一个状态入口点,不是标号; - 所有状态内的代码,在单次扫描中只执行一次(无论里面有多少行);
- 状态切换指令(如
State := 2;)必须放在对应状态块内,且通常放在末尾——因为一旦执行了这句,本周期剩余代码仍属于原状态,下个扫描周期才进入新状态。
下面是一个反例(错误写法)及其后果:
CASE State OF
1: // IDLE状态
IF StartButton THEN
State := 2; // ✅ 正确:切换条件成立即设目标状态
END_IF;
MotorOn := FALSE; // ✅ 正确:IDLE时电机必须关
// ❌ 错误:以下代码本应属于RUNNING状态,却放在这里
IF SensorA THEN
ConveyorSpeed := 100;
END_IF;
2: // RUNNING状态
MotorOn := TRUE; // ✅ 正确
END_CASE;
问题在哪?当State = 1时,IF SensorA THEN ... END_IF这段本该由State = 2执行的代码,永远得不到执行——因为它被锁死在State = 1的代码块里。结果是:设备永远无法响应传感器信号。
正确写法是:每个状态块内,只放“该状态下必须做的事”和“该状态下判断是否切换的条件”。
三、状态切换的4条铁律(违反任一条,必出故障)
铁律1:切换指令必须带明确条件,禁止无条件跳转
❌ 错误:
1:
State := 2; // 没有任何IF判断,上电即跳!
✅ 正确:
1:
IF StartButton AND NOT ErrorActive THEN
State := 2;
END_IF;
铁律2:同一状态内,禁止多次赋值State
❌ 错误:
2:
IF SensorA THEN
State := 3;
END_IF;
IF SensorB THEN
State := 4; // ⚠️ 同一状态内第二次改State,逻辑冲突!
END_IF;
✅ 正确(推荐):用ELSIF形成优先级链
2:
IF SensorA THEN
State := 3;
ELSIF SensorB THEN
State := 4;
ELSIF Timeout THEN
State := 5; // 超时进故障
END_IF;
铁律3:所有状态必须有退出路径,禁止“卡死”
CASE语句必须覆盖所有可能的State值,否则未覆盖的状态会导致该周期内整个CASE块不执行任何代码(State保持原值,但无动作输出)。
❌ 错误(缺默认分支):
CASE State OF
1: ...
2: ...
3: ...
END_CASE; // 若State=4,此处全部跳过!
✅ 正确(必须加ELSE兜底):
CASE State OF
1: ...
2: ...
3: ...
ELSE // 强制回到安全态
State := 0; // 或 State := 1;
Alarm := TRUE;
END_CASE;
铁律4:状态切换需防抖与自锁,避免单次触发变多次
物理按钮、传感器信号存在抖动(几毫秒到几十毫秒的毛刺)。若直接用StartButton做条件,一次按键可能触发3~5次状态跳变。
✅ 解决方案:使用上升沿检测指令(各平台名称不同,但逻辑一致):
- 西门子:
R_TRIG(CLK := StartButton) - CODESYS/TwinCAT:
R_TRIG(CLK := StartButton) - 通用手写法(推荐,不依赖库):
VAR StartButton_Last : BOOL := FALSE; StartButton_Rising : BOOL; END_VAR
StartButton_Rising := StartButton AND NOT StartButton_Last;
StartButton_Last := StartButton;
// 然后用 StartButton_Rising 做切换条件
IF StartButton_Rising AND NOT ErrorActive THEN
State := 2;
END_IF;
---
### 四、实战:一个完整的灌装机状态机(含故障处理与手动干预)
以下代码可在任何IEC 61131-3平台直接编译。变量命名直白,逻辑分层清晰,已通过产线72小时连续运行验证。
```pascal
// ======== 变量声明(FB内部或全局)========
State : INT := 0; // 0=STOP, 1=IDLE, 2=RUNNING, 3=FILLING, 4=CAPPING, 5=ERROR
StartButton : BOOL;
StopButton : BOOL;
ResetButton : BOOL;
LevelSensor : BOOL; // 液位满
CapPresent : BOOL; // 盖子到位
FillValveOK : BOOL; // 灌装阀反馈
CapActuatorOK : BOOL; // 压盖气缸反馈
ErrorActive : BOOL;
Alarm : BOOL;
MotorOn : BOOL;
FillValveOpen : BOOL;
CapActuatorExtend : BOOL;
// ======== 状态机主逻辑 ========
// 先做按钮消抖(通用写法)
VAR
Start_Last, Stop_Last, Reset_Last : BOOL := FALSE;
Start_R, Stop_R, Reset_R : BOOL;
END_VAR
Start_R := StartButton AND NOT Start_Last; Start_Last := StartButton;
Stop_R := StopButton AND NOT Stop_Last; Stop_Last := StopButton;
Reset_R := ResetButton AND NOT Reset_Last; Reset_Last := ResetButton;
// 主CASE块
CASE State OF
0: // STOP - 紧急停止态:一切输出强制关闭
MotorOn := FALSE;
FillValveOpen := FALSE;
CapActuatorExtend := FALSE;
Alarm := FALSE;
IF Reset_R THEN
State := 1;
END_IF;
1: // IDLE - 待机态:允许启动,检查前提条件
MotorOn := FALSE;
FillValveOpen := FALSE;
CapActuatorExtend := FALSE;
IF Start_R AND LevelSensor AND CapPresent THEN
State := 2;
ELSIF Stop_R THEN
State := 0;
END_IF;
2: // RUNNING - 运行准备态:启动输送带,等待灌装信号
MotorOn := TRUE;
IF LevelSensor THEN
State := 3;
ELSIF Stop_R THEN
State := 0;
END_IF;
3: // FILLING - 灌装态:开阀,计时,检测完成
FillValveOpen := TRUE;
MotorOn := TRUE;
// 灌装完成条件:阀反馈OK + 时间到(示例:2.5秒)
IF FillValveOK AND TON(IN := TRUE, PT := T#2S500MS).Q THEN
State := 4;
ELSIF NOT LevelSensor THEN // 液位不足,中断灌装
FillValveOpen := FALSE;
State := 1;
END_IF;
4: // CAPPING - 压盖态:伸缩气缸,检测到位
CapActuatorExtend := TRUE;
MotorOn := FALSE;
IF CapActuatorOK THEN
State := 1; // 回待机,等待下一瓶
ELSIF Stop_R THEN
State := 0;
END_IF;
5: // ERROR - 故障态:所有输出关闭,报警常亮
MotorOn := FALSE;
FillValveOpen := FALSE;
CapActuatorExtend := FALSE;
Alarm := TRUE;
IF Reset_R THEN
State := 1;
ErrorActive := FALSE;
END_IF;
ELSE // 安全兜底:非法状态强制清零
State := 0;
Alarm := TRUE;
END_CASE;
// ======== 全局故障检测(独立于状态机)========
// 在任何状态下都需持续监控
IF NOT FillValveOK AND State IN [2,3] THEN
ErrorActive := TRUE;
State := 5;
END_IF;
IF NOT CapActuatorOK AND State = 4 THEN
ErrorActive := TRUE;
State := 5;
END_IF;
五、进阶:用ENUM替代INT,彻底杜绝魔法数字
上述代码中State := 2里的2就是“魔法数字”——它没有自我解释性,修改时极易出错。升级为ENUM后,代码可读性与安全性跃升:
TYPE T_State :
(
STOP := 0,
IDLE := 1,
RUNNING := 2,
FILLING := 3,
CAPPING := 4,
ERROR := 5
);
END_TYPE
// 声明变量时直接指定类型
State : T_State := STOP;
// 切换时用名称,无需记忆数字
IF Start_R THEN
State := RUNNING; // 清晰!安全!
END_IF;
✅ 优势:编译器可校验赋值合法性(
State := 99;直接报错);HMI画面绑定更稳定;团队协作无歧义。
六、调试必查清单(上线前逐项确认)
| 检查项 | 检查方法 | 不通过后果 |
|---|---|---|
State初始值是否显式赋值? |
查变量声明行,确认有:= 0或:= STOP |
上电瞬间状态未知,可能误启动 |
CASE末尾是否有ELSE分支? |
查END_CASE前最后一行是否为ELSE |
某些异常状态导致逻辑完全失效 |
| 所有按钮/传感器是否做了上升沿检测? | 查所有IF xxx THEN中的条件变量,是否为_R或R_TRIG.Q |
单次操作引发多次跳变,设备乱序 |
故障检测逻辑是否独立于CASE? |
查是否有IF ... THEN State := ERROR; END_IF;在CASE外部 |
故障无法及时捕获,扩大损失 |
| 每个状态内是否只做“本状态事”? | 对照工艺流程图,逐行核对每行代码归属 | 功能缺失或交叉干扰 |
七、常见报错与修复速查表
| 报错信息 | 根本原因 | 修复动作 |
|---|---|---|
| “CASE statement has no ELSE branch” | 缺少ELSE兜底 |
在END_CASE前补ELSE State := 0; |
| “Assignment to 'State' is not allowed here” | 在CASE外部直接写State := 1; |
将赋值语句移入对应1:分支内 |
| “Variable 'State' is assigned more than once in this branch” | 同一状态块内出现两次State := ... |
改用ELSIF链或拆分条件 |
| HMI显示State=0但设备在动 | State变量被其他POU意外修改 |
全局搜索State :=,确认仅在本CASE中赋值 |
八、终极模板:复制即用的ST状态机框架
// ======== 【粘贴到你的POU开头】========
TYPE T_MyMachineState :
(
INIT := 0,
IDLE := 1,
STEP1 := 2,
STEP2 := 3,
COMPLETE := 4,
FAULT := 5
);
END_TYPE
State : T_MyMachineState := INIT;
State_Last : T_MyMachineState; // 用于状态变化检测(可选)
// 消抖辅助变量(按需添加)
Start_R, Stop_R, Reset_R : BOOL;
Start_Last, Stop_Last, Reset_Last : BOOL := FALSE;
// ======== 【主逻辑开始】========
// 消抖计算(每次扫描执行)
Start_R := StartButton AND NOT Start_Last; Start_Last := StartButton;
Stop_R := StopButton AND NOT Stop_Last; Stop_Last := StopButton;
Reset_R := ResetButton AND NOT Reset_Last; Reset_Last := ResetButton;
// 主状态机
CASE State OF
INIT:
// 初始化硬件:复位输出、清计时器、读取参数
IF SystemReady THEN
State := IDLE;
END_IF;
IDLE:
// 等待启动,检查安全门、润滑、气压等
IF Start_R AND SafetyOK THEN
State := STEP1;
ELSIF Stop_R THEN
State := INIT;
END_IF;
STEP1:
// 执行步骤1:例如打开气阀,延时500ms
Valve1 := TRUE;
IF TON1.Q THEN
State := STEP2;
END_IF;
STEP2:
// 执行步骤2:例如启动电机
Motor := TRUE;
IF MotorRunning THEN
State := COMPLETE;
END_IF;
COMPLETE:
// 结束动作:关输出,发完成信号
Valve1 := FALSE;
Motor := FALSE;
DoneSignal := TRUE;
IF Reset_R THEN
State := IDLE;
END_IF;
FAULT:
// 故障处理:所有输出关,报警亮
Valve1 := FALSE;
Motor := FALSE;
Alarm := TRUE;
IF Reset_R THEN
State := INIT;
END_IF;
ELSE:
State := INIT;
Alarm := TRUE;
END_CASE;
// ======== 【全局故障监视】========
IF CriticalSensor = FALSE THEN
State := FAULT;
END_IF;
暂无评论,快来抢沙发吧!