4. 中断系统与NVIC管理:中断优先级分组,中断向量表重定向,中断嵌套与抢占,实战:按键中断

中断系统,说白了就是MCU的“紧急事务处理机制”。没有中断,你的程序就得轮询——CPU像个傻子一样不停问“按键按了吗?按了吗?”,浪费大量算力。有了中断,CPU该干嘛干嘛,事件来了再响应,这才是嵌入式系统的精髓。

我刚开始用STM32时,对中断的理解就停留在“配置一下就能用”的层面。直到有一次产品在现场频繁死机,排查了三天才发现是中断优先级配置冲突导致的。嗯,从那以后我再也不敢轻视NVIC了。

4.1 中断优先级分组:谁先响应谁后响应

STM32的NVIC支持最多16级抢占优先级(0-15,0最高)。但具体怎么分配,取决于一个叫“优先级分组”的寄存器——SCB->AIRCR。

优先级分组决定了:抢占优先级占几位,子优先级占几位。总共4位,可以自由切分。

分组编号 抢占优先级位数 子优先级位数 抢占优先级范围 子优先级范围
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_PriorityGroup_2——2位抢占(0-3),2位子优先级(0-3)。这样既能满足大部分项目的嵌套需求,又不会让优先级管理变得太复杂。

// 优先级分组配置(系统启动时只调用一次)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

4.2 中断向量表重定向:让程序跑在正确的位置

中断向量表,就是一张“中断服务函数地址表”。CPU收到中断信号后,从这张表里找到对应的函数地址,跳过去执行。

默认情况下,向量表放在Flash起始地址0x08000000。但如果你做IAP在线升级,或者把程序放在RAM里运行,就得重定向向量表。

我曾经踩过的坑:做IAP时,APP程序跑在0x08010000,但忘了重定向向量表。结果一进中断,CPU跑到0x08000000找函数,直接进HardFault。查了整整一天才找到原因。

// 重定向向量表到0x08010000
SCB->VTOR = 0x08010000;

重定向的时机很关键:必须在使能任何中断之前完成。我一般在SystemInit()函数末尾或者main()的第一行就做这件事。

4.3 中断嵌套与抢占:谁可以打断谁

中断嵌套,就是高优先级中断打断低优先级中断的执行。理解这个机制,是调试复杂系统的前提。

规则其实很简单:

  • 抢占优先级不同:高抢占优先级可以打断低抢占优先级的中断服务函数。
  • 抢占优先级相同:不能互相打断。如果同时发生,先比较子优先级,子优先级高的先执行;子优先级也相同,则看中断通道号,编号小的先执行。
  • 正在执行的中断:不会被同级或更低优先级的中断打断。

我的建议:如果项目里中断不多,把所有中断的抢占优先级设成一样,用子优先级区分响应顺序。这样能避免很多诡异的嵌套问题。

下面这张图展示了中断嵌套的典型流程:

中断嵌套与抢占执行流程 主程序(main) 低优先级中断ISR 高优先级中断ISR 高优先级执行完,返回低优先级继续 低优先级执行完,返回主程序 执行顺序说明: 1. 主程序正在执行,低优先级中断触发 → 主程序被暂停 2. 低优先级ISR开始执行,此时高优先级中断触发 3. 低优先级ISR被暂停,高优先级ISR开始执行 4. 高优先级ISR执行完毕 → 返回低优先级ISR继续 5. 低优先级ISR执行完毕 → 返回主程序

4.4 实战:按键中断

理论说完了,咱们直接上手。按键中断是STM32入门最经典的实战,但很多人连这个都写不严谨。

先看硬件连接:按键一端接PA0,另一端接GND。PA0配置为上拉输入,按键按下时读到低电平。

配置步骤分四步:

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置PA0为输入模式,并使能外部中断线
  3. 配置NVIC优先级并使能中断通道
  4. 编写中断服务函数
// 步骤1:使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SYSCFG, ENABLE);

// 步骤2:配置GPIO和外部中断
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStructure);

SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_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);

// 步骤3:配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 步骤4:中断服务函数
void EXTI0_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 延时消抖(实际项目中建议用定时器)
        Delay_ms(20);
        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
        {
            // 按键确实被按下,执行动作
            LED_Toggle();
        }
        
        // 清除中断标志位
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

注意:中断服务函数里不要做耗时操作。延时消抖虽然简单,但会阻塞其他中断。我建议用定时器+状态机的方式做消抖,或者用硬件消抖电路。

4.5 中断调试的实用技巧

调试中断系统时,有几个问题特别容易让人抓狂:

  • 中断进不去?先检查中断标志位有没有清除,再检查NVIC使能了没有,最后看GPIO配置对不对。
  • 中断频繁触发?大概率是电平触发没处理好,或者外部干扰。改成边沿触发,或者加硬件滤波。
  • 系统卡死?检查中断优先级,看有没有死锁——比如两个中断互相等待对方释放资源。

我的调试习惯:在中断服务函数入口和出口各放一个GPIO翻转。用示波器看这个GPIO的波形,就能直观看到中断触发的频率和持续时间。比看代码快多了。

中断系统是STM32的基石,也是很多疑难杂症的根源。把优先级分组、向量表重定向、嵌套抢占这三个概念吃透,你就能应对90%的中断相关问题。剩下的10%,靠经验和示波器解决。