还在用 while 轮询按键、让CPU“傻等”吗?


前言:CPU 的“瞎等”困境

假设我们写了一个程序,让单片机检测按键是否按下,然后点亮 LED。最直觉的写法是:

while (1) {
    if (GPIO_ReadInputPin(BUTTON) == PRESSED) {
        LED_ON();
    }
}

这叫轮询(Polling)。CPU 像个勤恳的保安,不停地在门口张望:“按键没?按键没?按键没?”——99.9% 的时间,答案都是“没”,但 CPU 依然忙得团团转,无法去做其他事。

这不仅浪费算力,而且响应延迟不可控:假如循环里还做了别的事,检测按键的周期可能长达几毫秒,按键按下到响应的时间就变成“随缘”了。

有没有办法让 CPU 平时安心做自己的事,一旦按键按下就立刻放下手头工作去处理?这就是中断干的事。


一、中断是什么?

中断(Interrupt) 是硬件或软件向 CPU 发出的一个“紧急请求”,让 CPU 暂停当前正在执行的任务,转而去执行一段特定的代码(中断服务程序),处理完后再回到原来的任务继续执行。

我们可以把 CPU 想象成一个正在写作业的学生:

  • 轮询:学生每隔 1 秒抬头看一眼门口有没有人敲门。效率极低。
  • 中断:学生安心写作业。有人敲门时,门铃(中断信号)响起,学生记下作业进度,去开门,处理完再回来继续写。

中断的三大好处:

  1. 实时响应:事件发生时立刻处理,延迟只有几个时钟周期。
  2. 高效利用 CPU:没有事件时 CPU 可以全力做主要任务。
  3. 低功耗:没事做时可以进入睡眠模式,中断将其唤醒。

二、中断的来源与分类

1. 外部中断(引脚中断)

由芯片引脚上的电平变化触发(上升沿、下降沿、高电平、低电平)。比如按键按下、传感器输出跳变。

2. 内部外设中断

由片内外设产生:定时器溢出、串口接收完成、ADC 转换结束、DMA 传输完成等。

3. 系统异常(内核中断)

由内核自身产生:复位、硬件错误(HardFault)、系统调用(SVC)等。

在 STM32 中,中断控制器叫 NVIC(Nested Vectored Interrupt Controller),负责管理所有中断的使能、优先级、嵌套。


三、中断向量表:ISR 的“通讯录”

当中断发生时,CPU 怎么知道该跳转到哪个地址执行代码?

答案是一张固定的表——中断向量表。它存放在 Flash 的起始地址(通常是 0x08000000),里面按顺序存放着每个中断对应的中断服务函数(ISR)的入口地址

比如:

  • 地址 0x08000004:复位中断(程序入口)
  • 地址 0x08000008:NMI(不可屏蔽中断)
  • 地址 0x0800000C:HardFault
  • ……
  • 地址 0x080000XX:EXTI0 中断(外部引脚 0)
  • 地址 0x080000YY:USART1 中断

我们写的 void USART1_IRQHandler(void) 这个函数名,就是通过链接脚本“注册”到向量表中对应位置的。

在 STM32 HAL 库中,很多中断已经帮你写好了弱定义(__weak)的空函数,你只需要重写(override)即可。


四、中断优先级与嵌套

如果多个中断同时发生,或者一个中断正在执行时另一个更紧急的中断到来,怎么办?

NVIC 给每个中断分配了一个优先级。优先级分为:

  • 抢占优先级(Preemption Priority):决定能否打断正在执行的中断。数值越小,优先级越高。高抢占优先级可以打断低抢占优先级。
  • 子优先级(Sub Priority):当抢占优先级相同时,决定谁先执行;但不能互相打断。

STM32 通常用 4 位表示优先级,可以配置分组(比如 3 位抢占 + 1 位子优先级,或 2+2 等)。

例子

  • 中断 A(抢占优先级 0)正在执行,中断 B(抢占优先级 1)到来 → B 必须等 A 完成。
  • 中断 A(抢占优先级 0)正在执行,中断 C(抢占优先级 0,子优先级 1)到来 → C 等待 A 完成,但若 A 和 C 同时发生,子优先级高的(数字小)先执行。

注意:中断服务程序应尽量短小精悍,避免在 ISR 中做耗时操作。如果需要处理大量数据,通常的做法是在 ISR 中置一个标志位,然后由主循环或任务去处理。


五、中断服务程序(ISR)的“三大纪律”

写 ISR 时,必须遵守以下原则,否则系统可能崩溃:

1. 快进快出

不要在 ISR 里做延时、复杂计算、打印长字符串。理想情况下,ISR 只做:

  • 清除中断标志(重要!否则会反复进入)
  • 读取/保存少量数据
  • 设置一个标志位
  • 通过信号量/消息队列唤醒一个任务(在 RTOS 中)

2. 保护共享资源

如果 ISR 和主循环(或其他中断)访问同一个全局变量,必须使用 volatile 关键字,并考虑原子操作或禁用中断。

volatile uint8_t flag = 0;

void EXTI0_IRQHandler(void) {
    flag = 1;
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}

3. 避免调用不可重入函数

printfmalloc 等函数不可重入(即不能在中断中安全调用)。如需调试输出,可以用轮询方式或 RTOS 的专用接口。


六、从轮询到中断:一个按键点灯的完整例子

下面用 STM32 HAL 库展示如何配置外部中断。

1. GPIO 配置(使能中断)

GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;          // 假设按键接 PA0
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发(按键按下时电平从高到低)
GPIO_InitStruct.Pull = GPIO_PULLUP;         // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能 EXTI0 中断
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

2. 编写中断服务函数(在 stm32f1xx_it.c 中,或自己定义)

void EXTI0_IRQHandler(void) {
    // 检查是否是 PA0 引起的中断
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        // 翻转 LED(假设 LED 接 PC13)
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        // 清除中断标志(必须!)
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}

3. 主循环可以“悠闲”做其他事

int main(void) {
    HAL_Init();
    SystemClock_Config();
    // ... 初始化 LED 和按键中断 ...
    while (1) {
        // 这里可以跑其他任务,比如刷新屏幕、处理串口数据
        // 按键按下时会自动触发中断,无需轮询
    }
}

七、事件驱动思想:中断之上的编程模型

有了中断,我们可以构建事件驱动的程序架构:

  • 裸机事件驱动:主循环中检查由 ISR 设置的标志位,执行相应处理。
  • RTOS 事件驱动:ISR 中释放信号量、发送消息队列,唤醒等待的任务。

这种模式让系统变得更加模块化、低耦合。例如:

  • 串口接收中断:每收到一个字节,放入环形缓冲区,并置位“数据可用”标志。
  • 主循环检测到标志,从缓冲区取数据进行协议解析。

相比轮询,事件驱动能极大提高 CPU 利用率和响应实时性。


八、中断的“副作用”与注意事项

1. 中断延迟

从硬件事件发生到 ISR 第一条指令执行,需要一段时间(保存现场、查向量表等)。STM32 通常为 12 个时钟周期左右,可以接受。但如果关中断时间过长,会显著增加延迟。

2. 临界区

某些时候,你需要禁止中断来保护一段代码(例如操作一个不能被中断打断的共享变量)。使用 __disable_irq()__enable_irq(),但临界区应尽可能短。

__disable_irq();
// 操作敏感数据
__enable_irq();

3. 优先级反转

虽然不如 RTOS 中常见,但在裸机中如果低优先级中断长时间占用 CPU,高优先级中断也可能被延迟。解决方法是保持 ISR 短小,或合理配置优先级。

4. 中断丢失

如果同一个中断在 ISR 执行期间再次发生,是否会丢失?

  • 对于电平触发的中断:只要引脚电平保持有效状态,退出 ISR 后会再次触发。
  • 对于边沿触发的中断:在 ISR 执行期间发生的边沿可能被记录(挂起),退出后立即再次进入。
    但若 ISR 执行时间过长,连续多个边沿可能只响应一次。一般设计应保证 ISR 足够快。

九、总结

  • 中断:硬件向 CPU 发出的紧急请求,实现实时响应和高效利用 CPU。
  • NVIC:STM32 的中断管理器,支持优先级嵌套。
  • ISR 准则:快进快出、保护共享资源、不可重入函数要当心。
  • 事件驱动:基于中断的编程思想,让系统更高效、模块化。
  • 注意事项:中断延迟、临界区、优先级反转、中断丢失。

掌握了中断,我们就从“顺序执行”的思维迈入了“异步事件驱动”的思维。这是嵌入式系统进阶的关键一步。


系列全篇快速索引:

篇目 标题 核心关键词
1 嵌入式到底是什么? LED、时钟、GPIO、ARM vs C51 vs STM32
2 串口江湖 UART、RS-232、RS-485、TTL
番外 波特率解析 波特率、比特率、晶振分频
3 两线走天下 I2C、开漏、上拉、地址、应答
4 极速先锋 SPI、四线、CPOL/CPHA、全双工
5 嵌入式大脑 中断、NVIC、ISR、事件驱动
6 时间管理大师 定时器、PWM、输入捕获、SysTick
7 存储与地址 内存映射、大小端、4GB 寻址
8 从裸机到 RTOS 任务、调度、FreeRTOS、优先级
9 任务间悄悄话 队列、信号量、互斥量、IPC
10 位运算的艺术 &、
11 芯片选型 STM32 vs ESP32、选型框架
12 调试与工具链 printf、调试器、逻辑分析仪
13 总结 读手册、写可维护代码、回顾
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐