状态机设计的正确模式:回调注册表替代 switch-case

Babel36acl 架构与重构 无~ 8 次阅读 预计阅读时间: 9 分钟 发布于 1 天前 最后更新于 1 小时前 1903 字


架构与重构 状态机

状态机设计的正确模式:回调注册表替代 switch-case

一个 18 状态的嵌入式状态机,如果全部塞在一个 switch-case 里,一个文件 900 行。每次改一个状态都要读懂全部逻辑。每次 merge 冲突都在同一个文件。这是不可持续的。

## 传统 switch-case 的问题

大多数嵌入式项目的状态机长这样:

void task_once(void) {
    switch (state) {
    case STATE_A:
        // 50行逻辑...
        // 超时检查...
        // 传感器读取...
        // 状态切换...
        break;
    case STATE_B:
        // 又是50行...
        break;
    // ... 18 个 case
    }
}

这个模式有三个死穴:

  • 单一文件膨胀 — 每个状态 50 行,18 个状态就是 900 行。改一个状态要滚动翻页。
  • merge 冲突集中营 — 多人改不同状态,但都在同一个 switch 函数里,git 不能自动合并。
  • 新增状态需要读全部 — 要加一个新状态,先得搞懂既有的流转逻辑,心理门槛极高。

## 回调注册表模式

核心思路:状态机框架只维护"当前状态"和"处理器注册表"。每个状态写在独立文件里,注册自己的三回调。

// 框架侧 — psh_task_once()
void psh_task_once(void) {
    // 1. 前置检查(超时、中断控制、错误条件)
    if (check_exit_conditions()) {
        // 调用当前状态的 on_exit
        current_handler->on_exit();
        // 切换到新状态
        current_handler = find_handler(new_state);
        // 调用新状态的 on_enter
        current_handler->on_enter();
        return;
    }
    // 2. 周期推进当前状态
    current_handler->on_run();
}
// 每个状态一个文件 — 以出袋校准为例
#include "packer_state_handler.h"

static void on_enter(packer_state_t prev) {
    packer_actuator_start_bag_calib();
    s_motion_seen = 0;
}

static void on_run(void) {
    if (!s_motion_seen && is_axis_running(1)) {
        s_motion_seen = 1;
    }
    if (s_motion_seen && is_axis_done(1)) {
        psh_transition(PACKER_STATE_NEXT);
    }
    if (check_timeout()) {
        psh_transition(PACKER_STATE_ERROR);
    }
}

static void on_exit(void) {
    // 清理本状态特有的资源
}

const packer_state_handler_t s_handler = {
    .state    = PACKER_STATE_BAG_CALIB_SEEK,
    .on_enter = on_enter,
    .on_run   = on_run,
    .on_exit  = on_exit,
    .name     = "BAG_CALIB_SEEK"
};

void sm_bag_flow_init(void) {
    psh_register(&s_handler);
}

状态切换的时序保障

核心调度器保证三回调的执行顺序:

psh_transition(new_state):
    // 1. 调用旧状态的 on_exit
    current_handler->on_exit();
    // 2. 更新全局状态变量
    s_state = new_state;
    // 3. 调用新状态的 on_enter
    new_handler->on_enter();
    // 4. 通知状态变更回调
    if (s_on_state_change_cb) s_on_state_change_cb(new_state);

这个顺序很重要。如果先更新状态再调 on_exit,on_exit 查到的新状态是错的。

on_enter / on_run / on_exit 的职责划分

回调 触发时机 典型职责
on_enter 进入状态时,执行一次 下发执行器动作、重置子阶段计数器、初始化超时
on_run 每个周期,反复执行 检查完成条件、传感器输入、超时、推进子阶段
on_exit 离开状态时,执行一次 清理临时资源、通知其他模块状态即将改变

与 switch-case 的对比

维度 switch-case 回调注册表
文件组织 一个文件 900+ 行 每个状态一个 .c 文件,约 30-50 行
新增状态 读懂整个 switch 再加 case 新建 .c + 一行 psh_register()
merge 冲突 容易全文件冲突 只有 psh_register 那一行可能冲突
可见性 所有状态互相可见,容易误改 每个状态只知道自己的回调
状态间共享数据 局部 static 变量,猜依赖 显式通过 psh_* 接口访问
框架稳定性 核心调度和业务代码混在一起 psh_task_once 稳定后几乎不改

适用边界

这个模式适用于 10 个状态以上的复杂流程。如果你的状态只有 3-5 个,switch-case 更简单直接。

状态机是业务代码,调度器是基础设施。混在一起两个都烂。

配套工具

注册完成后,可以用 psh_log_registry() 输出所有已注册状态的名称和数量,用于调试:

[SM] Handler registry: 18 handlers registered
  [0] POWER_ON           — sm_self_check.c
  [1] SELF_CHECK         — sm_self_check.c
  [2] IDLE               — builtin
  [3] BAG_CALIB_CHECK    — sm_bag_flow.c
  [4] BAG_CALIB_CLEAR    — sm_bag_flow.c
  ...
此作者没有提供个人介绍。
最后更新于 2026-05-30