2. 基础知识
中断,说白了就是CPU正在干一件事,突然被更紧急的事打断,跑去处理完再回来继续。STM32的中断系统,NVIC(嵌套向量中断控制器)是核心调度官。HAL库把这套机制封装得挺友好,但如果你不清楚底层原理,出bug时就会一头雾水。
我个人习惯,拿到一个新项目,第一件事就是理清中断路径:从外设触发 → NVIC仲裁 → 中断服务函数 → HAL回调。这条链路通了,项目就稳了一半。
2.1 NVIC配置原理:中断的“交通警察”
NVIC负责管理STM32所有中断的使能、挂起、优先级和嵌套。Cortex-M3/M4内核最多支持240个中断,STM32实际只用了其中一部分(比如STM32F103有60个)。
配置NVIC,核心就三件事:
- 使能中断通道:告诉NVIC,这个外设的中断请求我允许它进来。
- 设置抢占优先级和子优先级:决定中断嵌套的规则——谁可以打断谁。
- 设置中断优先级分组:分配多少位给抢占优先级,多少位给子优先级。
优先级分组通过 HAL_NVIC_SetPriorityGrouping() 设置,整个系统只调用一次。STM32用4位二进制表示优先级,分组方式如下:
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 抢占优先级取值范围 | 子优先级取值范围 |
|---|---|---|---|---|
| NVIC_PRIORITYGROUP_0 | 0 | 4 | 无 | 0~15 |
| NVIC_PRIORITYGROUP_1 | 1 | 3 | 0~1 | 0~7 |
| NVIC_PRIORITYGROUP_2 | 2 | 2 | 0~3 | 0~3 |
| NVIC_PRIORITYGROUP_3 | 3 | 1 | 0~7 | 0~1 |
| NVIC_PRIORITYGROUP_4 | 4 | 0 | 0~15 | 无 |
配置单个中断的代码很简单:
// 设置抢占优先级为1,子优先级为0
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
// 使能中断通道
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
这里有个坑:优先级数值越小,优先级越高。0是最高优先级,15是最低。我刚入门时总搞反,以为数值大优先级高,结果中断响应顺序全乱套了。
2.2 HAL库中断回调机制:从硬件到软件的“快递链”
HAL库的中断处理,是一条清晰的“快递链”:
- 硬件触发:外设产生中断信号(比如GPIO引脚电平变化)。
- NVIC仲裁:判断当前中断优先级是否足够高,决定是否响应。
- 跳转到中断向量表:CPU自动从向量表取出对应的中断服务函数地址,跳转执行。
- 执行中断服务函数:HAL库提供的弱函数(如
EXTI0_IRQHandler)。 - 调用HAL库中断处理函数:如
HAL_GPIO_EXTI_IRQHandler(),负责清除中断标志位。 - 触发用户回调:调用
HAL_GPIO_EXTI_Callback(),让你写业务逻辑。
为什么要分这么多层?说白了,HAL库想让你只关心“中断来了我要干什么”,而不必操心“怎么清除标志位”、“怎么处理嵌套”。
2.3 HAL_GPIO_EXTI_Callback 源码分析:剥开HAL的“糖衣”
我们直接看HAL库的源码,理解它的设计思路。以STM32F1系列为例,GPIO外部中断的完整调用链如下:
第一步:中断向量表跳转到 ISR
在 stm32f1xx_it.c 中,有类似这样的函数:
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
这个函数是弱定义,你可以重写。但HAL库建议你不要动它,直接用它默认的实现。
第二步:HAL_GPIO_EXTI_IRQHandler 处理中断标志
打开 stm32f1xx_hal_gpio.c,找到这个函数:
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
逻辑非常清晰:
- 先检查中断挂起标志位是否被置位。
- 如果置位了,立即清除它(防止重复触发)。
- 然后调用用户回调函数。
第三步:用户回调函数 HAL_GPIO_EXTI_Callback
这个函数是 __weak 弱定义,默认是个空函数:
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 用户需要重写此函数 */
}
你在自己的代码里重新定义这个函数,HAL库会自动链接你的版本,覆盖弱定义。
HAL_GPIO_EXTI_IRQHandler(),导致递归调用,栈溢出。记住,回调函数里只写你的业务逻辑,不要再调用HAL的中断处理函数。
2.4 知识结构图:中断处理全流程
下面这张图展示了从硬件触发到用户回调的完整路径,我建议你把它记在脑子里:
2.5 实战要点总结
学完这一节,你至少应该记住以下几点:
- NVIC配置三步走:设置分组 → 设置优先级 → 使能中断。顺序别搞反。
- 回调函数是弱定义:你重写它,HAL库自动调用你的版本。不要修改HAL库源码。
- 中断标志位自动清除:HAL_GPIO_EXTI_IRQHandler 已经帮你清了,回调里不需要再清。
- 回调函数里别干重活:中断上下文里,时间就是生命。复杂逻辑放到主循环或任务里处理。
volatile uint8_t exti_flag = 0;void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { exti_flag = 1; }while(1) { if(exti_flag) { exti_flag = 0; /* 处理业务 */ } }
嗯,这一节的内容就到这里。中断是嵌入式开发的基石,把HAL库的这套回调机制吃透,后面讲LL库混合驱动时你才能游刃有余。