16、宏实现状态机:使用宏定义状态与事件,状态转换表的宏实现,简单状态机框架

状态机这东西,做嵌入式的基本都绕不开。按键消抖、通信协议解析、菜单切换……说白了,就是让程序知道“现在在干嘛,来了信号该干嘛”。

我早年刚入行时,写状态机就是一堆 switch-case 嵌套,代码又臭又长。后来被一个老工程师点醒——用宏来定义状态和事件,再配合一张转换表,代码立马清爽了。今天我就把这套方法掰开揉碎讲给你。

16.1 状态机的基本要素

一个状态机,核心就三样东西:状态事件转换规则

  • 状态:系统当前所处的模式。比如按键的“按下”、“释放”。
  • 事件:触发状态变化的条件。比如“检测到电平变化”。
  • 转换:在某个状态下,遇到某个事件,下一步该去哪个状态。

用宏来定义这些,好处是直观、可读性强,而且修改起来方便。你想想看,如果状态和事件都用裸数字,过两周你自己都看不懂。

16.2 用宏定义状态与事件

我个人习惯,先把所有状态和事件用 #defineenum 列出来。但为了灵活性,我更喜欢用宏,因为可以配合后面的转换表做字符串化。

/* 状态定义 */
#define STATE_IDLE       0
#define STATE_ACTIVE     1
#define STATE_ERROR      2
#define STATE_DONE       3

/* 事件定义 */
#define EVT_NONE         0
#define EVT_START        1
#define EVT_STOP         2
#define EVT_TIMEOUT      3
#define EVT_DATA_READY   4

嗯,这里要注意:状态和事件的编号不要冲突。我一般把状态从0开始,事件从100开始,或者干脆用不同的枚举类型。但用宏的话,你只要自己心里有数就行。

我的小习惯: 我会在宏定义后面加个注释,说明这个状态/事件在什么场景下出现。比如 /* 用户按下启动键 */,这样半年后回来看代码,不用重新猜。

16.3 状态转换表的宏实现

状态转换表,说白了就是一个二维数组:行是当前状态,列是事件,单元格里存的是下一个状态。

但直接写二维数组太死板了。我更喜欢用宏来“生成”这张表。这样改一个状态或事件,表会自动调整,不容易出错。

/* 定义转换表宏:当前状态、事件、下一状态 */
#define TRANS(curr, evt, next)  \
    [curr][evt] = next

/* 声明转换表 */
static uint8_t state_table[4][5];  /* 4个状态,5个事件 */

/* 初始化转换表 */
void state_table_init(void)
{
    /* 清零 */
    memset(state_table, 0xFF, sizeof(state_table));

    /* 填充转换规则 */
    TRANS(STATE_IDLE,   EVT_START,       STATE_ACTIVE);
    TRANS(STATE_ACTIVE, EVT_STOP,        STATE_IDLE);
    TRANS(STATE_ACTIVE, EVT_TIMEOUT,     STATE_ERROR);
    TRANS(STATE_ERROR,  EVT_DATA_READY,  STATE_DONE);
    TRANS(STATE_DONE,   EVT_START,       STATE_ACTIVE);
}

你看,这样写是不是很清晰?每个转换规则就是一行宏。想加一个新规则,直接加一行 TRANS(...) 就行。我曾经在一个项目里维护了30多个状态、20多个事件,全靠这张表撑着,没出过逻辑混乱。

避坑指南: 我曾经犯过一个低级错误——忘了给未定义的转换填默认值。结果某个状态遇到一个不该出现的事件,查表查到了0xFF,程序直接跑飞。后来我强制要求:所有未定义的转换,要么指向一个“错误状态”,要么指向自身(保持不动)。千万别留空洞。

16.4 简单状态机框架

有了状态和事件的定义,有了转换表,剩下的就是跑框架了。一个典型的状态机执行流程是这样的:

  1. 等待事件到来
  2. 查表,得到下一个状态
  3. 执行状态进入/退出动作(可选)
  4. 更新当前状态
  5. 循环

下面是一个极简的框架实现:

/* 当前状态 */
static uint8_t current_state = STATE_IDLE;

/* 状态机主循环 */
void state_machine_run(uint8_t event)
{
    uint8_t next_state;

    /* 查表获取下一状态 */
    next_state = state_table[current_state][event];

    /* 如果查到的状态无效,保持不动 */
    if (next_state == 0xFF) {
        next_state = current_state;
    }

    /* 如果状态真的变了,执行切换 */
    if (next_state != current_state) {
        /* 退出当前状态(可扩展) */
        /* 进入新状态(可扩展) */
        current_state = next_state;
    }

    /* 执行当前状态的动作(可选) */
    switch (current_state) {
        case STATE_IDLE:
            /* 空闲时做什么 */
            break;
        case STATE_ACTIVE:
            /* 活跃时做什么 */
            break;
        /* ... */
    }
}

这个框架虽然简单,但已经能覆盖大部分嵌入式场景了。你可以在状态切换时加入回调函数,比如 on_enter_state()on_exit_state(),这样每个状态进入和离开时都能做点特定的事。

16.5 用SVG画一张状态机转换图

光说可能不够直观,我画了一张图,把上面例子里的状态转换关系展示出来。你看一眼就明白了。

STATE_IDLE STATE_ACTIVE STATE_ERROR STATE_DONE EVT_START EVT_STOP EVT_TIMEOUT EVT_DATA_READY EVT_START 图例: EVT_START EVT_STOP EVT_TIMEOUT EVT_DATA_READY

16.6 宏实现状态机的进阶技巧

上面讲的是基础框架。在实际项目中,我还会加一些“佐料”,让状态机更健壮、更好用。

  • 状态进入/退出钩子:用宏定义 STATE_ENTER(state)STATE_EXIT(state),在状态切换时自动调用。这样每个状态可以有自己的初始化/清理逻辑。
  • 事件队列:如果事件来得太快,状态机处理不过来,可以用一个环形缓冲区暂存事件。宏定义 EVT_QUEUE_PUSH(evt)EVT_QUEUE_POP() 来操作。
  • 状态机调试:我习惯在宏里加一个调试打印开关。比如 #ifdef DEBUG_FSM 时,每次状态切换都打印“从XX状态,因XX事件,切换到XX状态”。这个习惯帮我抓过不少逻辑bug。

核心要点: 宏定义状态机的精髓,在于把“状态-事件-转换”这三者的关系,从散落在代码各处的 if-else 中抽离出来,集中到一张表里。这样逻辑一目了然,修改也安全。

好了,关于宏实现状态机,我就讲这么多。你回去可以试着把项目里的一个 switch-case 状态机改成这种表驱动的方式。改完之后你会发现——嗯,代码确实顺眼多了。


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