第3章 中断系统与NVIC配置:外部中断EXTI、中断优先级分组、中断服务函数编写、中断嵌套实战

中断系统,说白了就是CPU的“紧急响应机制”。没有中断,你的STM32就得不停地轮询——就像一个人每隔一秒就去检查一下门铃有没有响,效率极低。而有了中断,CPU可以安心干自己的活,门铃一响,立刻放下手头的事去开门。

我刚开始做嵌入式开发时,对中断的理解就停留在“按个按钮触发一下”的层面。直到有一次做无线通信项目,数据包频繁丢失,查了三天才发现是中断优先级没配好,高优先级的中断把低优先级的给“饿死”了。从那以后,我对NVIC的配置再也不敢马虎。

3.1 中断系统的整体架构

STM32的中断系统由两部分组成:NVIC(嵌套向量中断控制器)和EXTI(外部中断/事件控制器)。NVIC是ARM Cortex-M内核自带的,负责中断优先级管理和向量表调度;EXTI则是ST外设,负责管理GPIO引脚的中断触发。

核心关系:EXTI负责“检测到中断事件”,NVIC负责“决定要不要响应以及何时响应”。两者配合,缺一不可。

STM32中断系统整体架构 GPIO引脚(PA0~PG15) RTC闹钟/秒中断 PVD电压检测 其他外设事件 EXTI控制器 上升沿/下降沿/双边沿 软件中断触发 中断屏蔽寄存器 挂起请求寄存器 事件/中断选择 中断请求 NVIC控制器 中断优先级分组 抢占优先级管理 子优先级管理 中断使能/失能 向量表调度 响应 EXTI负责“检测到中断事件”,NVIC负责“决定要不要响应以及何时响应” 两者配合,缺一不可

3.2 EXTI外部中断配置

EXTI支持最多16条外部中断线(EXTI0~EXTI15),每条线对应一组GPIO引脚。比如EXTI0可以映射到PA0、PB0、PC0……但同一时刻只能选一个。

个人经验:我习惯把按键中断都放在EXTI0上,因为EXTI0的中断向量在向量表中排最前面,响应速度最快。虽然实际差异微乎其微,但心理上觉得踏实。

3.2.1 EXTI配置步骤

  1. 使能GPIO时钟——没有时钟,外设就是一块死硅。
  2. 配置GPIO为输入模式——上拉还是下拉?看你的电路设计。
  3. 将GPIO连接到EXTI线——通过SYSCFG_EXTICR寄存器。
  4. 配置EXTI触发边沿——上升沿、下降沿或双边沿。
  5. 使能EXTI中断线——不使能,触发信号进不来。
  6. 配置NVIC并使能中断——最后一步,让CPU知道这个中断的存在。
// 以PA0为例,配置EXTI0中断
void EXTI0_Config(void)
{
    // 1. 使能GPIOA时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 2. 配置PA0为输入,上拉
    GPIOA->MODER &= ~(0x03 << 0);   // 00: 输入模式
    GPIOA->PUPDR |= (0x01 << 0);    // 01: 上拉

    // 3. 将PA0连接到EXTI0
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;

    // 4. 配置下降沿触发(按键按下)
    EXTI->FTSR |= EXTI_FTSR_TR0;

    // 5. 使能EXTI0中断线
    EXTI->IMR |= EXTI_IMR_MR0;

    // 6. 配置NVIC
    NVIC_EnableIRQ(EXTI0_IRQn);
    NVIC_SetPriority(EXTI0_IRQn, 0x02);  // 优先级设为2
}

注意:配置SYSCFG之前,一定要先使能SYSCFG时钟(RCC->APB2ENR)。我曾经在这个坑里摔过一次——代码逻辑完全正确,但中断就是不触发,查了半天才发现SYSCFG时钟没开。

3.3 中断优先级分组

这是整个中断系统里最容易出问题的地方。STM32的NVIC支持最多16级抢占优先级和16级子优先级,但具体怎么分配,取决于优先级分组。

分组编号 抢占优先级位数 子优先级位数 抢占优先级范围 子优先级范围
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位抢占优先级、2位子优先级。为什么?因为大多数项目里,中断的层级关系不需要太复杂,4级抢占优先级足够用了,而且2位子优先级还能做点精细调整。

关键规则:

  • 抢占优先级高的中断可以打断抢占优先级低的中断。
  • 子优先级只在抢占优先级相同时起作用——谁子优先级高,谁先执行。
  • 整个系统只能调用一次NVIC_PriorityGroupConfig(),中途不能改。

3.4 中断服务函数编写

中断服务函数(ISR)的写法有固定套路。函数名必须和启动文件里的向量表一致,比如EXTI0的中断服务函数就叫EXTI0_IRQHandler。

void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志位
    if (EXTI->PR & EXTI_PR_PR0)
    {
        // 2. 清除中断标志位(必须!)
        EXTI->PR = EXTI_PR_PR0;

        // 3. 执行用户逻辑
        // 注意:ISR里不要做复杂操作,尽量短小精悍
        LED_Toggle();
    }
}

我曾经犯过的错:在ISR里调用了printf(),结果程序一进中断就卡死。后来才想起来,printf内部用了互斥锁,而中断里不能使用任何阻塞操作。记住:ISR里只做标志位设置、数据拷贝、简单IO操作,复杂逻辑放到主循环里处理。

3.4.1 ISR编写黄金法则

  • 快进快出——ISR执行时间越短越好,别在里面做延时。
  • 清除标志位——不清除标志位,中断会反复触发,系统直接死机。
  • 避免浮点运算——除非你开了FPU并且做了上下文保存。
  • 不要调用不可重入函数——比如malloc、printf等。

3.5 中断嵌套实战

中断嵌套,说白了就是一个中断正在执行,另一个更高优先级的中断来了,CPU先跑去处理高优先级的,处理完再回来继续执行原来的中断。

你想想看,如果两个中断的抢占优先级相同,那它们之间不会发生嵌套——必须等当前ISR执行完,下一个才能进来。这就是为什么抢占优先级的设计如此重要。

3.5.1 实战场景:按键中断 + 定时器中断

假设我们有两个中断源:

  • EXTI0(按键):抢占优先级1,子优先级0
  • TIM2(定时器):抢占优先级0,子优先级0

因为TIM2的抢占优先级(0)高于EXTI0(1),所以当TIM2中断正在执行时,按键中断无法打断它。反过来,如果按键中断正在执行,TIM2中断来了,按键中断会被立刻打断,CPU跳去执行TIM2的ISR。

// 配置两个中断的优先级
NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_PriorityGroup_2, 1, 0));
NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_PriorityGroup_2, 0, 0));

// TIM2中断服务函数
void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF)
    {
        TIM2->SR = ~TIM_SR_UIF;
        // 模拟耗时操作
        for (volatile uint32_t i = 0; i < 100000; i++);
    }
}

// EXTI0中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0)
    {
        EXTI->PR = EXTI_PR_PR0;
        // 模拟耗时操作
        for (volatile uint32_t i = 0; i < 50000; i++);
    }
}

调试技巧:我习惯在ISR里翻转一个GPIO引脚,用示波器观察中断的实际响应时间和嵌套情况。比如TIM2中断里拉高PB0,EXTI0中断里拉高PB1,一看波形就知道谁打断了谁。

3.6 常见问题与避坑指南

问题现象 可能原因 解决方案
中断不触发 SYSCFG时钟未使能 检查RCC->APB2ENR
中断反复触发 未清除标志位 在ISR开头清除EXTI->PR
低优先级中断被饿死 高优先级中断频繁触发 调整优先级分组或降低高优先级中断频率
中断响应延迟大 ISR内执行了耗时操作 将耗时操作移到主循环
系统进入HardFault ISR内调用了不可重入函数 检查ISR中的函数调用

我曾经踩过的坑:有一次做多中断项目,EXTI中断和USART中断同时工作,结果USART数据总是丢字节。查到最后发现,EXTI中断的抢占优先级设得太高,每次按键按下都打断USART的接收过程,导致数据溢出。解决办法很简单——把USART的抢占优先级设得比EXTI高一级,保证数据接收优先。

嗯,中断系统这块内容确实有点绕,但只要你理解了NVIC的优先级分组机制,再配合EXTI的配置流程,大部分项目的中断需求都能搞定。记住:中断优先级的设计一定要从系统整体角度考虑,哪个中断最紧急、哪个中断不能丢数据、哪个中断可以等一等——想清楚了再动手配置。