3. Polling 模式介绍:主动轮询与被动中断

这一章我们来聊聊 Polling 模式。说白了,就是状态机怎么跟外界“打招呼”。是它主动去问“你好了没?”,还是等着对方通知“我好了,快来”!

这两种方式,就是主动轮询和被动中断。我刚入行那会儿,总觉得轮询简单粗暴,啥都用轮询。直到有一次,一个高吞吐的数据采集设备,因为轮询太频繁,CPU 被吃光了,连控制台都卡死。那时候我才意识到,选哪种模式,不是拍脑袋决定的。

3.1 主动轮询:我主动去问

主动轮询,就是状态机每隔一段时间,自己去检查外部条件是否满足。

举个例子:你在等快递。轮询模式就是,你每隔 10 分钟去门口看一眼,快递到了没。要是没到,就回去继续干活,过一会儿再去看。

代码里怎么写?大概是这样:

while(1) {
    if (check_sensor() == DATA_READY) {
        read_data();
        process_data();
    }
    delay(100); // 轮询间隔
}

简单吧?但这里有个坑:轮询间隔设短了,CPU 忙死;设长了,响应慢。

我记得有个项目,需要响应一个按键。我一开始把轮询间隔设成 1 毫秒,结果 CPU 占用率直接飙到 40%,别的任务干不了。后来改成 10 毫秒,CPU 占用降下来了,但按键响应偶尔有延迟,用户体验不好。

3.2 被动中断:等对方来通知

被动中断就完全不一样了。还是等快递的例子:中断模式就是,你给快递员打电话,说“到了给我发个短信”。然后你就可以安心睡觉、看书、打游戏。等短信一来,再去门口拿。

中断模式下,状态机可以安心干自己的事。直到外部触发了一个“中断信号”,它才暂停当前工作,去处理这个事件。

硬件上通常这样配置:

enable_interrupt(SENSOR_IRQ);
while(1) {
    // 正常任务,不用管传感器
    do_other_stuff();
}

// 中断服务函数
void sensor_irq_handler() {
    read_data();
    process_data();
}

中断的好处很明显:CPU 利用率高,响应快。但坏处也很致命:中断服务函数不能写太长,否则会阻塞其他中断。而且,多个中断同时来了怎么办?优先级怎么处理?这都需要仔细设计。

核心区别:轮询是状态机主动“驱动”流程;中断是外部事件“驱动”状态机。

3.3 两种模式的优缺点对比

话不多说,直接上表。这个表我在好多项目里都贴墙上,算是经验总结。

特性 主动轮询 被动中断
CPU 占用 高,尤其是短间隔轮询 低,只在事件发生时占用
响应延迟 取决于轮询间隔,可能不稳定 几乎实时,取决于中断优先级
实现复杂度 低,就是 if-check 和 delay 高,需要配置中断控制器、优先级、嵌套等
多事件处理 容易,循环里顺序检查就行 麻烦,需要设计中断屏蔽和嵌套逻辑
可预测性 高,时序是固定的 低,中断到达时间不确定
调试难度 低,加个打印就能看 高,中断上下文难打印,容易死锁

我个人的习惯是:如果事件发生频率很高(比如每秒几百次),用轮询更简单,因为中断开销反而更大。如果事件发生很稀疏(比如按键、报警信号),用中断更合理,CPU 不用白白等着。

一个小技巧:你可以把轮询和中断结合起来。比如,用中断来“唤醒”一个低功耗的轮询任务。我曾经在一个电池供电的设备里这么干过,效果极好。

3.4 状态机里的具体应用

在状态机里,轮询和中断的体现方式也不同。

轮询模式下,状态机自己问“条件满足了没”:

// 轮询式状态机
void state_machine_poll() {
    switch(state) {
        case IDLE:
            if (flag_ready == true) { state = READ; }
            break;
        case READ:
            if (data_valid == true) { state = PROCESS; }
            break;
        case PROCESS:
            // ...
            break;
    }
}

中断模式下,状态机是被动的:

// 中断驱动状态机
void irq_handler() {
    // 直接推进状态机
    state = READ;
    data = read_data();
    // 可能会有状态标记,让主循环去处理
}

void main_loop() {
    if (state_changed) {
        process_state(state);
        state_changed = false;
    }
}

注意:在中断里直接改变状态机的状态,要非常小心。如果状态机正在处理上一个状态,你突然改了,会导致逻辑混乱。我一般会在中断里只设一个“事件标记”,然后在主循环里统一处理。

千万别在中断里做耗时操作! 我曾经见过有人把文件写入操作放在中断里,结果系统直接卡死。中断里只做三件事:读寄存器、设标记、清中断。其他事情扔给主循环。

你想想看,为什么很多嵌入式工程师讨厌中断?因为调试太麻烦了。一进中断,程序跑到哪了都不知道,加个 printf 还可能导致死循环。所以,能用轮询解决的事,就别上中断。

但是,如果你做的是实时控制系统,比如电机控制、无人机飞控,那必须用中断。轮询那点延迟,可能直接导致系统抖起来。

3.5 本章核心知识结构

下面的图,是我自己画的。把主动轮询和被动中断的区别、优缺点、应用场景都梳理清楚了。你可以保存下来,做项目时对照着看。

主动轮询与被动中断对比图 主动轮询 vs 被动中断 核心对比 主动轮询 核心机制: CPU 周期性地检查外部条件。 优点: 实现简单,时序可预测,调试容易。 缺点: CPU 占用高,响应延迟取决于间隔。 被动中断 核心机制: 外部事件触发,CPU 暂停当前任务处理。 优点: CPU 利用率高,事件响应快、实时。 缺点: 实现复杂,调试困难,易产生死锁。 组合策略建议: 高频率稀疏事件 -> 用轮询;低频高实时事件 -> 用中断。 或者:中断负责“唤醒”,轮询负责“处理”。 场景 场景

你看这个图,左边是轮询,右边是中断。中间箭头连接,意思是你可以在同一个系统里混合使用。我个人强烈建议,刚开始做设计时,先确定事件频率,再决定用哪种模式。不要想当然。