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,纯裸机实现。你会发现,原来事件驱动并不神秘,几十行代码就能跑起来。

课后思考:如果你现在手头有一个项目,用的是纯轮询方式。试着找出其中「最不能忍」的三个痛点,想想如果用事件驱动,会怎么改进?

好,这一章就到这里。咱们下章见。