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 事件驱动模型的核心三要素

事件驱动架构听起来高大上,其实核心就三个东西:事件、调度器、任务。我画了一张图,你看完就明白了。

事件驱动模型核心三要素 事件驱动模型核心三要素 事件源 外部中断(按键/传感器) 定时器溢出 DMA 传输完成 通信接口数据到达 软件触发事件 调度器 事件队列 优先级仲裁 任务唤醒决策 低功耗模式管理 时间管理(定时器) 任务 按键处理任务 传感器采集任务 通信协议处理任务 显示刷新任务 日志记录任务 事件源产生事件 → 调度器排队并决策 → 唤醒对应任务执行 → 执行完毕继续睡眠

三个要素各自干什么,我简单说一下:

要素 职责 我见过的常见错误
事件 描述「发生了什么」—— 按键按下、数据到达、定时器超时 事件定义太粗,一个事件携带的信息不够,任务还得自己去查寄存器
调度器 管理事件队列,决定「谁先处理」以及「什么时候处理」 调度器里做复杂运算,导致事件堆积,低优先级事件永远得不到处理
任务 实际干活的人,处理完就阻塞等待下一个事件 任务里用阻塞延时(比如 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 以上。

你想想看,一个纽扣电池容量大概 200mAh。4μA 平均电流能用 5 年,2mA 只能用 4 天。这就是为什么低功耗系统必须用事件驱动——不是选择题,是生存题。

我曾经踩过的坑: 第一次做事件驱动时,我在任务里用了 printf 调试,printf 内部是阻塞的,导致调度器被卡住,后续事件全部丢失。后来我改用环形缓冲区 + DMA 输出日志,才解决这个问题。记住:任务里绝对不能有阻塞调用。

1.5 从轮询到事件驱动的思维转变清单

最后,我列一个清单,帮你快速完成思维切换:

轮询思维 事件驱动思维
「我每隔 X 毫秒检查一次」 「有事件来了再处理」
「主循环里按顺序执行所有任务」 「调度器按优先级分发事件」
「用延时等待某个条件」 「用定时器或回调通知条件满足」
「所有代码都在一个 while(1) 里」 「中断 + 任务 + 调度器三层分离」
「加新功能就加一个 if」 「加新功能就注册一个新事件 + 一个新任务」