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 无
我的习惯: 大部分项目用 NVIC_PRIORITYGROUP_2(2位抢占 + 2位子优先级)。抢占优先级决定能否嵌套,子优先级决定同抢占级时的响应顺序。别把分组设得太细,否则嵌套逻辑容易乱。

配置单个中断的代码很简单:

// 设置抢占优先级为1,子优先级为0
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
// 使能中断通道
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

这里有个坑:优先级数值越小,优先级越高。0是最高优先级,15是最低。我刚入门时总搞反,以为数值大优先级高,结果中断响应顺序全乱套了。

2.2 HAL库中断回调机制:从硬件到软件的“快递链”

HAL库的中断处理,是一条清晰的“快递链”:

  1. 硬件触发:外设产生中断信号(比如GPIO引脚电平变化)。
  2. NVIC仲裁:判断当前中断优先级是否足够高,决定是否响应。
  3. 跳转到中断向量表:CPU自动从向量表取出对应的中断服务函数地址,跳转执行。
  4. 执行中断服务函数:HAL库提供的弱函数(如 EXTI0_IRQHandler)。
  5. 调用HAL库中断处理函数:如 HAL_GPIO_EXTI_IRQHandler(),负责清除中断标志位。
  6. 触发用户回调:调用 HAL_GPIO_EXTI_Callback(),让你写业务逻辑。

为什么要分这么多层?说白了,HAL库想让你只关心“中断来了我要干什么”,而不必操心“怎么清除标志位”、“怎么处理嵌套”。

关键点: 中断服务函数(ISR)里不要做耗时操作。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 知识结构图:中断处理全流程

下面这张图展示了从硬件触发到用户回调的完整路径,我建议你把它记在脑子里:

HAL库外部中断处理流程 HAL库外部中断处理全流程 ① 硬件触发 GPIO电平变化 ② NVIC仲裁 优先级判断 ③ 中断向量表跳转 EXTI0_IRQHandler ④ HAL_GPIO_EXTI_IRQHandler 检查标志 → 清除标志 ⑤ 用户回调 HAL_GPIO_EXTI_Callback ⚠ 关键提醒 • 回调函数里不要做耗时操作 • 不要递归调用HAL中断处理 • 优先级数值越小优先级越高 📌 代码位置 stm32f1xx_it.c stm32f1xx_hal_gpio.c stm32f1xx_hal_gpio.h 用户代码(main.c等)

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库混合驱动时你才能游刃有余。