中断系统入门:NVIC优先级分组、外部中断EXTI配置、中断服务函数编写规范
中断,说白了就是CPU正在干活,突然被一个紧急事件打断,跑去处理完再回来继续干活。我刚开始学STM32时,觉得中断就是个“高级点的轮询”。后来做项目才发现,没有中断,很多实时性要求根本满足不了。
这一章,咱们把中断系统的三个核心问题讲透:NVIC怎么分组、EXTI怎么配置、中断服务函数怎么写才规范。嗯,这三个点搞明白了,中断这块就算入门了。
1. NVIC优先级分组——中断嵌套的“游戏规则”
NVIC(Nested Vectored Interrupt Controller)是Cortex-M内核的中断控制器。它支持最多256个中断源,每个中断都有独立的优先级。但STM32实际只用了其中一部分,比如F103系列有60个可屏蔽中断。
优先级分组,就是决定抢占优先级和子优先级各占多少位。抢占优先级决定能否打断正在执行的中断,子优先级决定同抢占优先级下的响应顺序。
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 优先级范围 |
|---|---|---|---|
| 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_PriorityGroupConfig(),一旦设定,所有中断的优先级分配都按这个规则来。我见过有人每个外设初始化都调一次,结果后调的把前面的覆盖了,中断行为完全乱套。
我个人习惯用NVIC_PriorityGroup_2(2位抢占 + 2位子优先级)。为什么?因为大多数项目里,中断嵌套层级不需要太深,2位抢占(0~3)够用了,子优先级还能区分4个等级,灵活性刚好。
小技巧:中断优先级数值越小,优先级越高。抢占优先级为0的中断可以打断抢占优先级为1的中断。子优先级只在同抢占优先级下比较。
2. 外部中断EXTI配置——从引脚到中断的完整链路
EXTI(External Interrupt/Event Controller)负责检测外部信号变化,产生中断或事件。STM32的EXTI支持最多20个中断线,但每个线只能映射到一个GPIO引脚。
配置EXTI的步骤,我总结为四步走:
- 使能GPIO时钟和AFIO时钟——GPIO需要时钟才能工作,AFIO负责引脚重映射和EXTI连线。
- 配置GPIO为输入模式——外部中断的引脚必须是输入模式,通常是上拉或下拉输入。
- 配置EXTI线——选择中断线、触发方式(上升沿、下降沿、双边沿)、使能中断。
- 配置NVIC——使能中断通道、设置优先级。
来看一个实际例子:配置PA0引脚为外部中断,下降沿触发。
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置EXTI线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 4. 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
注意:EXTI线0~15分别对应GPIO的Pin0~Pin15。同一个EXTI线不能同时映射到多个GPIO引脚。比如PA0和PB0不能同时使用EXTI0线。我曾经在一个项目里踩过这个坑,两个按键分别接PA0和PB0,结果只有一个能触发中断。
触发方式的选择也有讲究:
- 上升沿触发——适合按键松开检测、正脉冲信号。
- 下降沿触发——适合按键按下检测、负脉冲信号。
- 双边沿触发——适合编码器信号、脉冲宽度测量。
你想想看,如果按键按下时会产生抖动,单纯用下降沿触发可能会误触发多次。这时候要么加硬件去抖电路,要么在软件里做延时消抖。
3. 中断服务函数编写规范——别让ISR变成“坑”
中断服务函数(ISR)是中断系统的“最后一公里”。写得好,系统稳定高效;写得烂,各种诡异问题都来了。
我总结了三条铁律:
- 快进快出——ISR里不要做耗时操作,比如延时、打印、复杂计算。能设标志位就别干别的。
- 保存和恢复上下文——编译器会自动处理,但如果你用汇编写ISR,记得手动压栈出栈。
- 清除中断标志——不清除标志,ISR会反复进入,系统直接卡死。
来看一个规范的ISR写法:
void EXTI0_IRQHandler(void)
{
// 检查中断标志
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 设置标志位,通知主循环处理
g_uiButtonPressed = 1;
// 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
规范要点:
- 函数名必须与启动文件中的中断向量表一致(如EXTI0_IRQHandler)。
- 先检查中断标志,确认是预期的中断源。
- 处理逻辑尽量精简,只做必要操作。
- 最后一定要清除中断标志。
我曾经接手过一个项目,ISR里直接调用了printf和delay_ms。结果中断一触发,系统就卡死。后来查了半天,发现printf用了轮询方式发送,中断优先级比串口高,串口中断进不来,printf永远发不完——典型的死锁。
避坑指南:ISR里如果需要访问全局变量,记得加volatile关键字,防止编译器优化导致变量读取异常。多中断共享的变量,最好用临界段保护。
4. 知识结构总览
下面这张图把中断系统的三个核心模块串起来了。你可以看到,从外设信号到CPU响应,中间经过了EXTI配置、NVIC优先级裁决、ISR执行三个环节。
嗯,中断系统入门就讲这些。NVIC分组决定了中断的“阶级”,EXTI配置决定了中断的“触发条件”,ISR规范决定了中断的“执行质量”。三者缺一不可。