架构与重构 状态机
状态机设计的正确模式:回调注册表替代 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
...
Comments NOTHING