核心概念解析:事件、事件处理器、事件队列、事件分发器、事件循环

好,咱们正式开始啃核心概念。很多同学学事件驱动,上来就撸代码,结果越写越乱。为啥?因为脑子里这几个概念是糊的。今天我就把这五个东西拆开,一个一个讲透。

我个人习惯,学一个新架构,先画一张全景图,把每个零件的位置搞清楚。来,先看这张图:

事件驱动核心概念关系图 事件源 ISR / 定时器 / 用户输入 事件 Event_t 结构体 事件队列 FIFO / 优先级队列 事件分发器 Dispatcher 事件循环 while(1) 主循环 事件处理器 Handler / Callback 💡 数据流方向: 事件源 → 事件 → 事件队列 → 事件分发器 → 事件处理器 事件循环负责从队列取事件,交给分发器,分发器再调用对应的处理器。

1. 事件(Event)

事件是什么?说白了,就是「发生了某件事」的通知。在嵌入式系统里,事件就是一个结构体,里面装着:事件类型、发生时间、附带数据。

举个例子,你按下一个按键,系统产生一个按键事件。这个事件里至少要有:

  • 事件ID:比如 EVENT_KEY_PRESS
  • 时间戳:什么时候发生的
  • 数据:哪个按键、按了多久

我在项目中见过最坑的事,就是有人把事件定义成全局变量,到处乱改。结果一个事件还没处理完,就被另一个中断覆盖了。嗯,这里要注意:事件必须是不可变的数据快照,一旦生成,就不该被修改。

// 一个典型的事件结构体
typedef struct {
    uint32_t    id;          // 事件ID
    uint32_t    timestamp;   // 时间戳
    uint32_t    param;       // 参数
    void*       data;        // 扩展数据指针
    uint8_t     data_len;    // 数据长度
} Event_t;
我的习惯:事件ID用枚举定义,不要用宏。枚举在调试器里能直接看到名字,宏只能看到数字,排查问题的时候能省一半时间。

2. 事件处理器(Event Handler)

事件处理器就是处理事件的函数。每个事件类型对应一个处理器,或者多个处理器组成一个处理链。

处理器长什么样?就是一个函数指针:

typedef void (*EventHandler)(const Event_t* event);

我曾经在一个项目里,把所有事件处理逻辑都塞进一个巨大的 switch-case 里。刚开始只有十几个事件,还行。后来扩展到上百个事件,那个函数变得又臭又长,每次改都提心吊胆。后来我改成了一张事件-处理器映射表,清爽多了。

事件ID 处理器函数 优先级
EVENT_KEY_PRESS key_press_handler 高
EVENT_TIMER_TICK timer_tick_handler 中
EVENT_UART_RX uart_rx_handler 高
EVENT_LED_BLINK led_blink_handler 低
避坑指南:处理器里不要做阻塞操作!我曾经在一个处理器里调了 delay(100ms),结果整个系统卡住了,因为事件循环被堵死了。处理器应该快速返回,耗时操作扔到任务队列里。

3. 事件队列(Event Queue)

事件队列是事件的缓冲区。为什么需要队列?因为事件产生的速度和事件处理的速度往往不一致。比如按键中断可能瞬间产生多个事件,但处理器处理一个按键事件需要几毫秒。没有队列,事件就丢了。

队列的实现方式:

  • 环形缓冲区:固定大小,无动态内存分配,适合资源受限的MCU
  • 链表队列:动态分配,灵活但容易产生碎片
  • 优先级队列:高优先级事件插队处理

我个人在STM32上做项目,90%的情况用环形缓冲区就够了。简单、可靠、零碎片。

// 环形缓冲区实现的事件队列
#define EVENT_QUEUE_SIZE    16

typedef struct {
    Event_t buffer[EVENT_QUEUE_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} EventQueue_t;

// 入队
int event_queue_push(EventQueue_t* q, const Event_t* ev) {
    if (q->count >= EVENT_QUEUE_SIZE) return -1;  // 队列满
    q->buffer[q->head] = *ev;
    q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
    q->count++;
    return 0;
}

// 出队
int event_queue_pop(EventQueue_t* q, Event_t* ev) {
    if (q->count == 0) return -1;  // 队列空
    *ev = q->buffer[q->tail];
    q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
    q->count--;
    return 0;
}
关键点:队列满的时候怎么办?我一般用两种策略:要么丢弃最旧的事件(覆盖),要么丢弃新事件(丢包)。具体选哪个,看业务场景。按键事件我倾向于丢旧的,因为用户按的是最新的键;传感器数据我倾向于丢新的,因为旧数据更稳定。

4. 事件分发器(Event Dispatcher)

分发器是事件驱动架构的「交通警察」。它从队列里取出事件,查表找到对应的处理器,然后调用它。

分发器的核心逻辑其实很简单:

void dispatcher_dispatch(const Event_t* event) {
    // 查表找到处理器
    EventHandler handler = lookup_handler(event->id);
    if (handler) {
        handler(event);  // 调用处理器
    } else {
        // 没有注册处理器,可以记录日志或忽略
        log_warning("No handler for event: %d", event->id);
    }
}

你想想看,如果没有分发器,每个事件来了你都得手动判断类型、调用函数,代码会变成什么样?

// 没有分发器的写法——噩梦
void process_event(const Event_t* ev) {
    if (ev->id == EVENT_KEY_PRESS) {
        key_press_handler(ev);
    } else if (ev->id == EVENT_TIMER_TICK) {
        timer_tick_handler(ev);
    } else if (ev->id == EVENT_UART_RX) {
        uart_rx_handler(ev);
    }
    // ... 上百个 else if
}

有了分发器,你只需要注册一次,后面来什么事件,它自动帮你找到对应的函数。这就是解耦的魅力。

5. 事件循环(Event Loop)

事件循环是整个系统的引擎。它就是一个死循环,不停地做三件事:

  1. 从队列取事件
  2. 交给分发器
  3. 等待下一个事件

最简单的实现:

void event_loop_run(void) {
    Event_t event;
    while (1) {
        if (event_queue_pop(&g_event_queue, &event) == 0) {
            dispatcher_dispatch(&event);
        }
        // 这里可以加 idle 处理,比如低功耗休眠
    }
}

我记得第一次写事件循环的时候,觉得这玩意儿太简单了,不就是个 while(1) 吗?后来发现,真正的坑在「等待下一个事件」这一步。如果队列空了,CPU 在干嘛?空转?还是休眠?

在电池供电的设备里,队列空的时候应该进入休眠模式,等中断来了再唤醒。这个优化能让功耗降低一个数量级。

我的建议:事件循环里一定要留一个 idle hook,让上层应用决定队列空的时候做什么。不要硬编码成空转。

五个概念的关系总结

最后,我用一句话串起来这五个概念:

事件源产生事件,事件被放入事件队列,事件循环从队列取出事件交给事件分发器,分发器查找并调用对应的事件处理器。

这五个东西,少一个都不行。事件是数据,队列是缓冲区,循环是引擎,分发器是路由,处理器是执行者。它们各司其职,构成了事件驱动架构的骨架。