1. 事件驱动模型基础

各位同学,咱们今天聊点实在的。嵌入式GUI开发,说白了就是跟「事件」打交道。你按个按钮、滑一下屏幕、甚至系统定时器滴答一声——这些都是事件。那怎么处理它们?这就是事件驱动模型要解决的问题。

我刚开始做嵌入式开发那会儿,其实也没想这么深。觉得不就是检测按键嘛,轮询一下不就完了?后来项目越做越大,屏幕越来越复杂,才发现——嗯,这里面的门道还真不少。

事件驱动编程概念

事件驱动编程,核心思想就八个字:等待事件,响应事件。程序不是一直在跑,而是「睡」在那里,等事情发生才动。

你想想看,我们平时用的手机、平板、智能手表,哪个不是这样?你不动它,它就待机省电。你一触摸,它立刻响应。这就是事件驱动的典型场景。

事件驱动三要素:

  • 事件源——谁产生事件?按键、触摸屏、定时器、传感器……
  • 事件队列——事件来了放哪儿?先进先出的缓冲区。
  • 事件处理器——谁来处理?回调函数、状态机、任务调度器。

我在一个智能家居项目里遇到过这种情况:用户同时按了多个按键,如果不用事件队列,直接处理中断,那系统就乱套了。后来改成事件驱动模型,所有按键操作先入队,再按顺序处理,问题就解决了。

轮询与中断对比

轮询和中断,是两种截然不同的「等事件」方式。我习惯这么跟新人讲——

轮询就像你站在门口,每隔一秒问一次「有人来了吗?」。CPU大部分时间都在做无用功。

中断就像你装了个门铃,有人按铃你才去开门。CPU该干嘛干嘛,效率高得多。

对比项 轮询 中断
CPU占用 持续占用,即使无事件 仅处理时占用
响应延迟 取决于轮询周期 几乎实时
实现复杂度 简单,适合少量事件 较复杂,需处理嵌套
功耗 高(CPU一直跑) 低(可休眠)
典型场景 按键扫描、简单传感器 触摸屏、通信接口

我曾经踩过一个坑:在一个低功耗产品里,用了轮询方式检测按键。结果电池续航从3个月直接掉到2周。后来改成中断唤醒+事件队列,续航才恢复正常。所以,功耗敏感项目千万别偷懒用轮询。

但轮询也不是一无是处。我记得有个工业控制项目,要求按键响应延迟不超过1毫秒。用中断的话,中断服务程序里不能做复杂处理,还得靠任务调度。最后我用了定时器轮询+硬件去抖,反而更稳定。

事件队列原理

事件队列,说白了就是一个「先来后到」的缓冲区。事件产生后,不会立刻被处理,而是先排队。处理程序从队列头部一个一个取出来执行。

为什么要这么麻烦?直接处理不行吗?

原因有三:

  1. 解耦——事件产生者和处理者不用互相等待。
  2. 缓冲——事件爆发时不会丢失。
  3. 优先级——可以给紧急事件插队。

我的个人习惯:事件队列长度至少设为最大可能事件数的2倍。比如你的GUI最多同时响应10个触摸点,队列就开20个槽位。留点余量,防止溢出。

下面是一个简化的事件队列实现,我经常在项目里用这个模板:

/* 事件结构体 */
typedef struct {
    uint8_t  type;      /* 事件类型:按键、触摸、定时器... */
    uint16_t param1;    /* 参数1:坐标X、键值... */
    uint16_t param2;    /* 参数2:坐标Y、长按标志... */
    uint32_t timestamp; /* 时间戳,用于超时判断 */
} Event_t;

/* 事件队列 */
#define EVENT_QUEUE_SIZE  32

static Event_t eventQueue[EVENT_QUEUE_SIZE];
static uint8_t  head = 0;
static uint8_t  tail = 0;

/* 入队 */
int EventQueue_Push(Event_t *evt) {
    uint8_t next = (head + 1) % EVENT_QUEUE_SIZE;
    if (next == tail) {
        return -1;  /* 队列满 */
    }
    eventQueue[head] = *evt;
    head = next;
    return 0;
}

/* 出队 */
int EventQueue_Pop(Event_t *evt) {
    if (head == tail) {
        return -1;  /* 队列空 */
    }
    *evt = eventQueue[tail];
    tail = (tail + 1) % EVENT_QUEUE_SIZE;
    return 0;
}

这段代码看着简单,但有几个关键点要注意:

  • 环形缓冲区,避免内存碎片。
  • 头尾指针,判断满和空。
  • 时间戳字段,我后来加的——有一次调试触摸屏漂移,全靠时间戳定位问题。

下面这张图,是我画的事件驱动模型整体流程,你一看就明白:

事件驱动模型核心流程 事件源 中断 / 轮询 事件队列 事件分发器 按键处理 触摸处理 定时器处理 说明 1. 事件源产生事件 2. 中断/轮询捕获并入队 3. 分发器按类型派发

从这张图你能看到,整个流程是单向的:事件源 → 捕获 → 队列 → 分发 → 处理。每个环节各司其职,互不干扰。

最后,我想强调一点:事件驱动模型不是银弹。如果你的系统只有一两个事件,轮询反而更简单。但一旦涉及GUI、多任务、实时响应——事件驱动就是标配。

我见过太多新手,一上来就写中断服务程序里处理所有逻辑,结果中断嵌套把自己搞死。记住:中断里只做最少的操作,把事件扔到队列里就跑。剩下的,交给主循环或者任务调度器去处理。

一个小技巧:调试事件队列时,可以在入队和出队的地方各加一个GPIO翻转。用示波器一看,就知道队列有没有堵、处理有没有延迟。这招我用了十年,屡试不爽。