第一次接触倍福PLC编程时,最让我头疼的就是如何实现一个简单的流水灯效果。当时我按照传统思路,试图用一堆TON延时功能块串联起来控制LED灯的亮灭顺序,结果代码越写越长,逻辑越来越混乱。直到发现ST语言中的CASE OF语法结合R_TRIG功能块,才真正理解了PLC顺序控制的精髓——不是靠功能块堆砌,而是建立清晰的状态机思维。
很多PLC初学者(包括当年的我)最容易掉入的陷阱,就是试图用延时功能块串联来实现顺序控制。让我们先看一个典型反面案例:
pascal复制VAR
tDelay1 : TON;
tDelay2 : TON;
tDelay3 : TON;
iCounter : INT;
bStart : BOOL;
END_VAR
tDelay1(IN:=bStart, PT:=T#2S);
IF tDelay1.Q THEN
iCounter := 1;
tDelay1(IN:=FALSE);
tDelay2(IN:=TRUE, PT:=T#2S);
END_IF
tDelay2(IN:= , PT:=T#2S);
IF tDelay2.Q THEN
iCounter := 2;
tDelay2(IN:=FALSE);
tDelay3(IN:=TRUE, PT:=T#2S);
END_IF
// ...更多延时块串联
这种写法存在几个明显问题:
提示:TON延时块更适合用于独立的时间控制场景,而非流程步骤间的顺序控制
ST语言中的CASE OF语法(类似C语言的switch-case)配合状态变量,可以构建清晰的状态机结构。基本框架如下:
pascal复制VAR
iState : INT := 0; // 状态变量
rTrigStart : R_TRIG; // 上升沿检测
END_VAR
rTrigStart(CLK:=bStart);
IF rTrigStart.Q THEN
iState := 1; // 触发状态切换
END_IF
CASE iState OF
0: // 初始状态
// 初始化操作
1: // 状态1
// 执行动作A
iState := 2; // 状态转移
2: // 状态2
// 执行动作B
iState := 3;
// ...更多状态
ELSE
// 默认处理
END_CASE
这种模式的优势显而易见:
让我们用一个完整的流水灯示例展示如何应用这种模式。假设我们需要控制5个LED灯依次点亮,然后全部熄灭后循环。
首先定义状态变量和硬件IO映射:
pascal复制VAR
iState : INT := -1; // -1=停止, 0=初始, 1-5=点亮对应LED
rTrigStart : R_TRIG;
rTrigStop : R_TRIG;
bRun : BOOL := FALSE;
// 假设使用EL2809数字量输出模块
aLEDs AT %Q* : ARRAY[0..4] OF BOOL;
END_VAR
完整的状态机实现代码如下:
pascal复制// 启停控制
rTrigStart(CLK:=bStart);
rTrigStop(CLK:=bStop);
IF rTrigStart.Q THEN
bRun := TRUE;
iState := 0; // 回到初始状态
ELSIF rTrigStop.Q THEN
bRun := FALSE;
iState := -1; // 停止状态
END_IF
// 主状态机
CASE iState OF
-1: // 停止状态
// 关闭所有LED
aLEDs[0] := FALSE;
aLEDs[1] := FALSE;
aLEDs[2] := FALSE;
aLEDs[3] := FALSE;
aLEDs[4] := FALSE;
0: // 初始状态
IF bRun THEN
aLEDs[0] := TRUE; // 点亮第一个LED
iState := 1; // 进入状态1
END_IF
1: // LED1亮
IF bRun THEN
aLEDs[1] := TRUE; // 点亮第二个LED
iState := 2;
ELSE
iState := -1;
END_IF
2: // LED2亮
IF bRun THEN
aLEDs[2] := TRUE;
iState := 3;
ELSE
iState := -1;
END_IF
3: // LED3亮
IF bRun THEN
aLEDs[3] := TRUE;
iState := 4;
ELSE
iState := -1;
END_IF
4: // LED4亮
IF bRun THEN
aLEDs[4] := TRUE;
iState := 5;
ELSE
iState := -1;
END_IF
5: // 全部点亮后复位
IF bRun THEN
// 关闭所有LED
aLEDs[0] := FALSE;
aLEDs[1] := FALSE;
aLEDs[2] := FALSE;
aLEDs[3] := FALSE;
aLEDs[4] := FALSE;
iState := 0; // 回到初始状态循环
ELSE
iState := -1;
END_IF
END_CASE
如果需要每个状态保持固定时间,可以结合TON块实现:
pascal复制VAR
tStateTimer : TON;
END_VAR
// 在状态机中添加时间控制
CASE iState OF
// ...其他状态
1: // LED1亮
tStateTimer(IN:=TRUE, PT:=T#1S); // 定时1秒
IF tStateTimer.Q THEN
aLEDs[1] := TRUE;
tStateTimer(IN:=FALSE); // 复位定时器
iState := 2;
END_IF
// ...后续状态同样处理
END_CASE
掌握了基本模式后,我们可以进一步优化状态机实现:
对于复杂流程,建议使用枚举类型或常量定义状态:
pascal复制VAR_GLOBAL CONSTANT
STATE_IDLE : INT := 0;
STATE_STEP1 : INT := 1;
STATE_STEP2 : INT := 2;
// ...更多状态
END_VAR
将状态机封装为功能块(FC/FB)提高复用性:
pascal复制FUNCTION_BLOCK FB_LightSequencer
VAR_INPUT
bStart : BOOL;
bStop : BOOL;
tStepTime : TIME := T#1S;
END_VAR
VAR_OUTPUT
aLEDs : ARRAY[0..4] OF BOOL;
END_VAR
VAR
iState : INT;
rTrigStart : R_TRIG;
rTrigStop : R_TRIG;
tTimer : TON;
END_VAR
// 实现代码...
对于复杂状态机,可以先设计状态转移表:
| 当前状态 | 条件 | 动作 | 下一状态 |
|---|---|---|---|
| IDLE | 启动 | 初始化 | STEP1 |
| STEP1 | 定时到 | 点亮LED1 | STEP2 |
| STEP2 | 定时到 | 点亮LED2 | STEP3 |
| ... | ... | ... | ... |
pascal复制VAR
sStateDesc : STRING;
END_VAR
CASE iState OF
0: sStateDesc := 'IDLE';
1: sStateDesc := 'STEP1';
// ...
END_CASE
从TON堆砌到状态机思维的转变,是我PLC编程能力提升的关键转折点。在实际项目中,这种模式不仅适用于简单流水灯,还能轻松扩展到复杂工艺流程控制。记住,好的PLC程序不是功能块的简单串联,而是清晰的状态与逻辑表达。