第三章:事件驱动架构
好,我们进入正题。事件驱动架构,说白了就是让系统“活”起来。你想想看,起搏器要感知心跳、要响应异常、要调整参数——这些动作如果都靠主循环轮询,那CPU得累死,实时性也跟不上。所以,我们需要一套事件驱动的机制。
3.1 事件驱动编程模型
事件驱动编程,核心就三个字:等、收、发。系统大部分时间在“等”事件,一旦事件来了,就“收”下来处理,处理完可能再“发”新事件出去。
我在项目中遇到过一种情况:工程师把所有逻辑都写在中断里,结果中断嵌套把自己搞死了。嗯,这里要注意——事件驱动不是让你把所有事都塞进中断。正确的做法是:中断只负责“标记事件”,真正的处理交给后台任务。
核心模型:
- 事件源:谁产生事件?硬件中断、定时器、通信接口、用户操作……
- 事件分发器:把事件路由到正确的处理函数。
- 事件处理器:真正干活的地方,通常是一个状态机或回调函数。
举个例子,起搏器检测到心室早搏(PVC),这是一个事件。事件源是心电检测模块,分发器把它送到“PVC处理”状态机,状态机根据当前模式决定要不要起搏。整个过程,CPU只在事件发生时工作,其他时间可以休眠——这对电池寿命太重要了。
3.2 事件队列与调度
事件来了,不能直接处理,得排队。为什么?因为事件可能同时来,或者处理一个事件时又来了新事件。没有队列,系统就乱套了。
事件队列的设计要点:
- FIFO原则:先来先服务,保证公平性。
- 优先级队列:紧急事件(比如VF检测)插队到前面。
- 队列深度:太浅会丢事件,太深浪费内存。我一般按最坏情况估算,再加20%余量。
我的经验:起搏器里,我习惯用环形缓冲区实现事件队列。没有动态内存分配,没有碎片问题,性能也稳定。代码大概长这样:
// 事件队列结构体
typedef struct {
Event_t buffer[QUEUE_SIZE];
uint8_t head;
uint8_t tail;
uint8_t count;
} EventQueue_t;
// 入队
bool Enqueue(EventQueue_t *q, Event_t *evt) {
if (q->count >= QUEUE_SIZE) return false; // 队列满,丢事件
q->buffer[q->head] = *evt;
q->head = (q->head + 1) % QUEUE_SIZE;
q->count++;
return true;
}
// 出队
bool Dequeue(EventQueue_t *q, Event_t *evt) {
if (q->count == 0) return false; // 队列空
*evt = q->buffer[q->tail];
q->tail = (q->tail + 1) % QUEUE_SIZE;
q->count--;
return true;
}
调度方面,我建议用优先级+时间片混合调度。高优先级事件(比如起搏脉冲触发)必须立即处理,低优先级事件(比如日志记录)可以等一等。我曾经见过一个设计,所有事件都用同一个队列,结果一个日志事件把起搏事件堵住了——那后果可不敢想。
3.3 异步处理机制
异步处理,说白了就是“别等我,你先忙别的”。起搏器里很多操作是异步的:发送数据到程控仪、写日志到Flash、计算电池剩余电量……这些操作如果同步执行,系统就卡死了。
异步处理的三种常见模式:
| 模式 | 适用场景 | 注意事项 |
|---|---|---|
| 回调函数 | 简单事件响应 | 小心回调嵌套过深 |
| 消息传递 | 模块间通信 | 需要定义消息协议 |
| 状态机+事件 | 复杂流程控制 | 状态表要完整 |
我个人最喜欢用状态机+事件模式。举个例子,起搏器要执行一次“自动阈值测试”:
- 事件1:开始测试 → 状态进入“测试中”
- 事件2:测试完成 → 状态进入“结果评估”
- 事件3:评估通过 → 状态进入“正常起搏”
- 事件4:评估失败 → 状态进入“备用阈值”
整个过程,CPU不需要一直盯着。事件来了就处理一步,处理完继续休眠。这就是异步的魅力。
避坑指南:我曾经在一个项目中,异步回调里直接调用了阻塞函数(比如延时等待)。结果回调卡住了,后续事件全堵在队列里。记住:异步处理函数里绝对不能有阻塞操作。如果需要等待,用状态机切出去,等事件回来再继续。
3.4 实战中的事件驱动设计
好了,理论讲完了,咱们看看起搏器里具体怎么用。我一般把事件分成三类:
- 硬件事件:心电R波检测、起搏脉冲触发、电池低电压……
- 定时事件:每1ms的调度心跳、每100ms的传感器采样、每1s的电池检测……
- 软件事件:模式切换、参数更新、日志写入……
这三类事件,优先级从高到低。硬件事件必须最快响应,定时事件次之,软件事件可以排队慢慢来。我习惯用一个三级事件队列:
// 三级事件队列
EventQueue_t hwQueue; // 硬件事件,最高优先级
EventQueue_t timerQueue; // 定时事件,中等优先级
EventQueue_t swQueue; // 软件事件,最低优先级
// 调度主循环
void EventDispatcher(void) {
Event_t evt;
// 先处理硬件事件
if (Dequeue(&hwQueue, &evt)) {
ProcessEvent(&evt);
return;
}
// 再处理定时事件
if (Dequeue(&timerQueue, &evt)) {
ProcessEvent(&evt);
return;
}
// 最后处理软件事件
if (Dequeue(&swQueue, &evt)) {
ProcessEvent(&evt);
return;
}
// 没事件?进入低功耗模式
EnterSleepMode();
}
你想想看,这个调度器每次只处理一个事件,处理完就检查有没有更高优先级的事件。这样既保证了实时性,又不会让低优先级事件饿死。我在实际项目中,这个调度器跑了上万小时,没出过问题。
最后说一句:事件驱动架构不是银弹。如果你的系统逻辑简单、事件很少,用轮询反而更省事。但起搏器这种复杂系统,事件驱动几乎是唯一的选择。记住:事件是系统的血液,队列是血管,调度器是心脏。这三者配合好了,系统才能健康运行。
下一章,我们会深入状态机建模,看看事件和状态怎么完美结合。到时候,我会分享一个我踩过的坑——状态机漏了一个事件,结果起搏器在测试时差点出问题。嗯,到时候细说。