第三章 状态机设计:有限状态机(FSM)基础、剃须刀工作状态建模、状态转换表设计

状态机这东西,说白了就是嵌入式系统的「大脑」。我做了十几年嵌入式,见过太多因为状态混乱导致的bug——电机该转时不转,不该转时瞎转,甚至直接死机。嗯,今天我们就来聊聊怎么用有限状态机把剃须刀管得服服帖帖。

3.1 有限状态机(FSM)基础

先问个问题:你写if-else的时候,有没有遇到过这种情况?

if (button_pressed && motor_running) {
    // 停止电机
} else if (button_pressed && !motor_running) {
    // 启动电机
} else if (battery_low && motor_running) {
    // 强制停止
}

代码一多,逻辑就乱成一锅粥。我早期一个项目就吃过这个亏——剃须刀在充电时误触开关,电机突然转起来,差点把测试员的手指刮伤。从那以后,我强制团队所有嵌入式代码必须用状态机来管理主逻辑。

有限状态机有三个核心要素:

  • 状态(State):系统当前所处的模式。比如剃须刀的「待机」、「运行」、「充电」。
  • 事件(Event):触发状态变化的条件。比如按键按下、电池电量低、充电器插入。
  • 动作(Action):进入或离开状态时执行的操作。比如启动电机、点亮LED、关闭电源。

核心原则:一个状态机在任意时刻只能处于一个状态。事件驱动状态转换,转换时执行动作。就这么简单,但很多人写代码时就是记不住。

3.2 剃须刀工作状态建模

好,我们拿剃须刀来实战。我个人习惯先把所有可能的状态列出来,画一张图在脑子里过一遍。

剃须刀的主要状态:

  • IDLE(待机):默认状态,电机停转,等待用户操作。
  • RUNNING(运行):电机转动,正在剃须。
  • CHARGING(充电):插入充电器,电池在充电。
  • LOW_BATTERY(低电量):电池电压低于阈值,电机强制停止。
  • ERROR(故障):电机堵转、温度过高或其他异常。

你想想看,如果不用状态机,用全局变量加if-else来管理这5个状态,代码会变成什么样子?我曾经接手过一个项目,光状态判断就嵌套了6层if,改一个bug要花三天。后来我全部重构成状态机,两天就搞定了。

我的经验:状态数量控制在5~8个最合适。少于3个没必要用状态机,多于12个就要考虑分层状态机了。剃须刀这种消费电子产品,5个状态刚刚好。

3.3 状态转换表设计

状态建模完成后,下一步就是设计状态转换表。这张表是状态机的「宪法」,所有逻辑都得按它来。

状态转换表格式:

当前状态 事件 下一状态 动作
IDLE 按键按下 RUNNING 启动电机,点亮运行指示灯
RUNNING 按键按下 IDLE 停止电机,熄灭运行指示灯
RUNNING 电池低电量 LOW_BATTERY 停止电机,闪烁低电量指示灯
IDLE 充电器插入 CHARGING 点亮充电指示灯,禁止启动电机
CHARGING 充电器拔出 IDLE 熄灭充电指示灯,恢复启动功能
LOW_BATTERY 充电器插入 CHARGING 停止闪烁,点亮充电指示灯
任何状态 故障发生 ERROR 停止所有操作,显示故障码
ERROR 故障恢复 IDLE 清除故障码,恢复待机

这张表看起来简单,但实际项目中容易踩坑。我举个例子:

我曾经踩过的坑:在RUNNING状态下,如果同时发生「按键按下」和「电池低电量」两个事件怎么办?很多新手会写两个if判断,结果状态跳来跳去。正确的做法是给事件定义优先级——低电量事件优先级高于按键事件。所以表中只保留「电池低电量→LOW_BATTERY」这一条转换,按键事件在低电量状态下被忽略。

有了状态转换表,代码实现就水到渠成了。我一般用switch-case来实现,清晰又高效:

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_CHARGING,
    STATE_LOW_BATTERY,
    STATE_ERROR
} shaver_state_t;

typedef enum {
    EVENT_BUTTON_PRESS,
    EVENT_LOW_BATTERY,
    EVENT_CHARGER_INSERT,
    EVENT_CHARGER_REMOVE,
    EVENT_FAULT,
    EVENT_FAULT_CLEAR
} shaver_event_t;

shaver_state_t current_state = STATE_IDLE;

void shaver_fsm_run(shaver_event_t event) {
    switch (current_state) {
        case STATE_IDLE:
            if (event == EVENT_BUTTON_PRESS) {
                motor_start();
                led_run_on();
                current_state = STATE_RUNNING;
            } else if (event == EVENT_CHARGER_INSERT) {
                led_charge_on();
                motor_disable();
                current_state = STATE_CHARGING;
            }
            break;

        case STATE_RUNNING:
            if (event == EVENT_LOW_BATTERY) {
                motor_stop();
                led_low_battery_blink();
                current_state = STATE_LOW_BATTERY;
            } else if (event == EVENT_BUTTON_PRESS) {
                motor_stop();
                led_run_off();
                current_state = STATE_IDLE;
            } else if (event == EVENT_FAULT) {
                motor_stop();
                led_error_on();
                current_state = STATE_ERROR;
            }
            break;

        // 其他状态处理类似...
    }
}

小技巧:我习惯把状态转换表直接写成注释放在代码头部。这样半年后回来维护,看一眼注释就知道整个逻辑,不用一行行读代码。另外,每个状态的处理函数单独抽出来,别全塞在switch里,否则代码会膨胀到没法看。

最后说一句:状态机不是银弹。如果你的状态之间没有明确的转换关系,或者事件触发极其复杂,那可能是设计本身有问题。我见过有人把10个状态硬塞进状态机,结果转换表画了3页纸,代码比if-else还难维护。嗯,这时候就该考虑分层状态机或者状态模式了——那是我们后面章节要聊的内容。