1. 事件驱动架构概述:从轮询到中断,从裸机到RTOS

做嵌入式这些年,我见过太多新手一上来就写个超级循环,把所有事情都塞进 while(1)。说实话,我也干过这事。那时候项目简单,一个按键扫描、一个LED闪烁,轮询就够用了。但系统一复杂,问题就来了——CPU忙得要死,响应却慢得要命。

今天咱们聊聊事件驱动。这玩意儿不是什么新概念,但理解透了,你的架构能力会上一个台阶。

1.1 轮询的痛,你体会过吗?

先看个典型裸机代码:

void main(void) {
    while(1) {
        if (按键按下()) { 处理按键(); }
        if (串口有数据()) { 处理串口(); }
        if (定时器超时()) { 处理定时(); }
        // 还有十几个if...
    }
}

有什么问题?

  • CPU空转——大部分时间在检查“有没有事”,而不是“做事”
  • 响应延迟不可控——如果某个处理函数跑得久,其他事件就被堵住了
  • 扩展性差——加一个新功能,就得在循环里多塞一个if

我在一个工业控制器项目里吃过这个亏。当时用轮询处理三个串口,结果某个传感器偶尔发长帧,按键响应直接卡了200ms。客户按下去没反应,以为是死机了。嗯,从那以后我再也不敢轻视事件响应的实时性了。

1.2 中断来了,但别高兴太早

中断解决了“CPU空转”的问题。硬件检测到事件,直接打断当前任务,跳去执行中断服务程序(ISR)。

但中断不是银弹。你想想看:

  • ISR要短——在中断里做复杂处理,会阻塞其他中断
  • 共享数据要保护——主循环和ISR同时访问一个变量,不加锁就会出乱子
  • 优先级反转——低优先级中断占着资源,高优先级中断干等着

我见过一个同事,把整个协议栈解析放在ISR里。结果串口波特率一高,系统直接崩了。中断里只该做“标记事件”,真正的处理要放到任务上下文里。

1.3 事件驱动模型的核心思想

说白了,事件驱动就是:系统不主动干活,等事件来了再响应。就像你坐在工位上,邮件来了才处理,而不是每秒钟刷新一次收件箱。

核心三要素:

要素 说明 我的经验
事件源 谁产生事件?按键、定时器、串口、传感器…… 我习惯把事件源编号,方便调试时定位
事件队列 事件来了先排队,处理完一个再取下一个 队列长度要算好,我曾经因为队列溢出丢过事件
事件处理器 根据事件类型,调用对应的处理函数 用函数指针表比switch-case更灵活

事件驱动的本质:把“CPU主动查询”变成“事件通知CPU”。CPU只在有事件时工作,无事时休眠或跑低优先级任务。

1.4 从裸机到RTOS:事件驱动怎么落地?

裸机下也能实现事件驱动。我常用的方式:

  1. 中断或定时器轮询产生事件
  2. 事件放入全局队列(数组或链表)
  3. 主循环从队列取事件,分发处理

但裸机有个硬伤——一个任务卡住,整个系统就卡住。你处理按键时,串口数据可能就丢了。

RTOS解决了这个问题。每个事件处理器可以是一个独立任务,有自己独立的栈和优先级。事件驱动 + RTOS,效果是这样的:

// 伪代码:RTOS下的事件驱动
void 按键任务(void *param) {
    while(1) {
        等待事件(按键事件);          // 阻塞等待,不占CPU
        处理按键();
    }
}

void 串口任务(void *param) {
    while(1) {
        等待事件(串口事件);
        处理串口();
    }
}

两个任务互不干扰。按键任务等按键事件时,CPU可以跑串口任务。这才是真正的并发。

我的建议:刚接触RTOS时,别一上来就搞复杂同步机制。先用事件标志组或消息队列把事件驱动搭起来,后面再慢慢加信号量、互斥锁。

1.5 事件驱动 vs 传统轮询:一张图看懂

下面这张图对比了两种架构的流程差异。我画了很多次,每次给团队新人讲架构时都会拿出来。

事件驱动架构 vs 传统轮询架构对比 传统轮询架构 主循环开始 检查事件A 检查事件B 检查事件C 处理事件(若有) 回到循环开始 事件驱动架构 系统初始化 等待事件 事件到达 分发到处理器 执行处理函数 回到等待事件 左侧:CPU一直在轮询检查,即使没有事件也要跑一圈。右侧:CPU只在事件到达时工作,无事时休眠或跑其他任务。

1.6 事件驱动的优势,不只是省电

很多人觉得事件驱动就是为了省电。其实不止。

  • 响应更快——事件来了立刻处理,不用等一轮轮询结束
  • CPU利用率高——不做无用功,算力用在刀刃上
  • 代码结构清晰——每个事件一个处理器,模块化程度高
  • 易于扩展——加一个新事件,加一个处理器就行,不用改主循环
  • 调试方便——事件队列可以打印出来,复现问题路径

注意:事件驱动不是万能的。如果事件产生速度超过处理速度,队列会溢出。我曾经在一个高速数据采集项目里,事件队列设计得太短,结果丢了一堆采样点。后来改成动态队列 + 背压机制才解决。

1.7 什么时候该用事件驱动?

我个人判断标准很简单:

场景 推荐架构 理由
只有一个任务,响应要求不高 轮询 简单够用,没必要上事件驱动
多个外部事件,响应要求高 事件驱动 中断 + 事件队列,响应快且可控
系统复杂,多个任务并发 RTOS + 事件驱动 任务隔离,优先级管理,扩展性好
低功耗设备,电池供电 事件驱动 无事休眠,事件唤醒,省电

说白了,如果你的系统里只有一个循环在跑,而且不关心响应时间,轮询就够了。但一旦涉及多个外设、多个任务、或者对响应时间有要求,事件驱动就是更好的选择。

我记得第一次在项目里全面改用事件驱动架构,是在一个智能家居网关项目上。原来用轮询,CPU占用率常年80%以上,加了WiFi和蓝牙后直接跑不动。改成事件驱动后,CPU占用率降到30%,还多加了两个传感器。那次之后,我就再也没写过纯轮询的主循环了。