中断系统与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。

我曾经踩过的坑:有一次我把抢占优先级设成了5,但分组2只支持0~3。结果中断行为完全乱套,查了半天才发现是数值越界。记住:数值必须在分组允许的范围内,否则硬件会截断,行为不可预测。

3. 外部中断EXTI配置:从引脚到中断的完整链路

外部中断EXTI,就是GPIO引脚电平变化触发的中断。配置流程分三步:

  1. 配置GPIO为中断模式:用 HAL_GPIO_Init() 设置引脚模式为 GPIO_MODE_IT_FALLING 或 GPIO_MODE_IT_RISING。
  2. 设置中断优先级并使能:用 HAL_NVIC_SetPriority 和 HAL_NVIC_EnableIRQ。
  3. 编写中断服务函数:在 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);
关键点:EXTI线0~15对应GPIO的PIN0~PIN15。但同一编号的EXTI线只能映射到一个GPIO引脚。比如PA0和PB0不能同时使用EXTI0——硬件上只允许一个。

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. 知识结构总览

下面这张图帮你理清中断系统的整体脉络:

STM32中断系统知识结构图 STM32中断系统知识结构 中断源 EXTI配置(GPIO模式、触发边沿) NVIC管理(优先级分组、使能、设置优先级) 中断服务函数(ISR) HAL回调函数(业务逻辑) 关键原则 • 快进快出 • 清中断标志 • 避免嵌套过深

从图里可以清楚看到:中断源经过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末尾清标志。
我曾经踩过的坑:有一次我在中断里调用了printf,结果程序直接卡死。后来发现printf内部用了互斥锁,而中断里不能加锁。从那以后,我中断里只设标志位,所有打印都放主循环。

嗯,中断系统这块内容就这些。记住:配置顺序不能乱,ISR要短小精悍,优先级数值别越界。做到这三点,中断系统基本不会出问题。