1. 事件驱动架构概述:从轮询到中断的思维转变
各位做嵌入式开发的同行,咱们开门见山。今天聊的这件事,几乎决定了你做的产品能不能省电、能不能实时响应。说白了,就是任务调度这件事,你到底是用轮询还是用事件驱动?
我刚开始做嵌入式那几年,一直用轮询。觉得简单、直观、好控制。直到有一次做一款电池供电的传感器节点,满电只能用 6 小时……嗯,从那以后,我彻底转向了事件驱动。今天就把这个转变过程掰开揉碎讲清楚。
1.1 轮询模式:为什么它不适合低功耗系统
先看一段典型的轮询代码:
void main(void) {
while(1) {
if (button_pressed()) handle_button();
if (sensor_ready()) read_sensor();
if (uart_has_data()) process_uart();
/* 还有十几个 if... */
delay_ms(10); /* 怕跑太快?加个延时 */
}
}
这段代码有什么问题?我列几个关键点:
- CPU 一直在跑 —— 哪怕没有任何事件发生,CPU 也在循环检查。电流降不下来。
- 响应延迟不确定 —— 如果轮询周期是 10ms,按键按下后最多要等 10ms 才能响应。有些场景根本不能忍。
- 扩展性差 —— 加一个新任务就要加一个 if,轮询列表越来越长,延迟越来越不可控。
我曾经踩过的坑: 一个多传感器采集项目,用轮询写了 20 多个 if。调试时发现某个传感器偶尔丢数据,查了三天才发现是轮询顺序导致采样间隔被拉长,传感器超时了。从那以后,超过 5 个任务我就坚决不用纯轮询。
1.2 中断思维:让 CPU 只在需要时工作
中断的思路其实很朴素:没事就睡觉,有事再干活。CPU 平时进入低功耗模式,当外部事件(按键、定时器、数据到达)发生时,中断把它唤醒,处理完再睡回去。
看一个简单的中断驱动模型:
void main(void) {
init_peripherals();
enable_interrupts();
while(1) {
__WFI(); /* 等待中断,进入睡眠 */
/* 醒来后,中断服务程序已经处理了事件 */
}
}
void EXTI0_IRQHandler(void) {
/* 按键中断 —— 只做最必要的事 */
set_event_flag(EVENT_BUTTON);
/* 或者直接丢给调度器 */
}
这个模型的好处很明显:
- CPU 空闲时几乎不耗电(μA 级 vs mA 级)
- 响应延迟从「轮询周期」变成「中断延迟」,通常是微秒级
- 任务之间天然解耦,加新功能不影响已有逻辑
我个人习惯: 中断服务程序里只做「标记事件」或「放数据到队列」,绝不处理复杂逻辑。复杂逻辑放到任务里做,这样中断占用的时间极短,不会影响其他中断的响应。
1.3 事件驱动模型的核心三要素
事件驱动架构听起来高大上,其实核心就三个东西:事件、调度器、任务。我画了一张图,你看完就明白了。
三个要素各自干什么,我简单说一下:
| 要素 | 职责 | 我见过的常见错误 |
|---|---|---|
| 事件 | 描述「发生了什么」—— 按键按下、数据到达、定时器超时 | 事件定义太粗,一个事件携带的信息不够,任务还得自己去查寄存器 |
| 调度器 | 管理事件队列,决定「谁先处理」以及「什么时候处理」 | 调度器里做复杂运算,导致事件堆积,低优先级事件永远得不到处理 |
| 任务 | 实际干活的人,处理完就阻塞等待下一个事件 | 任务里用阻塞延时(比如 HAL_Delay),把整个调度器卡死 |
1.4 为什么低功耗系统必须用事件驱动
这个问题我直接给结论:轮询模式下,CPU 的功耗和事件频率无关,始终是满负荷。 事件驱动模式下,CPU 功耗 ≈ 事件频率 × 单次处理时间。
举个例子:
- 一个温度传感器,每 10 秒采集一次数据,每次处理耗时 1ms。
- 轮询模式:CPU 每 10ms 醒来检查一次,10 秒内醒来 1000 次,实际干活 1ms,浪费 999ms 在空转。
- 事件驱动模式:CPU 每 10 秒被定时器唤醒一次,干活 1ms,然后继续睡。功耗直接降到轮询的千分之一。
关键数据对比(以 STM32L0 系列为例):
运行模式(16MHz):约 2.5mA
睡眠模式(WFI):约 1.5μA
如果 99.9% 时间在睡眠,平均电流 ≈ 2.5mA × 0.1% + 1.5μA × 99.9% ≈ 4μA
而轮询模式即使什么都不做,平均电流也在 2mA 以上。
运行模式(16MHz):约 2.5mA
睡眠模式(WFI):约 1.5μA
如果 99.9% 时间在睡眠,平均电流 ≈ 2.5mA × 0.1% + 1.5μA × 99.9% ≈ 4μA
而轮询模式即使什么都不做,平均电流也在 2mA 以上。
你想想看,一个纽扣电池容量大概 200mAh。4μA 平均电流能用 5 年,2mA 只能用 4 天。这就是为什么低功耗系统必须用事件驱动——不是选择题,是生存题。
我曾经踩过的坑: 第一次做事件驱动时,我在任务里用了 printf 调试,printf 内部是阻塞的,导致调度器被卡住,后续事件全部丢失。后来我改用环形缓冲区 + DMA 输出日志,才解决这个问题。记住:任务里绝对不能有阻塞调用。
1.5 从轮询到事件驱动的思维转变清单
最后,我列一个清单,帮你快速完成思维切换:
| 轮询思维 | 事件驱动思维 |
|---|---|
| 「我每隔 X 毫秒检查一次」 | 「有事件来了再处理」 |
| 「主循环里按顺序执行所有任务」 | 「调度器按优先级分发事件」 |
| 「用延时等待某个条件」 | 「用定时器或回调通知条件满足」 |
| 「所有代码都在一个 while(1) 里」 | 「中断 + 任务 + 调度器三层分离」 |
| 「加新功能就加一个 if」 | 「加新功能就注册一个新事件 + 一个新任务」 |