一、状态机编程:从入门到实战

大家好,我是老张。在风电行业摸爬滚打了十几年,今天咱们聊聊状态机编程。

说实话,我刚入行那会儿,写PLC程序全靠一锅粥式的逻辑。一个功能块里塞满各种条件判断,调试起来那叫一个痛苦。后来接触了状态机,才明白什么叫「结构化编程」。今天这节内容,我希望能帮你彻底搞懂状态机。

1. 状态机的基本概念

状态机,说白了就是一个「有限状态自动机」。它有三个核心要素:

  • 状态(State):系统当前所处的模式或阶段
  • 事件(Event):触发状态切换的条件
  • 动作(Action):进入某个状态后执行的操作

举个例子,你想想看家里的电风扇。它有「停止」「低速」「中速」「高速」四个状态。你按一下按钮,它就切换一次状态。这就是最简单的状态机。

核心思想:系统在任何时刻只能处于一个状态,状态之间的切换由事件驱动。

我在项目中遇到过不少同事,把状态机想得太复杂。其实你只要记住一句话:状态机 = 状态 + 转移条件 + 动作。就这么简单。

2. 为什么风电PLC需要状态机?

风电系统有多复杂?一个偏航系统,就有待机、对风、解缆、紧急停止等十几个状态。如果用传统的梯形图硬写,代码量至少翻三倍,而且维护起来简直是噩梦。

状态机的好处很明显:

  • 逻辑清晰:每个状态独立,互不干扰
  • 调试方便:出问题了,看一眼当前状态就知道问题在哪
  • 扩展性强:加一个新状态,不影响其他状态
  • 可读性好:新人接手,看状态图就能理解逻辑

我的经验:状态机写得好不好,直接决定你后期调试要加多少班。我见过太多因为状态混乱导致的「幽灵故障」——明明条件满足了,程序就是不按预期走。最后查出来,都是状态切换逻辑没处理好。

3. 在PLC中实现状态机的三种方法

嗯,这里要注意。PLC实现状态机,主要有三种方式。每种都有它的适用场景,我一个个说。

方法一:IF-ELSIF 实现状态机

这是最直观的方法。用一个变量存储当前状态,然后用IF-ELSIF判断当前状态,执行对应的逻辑。

// 伪代码示例
CASE currentState OF
  0: // 待机状态
    IF startCondition THEN
      currentState := 1;
    END_IF
    
  1: // 运行状态
    IF stopCondition THEN
      currentState := 0;
    ELSIF errorCondition THEN
      currentState := 2;
    END_IF
    
  2: // 故障状态
    IF resetCondition THEN
      currentState := 0;
    END_IF
END_CASE

说实话,这种方法适合状态数量少(5个以内)的简单场景。状态多了,代码会变得又臭又长。我曾经在一个项目里用IF-ELSIF写了20个状态,调试的时候差点没把自己绕晕。

避坑指南:用IF-ELSIF时,一定要注意状态变量的初始化。我曾经因为忘记给状态变量赋初值,导致程序一启动就跑到一个未定义状态,风机直接报错停机。那叫一个尴尬。

方法二:CASE 语句实现状态机

这个方法比IF-ELSIF优雅得多。CASE语句天然就是为状态机设计的,结构清晰,可读性强。

// 使用CASE语句实现状态机
CASE state OF
  STATE_IDLE:
    // 待机逻辑
    IF startCmd THEN
      state := STATE_RUNNING;
    END_IF
    
  STATE_RUNNING:
    // 运行逻辑
    IF stopCmd THEN
      state := STATE_IDLE;
    ELSIF fault THEN
      state := STATE_FAULT;
    END_IF
    
  STATE_FAULT:
    // 故障处理逻辑
    IF resetCmd THEN
      state := STATE_IDLE;
    END_IF
    
  ELSE:
    // 默认处理,防止未定义状态
    state := STATE_IDLE;
END_CASE

我个人习惯用CASE语句。它比IF-ELSIF更直观,每个状态独立成块,修改一个状态不会影响其他状态。而且,CASE语句的执行效率也比多层IF嵌套高。

小技巧:定义状态时,用枚举类型或者常量,别直接用数字。比如用STATE_IDLE := 0代替直接写0。这样代码一看就懂,不用猜数字代表什么状态。

方法三:SFC(顺序功能图)实现状态机

SFC是IEC 61131-3标准中的一种编程语言,专门用来描述顺序控制逻辑。它用「步」表示状态,用「转移条件」表示事件,简直就是为状态机量身定做的。

SFC的好处是图形化,一眼就能看出整个控制流程。但缺点也很明显:

  • 不是所有PLC都支持SFC
  • 调试时不能像文本语言那样方便地加断点
  • 复杂逻辑用SFC画出来,图会变得很大

我个人建议:如果PLC支持SFC,而且你的控制逻辑是典型的顺序控制(比如启动流程、停止流程),用SFC会很爽。但如果是复杂的条件判断,还是用CASE更灵活。

4. 案例:偏航系统状态机

好了,理论说完了,咱们来个实战案例。偏航系统,是风机里最典型的状态机应用之一。

偏航系统的主要功能是让机舱始终对准风向。它有几个关键状态:

状态 描述 触发条件
IDLE 待机状态 系统上电或复位
YAWING 偏航进行中 风向偏差超过阈值
WAITING 偏航等待 偏航到位后等待下一次动作
UNCABLE 解缆状态 电缆缠绕超过设定圈数
FAULT 故障状态 偏航系统出现异常

下面我用CASE语句实现这个状态机:

// 偏航系统状态机 - CASE实现
CASE yawState OF
  YAW_IDLE:
    // 待机状态:检测风向偏差
    IF ABS(windDir - nacelleDir) > yawThreshold THEN
      yawState := YAW_YAWING;
      yawDirection := windDir > nacelleDir ? CW : CCW;
    END_IF
    
  YAW_YAWING:
    // 偏航进行中:执行偏航动作
    yawMotor := yawDirection;
    // 检测是否到位
    IF ABS(windDir - nacelleDir) < yawDeadband THEN
      yawState := YAW_WAITING;
      yawMotor := STOP;
      waitTimer := T#5S;
    END_IF
    // 检测是否需要解缆
    IF cableTwists > maxTwists THEN
      yawState := YAW_UNCABLE;
    END_IF
    
  YAW_WAITING:
    // 等待状态:防止频繁偏航
    IF waitTimer.Q THEN
      yawState := YAW_IDLE;
    END_IF
    
  YAW_UNCABLE:
    // 解缆状态:反向旋转解缆
    yawMotor := REVERSE;
    IF cableTwists <= 0 THEN
      yawState := YAW_IDLE;
      yawMotor := STOP;
    END_IF
    
  YAW_FAULT:
    // 故障状态:停止所有动作
    yawMotor := STOP;
    alarm := TRUE;
    IF resetCmd AND faultCleared THEN
      yawState := YAW_IDLE;
      alarm := FALSE;
    END_IF
    
  ELSE:
    // 默认处理
    yawState := YAW_IDLE;
END_CASE

你看,每个状态独立成块,逻辑一目了然。如果后期要加一个新状态,比如「手动偏航」,只需要在CASE里加一个分支就行,完全不影响其他状态。

关键点:状态机编程的核心是「状态隔离」。每个状态只关心自己的事情,不要跨状态操作变量。这样即使出了问题,也能快速定位到是哪个状态出的问题。

5. 状态机编程的避坑指南

最后,分享几个我踩过的坑:

  • 状态变量一定要初始化:PLC上电时,状态变量可能是随机值。一定要在初始化程序里赋一个有效状态。
  • 小心状态跳转的死循环:比如状态A跳转到B,B又跳回A,中间没有延时或条件限制。程序会卡死在这两个状态之间。
  • 别忘了ELSE分支:CASE语句一定要加ELSE,处理未定义的状态。我曾经见过一个项目,因为状态变量被意外修改,程序跑到了一个未定义状态,整个系统瘫痪。
  • 状态切换时注意输出处理:离开一个状态时,记得把该状态的输出复位。否则会出现「幽灵输出」——明明已经离开这个状态了,输出还保持在上一个状态的值。

我的习惯:每个状态都用一个独立的函数块或子程序实现。这样不仅代码整洁,而且方便单元测试。调试的时候,我可以单独测试每个状态的行为,不用跑整个程序。

好了,状态机编程的基本内容就这些。记住,状态机不是银弹,但它绝对是PLC编程中最实用的设计模式之一。下次写程序前,先画个状态图,你会发现代码质量提升不止一个档次。


公众号:蓝海资料掘金营,微信deep3321