中断与轮询:什么时候该用中断
GearBreak
4 min read
中断还是轮询,判断标准只有一条:事件是否稀疏且需要即时响应,同时系统是否要省电。满足就用中断;事件密集到 CPU 本来就在忙、或逻辑简单到轮询更清晰时,轮询反而更稳。没有放之四海皆准的选法,要看事件特征和系统约束。
中断的本质成本
中断会打断当前执行流,保存上下文、跳转到 ISR、恢复现场,这套开销虽小但有固定成本。当事件每秒发生上万次,中断频率本身就会吃掉可观的 CPU,此时轮询(尤其配合 DMA 或硬件标志)更划算。反过来,按键、报警、串口收完一帧这类很久才来一次的事件,用轮询会让 CPU 空转或无法及时响应,中断才是正确的。
另一个角度是确定性:中断的响应延迟取决于当前是否在跑更高优先级的中断、是否关了中断,存在抖动;轮询的延迟取决于轮询周期,在周期远小于事件间隔时反而更可预测。实时性要求极高的控制回路,往往用高优先级中断配合 DMA,把轮询的不确定性彻底去掉。
低功耗场景几乎只能用中断
睡眠状态下的 MCU 主循环停摆,只有中断能把内核唤醒。靠轮询进入睡眠的设备无法被外部事件叫醒,除非用定时器周期性唤醒轮询——这本身也是一种定时中断。所以电池设备唤醒源普遍是外部中断加低功耗定时器,平时深睡,来了事件才醒。
代码对比
中断方式(按键唤醒):
void EXTI0_IRQHandler(void) {
if (EXTI->PR & (1 << 0)) {
EXTI->PR = (1 << 0); // 清标志
wakeup_task();
}
}
// 主循环可长期 WFI 睡眠
while (1) { __WFI(); }
轮询方式:
while (1) {
if (GPIOA->IN & (1 << 0)) handle_key();
do_other_work();
}
边界与坑
ISR 必须短小,绝不能在里面做延时、打印、动态分配或阻塞等待,否则会饿死其他中断甚至死锁。共享变量要用 volatile 并在主循环访问时考虑临界区保护,否则编译器优化会让你读到旧值。按键类中断必须做去抖和防重复触发,否则一次抖动进多次 ISR。
中断风暴是另一隐患:某外设持续拉高中断线却不清除标志,ISR 会被反复调用,系统卡死。调试时先确认每次进 ISR 都清了对应标志位。多中断源还要注意优先级与嵌套,关键实时任务应给高优先级,但高优先级 ISR 也不能长时间占用 CPU。还有一个隐蔽问题:在临界区关中断期间发生的事件,如果硬件只锁存一个标志,可能丢失边沿;需要确认外设是锁存所有事件还是只锁存最近一个。
混合策略往往最稳
实际工程里很少二选一,常见的是中断打标、主循环轮询处理的组合:中断只做"置标志位"这一件最小的事,真正的业务逻辑放在主循环里按标志执行。这样既保住了响应的即时性,又避免 ISR 里做重活。配合 DMA 时更典型:DMA 完成一半或全部分传输时产生中断,主循环再去取数据,CPU 大部分时间在睡觉或干别的事。
两个典型误用案例
第一,在中断里调用 printf 或阻塞式串口发送。串口发送慢,中断上下文里等发送完成会把整个系统卡住,表现为通信一卡全卡。第二,共享标志没加 volatile,编译器把"读标志"优化成读一次寄存器缓存,主循环永远看不到变化,程序像死了一样。这类 bug 编译不报错、逻辑看着对,只能靠意识到"中断与主循环共享的变量必须 volatile 并考虑原子性"来规避。
决策清单
事件间隔远大于处理时间、且要求即时响应——用中断。事件密集、每秒上千次——用轮询或 DMA。系统要睡眠省电、靠外部事件唤醒——用中断做唤醒源。逻辑简单、CPU 本来就不忙——轮询更直观好调。这份清单比记住任何一条原则都实用,因为真实项目往往是这几条同时成立,需要按占比权衡。
1 view
0 likes
0 comments