3. 事件队列机制:FIFO 与优先级队列的实战选择

事件队列,说白了就是给事件排个队。你想想看,嵌入式系统里各种外设、定时器、触摸屏都在产生事件,CPU 不可能同时处理它们。那怎么办?先存起来,按顺序处理。这就是队列的核心价值。

我个人习惯把事件队列比作银行柜台。客户来了先取号,然后等着叫号。但这里有个问题——普通客户和 VIP 客户,是不是该区别对待?这就引出了两种队列模型:FIFO 和优先级队列。

3.1 先进先出(FIFO)队列:最朴素的公平

FIFO 队列,就是谁先来谁先办。新事件从队尾插入,处理线程从队头取出。这种模型实现简单,资源开销小,适合事件类型单一、实时性要求不高的场景。

我在项目中遇到过一个小型温控器,就用的 FIFO 队列。按键事件、温度采样事件、显示刷新事件,全都排在一个队列里。为什么?因为事件量不大,处理时间也短,没必要搞复杂。

FIFO 队列的核心特征:
  • 插入操作在队尾(tail),取出操作在队头(head)
  • 事件处理顺序 = 事件产生顺序
  • 不会发生事件饿死(starvation)
  • 实现成本极低,只需一个环形缓冲区

来看一个典型的环形 FIFO 实现。嗯,这里要注意,环形缓冲区最关键的是判空和判满逻辑。

/* 环形 FIFO 队列实现 */
#define QUEUE_SIZE 64

typedef struct {
    uint8_t buffer[QUEUE_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
} fifo_queue_t;

/* 初始化 */
void fifo_init(fifo_queue_t *q) {
    q->head = 0;
    q->tail = 0;
}

/* 判空 */
uint8_t fifo_is_empty(fifo_queue_t *q) {
    return q->head == q->tail;
}

/* 判满 */
uint8_t fifo_is_full(fifo_queue_t *q) {
    return ((q->tail + 1) % QUEUE_SIZE) == q->head;
}

/* 入队 */
uint8_t fifo_enqueue(fifo_queue_t *q, uint8_t data) {
    if (fifo_is_full(q)) return 0;  /* 队列满,丢弃 */
    q->buffer[q->tail] = data;
    q->tail = (q->tail + 1) % QUEUE_SIZE;
    return 1;
}

/* 出队 */
uint8_t fifo_dequeue(fifo_queue_t *q, uint8_t *data) {
    if (fifo_is_empty(q)) return 0; /* 队列空 */
    *data = q->buffer[q->head];
    q->head = (q->head + 1) % QUEUE_SIZE;
    return 1;
}
我的经验:环形 FIFO 的队列大小最好设为 2 的幂次方(如 64、128、256)。这样取模运算可以用位与操作替代,在 Cortex-M0 这类低端 MCU 上能省不少时钟周期。

3.2 优先级队列:给紧急事件插队权

但 FIFO 有个硬伤——它不认优先级。想象一下,你正在处理一个按键事件,突然来了个触摸屏中断,要求立即响应。如果还用 FIFO,触摸事件得排到队尾,等前面几十个事件处理完,用户早骂娘了。

优先级队列就是来解决这个问题的。每个事件带一个优先级字段,高优先级事件可以「插队」到队列前面。实现方式有两种:多队列法和堆排序法。

实现方式 原理 插入复杂度 取出复杂度 适用场景
多队列法 每个优先级一个 FIFO 队列 O(1) O(n)(n=优先级数) 优先级等级少(≤8级)
堆排序法 用二叉堆维护事件顺序 O(log n) O(log n) 优先级等级多,事件量大

我个人更推荐多队列法。为什么?因为嵌入式 GUI 的优先级等级通常不超过 4 级(紧急、高、普通、低),用多队列实现简单、可预测、没有动态内存分配。

我曾经踩过的坑:用堆排序实现优先级队列时,堆的调整操作在最坏情况下耗时不可控。有一次在中断服务函数里调用堆插入,结果堆调整时间超过了下一个中断的到达间隔,直接导致中断丢失。从那以后,我规定中断里只用 FIFO,优先级处理全部放到任务级。

3.3 两种队列的对比与选择

到底用 FIFO 还是优先级队列?别纠结,看场景。

对比维度 FIFO 队列 优先级队列
实现复杂度 极低,几十行代码 中等,需要维护优先级逻辑
内存开销 固定大小环形缓冲区 多队列需多个缓冲区,堆需额外节点
实时性 无优先级保障 高优先级事件可快速响应
事件饿死 不会发生 低优先级事件可能被饿死
典型应用 按键、定时器、日志 触摸中断、报警、紧急停止

我建议这样选:

  • 如果事件类型单一,或者所有事件处理时间都很短,用 FIFO 就够了。别为了炫技搞复杂。
  • 如果存在「必须立即响应」的事件(比如触摸屏、安全报警),用优先级队列。但记得给低优先级事件设置超时或老化机制,防止饿死。
  • 如果 MCU 资源紧张(比如只有 4KB RAM),老老实实用 FIFO。优先级队列的额外开销可能让你内存爆掉。

3.4 事件队列的核心流程图

下面这张图展示了事件从产生到被处理的完整流程,以及 FIFO 和优先级队列在其中的位置。

事件队列处理流程图 事件队列处理流程 事件源(外设/定时器/触摸) 中断/轮询采集 选择队列 类型? FIFO FIFO 环形队列 优先级 优先级多队列 事件处理循环 处理完成后回到事件源,等待下一个事件
实际项目中的做法:我通常会在系统初始化时创建两个队列——一个 FIFO 队列放普通事件,一个优先级队列放紧急事件。处理循环先检查优先级队列,再检查 FIFO 队列。这样既保证了紧急事件的响应速度,又避免了低优先级事件被饿死。

3.5 避坑指南:队列设计中的常见问题

做了这么多年嵌入式,队列相关的坑我踩过不少。挑几个典型的说说。

  • 队列溢出:我曾经在一个产品里把 FIFO 队列设成 16 字节,结果用户快速连击按键时事件丢失。后来改成 64 字节,问题解决。记住,队列大小要根据最坏情况下的事件产生速率来算,别拍脑袋定。
  • 优先级反转:用优先级队列时,如果高优先级事件持续涌入,低优先级事件可能永远得不到处理。我见过一个智能门锁,因为触摸中断太频繁,导致电池电量检测事件一直被推迟,最后电量耗尽自动关机。解决方案是给每个事件设置最大等待时间,超时后强制提升优先级。
  • 中断与任务的同步:中断里往队列写数据,任务里读数据。如果不用临界区保护,队列指针可能被破坏。我建议用关中断或原子操作来保护队列操作,别用信号量——中断里等信号量容易死锁。
特别提醒:不要在中断服务函数里做复杂的队列操作。中断里只做入队,出队和处理全部放到任务级。否则你的系统实时性会变得不可预测,调试起来能让你怀疑人生。

好了,事件队列的原理和实现就讲到这里。记住,没有最好的队列,只有最适合的队列。FIFO 简单可靠,优先级队列灵活高效,选哪个取决于你的系统需求。动手写代码的时候,多想想你的事件源有哪些、处理时间多长、优先级怎么分,这些想清楚了,队列设计就成功了一半。