中断系统入门: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的步骤,我总结为四步走:

  1. 使能GPIO时钟和AFIO时钟——GPIO需要时钟才能工作,AFIO负责引脚重映射和EXTI连线。
  2. 配置GPIO为输入模式——外部中断的引脚必须是输入模式,通常是上拉或下拉输入。
  3. 配置EXTI线——选择中断线、触发方式(上升沿、下降沿、双边沿)、使能中断。
  4. 配置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外部中断配置 中断服务函数规范 抢占优先级 子优先级 GPIO配置 触发方式 快进快出 清除标志 核心流程:从外设信号到CPU响应 1. 外设信号变化 → GPIO引脚检测到电平跳变 2. EXTI线捕获触发条件 → 产生中断请求 3. NVIC根据优先级裁决 → 是否打断当前任务 4. CPU自动压栈 → 跳转到中断向量表对应ISR 5. ISR执行 → 清除标志 → 出栈返回主程序 关键:整个流程从第3步到第5步,耗时通常在几十个时钟周期内

嗯,中断系统入门就讲这些。NVIC分组决定了中断的“阶级”,EXTI配置决定了中断的“触发条件”,ISR规范决定了中断的“执行质量”。三者缺一不可。