本篇继 [[ZZZ-08-GPIO输入]],重点学习“实验二:外部中断(EXTI)”中的内容
核心概念:什么是 NVIC
NVIC 是 Cortex-M 内核里的一个硬件模块,专门负责管理所有的中断。如果把 CPU 比作正在工作的医生,中断就是闯进诊室的病人,而 NVIC 就是分诊台的护士。在 CubeIDE 中,最让你困惑的可能就是 Priority(优先级)。
抢占优先级 vs 子优先级(最核心的区别)
STM32 的中断优先级分为两个维度:
抢占优先级 (Preemption Priority) —— “插队权”
- 动作: 正在处理中断 A 时,中断 B 来了。如果 B 的抢占优先级比 A 高,CPU 会立刻放下手头的 A,去处理 B。等 B 处理完,再回来接着处理 A。
- 本质: 这种现象叫 “中断嵌套”。
子优先级 (Sub Priority) —— “排队权”
- 动作: 如果中断 A 和中断 B 的抢占优先级一样,且它们同时发生了。
- 本质: 这时 CPU 不会发生嵌套,而是看谁的子优先级高,谁就先被处理。如果其中一个已经在运行了,另一个只能乖乖在门口等着,不能进去“插队”。
注意:
- 数字越小,优先级越高(0 是最高优先级)。
- 只有抢占优先级不同,才能发生“嵌套”(插队)。
优先级分组 (Priority Grouping)
在 CubeIDE 的 NVIC 设置里,你会看到一个 Priority Group 的选项。这是因为 STM32 分给优先级的“总位数”是有限的(通常是 4 位,即 $2^4=16$ 级)。
你可以决定这 4 位如何分配给“抢占”和“子”:
- Group 4 (4 bits for preemption, 0 for sub):有 16 级抢占优先级,没有子优先级(常用,最简单)。
- Group 2 (2 bits for preemption, 2 for sub):有 4 级抢占(0-3),4 级子(0-3)。
建议: 初学者直接选 Group 4 或者默认设置即可,没必要搞得太复杂。 ![[Pasted image 20251220205142.png]]
CubeIDE 配置界面里的关键项
当你点开 System Core -> NVIC 时,你会看到这几列:
- Enabled:打勾,硬件才会把中断信号传给内核。
- Preemption Priority:设置抢占优先级。
- Sub Priority:设置子优先级。
- Uses FreeRTOS functions:如果以后学习操作系统(RTOS),这里会提醒哪些中断可以调用系统 API。
中断背后的“潜规则” (Best Practices)
在实际开发中,有三条铁律一定要遵守:
- 快进快出:中断服务函数(ISR)就像急诊室,不能在里面“睡觉”(严禁使用
HAL_Delay())。如果里面写了耗时很长的循环,整个系统会卡死。 - 不要在中断里打印:
printf非常慢。如果你想看中断触发没,翻转个 LED 或者设置一个标志位(Flag)是最好的选择。 - 变量要加
volatile:如果你在中断里修改了一个全局变量,而main函数里要用到它,定义变量时请写成:volatile uint8_t flag = 0;这告诉编译器:这个变量随时可能被“天外来客”(中断)修改,不要自作聪明去优化它。
回调函数 (Callback) 的真相
你在代码里写的 HAL_GPIO_EXTI_Callback 并不是真正的中断入口。
- 硬件层面:触发 -> 查找 向量表 (Vector Table) -> 跳转到汇编定义的
EXTI0_IRQHandler。 - HAL 库层面:
EXTI0_IRQHandler调用HAL_GPIO_EXTI_IRQHandler(处理清除中断标志位等脏活),最后才调用你写的那个Callback。 - 好处:你不需要管底层如何清除标志位,只需要关心你的业务逻辑。
扩展资料:如何找到中断入口
第一条路:查看“户口本”(启动文件)
所有中断函数的“官方真名”都刻在芯片的启动文件里。
- 在 STM32CubeIDE 的左侧目录找到:
Core -> Startup -> startup_stm32f103c8tx.s(这是一个汇编文件)。 - 打开它,向下拉,你会看到一张巨大的表,全是类似
XXXX_IRQHandler的名字。
这就是真名: 无论什么中断,它的底层函数名一定叫
[外设名]_IRQHandler。
- 例如:
EXTI0_IRQHandler、TIM2_IRQHandler、USART1_IRQHandler。
第二条路:查看“值班表” (stm32f1xx_it.c)
当你用 STM32CubeMX 配置并生成代码后,IDE 会自动为你准备一个专门存放中断函数的文件。
- 打开:
Core -> Src -> stm32f1xx_it.c(it 代表 Interrupt)。 - 这里面已经写好了你开启的那些中断函数。比如你开了外部中断 0,你就能在这里找到:
![[Pasted image 20251220211210.png]]
第三条路:HAL 库的“回调函数”(Callback)—— 推荐用法
虽然 EXTI0_IRQHandler 是真正的入口,但 HAL 库为了方便我们编写业务逻辑,设计了一套“回调机制”。
重点理解这个流程(以按键中断为例):
(在 stm32f1xx_it.c 中)"] C --> D["HAL库通用处理: HAL_GPIO_EXTI_IRQHandler
(负责清除标志位等杂活)"] D --> E["开发者重写的函数: HAL_GPIO_EXTI_Callback
(你在 main.c 里写的业务代码)"] style E fill:#f96,stroke:#333
怎么知道回调函数叫什么
回调函数的名字是固定的。你可以在对应的 HAL 库驱动文件(.h)里找到它们。
| 中断类型 | 回调函数名 (你直接在 main.c 里重写即可) |
|---|---|
| 外部中断 | HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) |
| 定时器中断 | HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) |
| 串口接收完成 | HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) |
实战技巧:如何快速“瞬移”到函数名?
如果你在代码里看到了 HAL_GPIO_EXTI_IRQHandler,想知道它最后会调用哪个回调函数,你可以:
- 按住 Ctrl 键,鼠标左键点击这个函数名,跳转进源码。
- 在源码里继续向下翻,你会看到一个被标注为
__weak的函数定义。 __weak(虚函数) 就是你要找的名字!你在自己的代码里写一个一模一样的函数,编译器就会优先用你写的,而忽略这个“弱”的。
小结
- 物理入口:名字必带
_IRQHandler,在stm32f1xx_it.c中。 - 逻辑入口:名字必带
_Callback,是你写具体逻辑的地方。
作为初学者,你只需要在 main.c 的末尾,把官方那个带 __weak 的回调函数复制过来,去掉 __weak 关键字,然后往里面填你的逻辑(比如点灯)就行了。