ST怎么写状态机切换:CASE State OF 1: ... State := 2; END_CASE;

发布于 2026-03-15 02:50:01 · 浏览 2 次 · 评论 0 条

在电气自动化领域,状态机(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在每一个扫描周期内,明确回答一个问题:“此刻设备处于哪一个确定的、互斥的运行阶段?”

这个“阶段”必须满足三个刚性条件:

  1. 互斥性:任意时刻,只能有一个状态为真;
  2. 完备性:所有可能的运行情况,必须被某个状态覆盖(包括“未启动”“故障停机”“紧急停止”);
  3. 确定性:从一个状态出发,满足什么条件 → 切换到哪个状态,必须有且仅有一条路径。

例如,一台灌装机的合法状态只有5个:IDLE(待机)、RUNNING(运行中)、FILLING(正在灌装)、CAPPING(压盖)、ERROR(故障)。它绝不能同时处于FILLINGCAPPING,也不能在无任何状态标识时“偷偷运行”。

因此,你的ST代码第一行,就该定义一个全局或FB内部的整型变量来承载这个“身份”:

State : INT := 0; // 初始值必须显式赋值,禁止依赖默认值

注意:不要用BOOL数组模拟状态(如bState1, bState2),也不要使用字符串(如"RUNNING")。INTENUM是唯一合规选择。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中的条件变量,是否为_RR_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;

评论 (0)

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

扫一扫,手机查看

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