核心概念解析:事件、事件处理器、事件队列、事件分发器、事件循环
好,咱们正式开始啃核心概念。很多同学学事件驱动,上来就撸代码,结果越写越乱。为啥?因为脑子里这几个概念是糊的。今天我就把这五个东西拆开,一个一个讲透。
我个人习惯,学一个新架构,先画一张全景图,把每个零件的位置搞清楚。来,先看这张图:
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;
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 | 低 |
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)
事件循环是整个系统的引擎。它就是一个死循环,不停地做三件事:
- 从队列取事件
- 交给分发器
- 等待下一个事件
最简单的实现:
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 在干嘛?空转?还是休眠?
在电池供电的设备里,队列空的时候应该进入休眠模式,等中断来了再唤醒。这个优化能让功耗降低一个数量级。
五个概念的关系总结
最后,我用一句话串起来这五个概念:
事件源产生事件,事件被放入事件队列,事件循环从队列取出事件交给事件分发器,分发器查找并调用对应的事件处理器。
这五个东西,少一个都不行。事件是数据,队列是缓冲区,循环是引擎,分发器是路由,处理器是执行者。它们各司其职,构成了事件驱动架构的骨架。