3. 中断系统与NVIC:中断优先级管理、HAL库中断回调机制、外部中断(EXTI)驱动、中断服务函数编写规范
中断,说白了就是CPU正在干一件事,突然被一个更紧急的事件打断,跑去处理完再回来接着干。我刚开始学STM32时,觉得中断就是个“跳转+返回”,后来踩了不少坑才明白——中断优先级配不好,系统直接死给你看。
这一章,咱们把中断系统彻底捋清楚。从NVIC的优先级管理,到HAL库的回调机制,再到外部中断的驱动编写,最后给出中断服务函数的规范写法。嗯,都是实战中必须拿捏的东西。
3.1 NVIC中断优先级管理
NVIC(Nested Vectored Interrupt Controller)是Cortex-M内核的中断控制器。它负责管理所有中断的使能、挂起、优先级和嵌套。说白了,它就是中断系统的“调度中心”。
STM32的中断优先级分为抢占优先级和子优先级。抢占优先级决定能否打断正在执行的中断,子优先级决定同时到达的中断谁先执行。
| 优先级分组 | 抢占优先级位数 | 子优先级位数 | 典型应用场景 |
|---|---|---|---|
| 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级抢占,无子优先级 |
配置优先级分组,在HAL库中调用:
HAL_NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);
这个函数必须在系统初始化时调用一次,且只能调用一次。为什么?因为优先级分组一旦设定,整个系统的中断嵌套规则就固定了。中途改分组,中断行为会乱套。
3.2 HAL库中断回调机制
HAL库的中断处理,走的是“中断服务函数 + 回调函数”的套路。中断服务函数里做最必要的事(清标志、读数据),然后调用一个弱定义的回调函数,真正的业务逻辑写在回调里。
举个例子,串口接收中断的流程:
- 硬件触发USARTx_IRQHandler
- HAL库的HAL_UART_IRQHandler被调用
- 内部判断中断类型,调用HAL_UART_RxCpltCallback
- 你在回调里写接收完成后的处理逻辑
回调函数是弱符号(__weak),你可以在自己的代码里重新定义它。HAL库的默认实现是空函数,你不重写,它什么都不做。
常用的HAL库中断回调函数:
| 外设 | 回调函数名 | 触发条件 |
|---|---|---|
| UART | HAL_UART_RxCpltCallback | 接收完成 |
| UART | HAL_UART_TxCpltCallback | 发送完成 |
| TIM | HAL_TIM_PeriodElapsedCallback | 定时器溢出 |
| ADC | HAL_ADC_ConvCpltCallback | 转换完成 |
| EXTI | HAL_GPIO_EXTI_Callback | 外部中断触发 |
3.3 外部中断(EXTI)驱动
外部中断EXTI,就是GPIO引脚电平变化触发的中断。按键检测、传感器信号跳变、编码器脉冲捕获,都离不开它。
配置EXTI的步骤,我总结为四步走:
- 使能GPIO时钟:RCC相关,不用多说
- 配置GPIO为中断模式:设置触发边沿(上升沿、下降沿、双边沿)
- 配置NVIC:使能中断通道,设置优先级
- 编写中断服务函数和回调
用HAL库配置一个按键外部中断(PA0,下降沿触发):
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
中断服务函数和回调的写法:
// 中断服务函数(在stm32f4xx_it.c中)
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
// 回调函数(在你的应用代码中)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
// 按键按下,做你想做的事
// 注意:这里不要做耗时操作!
}
}
3.4 中断服务函数编写规范
中断服务函数(ISR)的编写,直接决定系统的稳定性和实时性。我见过太多人把中断当成普通函数来写,结果系统卡死、数据丢失、响应延迟。下面是我总结的几条硬规矩。
| 规范 | 说明 | 反面案例 |
|---|---|---|
| 短小精悍 | ISR里只做最必要的事,清标志、读数据、设状态 | 在ISR里做浮点运算、malloc |
| 避免阻塞 | 不要用延时、轮询、等待信号量 | HAL_Delay(10)放在ISR里 |
| 保护共享数据 | ISR和主循环共享的变量,加volatile | 全局变量不加volatile,优化后读不到最新值 |
| 不要调用不可重入函数 | printf、sprintf等标准库函数大多不可重入 | ISR里直接printf调试 |
| 中断嵌套要可控 | 高优先级中断不要做太多事,避免低优先级被饿死 | 所有中断优先级设成一样,没有嵌套 |
一个规范的ISR示例:
volatile uint8_t button_pressed = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
button_pressed = 1; // 只设标志,不做其他
}
}
// 主循环中
while(1)
{
if(button_pressed)
{
button_pressed = 0;
// 处理按键逻辑,这里可以放心延时、计算
}
}
3.5 中断系统知识结构图
下面这张图,把中断系统的核心脉络画出来了。从NVIC配置到中断触发,再到回调执行,一条线走通。
3.6 避坑指南与经验总结
中断系统这块,我踩过的坑能写满一页纸。挑几个最典型的说说。
- 优先级分组只设一次:HAL_NVIC_SetPriorityGrouping在整个系统生命周期里只能调用一次。我见过有人在外设初始化里反复调用,结果中断行为完全不可控。
- 外部中断引脚去抖:机械按键按下时会有抖动,一次按下可能触发多次中断。硬件上加RC滤波,或者软件里做延时去抖。我习惯在回调里设标志,主循环里做20ms延时去抖。
- 中断服务函数不要太长:ISR执行时间过长,会阻塞其他中断。高优先级中断尤其要注意。我曾经在TIM中断里做了个浮点运算,结果串口中断响应延迟,数据丢包。
- 共享变量加volatile:ISR和主循环共享的变量,一定要加volatile关键字。否则编译器优化后,主循环可能永远读不到ISR更新的值。
- HAL_Delay不能在中断里用:HAL_Delay依赖SysTick中断,如果在中断里调用,SysTick中断优先级低于当前中断,就会死锁。我刚开始不知道,调试了整整一天。