中断系统与NVIC管理:从理论到实战
中断,说白了就是CPU正在干活,突然被一个紧急事件打断,跑去处理完再回来继续干活。我刚开始学STM32时,总觉得中断很神秘,后来发现它其实就是个「优先级排队」的游戏。
这一章,我们重点搞定三件事:中断优先级怎么分组、NVIC怎么配置、外部中断EXTI怎么玩。最后再聊聊中断服务函数的写法规范——这块踩坑的人可不少。
1. 中断优先级分组:你得先定好游戏规则
STM32的中断优先级分两组:抢占优先级和响应优先级(也叫子优先级)。
- 抢占优先级:高优先级的可以打断低优先级的。数值越小,优先级越高。
- 响应优先级:不能打断,只在同时发生时决定谁先执行。也是数值越小越优先。
HAL库里用 HAL_NVIC_SetPriorityGrouping() 来设置分组。一共支持5种分组方式:
| 分组编号 | 抢占优先级位数 | 响应优先级位数 | 典型场景 |
|---|---|---|---|
| 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级子优先级也灵活。除非你有特别多的中断嵌套需求,否则别选GROUP_0或GROUP_4——一个太死板,一个太激进。
2. HAL_NVIC_SetPriority 与 HAL_NVIC_EnableIRQ
这两个函数是NVIC配置的核心。用法很简单:
// 设置分组(整个系统只调用一次,通常在SystemClock_Config里)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 设置某个中断的优先级
HAL_NVIC_SetPriority(USART1_IRQn, 1, 2);
// 使能中断
HAL_NVIC_EnableIRQ(USART1_IRQn);
这里有个坑:HAL_NVIC_SetPriority 的第二个参数是抢占优先级,第三个是响应优先级。但具体数值范围取决于你之前设置的分组。比如分组2,抢占优先级范围是0~3,响应优先级范围也是0~3。
3. 外部中断EXTI配置:从引脚到中断的完整链路
外部中断EXTI,就是GPIO引脚电平变化触发的中断。配置流程分三步:
- 配置GPIO为中断模式:用
HAL_GPIO_Init()设置引脚模式为GPIO_MODE_IT_FALLING或GPIO_MODE_IT_RISING。 - 设置中断优先级并使能:用
HAL_NVIC_SetPriority和HAL_NVIC_EnableIRQ。 - 编写中断服务函数:在
stm32f4xx_it.c里实现。
举个例子,配置PA0引脚下降沿触发中断:
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 设置中断优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
4. 中断服务函数编写规范
中断服务函数(ISR)的写法,直接决定系统的稳定性。我见过太多人在这里翻车。
先说命名规则:STM32的ISR名字是固定的,在启动文件里已经定义好了弱符号。你只需要在 stm32f4xx_it.c 里实现同名函数即可。
常见的中断服务函数:
| 中断源 | ISR名称 | 说明 |
|---|---|---|
| EXTI0 | EXTI0_IRQHandler | PA0/PB0/PC0等引脚中断 |
| EXTI1 | EXTI1_IRQHandler | PA1/PB1/PC1等引脚中断 |
| USART1 | USART1_IRQHandler | 串口1中断 |
| TIM2 | TIM2_IRQHandler | 定时器2中断 |
编写ISR时,记住三条铁律:
- 快进快出:中断里别做复杂运算,别调用printf,别用延时。能设个标志位就赶紧退出。
- 清除中断标志:HAL库的ISR里会自动调用
HAL_GPIO_EXTI_IRQHandler(),它会帮你清标志。但如果你自己写裸中断,记得手动清。 - 不要嵌套太深:虽然NVIC支持抢占,但嵌套层数多了容易栈溢出。我一般控制在3层以内。
一个标准的EXTI中断服务函数示例:
// 在stm32f4xx_it.c中
void EXTI0_IRQHandler(void)
{
// HAL库的中断处理函数(会自动清标志)
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
// 在main.c或其他文件中实现回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
// 你的业务逻辑,比如翻转LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
HAL_GPIO_EXTI_Callback 里只做最轻量的事。如果需要处理复杂逻辑,设一个全局标志位,在主循环里轮询处理。这样既保证了中断响应速度,又避免了中断里出bug。
5. 知识结构总览
下面这张图帮你理清中断系统的整体脉络:
从图里可以清楚看到:中断源经过EXTI配置后,进入NVIC进行优先级管理,然后由CPU执行ISR,最后通过HAL回调函数处理业务逻辑。每一层都有明确的职责,不能乱。
6. 避坑指南
最后,分享几个我实战中遇到的坑:
- 优先级分组只设一次:整个系统里只调用一次
HAL_NVIC_SetPriorityGrouping,重复调用会导致未定义行为。 - 中断里别用HAL_Delay:HAL_Delay依赖SysTick中断,如果SysTick优先级比当前中断低,就会死锁。我吃过这个亏。
- EXTI线冲突:同一编号的EXTI线不能同时映射到多个GPIO引脚。比如PA0和PB0不能同时用EXTI0。
- 中断标志位自动清除:HAL库的
HAL_GPIO_EXTI_IRQHandler会自动清除标志位,不需要手动操作。但如果你用标准库或裸机,记得在ISR末尾清标志。
嗯,中断系统这块内容就这些。记住:配置顺序不能乱,ISR要短小精悍,优先级数值别越界。做到这三点,中断系统基本不会出问题。