3. 事件队列设计与实现

事件队列设计与实现 学习卡片

任务定义

在嵌入式系统中,ISR与主循环之间、或不同任务之间的通信需要解耦。本案例设计并实现一个无锁单生产者-单消费者(SPSC)环形事件队列,支持固定大小的事件结构体入队与出队,并配套溢出保护与状态查询接口。该队列适用于中断服务程序向主循环投递事件,或单写单读的线程间通信场景。

队列数据结构设计

环形缓冲区内存布局 (容量 = 8) [0] [1] [2] [3] [4] [5] [6] [7] head (写) tail (读) 已占用事件 空闲槽位 head 写指针 tail 读指针 满判定: (head + 1) % capacity == tail | 空判定: head == tail | 有效容量 = capacity - 1

代码实现

以下实现采用单生产者-单消费者无锁设计:写方仅修改 head,读方仅修改 tail,各自独立推进,无需互斥锁。队列容量固定为 2 的幂,使用位与运算替代取模以提高效率。


// event_queue.h
#ifndef EVENT_QUEUE_H
#define EVENT_QUEUE_H

#include <stdint.h>
#include <stdbool.h>

#define EVENT_QUEUE_CAPACITY   8   // 必须是2的幂
#define EVENT_QUEUE_MASK       (EVENT_QUEUE_CAPACITY - 1)

typedef struct {
    uint8_t id;        // 事件ID
    uint8_t param;     // 参数
    uint16_t timestamp; // 时间戳
} event_t;

typedef struct {
    event_t buffer[EVENT_QUEUE_CAPACITY];
    volatile uint32_t head;  // 写索引(生产者独占)
    volatile uint32_t tail;  // 读索引(消费者独占)
} event_queue_t;

void     queue_init(event_queue_t *q);
bool     queue_push(event_queue_t *q, const event_t *evt);
bool     queue_pop(event_queue_t *q, event_t *out);
bool     queue_is_empty(const event_queue_t *q);
bool     queue_is_full(const event_queue_t *q);
uint32_t queue_count(const event_queue_t *q);

#endif

// event_queue.c
#include "event_queue.h"

void queue_init(event_queue_t *q)
{
    q->head = 0;
    q->tail = 0;
}

bool queue_push(event_queue_t *q, const event_t *evt)
{
    uint32_t next_head = (q->head + 1) & EVENT_QUEUE_MASK;
    
    // 满判定:下一个写位置若等于读位置则队列满
    if (next_head == q->tail) {
        return false;  // 溢出,丢弃事件
    }
    
    q->buffer[q->head] = *evt;
    // 内存屏障(嵌入式编译器通常提供,此处用volatile保证顺序)
    q->head = next_head;
    return true;
}

bool queue_pop(event_queue_t *q, event_t *out)
{
    if (q->head == q->tail) {
        return false;  // 空队列
    }
    
    *out = q->buffer[q->tail];
    q->tail = (q->tail + 1) & EVENT_QUEUE_MASK;
    return true;
}

bool queue_is_empty(const event_queue_t *q)
{
    return (q->head == q->tail);
}

bool queue_is_full(const event_queue_t *q)
{
    uint32_t next_head = (q->head + 1) & EVENT_QUEUE_MASK;
    return (next_head == q->tail);
}

uint32_t queue_count(const event_queue_t *q)
{
    return (q->head - q->tail) & EVENT_QUEUE_MASK;
}

// main.c 示例:ISR投递事件,主循环处理
#include "event_queue.h"
#include <stdio.h>

static event_queue_t g_queue;

// 模拟中断服务程序(实际中由硬件触发)
void ISR_ButtonPress(void)
{
    event_t evt;
    evt.id = 0x01;
    evt.param = 0xAA;
    evt.timestamp = 1000;
    queue_push(&g_queue, &evt);
}

int main(void)
{
    queue_init(&g_queue);
    
    // 模拟产生3个事件
    ISR_ButtonPress();
    ISR_ButtonPress();
    ISR_ButtonPress();
    
    // 主循环消费
    event_t evt;
    while (queue_pop(&g_queue, &evt)) {
        printf("处理事件 id=0x%02X param=0x%02X ts=%u\n",
               evt.id, evt.param, evt.timestamp);
    }
    
    printf("队列空,剩余事件数=%u\n", queue_count(&g_queue));
    return 0;
}

编译与运行


gcc -Wall -O2 -o event_queue_demo main.c event_queue.c
./event_queue_demo

验证方法与判定标准

功能验证:运行上述程序,预期输出为三行事件处理记录(id=0x01, param=0xAA, ts=1000),随后打印“队列空,剩余事件数=0”。若输出与预期一致,说明基本入队/出队逻辑正确。

溢出保护验证:连续调用 queue_push 超过 7 次(容量8,有效容量7),第8次应返回 false,且 queue_is_full 返回 true。可在测试代码中循环压入8个事件并检查返回值。

无锁正确性验证:在支持中断的嵌入式环境中,将 queue_push 放入ISR、queue_pop 放入主循环,长时间运行观察是否有数据错乱或丢失(溢出丢弃除外)。在PC上可用两个线程分别调用 push/pop 进行压力测试,但需注意PC线程调度可能引入重入问题——本设计仅保证单写单读场景安全。

判定标准:所有事件按 FIFO 顺序出队,无重复、无错位;溢出时新事件被丢弃且不影响已有数据;队列空时 pop 返回 false 且不修改输出参数。