1. 事件驱动架构概述

各位好,我是老周。做嵌入式这行快二十年了,从最早的8051裸机编程,到后来玩FreeRTOS、uC/OS,再到自己搭事件驱动框架,踩过的坑真不少。今天咱们聊聊事件驱动架构——说白了,就是让系统对外部或内部发生的事情做出反应。

你可能会问:这跟传统的轮询有啥区别?别急,咱们慢慢拆解。

1.1 什么是事件驱动

事件驱动,核心就四个字:来了再干。

系统平时处于空闲或低功耗状态。一旦有事件发生——比如按键按下、串口收到数据、定时器溢出——系统才被唤醒,去执行对应的处理函数。

我习惯把事件驱动比作餐厅服务员。客人不招手,服务员就站着等;客人一招手,服务员才过去服务。而不是每隔两秒就跑去问每个客人“您要点啥?”——那叫轮询。

事件驱动的本质: 系统行为由事件触发,而非时间片轮转或循环查询。

1.2 与轮询架构的对比

轮询架构,很多老工程师都熟悉。主循环里放一堆if语句,挨个检查各个外设或标志位。代码简单,但效率低。

我举个例子。早年做一款智能电表,用轮询方式检测按键和通信。主循环跑一圈要检查十几个标志位。结果呢?按键响应延迟有时超过200ms,用户投诉说“按了没反应”。后来改成事件驱动,响应时间降到10ms以内。

咱们用表格对比一下:

对比维度 轮询架构 事件驱动架构
CPU利用率 低(空转也耗电) 高(无事可做就休眠)
响应实时性 取决于轮询周期 事件触发即响应
代码复杂度 简单直观 中等(需要事件管理)
扩展性 差(加一个功能就要加轮询) 好(加一个事件处理器即可)
功耗 高(持续运行) 低(可深度休眠)
调试难度 低(线性执行) 中等(异步流程)

我的建议: 如果系统只有两三个外设,且对实时性要求不高,轮询完全够用。但一旦外设超过5个,或者有低功耗需求,果断上事件驱动。

1.3 核心概念

事件驱动架构有三个核心要素:事件、事件处理器、事件队列。咱们一个一个说。

1.3.1 事件(Event)

事件就是“发生了什么”。它可以是:

  • 外部事件: GPIO中断、UART接收、SPI传输完成
  • 内部事件: 定时器超时、任务完成、错误发生
  • 软件事件: 消息到达、状态变更、用户自定义信号

在代码里,事件通常用一个结构体表示:

typedef struct {
    uint32_t event_id;      // 事件ID,比如 EVT_BUTTON_PRESS
    uint32_t timestamp;     // 事件发生时间戳
    void    *data;          // 事件附带数据(可选)
    uint16_t data_len;      // 数据长度
} event_t;

我个人习惯把事件ID定义成枚举,清晰又好维护:

typedef enum {
    EVT_NONE = 0,
    EVT_BUTTON_PRESS,
    EVT_BUTTON_RELEASE,
    EVT_UART_RX,
    EVT_TIMER_TICK,
    EVT_SENSOR_READY,
    EVT_ERROR_OCCURRED
} event_id_t;

1.3.2 事件处理器(Event Handler)

事件处理器就是“事件来了怎么办”。每个事件对应一个处理函数。我见过很多新手把所有事件处理写在一个大switch-case里,那其实还是轮询思维。

正确的做法是:每个事件处理器独立成一个函数,通过注册表关联事件ID和处理器:

typedef void (*event_handler_t)(event_t *evt);

typedef struct {
    event_id_t       id;
    event_handler_t  handler;
} event_registration_t;

// 注册表
event_registration_t g_event_table[] = {
    { EVT_BUTTON_PRESS,    handle_button_press    },
    { EVT_UART_RX,         handle_uart_rx         },
    { EVT_TIMER_TICK,      handle_timer_tick      },
    // ...
};

注意: 事件处理器要尽量短小精悍。我曾经见过一个处理器里做了500ms的延时等待——直接把整个系统卡死了。事件处理器里不要做阻塞操作,耗时任务要拆解成多个事件或交给后台任务。

1.3.3 事件队列(Event Queue)

事件队列是事件驱动架构的“交通枢纽”。它负责:

  • 接收事件: 中断服务程序或任务把事件放入队列
  • 缓存事件: 防止事件丢失(尤其是中断中产生的事件)
  • 调度事件: 按优先级或FIFO顺序分发事件给处理器

队列的实现方式很多,我常用的是环形缓冲区:

#define EVENT_QUEUE_SIZE  32

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

// 入队(通常在中断中调用)
bool event_queue_push(event_queue_t *q, event_t *evt) {
    if (q->count >= EVENT_QUEUE_SIZE) return false;  // 队列满
    q->buffer[q->head] = *evt;
    q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
    q->count++;
    return true;
}

// 出队(在主循环或任务中调用)
bool event_queue_pop(event_queue_t *q, event_t *evt) {
    if (q->count == 0) return false;  // 队列空
    *evt = q->buffer[q->tail];
    q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
    q->count--;
    return true;
}

下面这张图展示了事件从产生到处理的全流程:

事件驱动架构核心流程 外部事件源 GPIO / UART / SPI 内部事件源 定时器 / 任务 / 错误 事件队列 环形缓冲区 事件1 | 事件2 | 事件3 ... | 事件N FIFO / 优先级 事件调度器 查找注册表 → 分发 事件处理器组 handle_button_press() handle_uart_rx() handle_timer_tick() handle_sensor_ready() ... 关键设计原则 • 事件源不直接调用处理器 • 队列解耦生产者和消费者 • 处理器不阻塞、不等待

1.4 事件驱动架构的典型工作模式

实际项目中,事件驱动架构通常这样运转:

  1. 初始化阶段: 注册事件处理器,初始化事件队列,启动外设
  2. 事件产生: 中断或任务调用 event_queue_push() 放入事件
  3. 事件调度: 主循环或调度任务调用 event_queue_pop() 取出事件
  4. 事件处理: 根据事件ID查找注册表,调用对应的处理器函数
  5. 返回空闲: 处理完成后,系统进入低功耗或等待下一个事件

避坑指南: 我曾经在一个项目中,中断里直接调用了事件处理器——结果导致中断嵌套过深,堆栈溢出。记住:中断只负责放事件,不负责处理事件。处理逻辑一定要放到任务或主循环里。

1.5 什么时候用事件驱动?

不是所有场景都适合事件驱动。我总结了几条经验:

  • 适合: 多外设、多中断、低功耗要求、需要异步处理
  • 不适合: 纯顺序执行、单外设、对延迟极度敏感(比如PWM控制)
  • 谨慎: 事件处理逻辑复杂且耗时——需要配合状态机或任务拆分

嗯,这一章就到这里。事件驱动不是银弹,但用好了,系统的可维护性和扩展性会提升一个档次。后面咱们会深入讲事件队列的实现细节、优先级调度、以及各种调试技巧——那些才是真正实战中要啃的硬骨头。