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图来直观对比:
从图上可以看得很清楚:
- 观察者模式:主题直接持有观察者列表,观察者必须注册到具体主题。这是一种紧耦合的关系。
- 发布-订阅模式:发布者和订阅者之间有一个事件通道(消息队列/事件总线),双方互不知道对方存在。这是完全解耦的。
在嵌入式场景下,我推荐优先使用发布-订阅模式。为什么?
避坑指南:我曾经在一个多传感器采集项目中用了观察者模式,每个传感器模块都直接注册到数据采集主题。后来要增加一个数据融合模块,需要同时监听多个传感器——观察者模式改起来非常痛苦,因为每个主题的观察者列表是硬编码的。换成发布-订阅后,新模块只需要订阅它关心的事件类型就行,完全不影响现有代码。
两种模式的具体对比:
| 维度 | 观察者模式 | 发布-订阅模式 |
|---|---|---|
| 耦合度 | 高(主题知道观察者) | 低(双方互不知道) |
| 通信方式 | 直接调用 | 通过事件通道间接通信 |
| 适用场景 | 单一对象状态变化通知 | 跨模块、跨任务通信 |
| 单元测试难度 | 中等(需要mock主题) | 低(可以单独测试发布者和订阅者) |
| 资源开销 | 低(无中间层) | 中等(需要事件通道管理) |
| 扩展性 | 差(新增观察者需修改主题) | 好(新增订阅者无需修改发布者) |
1.4 嵌入式场景下的选择建议
说了这么多,到底怎么选?我个人的经验是:
- 单芯片、单任务、模块内通信:用观察者模式就够了,简单直接,开销小。比如一个LED驱动模块内部的状态变化通知。
- 多任务、跨模块、系统级通信:必须用发布-订阅模式。比如传感器数据采集模块把数据发布到总线上,显示模块、存储模块、通信模块各自订阅需要的数据。
- 混合使用:很多成熟的嵌入式系统(比如Zephyr、FreeRTOS+TCP)都是两种模式混用。模块内部用观察者,模块之间用发布-订阅。
小技巧:在单元测试时,发布-订阅模式的优势特别明显。你可以单独测试发布者是否正确发布了事件,也可以单独测试订阅者是否正确处理了事件。中间的事件通道甚至可以替换成测试专用的“间谍通道”。这个咱们在后面的章节会详细展开。