4. 外部中断 EXTI:从理论到实战
中断,是嵌入式系统的灵魂。没有中断,你的 CPU 就得像个傻子一样轮询——不停地问“按键按了吗?按了吗?”。有了中断,CPU 就可以该干嘛干嘛,等事件发生了再被“叫醒”。
我个人习惯把中断比作“电话铃声”。你正在写代码,电话响了(中断来了),你接电话(执行中断服务函数),挂断后继续写代码(返回主程序)。就这么简单。
4.1 中断系统基础
先搞清楚几个核心概念。别急,我不喜欢一上来就堆寄存器。
- 中断源:谁触发了中断?可以是 GPIO 引脚、定时器、串口等外设。
- 中断向量表:一张“电话簿”,记录了每个中断源对应的服务函数地址。
- 中断服务函数 (ISR):接到“电话”后要执行的具体代码。
- 中断优先级:多个中断同时发生时,谁先被响应。
- 中断嵌套:高优先级中断可以打断低优先级中断的执行。
STM32 的 NVIC(嵌套向量中断控制器)负责管理这一切。它支持最多 16 级可编程优先级(取决于芯片型号)。
核心要点:中断服务函数要短、快、干净。不要在 ISR 里做延时、打印、复杂计算。我曾经见过一个同事在中断里跑了一个 500ms 的延时,结果整个系统卡成狗。记住:ISR 里只做标记,具体处理放到主循环。
4.2 EXTI 工作原理
EXTI(External Interrupt/Event Controller)就是外部中断控制器。它负责检测 GPIO 引脚上的电平变化,然后向 NVIC 发送中断请求。
说白了,EXTI 就是 CPU 的“门卫”。它盯着你指定的引脚,一旦发现上升沿、下降沿或者双边沿,就立刻通知 CPU。
STM32 的 EXTI 有 20 个中断线(EXTI0~EXTI19),但并不是每个引脚都有独立的中断线。比如 PA0、PB0、PC0 都共用 EXTI0 线。同一时间只能选择一个引脚连接到 EXTI0。
为什么会这样?因为芯片引脚有限,不可能给每个 GPIO 都配独立的中断通道。这是硬件设计的折中方案。
避坑指南:我曾经在项目里同时配置了 PA0 和 PB0 的 EXTI0 中断,结果两个引脚的中断互相干扰,按键完全失灵。后来查手册才发现,同一 EXTI 线只能映射一个 GPIO 引脚。切记!
下面这张图展示了 EXTI 从引脚到 CPU 的完整路径:
4.3 CubeMX 配置外部中断
用 CubeMX 配置 EXTI,其实就是点几下鼠标的事。我带你走一遍流程:
- 选择引脚:在 Pinout View 中点击目标引脚(比如 PA0),选择
GPIO_EXTI0。 - 配置 GPIO 模式:在 GPIO Settings 里设置:
- GPIO mode:External Interrupt Mode with Rising/Falling edge trigger
- Pull-up/Pull-down:根据外部电路选择(按键通常用 Pull-up)
- User Label:给引脚起个名字,比如
KEY_BUTTON
- 配置 NVIC:切换到 NVIC Settings 选项卡,勾选
EXTI line0 interrupt。 - 设置优先级:在 NVIC 中设置 Preemption Priority 和 Sub Priority。
- 生成代码:点击 GENERATE CODE,CubeMX 会自动生成初始化代码。
小技巧:我习惯在 User Label 里用 KEY_ 或 BTN_ 前缀命名按键引脚,这样在代码里一眼就能认出。比如 KEY_ENTER、KEY_UP。代码可读性很重要,你想想看,三个月后你自己回来看代码,能快速理解吗?
4.4 HAL_GPIO_EXTI_Callback 函数详解
CubeMX 生成的代码里,中断服务函数是 HAL_GPIO_EXTI_IRQHandler()。但你不需要动它。你只需要实现一个回调函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 你的代码写在这里
if(GPIO_Pin == KEY_ENTER_Pin)
{
// 处理按键事件
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
这个回调函数是 HAL 库的“钩子”。它会在中断服务函数里被调用。你只需要判断是哪个引脚触发了中断,然后做相应的处理。
为什么 HAL 库要这么设计?说白了,就是为了让你不用碰底层寄存器。你只需要关注业务逻辑,中断的进出、标志位清除都由 HAL 库帮你搞定。
重要提醒:回调函数是在中断上下文中执行的。不要在里边做延时、打印、动态内存分配等耗时操作。我一般只在回调里设置一个标志位,然后在主循环里处理具体逻辑。
4.5 中断优先级配置
中断优先级决定了多个中断同时发生时,谁先被响应。STM32 使用 4 位优先级寄存器,可以配置为:
| 分组方式 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 不支持抢占,只有子优先级 |
| NVIC_PriorityGroup_1 | 1 | 3 | 2 级抢占,8 级子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4 级抢占,4 级子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8 级抢占,2 级子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16 级抢占,不支持子优先级 |
我个人习惯用 NVIC_PriorityGroup_2,也就是 4 级抢占优先级和 4 级子优先级。这个配置在大多数项目中都够用。
配置优先级时记住一个原则:数值越小,优先级越高。抢占优先级高的可以打断抢占优先级低的。如果抢占优先级相同,则比较子优先级,但不会发生嵌套。
避坑指南:我曾经在一个项目里把定时器中断和外部中断都设成了相同的抢占优先级,结果定时器中断频繁触发,导致外部按键响应延迟严重。后来把外部中断的抢占优先级设得更高,问题就解决了。记住:实时性要求高的中断,优先级要设高一些。
在 CubeMX 中配置优先级很简单:在 NVIC Settings 里直接填写 Preemption Priority 和 Sub Priority 的数值即可。CubeMX 会自动调用 HAL_NVIC_SetPriority() 函数完成配置。
嗯,到这里外部中断的核心内容就讲完了。你可能会问:为什么我配置了中断,但按键按下去没反应?别急,检查一下这几个地方:
- GPIO 模式是否选对了?上升沿还是下降沿?
- NVIC 是否使能了对应的中断线?
- 回调函数里是否判断了正确的引脚?
- 外部电路是否正确?按键按下时电平是否变化?
这些坑我都踩过,所以提前告诉你。中断这东西,配置对了很简单,配置错了能让你查一整天。