3. 基于状态机的切换逻辑:有限状态机(FSM)设计、状态转移条件、状态动作定义

说到控制策略切换,我个人最推崇的方法就是有限状态机。为什么?因为它在复杂逻辑里能保持清晰,在混乱现场能守住秩序。说白了,FSM 就是给系统画了一张「行为地图」—— 当前在哪、能去哪、怎么去,一目了然。

我记得刚入行那会儿,接手过一个多段温控项目。前任工程师用了一堆 if-else 嵌套,足足 800 多行。每次加一个新工况,代码就崩一次。后来我全部重构成 FSM,状态数从 12 个减到 7 个,代码量砍了一半,稳定性反而上去了。嗯,这就是状态机的魅力。

3.1 有限状态机的基本概念

有限状态机,英文叫 Finite State Machine,简称 FSM。它由三部分组成:状态转移条件动作

  • 状态(State):系统在某一时刻所处的稳定工况。比如「预热中」、「恒温中」、「冷却中」。
  • 转移条件(Transition Condition):触发状态切换的事件或条件。比如「温度到达设定值」、「定时器超时」。
  • 动作(Action):进入某个状态后执行的操作。比如「开启加热器」、「关闭风扇」。

你想想看,这其实跟我们日常做事很像。比如你开车:状态有「停车」、「行驶」、「转弯」。条件呢?踩油门就进入行驶,打方向盘就进入转弯。动作就是对应的操作。FSM 就是把这种直觉逻辑,用工程化的方式固定下来。

核心原则: 一个 FSM 在任意时刻只能处于一个状态。这是它稳定性的根基。

3.2 状态转移条件的设计

转移条件,是 FSM 的灵魂。条件设计得好,系统切换丝滑;设计得不好,就会产生「状态抖动」—— 在两个状态之间来回跳,系统根本稳不住。

我在项目中遇到过最典型的例子:一个液位控制系统,液位在临界点附近波动,导致状态在「补水」和「停止」之间高频切换,水泵一天启停 200 多次。后来怎么解决的?加了一个 滞回区间

滞回区间说白了就是:进入条件 A 和退出条件 A 使用不同的阈值。比如:

  • 液位低于 20% → 进入「补水」状态
  • 液位高于 30% → 退出「补水」状态

中间这 10% 的区间,就是缓冲区。系统不会因为微小波动而频繁切换。

我的习惯: 每个转移条件都加一个「去抖延时」。比如条件满足后,再等 50ms 确认一下,才真正执行转移。这能过滤掉大部分噪声干扰。

3.3 状态动作的定义

动作分为三类:

  1. 进入动作(Entry Action):进入状态时执行一次。比如打开阀门、记录时间戳。
  2. 持续动作(Do Action):在状态内持续执行。比如 PID 调节、数据采集。
  3. 退出动作(Exit Action):离开状态时执行一次。比如关闭设备、保存日志。

我曾经犯过一个低级错误:把「关闭加热器」写在了持续动作里,结果每次循环都执行一次关闭指令。虽然逻辑上没错,但继电器被频繁开关,寿命大打折扣。后来改成退出动作,只在离开「加热」状态时执行一次,问题就解决了。

注意: 动作的执行顺序很重要。标准的 FSM 执行顺序是:先执行当前状态的退出动作 → 更新状态 → 执行新状态的进入动作 → 执行持续动作。千万别搞反了。

3.4 实战案例:三段式温度控制

下面是一个典型的 FSM 实现,用 C 语言伪代码展示。这个例子控制一个加热炉,有三个状态:升温、保温、降温。

// 状态定义
typedef enum {
    STATE_HEAT_UP,   // 升温
    STATE_HOLD,      // 保温
    STATE_COOL_DOWN  // 降温
} SystemState;

// 当前状态
SystemState currentState = STATE_HEAT_UP;

// 状态机主循环
void FSM_Run(void) {
    switch(currentState) {
        case STATE_HEAT_UP:
            // 持续动作:全功率加热
            Heater_SetPower(100);
            
            // 转移条件:温度达到目标
            if(Temperature_Get() >= TARGET_TEMP) {
                // 退出动作:记录保温开始时间
                holdStartTime = GetTick();
                // 状态转移
                currentState = STATE_HOLD;
                // 进入动作:切换到 PID 控制
                PID_Init();
            }
            break;
            
        case STATE_HOLD:
            // 持续动作:PID 调节
            PID_Run();
            
            // 转移条件:保温时间到
            if((GetTick() - holdStartTime) >= HOLD_DURATION) {
                // 退出动作:关闭加热器
                Heater_SetPower(0);
                // 状态转移
                currentState = STATE_COOL_DOWN;
                // 进入动作:开启风扇
                Fan_Start();
            }
            break;
            
        case STATE_COOL_DOWN:
            // 持续动作:自然冷却 + 风扇辅助
            Fan_Run();
            
            // 转移条件:温度低于安全值
            if(Temperature_Get() <= SAFE_TEMP) {
                // 退出动作:关闭风扇
                Fan_Stop();
                // 状态转移:回到初始状态
                currentState = STATE_HEAT_UP;
                // 进入动作:重置计时器
                Reset_Timer();
            }
            break;
    }
}

这段代码看起来简单,但包含了 FSM 的全部要素。每个 case 里,我都把「持续动作」、「转移条件」、「退出动作」、「进入动作」分得清清楚楚。这样写,三个月后回来看,自己也能秒懂。

3.5 FSM 的图形化表达

文字描述总是不够直观。下面我用 SVG 画了一张状态转移图,把刚才的三段式温控逻辑可视化出来。你一看就明白。

升温 STATE_HEAT_UP 保温 STATE_HOLD 降温 STATE_COOL_DOWN 温度 ≥ 目标值 保温时间到 温度 ≤ 安全值 图例: 状态节点 转移箭头(带条件) 每个状态包含:进入动作、持续动作、退出动作

这张图里,三个状态形成一个闭环。升温到保温的条件是「温度达到目标值」,保温到降温的条件是「保温时间到」,降温回到升温的条件是「温度低于安全值」。每个箭头上的文字,就是转移条件。

3.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点调试时间。

  • 状态爆炸:别把每个细节都定义成独立状态。我见过有人把「加热 1 秒」、「加热 2 秒」都做成状态,结果状态数飙到 50 多个。其实用定时器变量就能解决。
  • 死锁状态:确保每个状态都有出口。我曾经漏写了一个转移条件,系统卡在「初始化」状态里出不来,看门狗都救不了。
  • 状态跳转:非必要时不要跨状态跳转。比如从「升温」直接跳到「降温」,中间跳过了「保温」。这种跳转容易导致动作遗漏,设备可能出问题。
  • 日志记录:每次状态切换都打印一条日志。我习惯记录「时间 + 旧状态 + 转移条件 + 新状态」。调试时翻日志,问题定位快得飞起。

一个小技巧: 在代码里加一个「状态机健康检查」函数。每隔一段时间,检查当前状态是否合法、转移条件是否合理。这在现场调试时特别有用,能提前发现潜在问题。

好了,关于 FSM 的设计、转移条件和动作定义,就聊到这儿。这套方法我用了十几年,从单片机到 PLC 再到上位机,几乎每个项目都离不开它。你试试看,用 FSM 重构一次你的切换逻辑,你会发现世界清静了很多。


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