1. 课程导入

各位同学,咱们今天聊聊嵌入式世界里一个绕不开的话题——事件驱动架构。说实话,我早年做单片机裸奔程序时,全是超级循环里轮询标志位,代码写到最后自己都看不懂。后来转到事件驱动,才算是开了窍。

事件驱动,说白了就是:系统不主动干活,而是等事情发生再响应。你想想看,嵌入式设备大部分时间都在等——等按键按下、等数据包到达、等定时器溢出。与其让CPU空转轮询,不如让它睡大觉,有事件来了再醒。

1.1 核心概念:事件、事件源、事件处理器

先搞清楚三个基本角色:

角色 说明 嵌入式例子
事件 系统中发生的某件“事” GPIO中断、UART接收完成、定时器超时
事件源 产生事件的主体 外设、定时器、网络协议栈、用户操作
事件处理器 响应事件的回调函数或任务 按键消抖处理、协议解析、状态机转移

我刚开始做项目时,经常把事件和消息搞混。后来总结了一句话:事件是“已经发生了什么”,消息是“你要去做什么”。比如按键按下是一个事件,而“点亮LED”是一条消息。这个区别在单元测试时特别重要,后面会讲到。

关键原则:事件处理器必须短小精悍,不能在处理函数里做阻塞操作。我曾经在一个项目中,事件处理器里调了延时函数,结果整个系统卡死——这就是血的教训。

1.2 事件循环:嵌入式系统的“心脏”

事件循环是事件驱动架构的调度核心。它的工作模式很简单:排队 → 取出 → 分发 → 处理 → 继续排队。

来看一个典型的嵌入式事件循环伪代码:

// 嵌入式事件循环核心
while (1) {
    Event_t evt = event_queue_dequeue();  // 从队列取事件
    if (evt.id != EVENT_NONE) {
        event_dispatcher_dispatch(&evt);  // 分发给注册的处理器
    }
    // 低功耗模式:队列为空时进入休眠
    if (event_queue_is_empty()) {
        enter_sleep_mode();
    }
}

嗯,这里要注意:事件队列必须是线程安全的。中断服务程序和主循环都可能往队列里放事件。我见过一个产品,中断里直接操作队列没加锁,结果偶发死机,查了整整两周才定位到。

个人习惯:我一般用环形缓冲区实现事件队列,配合关中断或原子操作保证安全。队列深度根据最坏情况估算,再留50%余量。

事件循环的变体有很多:

  • 单线程事件循环:最常见,所有事件串行处理,适合资源受限的MCU
  • 优先级事件循环:高优先级事件插队处理,适合实时性要求高的场景
  • 多级事件循环:不同优先级对应不同队列,我曾在Zephyr项目中用过这种

1.3 发布-订阅模式 vs 观察者模式

这两个模式经常被混为一谈,但它们在嵌入式场景下的差异非常关键。咱们用一张SVG图来直观对比:

发布-订阅模式 vs 观察者模式 结构对比 观察者模式 Subject(主题) Observer A Observer B Observer C 主题直接持有观察者引用 观察者必须注册到具体主题 紧耦合,不适合跨模块通信 适合单一对象的状态变化通知 发布-订阅模式 Publisher(发布者) Event Channel(事件通道) 消息队列 / 事件总线 Subscriber A Subscriber B Subscriber C 发布者和订阅者完全解耦 通过事件通道间接通信 支持一对多、多对多通信

从图上可以看得很清楚:

  • 观察者模式:主题直接持有观察者列表,观察者必须注册到具体主题。这是一种紧耦合的关系。
  • 发布-订阅模式:发布者和订阅者之间有一个事件通道(消息队列/事件总线),双方互不知道对方存在。这是完全解耦的。

在嵌入式场景下,我推荐优先使用发布-订阅模式。为什么?

避坑指南:我曾经在一个多传感器采集项目中用了观察者模式,每个传感器模块都直接注册到数据采集主题。后来要增加一个数据融合模块,需要同时监听多个传感器——观察者模式改起来非常痛苦,因为每个主题的观察者列表是硬编码的。换成发布-订阅后,新模块只需要订阅它关心的事件类型就行,完全不影响现有代码。

两种模式的具体对比:

维度 观察者模式 发布-订阅模式
耦合度 高(主题知道观察者) 低(双方互不知道)
通信方式 直接调用 通过事件通道间接通信
适用场景 单一对象状态变化通知 跨模块、跨任务通信
单元测试难度 中等(需要mock主题) 低(可以单独测试发布者和订阅者)
资源开销 低(无中间层) 中等(需要事件通道管理)
扩展性 差(新增观察者需修改主题) 好(新增订阅者无需修改发布者)

1.4 嵌入式场景下的选择建议

说了这么多,到底怎么选?我个人的经验是:

  • 单芯片、单任务、模块内通信:用观察者模式就够了,简单直接,开销小。比如一个LED驱动模块内部的状态变化通知。
  • 多任务、跨模块、系统级通信:必须用发布-订阅模式。比如传感器数据采集模块把数据发布到总线上,显示模块、存储模块、通信模块各自订阅需要的数据。
  • 混合使用:很多成熟的嵌入式系统(比如Zephyr、FreeRTOS+TCP)都是两种模式混用。模块内部用观察者,模块之间用发布-订阅。

小技巧:在单元测试时,发布-订阅模式的优势特别明显。你可以单独测试发布者是否正确发布了事件,也可以单独测试订阅者是否正确处理了事件。中间的事件通道甚至可以替换成测试专用的“间谍通道”。这个咱们在后面的章节会详细展开。