3. 中断系统与NVIC:理解STM32的中断优先级,配置外部中断(EXTI)实现按键中断

中断,说白了就是CPU正在干活,突然被一个紧急事件叫停,转去处理这个事件,处理完再回来接着干。你想想看,如果没有中断,CPU就得不停地轮询按键有没有按下——那得多浪费啊。

我刚开始学STM32时,对中断优先级这块儿特别迷糊。什么抢占优先级、子优先级,一堆概念绕来绕去。后来在做一个工业控制项目时,因为优先级没配好,导致高优先级中断把低优先级的中断饿死了,现场设备直接失控。嗯,从那以后我再也不敢轻视NVIC的配置了。

3.1 中断系统的基本概念

STM32的中断系统由两部分组成:NVIC(嵌套向量中断控制器)和EXTI(外部中断/事件控制器)。NVIC是内核级别的,负责管理所有中断的使能、挂起和优先级;EXTI则是外设级别的,专门处理外部引脚的中断请求。

核心要点:NVIC是中断的"调度中心",EXTI是中断的"触发源"。两者配合,才能实现外部中断。

STM32支持多达16个中断优先级(4位优先级寄存器),但具体能用多少,取决于你配置的优先级分组。我个人习惯用NVIC_PriorityGroup_2,也就是2位抢占优先级、2位子优先级。这样既保证了足够的抢占层级,又能在同一抢占级别内做子优先级排序。

分组 抢占优先级位数 子优先级位数 抢占优先级等级数 子优先级等级数
Group 0 0 4 1 16
Group 1 1 3 2 8
Group 2 2 2 4 4
Group 3 3 1 8 2
Group 4 4 0 16 1

我的建议:如果你不确定选哪个分组,直接用Group 2。4级抢占优先级足够应对大多数场景,4级子优先级也能在同一个抢占级别内做精细调度。

3.2 EXTI的工作原理

EXTI支持16条外部中断线(EXTI0~EXTI15),每条线对应一个GPIO引脚。但注意,不是每个引脚都有独立的EXTI线——比如PA0、PB0、PC0都共用EXTI0线。所以同一时刻,只能有一个引脚映射到同一条EXTI线上。

我曾经在一个项目里踩过这个坑:同时配置了PA0和PB0的EXTI中断,结果两个引脚的中断互相干扰,按键响应完全乱套。后来查手册才发现,同一EXTI线只能选一个引脚。

EXTI的触发方式有三种:

  • 上升沿触发:信号从低到高时触发
  • 下降沿触发:信号从高到低时触发
  • 双边沿触发:上升沿和下降沿都触发

按键中断一般用下降沿触发,因为按键按下时引脚电平从高变低。但要注意硬件消抖——机械按键按下时会有抖动,如果不做处理,一次按下可能触发多次中断。

避坑指南:我曾经在一个产品中没做软件消抖,结果按键按下一次,串口打印了七八次中断信息。后来在中断服务函数里加了10ms的延时去抖,问题才解决。记住:硬件消抖(RC滤波)和软件消抖(延时判断)至少要选一个。

3.3 配置外部中断的完整步骤

配置一个按键中断,需要走以下流程:

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO为输入模式(上拉或下拉)
  3. 将GPIO引脚连接到EXTI线(通过SYSCFG)
  4. 配置EXTI的触发方式和中断使能
  5. 配置NVIC优先级并使能中断
  6. 编写中断服务函数

下面是一个完整的代码示例,实现PA0引脚(按键)的下降沿中断:

// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;  // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 3. 连接EXTI线
SYSCFG_EXTILineConfig(EXTI_PortSource_GPIOA, EXTI_PinSource0);

// 4. 配置EXTI
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);

// 5. 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;         // 子优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);

// 6. 中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 延时消抖(简单实现)
        Delay_ms(10);
        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
        {
            // 按键确实被按下
            // 在这里处理按键逻辑
        }
        
        // 清除中断标志位
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

注意:中断服务函数里一定要清除中断标志位,否则会一直触发中断。我见过有人忘了这步,结果程序卡死在中断里出不来。

3.4 中断优先级配置的实战经验

优先级配置看似简单,但实际项目中很容易出问题。我总结了几条经验:

  • 时间敏感的中断给高优先级:比如定时器中断、通信中断,这些需要快速响应。
  • 非关键中断给低优先级:比如按键中断、LED闪烁,慢一点没关系。
  • 避免中断嵌套过深:中断嵌套超过3层,栈空间可能不够用。
  • 中断服务函数要短:尽量只做标志位设置,具体处理放在主循环里。

举个例子,我在一个电机控制项目里,把PWM中断设为抢占优先级0(最高),把按键中断设为抢占优先级2(较低)。这样即使按键中断正在执行,PWM中断也能立即抢占,保证电机控制的实时性。

3.5 中断系统的知识结构

下面这张图展示了中断系统的整体架构和配置流程:

STM32中断系统架构与配置流程 STM32中断系统架构与配置流程 外部中断源(EXTI) 定时器中断 通信中断(UART/SPI/I2C) DMA中断 NVIC 中断优先级管理 中断使能/挂起 中断向量表 中断嵌套控制 Cortex-M3/M4 CPU核心 中断响应 现场保护/恢复 中断返回 配置流程 使能时钟 → 配置GPIO → 连接EXTI线 → 配置EXTI触发方式 → 配置NVIC优先级 → 编写中断服务函数 关键点:同一EXTI线只能选一个引脚 | 中断服务函数要短 | 必须清除中断标志位

3.6 常见问题与调试技巧

配置外部中断时,我遇到过不少问题,这里列几个最常见的:

问题现象 可能原因 解决方法
中断不触发 SYSCFG时钟未使能 检查RCC_APB2PeriphClockCmd是否包含SYSCFG
中断触发多次 未做消抖处理 加软件延时或硬件RC滤波
中断响应慢 优先级配置太低 提高抢占优先级
程序卡死在中断 未清除中断标志位 在ISR末尾调用EXTI_ClearITPendingBit
多个中断互相干扰 同一EXTI线映射了多个引脚 确保每个EXTI线只对应一个引脚

调试建议:我习惯在中断服务函数里加一个GPIO翻转,用示波器看波形。如果波形正常,说明中断触发了;如果波形不对,再查配置。这比单纯看代码效率高多了。

好了,中断系统和NVIC的内容就讲到这里。记住:中断是嵌入式系统的核心机制,配置得好,系统稳定高效;配置不好,各种奇怪问题都会冒出来。多动手、多调试,慢慢就能掌握其中的门道。