状态机(State Machine)
把一个过程拆成若干个"状态",每个状态只关心:当前能接受什么输入、收到后转移到哪个状态。
ebtn 的按键检测逻辑本身就是一个状态机。
一、为什么需要它
没有状态机时,处理"有先后顺序的事情"靠标志位堆叠:
// 检测"先按A、再按B"的序列——没有状态机的写法
static uint8_t a_pressed = 0;
static uint8_t waiting_b = 0;
static uint32_t a_time = 0;
void on_key(uint8_t key) {
if (key == A) {
a_pressed = 1;
waiting_b = 1;
a_time = HAL_GetTick();
}
if (key == B && waiting_b) {
if (HAL_GetTick() - a_time < 500) {
do_action(); // 有效序列
}
a_pressed = 0;
waiting_b = 0;
}
// 需求一变,这堆标志位就乱了
}
状态一多,标志位之间的关系就失控。状态机把"现在处于什么阶段"收敛成一个变量。
二、核心概念
状态机 = 状态 + 转移条件 + 动作
状态(State): 系统当前处于哪个阶段
输入(Input): 触发状态变化的事件或条件
转移(Transition):满足条件时从一个状态跳到另一个状态
动作(Action): 进入/离开/停留某状态时执行的操作
用图表示(以 ebtn 按键为例):
按下 & 消抖通过
┌─────────┐ ──────────────────→ ┌──────────┐
│ IDLE │ │ PRESSED │─────→ 发 ONPRESS
└─────────┘ ←────────────────── └──────────┘
↑ 释放 & 消抖通过 │
│ │ 按下时长 > max
│ ↓
│ ┌──────────────┐
│ 松手 │ LONG_PRESS │──→ 周期发 KEEPALIVE
└────────────────────────── └──────────────┘
三、实现方式①:switch-case(最常用)
typedef enum {
STATE_IDLE,
STATE_DEBOUNCE,
STATE_PRESSED,
STATE_LONG_PRESS,
} btn_state_t;
static btn_state_t state = STATE_IDLE;
static uint32_t state_enter_time = 0;
void btn_process(uint8_t is_pressed, uint32_t now_ms)
{
switch (state)
{
case STATE_IDLE:
if (is_pressed) {
state = STATE_DEBOUNCE;
state_enter_time = now_ms;
}
break;
case STATE_DEBOUNCE:
if (!is_pressed) {
state = STATE_IDLE; // 抖动,回到 IDLE
} else if (now_ms - state_enter_time >= 20) {
state = STATE_PRESSED; // 消抖通过
on_press();
}
break;
case STATE_PRESSED:
if (!is_pressed) {
state = STATE_IDLE;
on_release();
} else if (now_ms - state_enter_time >= 500) {
state = STATE_LONG_PRESS;
}
break;
case STATE_LONG_PRESS:
if (!is_pressed) {
state = STATE_IDLE;
on_release();
}
// 周期触发 KEEPALIVE 由 ebtn 内部的时间比较完成
break;
}
}
优点:直观、易读、零额外开销。
缺点:状态多时 switch 很长,状态行为分散在各个 case 里。
四、实现方式②:函数指针表(状态多时用)
每个状态对应一个函数,状态机变成"调用当前状态的函数":
typedef void (*state_fn_t)(uint8_t is_pressed, uint32_t now_ms);
static void state_idle (uint8_t, uint32_t);
static void state_debounce (uint8_t, uint32_t);
static void state_pressed (uint8_t, uint32_t);
static void state_long_press (uint8_t, uint32_t);
// 状态表:下标即状态编号
static const state_fn_t state_table[] = {
state_idle,
state_debounce,
state_pressed,
state_long_press,
};
static uint8_t current_state = 0; // STATE_IDLE
// 主循环只需一行
void btn_process(uint8_t is_pressed, uint32_t now_ms) {
state_table[current_state](is_pressed, now_ms);
}
// 每个状态自己写自己的逻辑
static void state_idle(uint8_t is_pressed, uint32_t now_ms) {
if (is_pressed) {
current_state = 1; // → STATE_DEBOUNCE
state_enter_time = now_ms;
}
}
static void state_debounce(uint8_t is_pressed, uint32_t now_ms) {
if (!is_pressed) { current_state = 0; return; }
if (now_ms - state_enter_time >= 20) { current_state = 2; on_press(); }
}
// ... 其余状态同理
优点:每个状态独立成函数,职责清晰,新增状态不改旧代码。
缺点:比 switch-case 多一层间接,阅读时需要跳函数。
五、进入/退出动作(Entry / Exit Action)
有些状态需要在进入时做初始化、在退出时做清理,直接在转移处插代码容易遗漏。
标准做法:转移时调用 exit_current → 更新状态 → 调用 enter_new:
typedef struct {
void (*on_enter)(void);
void (*on_exit) (void);
void (*on_tick) (uint8_t is_pressed, uint32_t now_ms);
} state_def_t;
static const state_def_t states[] = {
// STATE_IDLE
{ NULL, NULL, state_idle_tick },
// STATE_PRESSED
{ on_press, on_release, state_pressed_tick },
// ...
};
void transition_to(uint8_t new_state) {
if (states[current_state].on_exit) states[current_state].on_exit();
current_state = new_state;
if (states[current_state].on_enter) states[current_state].on_enter();
}
这是 ebtn 里 EBTN_FLAG_ONPRESS_SENT(ebtn.c:4)标志的作用——标记"已进入按下状态",本质上就是 entry action 的记录。
六、ebtn 源码里的状态机
ebtn 没有显式的 state 枚举,用标志位隐式表示状态,对照理解:
flags & ONPRESS_SENT == 0 且 new_state == 0 → IDLE
flags & ONPRESS_SENT == 0 且 new_state == 1 → DEBOUNCE(等消抖)
flags & ONPRESS_SENT == 1 且 new_state == 1 → PRESSED / LONG_PRESS
flags & ONPRESS_SENT == 1 且 new_state == 0 → RELEASE_DEBOUNCE
prv_process_btn(ebtn.c:18)就是整个状态机的 tick 函数,每次 ebtn_process() 调用时执行一次。
七、状态机 vs 标志位堆叠
| 标志位堆叠 | 状态机 | |
|---|---|---|
| 当前阶段 | 多个标志位组合推断 | 一个变量直接表示 |
| 新增状态 | 加标志位,改所有判断 | 加一个 case/函数 |
| 非法状态 | 容易出现(标志位组合爆炸) | 不存在(状态是枚举) |
| 可读性 | 差(要理解所有标志位含义) | 好(状态名即语义) |
判断标准:超过 2 个标志位联合控制同一个流程,就考虑换状态机。
八、在哪能看到这个 pattern
| 系统 | 位置 |
|---|---|
| ebtn | prv_process_btn,按键状态机 |
| LVGL | lv_anim,动画状态;lv_indev,手势识别 |
| FreeRTOS | 任务状态机(Ready/Running/Blocked/Suspended) |
| TCP/IP 协议栈 | TCP 连接状态(SYN_SENT / ESTABLISHED / TIME_WAIT …) |
| 串口协议解析 | 帧头→长度→数据→校验,每步一个状态 |
| bootloader | 等待→接收→校验→烧写→完成 |