4. 事件驱动状态机框架设计:事件队列与分发机制
好,咱们进入第四章。这一章我打算把事件驱动状态机框架里最核心的几个零件拆开来讲——事件队列、事件分发、还有那个永不停歇的状态机引擎循环。
说实话,这部分我踩过的坑最多。早期我设计过一个智能家居网关,事件队列用的动态链表,结果跑着跑着内存碎片化严重,最后系统直接挂掉。从那以后,我对队列实现的选择就变得非常谨慎。
4.1 事件队列的设计与实现
事件队列,说白了就是事件的“缓冲区”。生产者(中断、定时器、其他任务)往里扔事件,消费者(状态机引擎)往外取事件。队列设计的好坏,直接决定了系统的实时性和稳定性。
4.1.1 环形缓冲区(Ring Buffer)
我个人最偏爱的方式——环形缓冲区。它固定大小、无动态内存分配、O(1) 的入队出队,非常适合资源受限的嵌入式系统。
核心思路: 用数组 + 头尾指针实现循环。头指针指向下一个要读取的位置,尾指针指向下一个要写入的位置。
/* 环形缓冲区结构体 */
typedef struct {
Event_t buffer[EVENT_QUEUE_SIZE]; /* 固定数组 */
uint32_t head; /* 读指针 */
uint32_t tail; /* 写指针 */
uint32_t count; /* 当前元素数量 */
} RingBuffer_t;
/* 入队操作 */
bool RingBuffer_Push(RingBuffer_t *q, Event_t *evt) {
if (q->count >= EVENT_QUEUE_SIZE) {
return false; /* 队列满,丢弃事件 */
}
q->buffer[q->tail] = *evt;
q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
q->count++;
return true;
}
/* 出队操作 */
bool RingBuffer_Pop(RingBuffer_t *q, Event_t *evt) {
if (q->count == 0) {
return false; /* 队列空 */
}
*evt = q->buffer[q->head];
q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
q->count--;
return true;
}
我的经验: 队列大小一定要根据最坏情况来定。我曾经在一个 CAN 总线项目中,把队列设成 16,结果高负载时连续丢事件。后来改成 64,问题解决。建议用
2 的幂次 作为大小,这样取模可以用位运算加速。
4.1.2 链表队列(Linked List Queue)
环形缓冲区虽好,但有个硬伤——大小固定。如果事件数量波动很大,或者你无法预估峰值,链表队列就派上用场了。
链表队列的每个节点动态分配,理论上只受堆空间限制。但代价是什么?内存碎片、分配耗时、还有指针操作的开销。
/* 链表节点 */
typedef struct EventNode {
Event_t event;
struct EventNode *next;
} EventNode_t;
/* 链表队列 */
typedef struct {
EventNode_t *head; /* 出队端 */
EventNode_t *tail; /* 入队端 */
uint32_t count;
} LinkedListQueue_t;
/* 入队 */
bool LinkedListQueue_Push(LinkedListQueue_t *q, Event_t *evt) {
EventNode_t *node = (EventNode_t *)malloc(sizeof(EventNode_t));
if (!node) return false;
node->event = *evt;
node->next = NULL;
if (q->tail) {
q->tail->next = node;
} else {
q->head = node;
}
q->tail = node;
q->count++;
return true;
}
避坑指南: 我曾经在一个长期运行的设备上用链表队列,跑了三个月后系统变慢。查了半天,发现是频繁 malloc/free 导致堆碎片化,分配越来越慢。后来我改用内存池 + 固定节点数量的链表,才彻底解决。如果你用链表队列,一定要配合内存池。
4.1.3 两种队列的对比
| 特性 | 环形缓冲区 | 链表队列 |
|---|---|---|
| 内存分配 | 静态,编译时确定 | 动态,运行时分配 |
| 大小限制 | 固定,不可扩展 | 理论上无限(受堆限制) |
| 入队/出队速度 | O(1),极快 | O(1),但 malloc 有开销 |
| 内存碎片 | 无 | 有,长期运行需注意 |
| 适用场景 | 事件速率稳定、资源受限 | 事件波动大、需要弹性 |
4.2 事件分发机制
队列里存了一堆事件,怎么发给对应的状态机?这就是分发机制要干的事。
4.2.1 直接分发(Direct Dispatch)
最简单的做法——从队列取出事件,直接调用目标状态机的处理函数。每个事件都带一个 target 字段,指明发给谁。
void Dispatcher_Direct(Event_t *evt) {
StateMachine_t *sm = evt->target_sm;
if (sm && sm->handler) {
sm->handler(sm, evt); /* 直接调用 */
}
}
直接分发的优点是延迟极低,没有中间环节。但缺点也很明显——如果某个状态机处理时间过长,会阻塞后续所有事件的分发。
4.2.2 优先级分发(Priority Dispatch)
现实世界的事件是有轻重缓急的。比如按键事件可以等一等,但紧急停止按钮的事件必须立刻处理。这时候就需要优先级分发。
实现方式有两种:
- 多队列方案: 每个优先级一个队列,分发时从高优先级队列开始轮询。
- 单队列 + 优先级排序: 所有事件进一个队列,但按优先级排序,高优先级事件插队到前面。
我推荐多队列方案: 实现简单,入队 O(1),出队时只需按优先级顺序检查各队列。单队列排序在事件多时性能会急剧下降。
/* 多优先级队列 */
#define PRIORITY_LEVELS 3
RingBuffer_t priorityQueues[PRIORITY_LEVELS];
/* 分发循环 */
void Dispatcher_Priority(void) {
for (int pri = PRIORITY_LEVELS - 1; pri >= 0; pri--) {
Event_t evt;
while (RingBuffer_Pop(&priorityQueues[pri], &evt)) {
StateMachine_t *sm = evt.target_sm;
if (sm && sm->handler) {
sm->handler(sm, &evt);
}
}
}
}
注意: 优先级反转问题在这里同样存在。如果一个低优先级事件的处理函数中产生了高优先级事件,而高优先级事件又依赖低优先级状态机的状态...嗯,你想想看,是不是死锁了?我建议在事件处理中不要嵌套产生新事件,或者用独立的事件队列来规避。
4.3 状态机引擎核心循环
好了,队列有了,分发机制有了,现在把它们串起来——这就是状态机引擎的核心循环。
核心循环的逻辑其实很简单:
- 从事件队列取出一个事件
- 根据事件的目标状态机,调用对应的状态处理函数
- 状态处理函数执行状态转移逻辑
- 回到步骤 1,继续取下一个事件
void StateMachineEngine_Run(void) {
while (1) { /* 永不停止的主循环 */
Event_t evt;
/* 1. 从队列取事件 */
if (!RingBuffer_Pop(&g_eventQueue, &evt)) {
/* 队列空,可以进入低功耗模式或执行空闲任务 */
IdleHook();
continue;
}
/* 2. 分发事件 */
StateMachine_t *sm = evt.target_sm;
if (sm && sm->handler) {
sm->handler(sm, &evt);
}
/* 3. 检查是否有定时器超时事件需要插入 */
Timer_CheckAndPost();
}
}
这个循环看起来简单,但实际工程中要考虑几个关键点:
- 空闲处理: 队列空时别空转浪费 CPU,可以进入睡眠模式,或者执行一些低优先级的后台任务。
- 看门狗喂食: 如果某个状态处理函数执行时间过长,看门狗可能会超时复位。我习惯在循环顶部喂一次狗。
- 事件超时: 有些事件有时效性,比如 100ms 内必须处理,否则就失效。可以在出队时检查时间戳。
我曾经踩过的坑: 在一个电机控制项目中,核心循环里直接调用了状态处理函数,而某个状态处理中有一个阻塞式的传感器读取操作,耗时 50ms。结果所有事件都被堵住了,系统响应变得极其迟钝。后来我把阻塞操作改成异步 + 状态机内部状态切换,才解决问题。记住:状态处理函数里不要做阻塞操作。
4.4 整体框架图
下面这张图把事件队列、分发机制和引擎循环的关系画出来了。你看一眼就能明白整个框架的脉络。