3. 事件队列设计与实现
任务定义
在嵌入式系统中,ISR与主循环之间、或不同任务之间的通信需要解耦。本案例设计并实现一个无锁单生产者-单消费者(SPSC)环形事件队列,支持固定大小的事件结构体入队与出队,并配套溢出保护与状态查询接口。该队列适用于中断服务程序向主循环投递事件,或单写单读的线程间通信场景。
队列数据结构设计
代码实现
以下实现采用单生产者-单消费者无锁设计:写方仅修改 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 且不修改输出参数。