8. 状态模式:状态机实现、状态表驱动、状态切换逻辑

状态模式,说白了就是让对象的行为随着内部状态的改变而改变。你想想看,嵌入式系统里到处都是状态——按键按下、松开,设备初始化、运行、休眠,通信协议的空闲、接收、发送……每个状态下的行为都不一样,而且状态之间还有复杂的转换关系。

我刚开始做嵌入式那会儿,处理状态切换就是一堆 if-else 嵌套。代码写到后面,自己都看不懂了。后来接触了状态模式,才算是找到了正解。

8.1 状态机的三种实现方式

状态机的实现,我个人习惯分成三种:嵌套 if-elseswitch-case状态表驱动。前两种大家应该都用过,咱们重点聊聊状态表驱动。

8.1.1 嵌套 if-else 与 switch-case

先看个简单的例子。一个交通灯系统,有红灯、绿灯、黄灯三种状态。用 switch-case 实现是这样的:

typedef enum {
    STATE_RED,
    STATE_GREEN,
    STATE_YELLOW
} TrafficLightState;

void traffic_light_run(TrafficLightState state) {
    switch (state) {
        case STATE_RED:
            turn_on_red();
            // 延时后切换到绿灯
            break;
        case STATE_GREEN:
            turn_on_green();
            // 延时后切换到黄灯
            break;
        case STATE_YELLOW:
            turn_on_yellow();
            // 延时后切换到红灯
            break;
        default:
            break;
    }
}

这种写法简单直观,但问题也很明显。状态一多,case 分支就膨胀。而且每个状态里的行为逻辑和切换逻辑混在一起,改一个地方可能影响全局。

我曾经在一个项目里接手过别人写的代码,一个函数里嵌套了七八层 if-else,状态变量有十几个。改一个 bug 要花半天时间梳理逻辑。从那以后,我坚决不用这种写法处理复杂状态机。

8.1.2 状态表驱动

状态表驱动,说白了就是把状态和事件的关系抽出来,放到一张表里。这张表告诉你:当前在什么状态,来了什么事件,应该执行什么动作,然后切换到什么状态。

先定义状态和事件:

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED,
    STATE_STOPPED
} State;

typedef enum {
    EVENT_START,
    EVENT_PAUSE,
    EVENT_RESUME,
    EVENT_STOP
} Event;

然后定义状态表的结构:

typedef struct {
    State   current_state;
    Event   event;
    void    (*action)(void);
    State   next_state;
} StateTableEntry;

接着填充状态表:

StateTableEntry state_table[] = {
    {STATE_IDLE,    EVENT_START,  action_start,  STATE_RUNNING},
    {STATE_RUNNING, EVENT_PAUSE,  action_pause,  STATE_PAUSED},
    {STATE_RUNNING, EVENT_STOP,   action_stop,   STATE_STOPPED},
    {STATE_PAUSED,  EVENT_RESUME, action_resume, STATE_RUNNING},
    {STATE_PAUSED,  EVENT_STOP,   action_stop,   STATE_STOPPED},
    // ... 其他条目
};

最后是状态机的执行引擎:

State current_state = STATE_IDLE;

void state_machine_run(Event event) {
    for (int i = 0; i < sizeof(state_table)/sizeof(state_table[0]); i++) {
        if (state_table[i].current_state == current_state &&
            state_table[i].event == event) {
            if (state_table[i].action) {
                state_table[i].action();
            }
            current_state = state_table[i].next_state;
            return;
        }
    }
    // 未找到匹配项,可以记录错误或忽略
}

核心思想:状态表把「状态-事件-动作-下一状态」的映射关系集中管理。增加新状态或新事件,只需要在表里加一行,不需要修改核心引擎代码。

8.2 状态切换逻辑的细节

状态切换看着简单,但实际项目中坑不少。我总结几个关键点。

8.2.1 进入动作与退出动作

很多时候,进入一个状态和离开一个状态需要执行特定的操作。比如进入休眠状态前要保存上下文,离开休眠状态后要恢复外设配置。

我建议在状态表里增加两个字段:entry_actionexit_action。进入状态时调用 entry_action,离开状态时调用 exit_action。

typedef struct {
    State   current_state;
    Event   event;
    void    (*entry_action)(void);
    void    (*action)(void);
    void    (*exit_action)(void);
    State   next_state;
} StateTableEntry;

执行引擎也要相应调整:

void state_machine_run(Event event) {
    for (int i = 0; i < sizeof(state_table)/sizeof(state_table[0]); i++) {
        if (state_table[i].current_state == current_state &&
            state_table[i].event == event) {
            // 退出当前状态
            if (state_table[i].exit_action) {
                state_table[i].exit_action();
            }
            // 执行动作
            if (state_table[i].action) {
                state_table[i].action();
            }
            // 进入新状态
            current_state = state_table[i].next_state;
            if (state_table[i].entry_action) {
                state_table[i].entry_action();
            }
            return;
        }
    }
}

小技巧:entry_action 和 exit_action 可以用来做状态切换时的资源管理。比如进入某个状态时申请内存,退出时释放。这样不容易遗漏。

8.2.2 状态表的优化

状态表如果很大,线性查找可能影响性能。我常用的优化方法有两种:

  • 二维数组索引:用状态和事件作为二维数组的下标,直接定位到对应的表项。查找时间固定为 O(1)。
  • 哈希表:如果状态和事件的数量都很大,可以用哈希表加速查找。

二维数组的方式在嵌入式里更常见:

#define MAX_STATE  4
#define MAX_EVENT  4

StateTableEntry state_matrix[MAX_STATE][MAX_EVENT];

void state_machine_init(void) {
    // 初始化所有表项为无效
    for (int s = 0; s < MAX_STATE; s++) {
        for (int e = 0; e < MAX_EVENT; e++) {
            state_matrix[s][e].action = NULL;
            state_matrix[s][e].next_state = s; // 默认不切换
        }
    }
    // 设置有效转换
    state_matrix[STATE_IDLE][EVENT_START] = (StateTableEntry){
        .action = action_start,
        .next_state = STATE_RUNNING
    };
    // ... 其他设置
}

void state_machine_run(Event event) {
    StateTableEntry *entry = &state_matrix[current_state][event];
    if (entry->action) {
        entry->action();
        current_state = entry->next_state;
    }
}

这种方式查找速度快,但内存占用固定。如果状态和事件数量不多,很实用。

8.3 状态模式在嵌入式中的典型应用

我在项目中用过状态模式的地方不少。举几个例子:

  • 按键扫描:空闲、消抖、按下、长按、释放,每个状态处理不同的按键逻辑。
  • 通信协议:比如 I2C 的起始、地址、数据、停止状态,每个状态处理不同的字节。
  • 电源管理:运行、空闲、休眠、深度休眠,不同状态下的功耗策略不同。

我记得有一次做电池供电的设备,电源管理状态机有七八个状态。用状态表驱动实现后,新增一个「低电量休眠」状态,只需要在表里加两行代码,测试也很快通过。

注意:状态表驱动虽然好,但也不是万能的。如果状态之间的转换逻辑非常复杂,比如同一个事件在不同条件下走向不同状态,那状态表就不好表达了。这时候可以考虑用状态模式 + 条件判断的组合方式。

8.4 状态模式的核心知识体系

下面这张图总结了状态模式的核心逻辑,包括三种实现方式、状态表的结构以及状态切换的完整流程。

状态模式核心知识体系 三种实现方式 嵌套 if-else switch-case 状态表驱动(推荐) 状态表结构 当前状态 事件 动作函数指针 下一状态 状态切换流程 接收事件 查找状态表 执行退出动作 执行动作 更新当前状态 执行进入动作 核心优势:将状态与行为解耦,新增状态只需添加表项,无需修改核心逻辑 适用场景:按键扫描、通信协议、电源管理、UI导航等嵌入式系统

8.5 避坑指南

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

  • 状态表不要太大:如果状态和事件都超过 20 个,表会变得很庞大。这时候考虑分层状态机,把大状态拆成小状态。
  • 注意空指针:动作函数指针可能为 NULL,调用前一定要判断。我见过因为忘记判断导致跑飞的案例。
  • 状态切换的原子性:如果在中断里执行状态切换,要注意保护临界区。否则可能切换一半被打断,状态不一致。
  • 默认处理:状态表里找不到匹配项时,要有默认处理逻辑。可以记录日志,或者保持当前状态不变。

嗯,状态模式就聊到这儿。记住一句话:状态表驱动不是银弹,但对付大部分嵌入式状态机场景,它足够优雅且可靠。


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