第三章:嵌入式状态机设计模式:查表法、嵌套switch-case法、状态模式

大家好,我是你们的老朋友。今天我们来聊聊状态机的三种经典实现方式。说实话,我在嵌入式这行摸爬滚打十几年,这三种方法都反复用过。每种方法都有它的脾气,选对了事半功倍,选错了...嗯,后面调试的时候你会想哭的。

3.1 嵌套switch-case法:最直观的写法

先说说最基础的方法。你想想看,刚学编程的时候,谁没用过switch-case?把状态和事件都塞进去,简单粗暴。

// 嵌套switch-case示例
void process_event(State_t current_state, Event_t event) {
    switch(current_state) {
        case STATE_IDLE:
            switch(event) {
                case EVENT_START:
                    // 进入测量状态
                    current_state = STATE_MEASURING;
                    break;
                case EVENT_STOP:
                    // 保持空闲
                    break;
                default:
                    break;
            }
            break;
            
        case STATE_MEASURING:
            switch(event) {
                case EVENT_TIMEOUT:
                    // 测量完成
                    current_state = STATE_COMPLETE;
                    break;
                case EVENT_ERROR:
                    current_state = STATE_ERROR;
                    break;
                default:
                    break;
            }
            break;
        // ... 更多状态
    }
}

我个人习惯在小项目里用这种方法。比如一个简单的按键消抖,状态不超过5个,事件不超过3个。为什么?因为代码量少,一眼就能看明白。

优点:

  • 代码直观,新手也能看懂
  • 不需要额外数据结构
  • 调试方便,单步执行就能跟踪

缺点:

  • 状态多了以后,代码膨胀得厉害
  • 状态和事件耦合太紧,改一个地方可能影响一片
  • 可维护性差,20个状态以上基本没法看

避坑指南:我曾经在一个项目里用嵌套switch-case写了30多个状态,结果客户要加一个新功能,我改了三天,改完又测了两天,最后还是出了bug。从那以后,超过10个状态的项目,我坚决不用这种方法。

3.2 查表法:用空间换时间

接下来聊聊查表法。说白了,就是把状态转移关系存到一张表里。运行时查表决定下一步怎么走。

// 查表法示例
typedef struct {
    State_t  current_state;
    Event_t  event;
    State_t  next_state;
    Action_t action;
} Transition_t;

// 状态转移表
const Transition_t state_table[] = {
    {STATE_IDLE,       EVENT_START,   STATE_MEASURING, action_start_measure},
    {STATE_MEASURING,  EVENT_TIMEOUT, STATE_COMPLETE,  action_save_result},
    {STATE_MEASURING,  EVENT_ERROR,   STATE_ERROR,     action_handle_error},
    {STATE_COMPLETE,   EVENT_RESET,   STATE_IDLE,      action_reset_device},
    // ... 更多转移
};

State_t process_event(State_t current, Event_t event) {
    for(int i = 0; i < TABLE_SIZE; i++) {
        if(state_table[i].current_state == current && 
           state_table[i].event == event) {
            // 执行动作
            state_table[i].action();
            // 返回下一个状态
            return state_table[i].next_state;
        }
    }
    return current; // 没有匹配的转移
}

这个方法我特别喜欢。为什么?因为状态和事件的关系一目了然。你想想看,要改一个转移,只需要改表里的一行数据,不用翻来覆去找代码。

我的经验:在体温计项目里,我用了查表法。体温计有8个状态:待机、测量中、完成、错误、低电量、校准、关机、休眠。事件有6个。用查表法,整个状态机只有48行表数据,加上处理函数,总共不到200行代码。后来客户要加一个「温度过高报警」状态,我只花了10分钟就加好了。

对比项 嵌套switch-case 查表法
代码量(10个状态) 约300行 约100行
修改难度
运行速度 快(直接跳转) 中等(查表循环)
内存占用 较高(需要存储表)
适合场景 小规模(<10状态) 中规模(10-50状态)

3.3 状态模式:面向对象的优雅方案

最后说说状态模式。这个方法在C++里用得比较多,但C语言也能实现。核心思想是:每个状态都是一个独立的对象,有自己的处理函数。

// 状态模式示例(C语言实现)
typedef struct StateMachine StateMachine_t;

// 状态接口
typedef struct {
    void (*enter)(StateMachine_t *sm);
    void (*exit)(StateMachine_t *sm);
    State_t (*handle_event)(StateMachine_t *sm, Event_t event);
} State_t;

// 具体状态
void idle_enter(StateMachine_t *sm) {
    // 进入空闲状态
    turn_off_display();
}

State_t idle_handle_event(StateMachine_t *sm, Event_t event) {
    switch(event) {
        case EVENT_START:
            return &measuring_state;  // 切换到测量状态
        default:
            return &idle_state;       // 保持当前状态
    }
}

// 状态机主体
typedef struct {
    State_t *current_state;
    // 其他数据...
} StateMachine_t;

void state_machine_process(StateMachine_t *sm, Event_t event) {
    State_t *next = sm->current_state->handle_event(sm, event);
    if(next != sm->current_state) {
        sm->current_state->exit(sm);
        sm->current_state = next;
        sm->current_state->enter(sm);
    }
}

这个方法的好处是:每个状态都是独立的,改一个状态不会影响其他状态。我在一个复杂的医疗设备项目里用过,那个设备有50多个状态,用状态模式管理得井井有条。

什么时候用状态模式?

  1. 状态数量多(20个以上)
  2. 状态之间差异大,每个状态的行为完全不同
  3. 需要频繁增删状态
  4. 团队协作开发,每个人负责不同的状态

注意:状态模式在资源受限的MCU上要慎用。每个状态都需要一个结构体,函数指针也会占用额外的RAM。我曾经在一个只有2KB RAM的芯片上试过,结果内存不够用,最后只能换回查表法。

3.4 如何选择?我的建议

说了这么多,到底该用哪种?我给大家一个简单的判断标准:

  • 状态少于5个:用嵌套switch-case,简单直接
  • 状态5-20个:用查表法,好维护
  • 状态超过20个:用状态模式,或者查表法+状态模式混合

其实没有绝对的好坏。我见过有人用查表法写了1000多个状态,也见过有人用switch-case管理50个状态。关键是要适合你的项目。

最后说一句:不管你选哪种方法,一定要画状态图。我每次做状态机,第一件事就是画图。画完图再写代码,思路清晰,bug也少。这个习惯我保持了十几年,从来没出过大的状态机问题。

好了,这一章就到这里。下一章我们聊聊状态机的测试方法,这可是个大学问。到时候见!