2、核心概念解析:事件、事件处理器、事件队列、事件循环——四大金刚的定义与关系

好,咱们直接进入正题。

上一章我们聊了事件驱动的基本思想,说白了就是「等事情发生,再干活」。但光有思想不够,你得有具体的零件来搭这个框架。我把它总结为四个核心概念,也就是我常说的「四大金刚」:事件事件处理器事件队列事件循环

这四个东西,你搞懂了,事件驱动框架就掌握了一半。我个人习惯把它们比作一个工厂流水线:事件是原材料,事件处理器是工人,事件队列是传送带,事件循环就是那个不停按启动按钮的车间主任。

2.1 事件(Event)—— 发生了什么?

事件,就是「发生了某件事」的抽象表示。在单片机里,它可能是一个按键被按下、一个定时器溢出、一串UART数据到达。

我见过很多新手把事件直接跟中断挂钩,其实不完全对。中断是硬件层面的通知,事件是软件层面的消息。你可以把中断理解成门铃响了,事件是你知道「有人来了」这个事实。

事件的核心三要素:

  • 事件类型:用枚举或宏定义,比如 EVENT_BUTTON_PRESSEVENT_TIMER_TICK
  • 事件数据:携带的参数,比如按键的编号、定时器的计数值
  • 事件来源:谁产生的?中断?任务?还是其他模块?

代码里长什么样?我一般这样定义:

typedef struct {
    uint16_t type;      // 事件类型
    uint16_t param;     // 参数,比如按键ID
    uint32_t timestamp; // 可选,记录发生时间
} Event_t;

嗯,这里要注意:事件结构体尽量精简。我在项目中遇到过有人把事件结构体搞成几十个字节,结果队列一满,内存直接炸了。事件是高频传递的东西,越小越好。

2.2 事件处理器(Event Handler)—— 谁来干活?

事件处理器,就是处理事件的函数。说白了,就是「当这个事件发生时,执行这段代码」。

你想想看,传统的前后台系统里,所有逻辑都堆在main函数的while循环里,或者散落在中断里。事件驱动不一样,我们把每个事件的处理逻辑封装成一个独立的函数,叫Handler。

我的习惯:每个模块只暴露一个事件处理入口,比如 void Button_EventHandler(Event_t *evt)。模块内部再根据事件类型做分支。这样接口清晰,也好维护。

举个例子:

void Button_EventHandler(Event_t *evt) {
    switch(evt->type) {
        case EVENT_BUTTON_PRESS:
            // 处理按键按下
            LED_Toggle(evt->param);
            break;
        case EVENT_BUTTON_RELEASE:
            // 处理按键释放
            break;
        default:
            break;
    }
}

我曾经犯过一个错:在事件处理器里直接调用延时函数,比如 HAL_Delay(100)。结果整个事件循环被卡住,其他事件全堵在队列里。记住:事件处理器要快进快出,别在里面干耗时的活。

2.3 事件队列(Event Queue)—— 排队等候

事件队列,就是一个缓冲区。事件产生后,不会立刻被处理,而是先放进队列里排队。事件循环再从队列头部一个一个取出来处理。

为什么要排队?因为事件产生的速度可能比处理速度快。比如按键抖动,一次按下可能触发多次中断,如果不排队,直接处理,程序就乱套了。队列起到了「削峰填谷」的作用。

队列的两种常见实现:

类型 特点 适用场景
环形缓冲区(Ring Buffer) 固定大小,无动态内存分配 资源受限的MCU,如STM32F0
链表队列(Linked List) 可动态扩展,但需要内存管理 资源较丰富的MCU,如ESP32

我个人偏爱环形缓冲区,简单可靠,没有内存碎片问题。代码大概这样:

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

bool Queue_Push(EventQueue_t *q, Event_t *evt) {
    if(q->count >= QUEUE_SIZE) return false; // 队列满
    q->buffer[q->head] = *evt;
    q->head = (q->head + 1) % QUEUE_SIZE;
    q->count++;
    return true;
}

bool Queue_Pop(EventQueue_t *q, Event_t *evt) {
    if(q->count == 0) return false; // 队列空
    *evt = q->buffer[q->tail];
    q->tail = (q->tail + 1) % QUEUE_SIZE;
    q->count--;
    return true;
}

避坑指南:我曾经在队列满的时候直接丢弃新事件,结果导致按键丢失。后来我改成「覆盖最旧事件」的策略,或者增加队列深度。具体怎么选,看你的业务容忍度。

2.4 事件循环(Event Loop)—— 永动机

事件循环,就是那个永远在跑的while循环。它的工作极其简单:从队列里取一个事件,找到对应的处理器,调用它。然后重复,重复,再重复。

说白了,事件循环就是整个系统的调度核心。没有它,事件就没人管。

void EventLoop_Run(void) {
    Event_t evt;
    while(1) {
        if(Queue_Pop(&g_eventQueue, &evt)) {
            // 根据事件类型,找到对应的处理器
            EventHandler_t handler = FindHandler(evt.type);
            if(handler) {
                handler(&evt);
            }
        }
        // 可以在这里做空闲任务,比如低功耗处理
    }
}

你可能会问:如果队列一直为空,CPU不就空转了吗?没错,所以很多框架会在事件循环里加入「空闲钩子(Idle Hook)」,让CPU在没事干的时候进入睡眠模式,省电。我在做低功耗项目时,这个技巧帮了大忙。

2.5 四大金刚的关系——一张图说清楚

文字说再多,不如一张关系图来得直观。我试着用文字描述一下这个闭环:

  1. 外部触发(中断、定时器、任务)产生一个事件
  2. 事件被推入事件队列,排队等候。
  3. 事件循环不断从队列头部取出事件。
  4. 事件循环根据事件类型,调用对应的事件处理器
  5. 处理器执行完毕后,回到步骤3,继续取下一个事件。

这个循环一旦启动,就不会停止,直到系统断电。所以我说事件循环是「永动机」。

总结一下四个角色的职责:

  • 事件:描述「发生了什么」,是信息的载体。
  • 事件处理器:定义「怎么处理」,是具体的业务逻辑。
  • 事件队列:解决「排队问题」,是缓冲和异步的关键。
  • 事件循环:负责「调度执行」,是系统的引擎。

嗯,到这里,四大金刚的定义和关系你应该清楚了。下一章我们会动手搭建一个最小的事件驱动框架,把今天讲的这些概念真正跑起来。到时候你会发现,原来单片机程序也可以写得这么清爽。