1. 事件驱动思想启蒙:从轮询到中断,为什么需要事件驱动?
大家好,我是你们的讲师。今天咱们聊聊嵌入式开发里一个绕不开的话题——事件驱动。
说实话,我刚开始做单片机开发那会儿,脑子里全是「轮询」。就是那种主循环里挨个查标志位、查电平、查时间的写法。那时候觉得,嗯,这很合理啊,CPU闲着也是闲着,不如让它一直干活。
直到有一次,我在一个项目里同时要处理按键、串口、LED闪烁,还有电机控制。轮询写到最后,代码乱成一锅粥,响应还老是慢半拍。按键按下去,LED要等几百毫秒才亮——用户体验极差。
从那天起,我开始认真思考:到底有没有更好的方式?
1.1 轮询模式:简单但低效
先看一个最经典的例子——LED闪烁。
假设我们要让一个LED以500ms的周期闪烁。用轮询怎么写?
// 轮询方式:LED闪烁
void main() {
while(1) {
// 延时500ms
delay_ms(500);
// 翻转LED
LED_TOGGLE();
}
}
这段代码,说白了就是「死等」。CPU在这500ms里什么都干不了。你想想看,如果这时候来了一个按键中断,或者串口收到一帧数据——对不起,等延时结束再说。
我在项目中遇到过类似的情况。一个温控器项目,用轮询处理温度采样和LED显示。结果温度采样间隔是200ms,LED闪烁周期是500ms,两个延时函数互相嵌套,最后LED闪得忽快忽慢,温度采样也丢了好几组数据。
轮询的痛点很明显:
- CPU被阻塞,无法响应其他任务
- 实时性差,高优先级事件被低优先级任务拖累
- 代码耦合度高,维护困难
- 功耗高——CPU一直在跑,没法进入休眠
核心问题:轮询是「CPU主动去找事件」,而事件的发生是随机的。这种「主动找」的方式,注定效率低下。
1.2 中断模式:让CPU学会「被动响应」
那中断呢?中断的思路完全相反——让事件来找CPU。
还是那个LED闪烁的例子。用定时器中断来实现:
// 定时器中断方式:LED闪烁
volatile uint8_t led_tick = 0;
void TIMER_IRQHandler() {
led_tick++;
if(led_tick >= 10) { // 假设10ms中断一次,500ms = 50次
led_tick = 0;
LED_TOGGLE();
}
}
void main() {
init_timer(10); // 配置定时器,10ms中断一次
while(1) {
// CPU可以在这里做其他事情!
// 比如处理按键、串口通信、传感器采样...
process_key();
process_uart();
process_sensor();
}
}
看到了吗?主循环里不再有delay_ms()。CPU可以自由地去做其他事情。定时器每10ms打断一次CPU,翻转LED,然后继续回去干活。
嗯,这里要注意:中断服务函数里不要做太多事情。我建议只做标志位设置或简单的计数,复杂的逻辑放到主循环里处理。否则中断嵌套多了,系统会变得不稳定。
个人习惯:中断里只做「最小必要操作」。比如设置一个标志位、递增一个计数器。真正的处理逻辑,放到主循环或者任务调度器里。这样既保证了实时性,又避免了中断过长导致的问题。
1.3 从中断到事件驱动:只差一步
中断解决了「CPU被阻塞」的问题,但还不够。
你想想看,如果系统里有10个中断源——定时器、按键、串口、ADC、SPI、I2C……每个中断都设置一堆标志位,主循环里再挨个查这些标志位——这不又回到轮询了吗?
只不过这次轮询的是标志位,而不是硬件引脚。
事件驱动的核心思想:把「发生了什么」和「怎么处理」彻底分开。
具体来说:
- 中断只负责「报告事件」——比如「按键按下」、「定时器溢出」、「数据接收完成」
- 主循环或调度器负责「分发事件」——把事件交给对应的处理函数
- 处理函数负责「响应事件」——执行具体的业务逻辑
用代码表示就是:
// 事件驱动框架雏形
typedef enum {
EVENT_NONE,
EVENT_KEY_PRESS,
EVENT_TIMER_TICK,
EVENT_UART_RX,
// ... 更多事件
} Event_t;
Event_t g_event = EVENT_NONE;
void TIMER_IRQHandler() {
g_event = EVENT_TIMER_TICK; // 报告事件
}
void KEY_IRQHandler() {
g_event = EVENT_KEY_PRESS; // 报告事件
}
void main() {
while(1) {
switch(g_event) {
case EVENT_TIMER_TICK:
LED_TOGGLE();
g_event = EVENT_NONE;
break;
case EVENT_KEY_PRESS:
handle_key();
g_event = EVENT_NONE;
break;
// ... 更多事件处理
default:
// 没有事件时,CPU可以休眠!
CPU_SLEEP();
break;
}
}
}
这段代码虽然简单,但已经具备了事件驱动的雏形。注意看default分支——没有事件时,CPU可以进入休眠模式。这在电池供电的设备里,功耗能降低好几个数量级。
我曾经踩过的坑:事件标志位一定要用volatile修饰!而且中断和主循环共享的变量,最好用原子操作或者关中断保护。否则会出现「事件丢失」或者「事件重复处理」的诡异问题。调试起来非常头疼。
1.4 为什么需要事件驱动?三个理由
好了,说了这么多,总结一下为什么我们需要事件驱动。
| 对比维度 | 轮询模式 | 中断模式 | 事件驱动模式 |
|---|---|---|---|
| CPU利用率 | 低(大量空转) | 中(中断打断) | 高(无事休眠) |
| 实时性 | 差(依赖轮询周期) | 好(中断立即响应) | 好(事件优先调度) |
| 代码可维护性 | 差(逻辑耦合) | 中(中断与主循环混杂) | 好(事件与处理分离) |
| 功耗控制 | 差(CPU持续运行) | 中(可休眠但需中断唤醒) | 好(无事件时深度休眠) |
| 扩展性 | 差(新增任务需改主循环) | 中(新增中断需小心优先级) | 好(新增事件只需注册处理函数) |
说白了,事件驱动就是让CPU从「打工仔」变成「管理者」。
打工仔模式:CPU不停地问「有事吗?有事吗?有事吗?」——累死累活,效率还低。
管理者模式:CPU坐在那里等,有人来报告「老板,按键按下了」「老板,定时器到点了」——然后CPU根据事件类型,分配对应的人去处理。没人报告?那就喝茶睡觉(休眠)。
你想想看,哪个模式更优雅?
1.5 小结与预告
这一章我们从一个简单的LED闪烁案例出发,对比了轮询、中断和事件驱动三种模式。核心就一句话:事件驱动让CPU从「主动查询」变成「被动响应」,从而大幅提升系统的实时性、可维护性和能效。
下一章,我会带大家手写一个最小的事件驱动框架。不依赖任何RTOS,纯裸机实现。你会发现,原来事件驱动并不神秘,几十行代码就能跑起来。
课后思考:如果你现在手头有一个项目,用的是纯轮询方式。试着找出其中「最不能忍」的三个痛点,想想如果用事件驱动,会怎么改进?
好,这一章就到这里。咱们下章见。