博途SCL的CASE语句是实现状态机控制的利器,相比梯形图的复杂跳转网络,它能用结构化文本清晰表达多状态切换逻辑。本文从实际工程角度,手把手教你用CASE语句构建可靠的状态机。
为什么选CASE语句做状态机
状态机的核心需求:根据当前状态和触发条件,决定下一步去哪。CASE语句天生匹配这个模型——它的结构就是"先查状态,再执行对应动作",代码可读性远超梯形图的线圈-触点网络。
实际优势体现在三方面:
- 代码集中:一个CASE块包揽所有状态逻辑,无需跨页面追踪
- 修改安全:新增状态时,不会意外触动既有状态的跳转条件
- 移植方便:纯文本结构,复制到其他项目或PLC品牌时改动最小
状态机的三要素设计
动手写代码前,必须明确三个东西:状态定义、触发条件、动作输出。
第一步:定义状态变量
用枚举类型(自定义数据类型)管理状态,不要用裸数字。在博途的"PLC数据类型"中新建:
类型名称: eMachineState
枚举值:
- Idle := 0 (*待机*)
- Init := 1 (*初始化*)
- Running := 2 (*运行中*)
- Error := 3 (*故障*)
- Stop := 4 (*停止中*)
然后在程序块中声明状态变量:
VAR
stateCurrent : eMachineState; (*当前状态*)
stateNext : eMachineState; (*下一状态预备*)
timerState : TON; (*状态驻留计时*)
END_VAR
第二步:梳理状态转移图
用Mermaid可视化你的状态机,确保没有死循环和不可达状态。以下是一个典型传送带控制的状态机:
graph TD
A["Idle\n待机"] -- "启动信号\n且急停未触发" --> B["Init\n初始化"]
B -- "初始化完成\n定时2秒" --> C["Running\n运行中"]
C -- "物料检测超时\n5秒无料" --> D["Stop\n停止中"]
C -- "急停按下\n或变频器故障" --> E["Error\n故障"]
D -- "完全停止\n定时1秒" --> A
E -- "故障复位\n且异常解除" --> B
C -- "正常停止按钮" --> D
第三步:设计每个状态的动作
| 状态 | 核心动作 | 跳转条件 |
|---|---|---|
Idle |
所有输出置零,等待启动 | startButton AND NOT eStop |
Init |
复位变频器、清计数器、预置速度 | timerState.Q(2秒到) |
Running |
使能运行、监控物料流 | 停止信号/故障/无料超时 |
Error |
急停输出、记录故障码、闪报警灯 | resetButton AND faultGone |
Stop |
降速停车、保持制动 | timerState.Q(1秒到) |
核心代码:CASE语句的完整写法
在博途的SCL编辑器中,右键程序块选择"添加新块"→"函数块"→语言选SCL。
基础框架(单CASE结构)
FUNCTION_BLOCK "FB_StateMachine_Basic"
VAR_INPUT
startButton : Bool;
stopButton : Bool;
eStop : Bool; (*急停,常闭触点*)
faultVFD : Bool; (*变频器故障*)
materialSensor : Bool; (*物料检测*)
END_VAR
VAR_OUTPUT
runMotor : Bool;
alarmHorn : Bool;
statusLight : Byte; (*0=灭, 1=绿, 2=黄, 3=红*)
END_VAR
VAR
stateCurrent : eMachineState := eMachineState#Idle;
timerState : TON;
tonMaterial : TON; (*物料监控定时*)
faultCode : Word;
END_VAR
BEGIN
(*状态机核心:CASE语句*)
CASE stateCurrent OF
eMachineState#Idle:
(*动作:待机状态*)
runMotor := FALSE;
alarmHorn := FALSE;
statusLight := 0; (*全灭*)
(*跳转判断*)
IF startButton AND NOT eStop THEN
stateCurrent := eMachineState#Init;
END_IF;
eMachineState#Init:
(*动作:初始化序列*)
timerState(IN := TRUE, PT := T#2S);
statusLight := 2; (*黄灯闪烁,实际用闪烁位*)
(*初始化完成判断*)
IF timerState.Q THEN
timerState(IN := FALSE); (*复位定时器*)
tonMaterial(IN := FALSE, PT := T#5S); (*预清物料定时*)
stateCurrent := eMachineState#Running;
END_IF;
(*紧急打断*)
IF eStop THEN
timerState(IN := FALSE);
stateCurrent := eMachineState#Error;
END_IF;
eMachineState#Running:
(*动作:正常运行*)
runMotor := TRUE;
statusLight := 1; (*绿灯常亮*)
(*物料监控:5秒无料则停*)
tonMaterial(IN := NOT materialSensor, PT := T#5S);
(*跳转判断:优先级从高到低*)
IF eStop OR faultVFD THEN
faultCode := IF eStop THEN 16#0001 ELSE 16#0002;
stateCurrent := eMachineState#Error;
ELSIF stopButton THEN
stateCurrent := eMachineState#Stop;
ELSIF tonMaterial.Q THEN
faultCode := 16#0003; (*无料报警代码*)
stateCurrent := eMachineState#Stop;
END_IF;
eMachineState#Error:
(*动作:故障处理*)
runMotor := FALSE;
alarmHorn := TRUE;
statusLight := 3; (*红灯常亮*)
(*跳转判断:严格的安全确认*)
IF resetButton AND NOT eStop AND NOT faultVFD THEN
alarmHorn := FALSE;
stateCurrent := eMachineState#Init; (*必须重新初始化*)
END_IF;
eMachineState#Stop:
(*动作:有序停止*)
runMotor := FALSE; (*变频器减速停车由外部处理*)
timerState(IN := TRUE, PT := T#1S);
IF timerState.Q THEN
timerState(IN := FALSE);
stateCurrent := eMachineState#Idle;
END_IF;
ELSE
(*异常防护:非法状态自动复位*)
stateCurrent := eMachineState#Idle;
faultCode := 16#00FF;
END_CASE;
END_FUNCTION_BLOCK
进阶:双CASE分离结构与动作
复杂设备建议把"状态转移逻辑"和"状态输出动作"分开,避免修改时牵一发而动全身。
第一CASE:只算下一个状态
(*状态转移计算 - 纯逻辑,无输出*)
CASE stateCurrent OF
eMachineState#Idle:
stateNext := IF startTrigger THEN eMachineState#Init
ELSE stateCurrent;
eMachineState#Init:
stateNext := IF initDone THEN eMachineState#Running
ELSE IF eStop THEN eMachineState#Error
ELSE stateCurrent;
(*...其他状态...*)
END_CASE;
(*上升沿检测:状态切换时刻*)
stateChanged := (stateCurrent <> stateNext);
IF stateChanged THEN
stateCurrent := stateNext;
timerState(IN := FALSE); (*切状态时复位通用定时*)
END_IF;
第二CASE:只算输出动作
(*输出动作 - 纯组合逻辑,无状态跳转*)
CASE stateCurrent OF
eMachineState#Idle:
runMotor := FALSE;
doSomethingElse := FALSE;
eMachineState#Running:
runMotor := TRUE;
doSomethingElse := conditionA AND NOT timeout;
(*...*)
END_CASE;
这种结构的优势:调试时可以强制stateCurrent到任意状态,观察输出是否正确,而不用担心触发意外跳转。
关键细节与防坑指南
定时器的正确用法
SCL中的TON调用要注意:每次扫描都必须执行,不能把调用藏在IF里。正确写法:
(*错误:定时器只在某状态运行,其他状态不刷新*)
IF stateCurrent = eMachineState#Init THEN
timerState(IN := TRUE, PT := T#2S); (*危险:退出状态时定时器冻结*)
END_IF;
(*正确:所有状态的定时器统一刷新,用IN位控制*)
timerState(IN := (stateCurrent = eMachineState#Init), PT := T#2S);
状态切换的上升沿处理
需要"进入某状态时只执行一次"的操作(如发脉冲命令、记录时间戳),用状态变化位触发:
stateChanged := (stateCurrent <> stateLast);
stateLast := stateCurrent; (*下一周期用*)
IF stateChanged AND (stateCurrent = eMachineState#Init) THEN
resetCounter := TRUE; (*初始化时清计数器,只清一次*)
ELSE
resetCounter := FALSE;
END_IF;
安全状态的兜底设计
务必保留ELSE分支,处理程序跑飞、断电恢复等异常情况:
CASE stateCurrent OF
(*正常状态...*)
ELSE
(*任何非法状态都到这里*)
stateCurrent := eMachineState#Idle;
logEvent(code := 16#8001, msg := "非法状态恢复");
triggerSafeOutput(); (*触发安全输出*)
END_CASE;
实战:扩展为多层次状态机
真实设备往往需要"主状态机+子状态机"的层次结构。例如:
- 主状态:生产模式 / 维护模式 / 调试模式
- 子状态(生产模式下):上料→加工→检测→下料
实现方式:用结构体嵌套CASE,或分开多个FB。
TYPE eMainState : (Mode_Prod, Mode_Maint, Mode_Debug);
TYPE eSubState_Prod : (Sub_Load, Sub_Process, Sub_Check, Sub_Unload);
VAR
mainState : eMainState;
subState : eSubState_Prod;
END_VAR
(*主状态机 - 模式切换*)
CASE mainState OF
Mode_Prod:
(*嵌入子状态机*)
CASE subState OF
Sub_Load: (*...*) ;
Sub_Process: (*...*) ;
(*...*)
END_CASE;
IF switchToMaint THEN
mainState := Mode_Maint;
END_IF;
Mode_Maint: (*...*);
Mode_Debug: (*...*);
END_CASE;
调试技巧
| 场景 | 操作方法 |
|---|---|
| 强制某状态 | 博途在线→变量表→修改值stateCurrent,触发一次 |
| 查看状态流 | 添加监控表,把stateCurrent拖入,观察数值跳变 |
| 抓取瞬态故障 | 在ELSE分支添加faultCode记录,配合诊断缓冲区 |
| 单步执行 | 用OB100启动复位+manualStep输入,配合按钮逐状态推进 |
掌握CASE语句状态机后,你会发现:原本需要几十行梯形图的互锁网络,现在十几个CASE分支清晰搞定;原本改一处逻辑要翻三页程序,现在上下扫一眼就能定位。这才是SCL该有的效率。

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