核心思想:中断/定时器只投递事件,主循环或 RTOS 任务消费事件,状态机根据“当前状态 + 事件”决定转移和动作。
场景定义
短按按键:切换 LED 开/关
长按 2 秒:进入/退出 LED 闪烁
按键需要 20ms 去抖
事件源:
按键 GPIO 中断:产生
EV_KEY_DOWN/EV_KEY_UP10ms 定时器中断:产生
EV_TICK_10MS
事件和状态定义
typedef enum { EV_KEY_DOWN, EV_KEY_UP, EV_TICK_10MS } Event; typedef enum { ST_IDLE, // 空闲 ST_DEBOUNCE_DOWN, // 按下去抖 ST_PRESSED, // 已按下,等待短按/长按 ST_LONG_PRESS, // 长按已触发 ST_DEBOUNCE_UP // 松手去抖 } State;事件队列:ISR 只投递事件
#define EVQ_SIZE 16 static Event evq[EVQ_SIZE]; static volatile uint8_t head = 0, tail = 0; bool event_post_from_isr(Event ev) { uint8_t next = (head + 1) % EVQ_SIZE; if (next == tail) { return false; // 队列满,可记录溢出 } evq[head] = ev; head = next; return true; } bool event_get(Event *ev) { if (tail == head) { return false; } *ev = evq[tail]; tail = (tail + 1) % EVQ_SIZE; return true; }按键中断:
void key_isr(void) { if (gpio_read(KEY_PIN) == 0) { event_post_from_isr(EV_KEY_DOWN); } else { event_post_from_isr(EV_KEY_UP); } }10ms 定时器中断:
void timer_10ms_isr(void) { event_post_from_isr(EV_TICK_10MS); }状态机处理
static State state = ST_IDLE; static uint16_t press_ms = 0; void fsm_dispatch(Event ev) { switch (state) { case ST_IDLE: if (ev == EV_KEY_DOWN) { press_ms = 0; state = ST_DEBOUNCE_DOWN; } break; case ST_DEBOUNCE_DOWN: if (ev == EV_TICK_10MS) { press_ms += 10; if (press_ms >= 20) { // 去抖 20ms press_ms = 0; state = ST_PRESSED; } } else if (ev == EV_KEY_UP) { state = ST_IDLE; // 抖动,忽略 } break; case ST_PRESSED: if (ev == EV_TICK_10MS) { press_ms += 10; if (press_ms >= 2000) { // 长按 2s led_blink_toggle(); state = ST_LONG_PRESS; } } else if (ev == EV_KEY_UP) { led_toggle(); // 短按动作 press_ms = 0; state = ST_DEBOUNCE_UP; } break; case ST_LONG_PRESS: if (ev == EV_KEY_UP) { press_ms = 0; state = ST_DEBOUNCE_UP; } break; case ST_DEBOUNCE_UP: if (ev == EV_TICK_10MS) { press_ms += 10; if (press_ms >= 20) { state = ST_IDLE; } } break; default: state = ST_IDLE; break; } }主循环消费事件
void main_loop(void) { Event ev; while (1) { if (event_get(&ev)) { fsm_dispatch(ev); } // 可以在这里进入低功耗,等待中断唤醒 // __WFI(); } }如果是 RTOS,可以把fsm_dispatch放在一个任务里,事件队列换成xQueueSendFromISR()/xQueueReceive()。
按键中断不直接切换 LED,也不做去抖计时,只投递
EV_KEY_DOWN/EV_KEY_UP。定时器中断不判断长按,只投递
EV_TICK_10MS。真正逻辑在
fsm_dispatch()里,根据state + event决定:是否转移状态
是否执行动作,比如
led_toggle()、led_blink_toggle()是否累计时间、去抖、判断长按