1. 状态机基础:什么是状态机、状态机的三要素、在嵌入式中的应用

大家好,我是你们的老朋友。今天咱们开始聊《按摩仪多模式切换状态机设计》这门课。第一节课,我想先带大家把状态机的基础打牢。

说实话,我刚开始做嵌入式那会儿,对状态机也是一知半解。觉得这东西不就是个 switch-case 嘛,有啥好学的?后来踩了不少坑,才明白——状态机是嵌入式系统的灵魂之一。你想想看,一个按摩仪要切换揉捏、敲打、热敷、组合模式,还要响应按键、定时、过温保护……没有状态机,代码写出来就是一坨浆糊。

什么是状态机?

状态机,全称是有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于有限个状态中的一个。它根据输入的事件,从一个状态跳转到另一个状态,同时执行相应的动作。

我习惯用一个比喻来理解:状态机就像一台自动售货机。它可能处于“待机”、“投币中”、“选择商品”、“出货中”这几个状态。你投币(事件),它从“待机”跳到“投币中”(状态转移),同时点亮投币指示灯(动作)。就这么简单。

核心要点:状态机不是万能的,但没有状态机的嵌入式代码,往往是灾难性的。我在项目中见过一个同事,用一堆全局变量和 if-else 来管理按摩仪的模式切换,结果加了三个模式之后,代码就再也维护不下去了。

状态机的三要素:状态、事件、动作

这三个要素,是状态机的基石。咱们一个一个说。

1. 状态(State)

状态,就是系统在某一时刻所处的稳定情形。比如按摩仪的“待机状态”、“揉捏状态”、“敲打状态”、“热敷状态”、“组合模式状态”。

注意,状态必须是互斥且完备的。互斥的意思是,同一时刻只能处于一个状态;完备的意思是,所有可能的情况都要覆盖到。嗯,这里要注意:很多新手容易漏掉“空闲”或“错误”状态,导致系统跑飞。

2. 事件(Event)

事件,是触发状态转移的外部或内部信号。比如按键按下、定时器超时、传感器检测到温度过高、串口收到指令等等。

我个人习惯把事件分为三类:

  • 外部事件:用户按键、红外遥控、蓝牙指令
  • 内部事件:定时器溢出、ADC转换完成、看门狗复位
  • 系统事件:上电初始化、低电压检测、故障自检

我曾经在一个项目中,因为没处理好“按键抖动”这个事件,导致按摩仪在揉捏和敲打之间疯狂跳转。后来加了一个20ms的去抖延时,问题才解决。避坑指南:事件一定要做防抖和滤波处理,尤其是在机械按键场景下。

3. 动作(Action)

动作,就是状态转移时执行的具体操作。比如:

  • 进入揉捏状态时,启动揉捏电机
  • 退出揉捏状态时,停止揉捏电机
  • 检测到过温时,关闭所有电机并点亮报警灯

动作可以发生在三个时机:

  • 进入状态时(entry action)
  • 退出状态时(exit action)
  • 状态内部(internal action,比如定时刷新显示)

我的小技巧:把动作尽量放在“进入状态”和“退出状态”里执行,不要在状态转移的“路上”做太多事。这样代码逻辑更清晰,调试时也容易定位问题。

状态机在嵌入式中的应用

状态机在嵌入式系统里,可以说是无处不在。我随便举几个例子:

应用场景 典型状态 典型事件
按键扫描 空闲、按下、抖动、长按、释放 电平变化、定时器超时
串口通信 空闲、接收头、接收数据、校验、处理 字节到达、超时、校验错误
按摩仪模式切换 待机、揉捏、敲打、热敷、组合、过温保护 按键按下、定时结束、温度超限
电池充电管理 预充、恒流充、恒压充、充满、错误 电压检测、电流检测、温度检测

你看,这些场景都有一个共同点:系统行为是由离散的事件驱动的,而且状态数量是有限的。这正是状态机的用武之地。

我印象最深的一个项目,是做一款多功能理疗仪。一开始用裸机+大循环轮询,代码里全是 if-else 嵌套,改一个功能要翻半天。后来我重构成了状态机架构,把每个模式封装成一个状态,事件通过消息队列传递。结果呢?代码量减少了40%,bug率下降了60%。

警告:状态机虽好,但不要滥用。如果你的系统状态超过20个,或者状态转移图复杂得像蜘蛛网,那就要考虑用分层状态机或者状态模式(State Pattern)了。否则,维护成本会直线上升。

一个简单的状态机代码示例

咱们用 C 语言写一个按摩仪模式切换的骨架,感受一下:

// 定义状态枚举
typedef enum {
    STATE_IDLE,       // 待机
    STATE_KNEAD,      // 揉捏
    STATE_TAP,        // 敲打
    STATE_HEAT,       // 热敷
    STATE_COMBO,      // 组合模式
    STATE_ERROR       // 错误保护
} MassageState_t;

// 定义事件枚举
typedef enum {
    EVENT_MODE_BTN,   // 模式按键
    EVENT_TIMEOUT,    // 定时超时
    EVENT_OVER_TEMP,  // 过温
    EVENT_NONE
} MassageEvent_t;

// 状态转移表(简化版)
MassageState_t next_state[6][4] = {
    // 当前状态         EVENT_MODE_BTN    EVENT_TIMEOUT    EVENT_OVER_TEMP
    [STATE_IDLE]   = { STATE_KNEAD,      STATE_IDLE,      STATE_ERROR     },
    [STATE_KNEAD]  = { STATE_TAP,        STATE_IDLE,      STATE_ERROR     },
    [STATE_TAP]    = { STATE_HEAT,       STATE_IDLE,      STATE_ERROR     },
    [STATE_HEAT]   = { STATE_COMBO,      STATE_IDLE,      STATE_ERROR     },
    [STATE_COMBO]  = { STATE_IDLE,       STATE_IDLE,      STATE_ERROR     },
    [STATE_ERROR]  = { STATE_IDLE,       STATE_ERROR,     STATE_ERROR     },
};

// 状态机处理函数
void MassageFSM_Process(MassageEvent_t event) {
    MassageState_t new_state = next_state[current_state][event];
    
    if (new_state != current_state) {
        // 执行退出动作
        ExitAction(current_state);
        // 状态转移
        current_state = new_state;
        // 执行进入动作
        EntryAction(current_state);
    }
}

这段代码虽然简单,但已经体现了状态机的核心思想:用表格驱动状态转移,把逻辑和数据分离。你想想看,如果以后要增加一个“脉冲模式”,只需要在枚举里加一个状态,在表格里加一行,再写对应的 EntryAction 和 ExitAction 就行了。完全不用动原来的代码。

总结一下:状态机不是什么高深莫测的东西。它就是一套“在什么状态,做什么事;遇到什么事件,跳到什么状态”的规则。掌握了这个思想,你写嵌入式代码就会有一种“一切尽在掌控”的感觉。

好了,第一节课就到这里。下一节,咱们会深入讲状态机的两种实现方式:switch-case 和 状态表,并对比它们的优缺点。到时候我会分享一个我踩过的坑——用 switch-case 实现了一个 15 个状态的状态机,结果代码膨胀到 2000 行……嗯,咱们下节课见。