状态机(State Machine)

状态机(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_SENTebtn.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_btnebtn.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 等待→接收→校验→烧写→完成