3、事件驱动架构基础:状态机理论入门、事件与状态的定义、事件队列的实现

好,我们正式开始事件驱动架构。这一章是整门课的基石,说白了,你后面所有低功耗设计的思路,都从这里长出来。

我记得刚入行那会儿,接手一个智能门锁项目。需求很简单:按键开锁、刷卡开锁、超时报警。我用了一个超级循环,里面堆了十几个 if-else 判断状态。结果呢?功耗高得离谱,而且加一个新功能就得改一大片代码。后来我才明白,问题出在「状态管理」上——我根本没有清晰地定义状态和事件,代码自然就成了一团乱麻。

3.1 状态机理论入门

状态机,英文叫 Finite State Machine (FSM)。名字听着唬人,其实核心就三个东西:状态、事件、转移。

  • 状态:系统在某个时刻的稳定情形。比如门锁的「待机」、「开锁」、「报警」。
  • 事件:触发状态变化的东西。比如「按键按下」、「刷卡成功」、「超时」。
  • 转移:从一个状态到另一个状态的过程,通常伴随着动作。

你想想看,一个嵌入式系统本质上就是在不同状态之间跳来跳去。按键按下,从待机跳到开锁;开锁后 5 秒没操作,跳回待机。就这么简单。

核心原则:一个状态机在任意时刻,只能处于一个状态。这是确定性,也是我们调试的底气。

我在项目中遇到过一种情况:有人把状态机画得跟蜘蛛网一样,状态之间互相乱跳。结果代码写出来,自己都看不懂。所以,我建议你一开始就用 状态转移表 来约束设计。

下面是一个简单的门锁状态转移表:

当前状态 事件 下一状态 动作
待机 按键按下 开锁 驱动电机,点亮指示灯
待机 刷卡成功 开锁 驱动电机,点亮指示灯
开锁 超时 (5秒) 待机 关闭电机,熄灭指示灯
开锁 按键按下 待机 关闭电机,熄灭指示灯
待机 连续错误 3 次 报警 启动蜂鸣器
报警 管理员复位 待机 关闭蜂鸣器

这张表就是你的设计契约。写代码之前,先把表画清楚。后面所有逻辑都按表来,不会乱。

3.2 事件与状态的定义

定义状态和事件,是状态机设计的第一步。这一步做不好,后面全是坑。

我个人习惯用枚举来定义。C 语言里,枚举就是干这个的。清晰、可读、编译器还能帮你检查。

// 状态枚举
typedef enum {
    STATE_IDLE,      // 待机
    STATE_UNLOCK,    // 开锁
    STATE_ALARM,     // 报警
    STATE_MAX
} state_t;

// 事件枚举
typedef enum {
    EVENT_BUTTON_PRESS,     // 按键按下
    EVENT_CARD_SUCCESS,     // 刷卡成功
    EVENT_TIMEOUT,          // 超时
    EVENT_ERROR_3_TIMES,    // 连续错误3次
    EVENT_ADMIN_RESET,      // 管理员复位
    EVENT_MAX
} event_t;

小技巧:在枚举末尾加一个 `_MAX` 成员,可以用来做数组边界检查,或者循环遍历。这个习惯我用了十年,很实用。

这里有个容易犯的错:不要把状态和事件混在一起。我曾经见过有人把「开锁中」既当状态又当事件,结果逻辑绕得跟迷宫一样。记住,状态是「现在是什么」,事件是「发生了什么」。两者是正交的。

避坑指南:我曾经在一个项目中,把事件定义成了 `uint8_t` 的宏,结果事件值冲突了,导致状态机跳到了错误的状态。从那以后,我坚持用枚举,并且每个事件都显式赋值,避免隐式冲突。

3.3 事件队列的实现

有了状态和事件,接下来就是怎么把事件「喂」给状态机。最直接的方式是函数调用,但这样会阻塞。在低功耗系统里,我们通常用 事件队列。

事件队列说白了就是一个 FIFO(先进先出)缓冲区。ISR(中断服务程序)把事件丢进去,主循环或者任务从里面取出来处理。这样 ISR 可以快速退出,系统功耗也更低。

下面是一个简单的环形缓冲区实现:

#define EVENT_QUEUE_SIZE 16

typedef struct {
    event_t buffer[EVENT_QUEUE_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} event_queue_t;

static event_queue_t s_event_queue;

// 初始化队列
void event_queue_init(void) {
    s_event_queue.head = 0;
    s_event_queue.tail = 0;
    s_event_queue.count = 0;
}

// 入队(通常在 ISR 中调用)
bool event_queue_push(event_t event) {
    if (s_event_queue.count >= EVENT_QUEUE_SIZE) {
        return false; // 队列满,丢事件
    }
    s_event_queue.buffer[s_event_queue.tail] = event;
    s_event_queue.tail = (s_event_queue.tail + 1) % EVENT_QUEUE_SIZE;
    s_event_queue.count++;
    return true;
}

// 出队(在主循环中调用)
bool event_queue_pop(event_t *event) {
    if (s_event_queue.count == 0) {
        return false; // 队列空
    }
    *event = s_event_queue.buffer[s_event_queue.head];
    s_event_queue.head = (s_event_queue.head + 1) % EVENT_QUEUE_SIZE;
    s_event_queue.count--;
    return true;
}

关键点:入队操作在 ISR 中执行,必须保证原子性。如果 MCU 不支持原子操作,需要关中断保护。出队操作在主循环中执行,通常不需要关中断,因为 ISR 不会修改 head 指针。

嗯,这里要注意:队列大小要合理。太小了容易丢事件,太大了浪费 RAM。我一般根据系统中最快的事件产生频率来估算。比如按键去抖后 50ms 产生一个事件,主循环 10ms 处理一次,那队列深度 4 就够用了。但为了安全,我通常会留一倍余量。

下面这张图展示了事件队列在整个系统中的位置:

事件驱动架构核心流程 中断源 按键 / 定时器 / 外设 ISR 快速入队 事件队列 环形缓冲区 FIFO 顺序 生产者-消费者 ISR 写 / 主循环读 原子操作保护 状态机 状态转移表 事件驱动 执行动作 状态切换 低功耗决策 说明: 中断源产生事件 -> ISR 快速入队 -> 主循环从队列取出事件 -> 喂给状态机处理。 整个过程 ISR 只做最小工作,主循环按自己的节奏消费事件,系统功耗可控。

主循环的典型处理逻辑是这样的:

void main_loop(void) {
    event_t event;
    while (1) {
        // 进入低功耗模式前,检查队列
        if (event_queue_pop(&event)) {
            // 有事件,处理
            state_machine_handle_event(event);
        } else {
            // 没有事件,进入低功耗模式
            enter_sleep_mode();
        }
    }
}

低功耗关键:事件队列为空时,让 CPU 进入睡眠模式。中断来了,ISR 入队,然后唤醒 CPU 处理。这样 CPU 大部分时间都在睡觉,功耗自然就降下来了。这个模式,我几乎在每个低功耗项目里都用。

最后,我建议你把状态机处理函数也封装一下:

void state_machine_handle_event(event_t event) {
    state_t next_state = s_current_state;
    // 根据当前状态和事件,查表决定下一状态
    switch (s_current_state) {
        case STATE_IDLE:
            if (event == EVENT_BUTTON_PRESS || event == EVENT_CARD_SUCCESS) {
                next_state = STATE_UNLOCK;
                // 执行动作:开锁
                motor_drive(OPEN);
                led_on();
            } else if (event == EVENT_ERROR_3_TIMES) {
                next_state = STATE_ALARM;
                buzzer_on();
            }
            break;
        case STATE_UNLOCK:
            if (event == EVENT_TIMEOUT || event == EVENT_BUTTON_PRESS) {
                next_state = STATE_IDLE;
                motor_drive(CLOSE);
                led_off();
            }
            break;
        case STATE_ALARM:
            if (event == EVENT_ADMIN_RESET) {
                next_state = STATE_IDLE;
                buzzer_off();
            }
            break;
        default:
            break;
    }
    s_current_state = next_state;
}

嗯,这一章的内容就到这里。事件驱动架构的核心就是这三板斧:状态机、事件定义、事件队列。你把这套东西练熟了,后面讲 RTOS 的任务通信、低功耗 Tickless 模式,都会觉得顺理成章。