第三章:事件驱动架构

好,我们进入正题。事件驱动架构,说白了就是让系统“活”起来。你想想看,起搏器要感知心跳、要响应异常、要调整参数——这些动作如果都靠主循环轮询,那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. 事件1:开始测试 → 状态进入“测试中”
  2. 事件2:测试完成 → 状态进入“结果评估”
  3. 事件3:评估通过 → 状态进入“正常起搏”
  4. 事件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();
}

你想想看,这个调度器每次只处理一个事件,处理完就检查有没有更高优先级的事件。这样既保证了实时性,又不会让低优先级事件饿死。我在实际项目中,这个调度器跑了上万小时,没出过问题。

最后说一句:事件驱动架构不是银弹。如果你的系统逻辑简单、事件很少,用轮询反而更省事。但起搏器这种复杂系统,事件驱动几乎是唯一的选择。记住:事件是系统的血液,队列是血管,调度器是心脏。这三者配合好了,系统才能健康运行。

下一章,我们会深入状态机建模,看看事件和状态怎么完美结合。到时候,我会分享一个我踩过的坑——状态机漏了一个事件,结果起搏器在测试时差点出问题。嗯,到时候细说。