第4章 中断系统深入:NVIC嵌套向量中断控制器、中断优先级分组、中断服务函数编写、中断延迟优化

中断系统,说白了就是MCU的“紧急事务处理机制”。

我刚开始做嵌入式那会儿,总觉得中断就是个“来了就跳进去执行”的东西。直到有一次,两个中断同时触发,系统直接跑飞了……嗯,从那以后我才真正重视起NVIC和优先级分组。

4.1 NVIC嵌套向量中断控制器

NVIC是Cortex-M内核的中断管家。它管三件事:中断的使能/除能、优先级控制、以及中断嵌套。

什么叫嵌套?就是高优先级中断可以打断低优先级中断。我见过不少新手,以为中断优先级设得越高越好,结果高优先级中断一直打断低优先级,低优先级的任务永远得不到执行——这叫“中断饿死”。

核心概念:NVIC支持最多240个中断源,每个中断都有独立的优先级配置。但具体能用多少个,得看芯片厂商的设计。比如STM32F103,它只用了60个左右。

NVIC的寄存器操作,我习惯用CMSIS标准库。直接操作寄存器当然也行,但可读性差,容易出错。

// 使能USART1中断
NVIC_EnableIRQ(USART1_IRQn);

// 设置优先级
NVIC_SetPriority(USART1_IRQn, 2);

// 清除挂起位
NVIC_ClearPendingIRQ(USART1_IRQn);

这里有个坑:NVIC_SetPriority的参数值越小,优先级越高。0是最高优先级。我刚开始就搞反过,以为数字越大越优先……

4.2 中断优先级分组

Cortex-M3/M4支持中断优先级分组。说白了,就是把优先级寄存器里的几个位,拆成“抢占优先级”和“子优先级”两部分。

分组方式 抢占优先级位数 子优先级位数 优先级总数
NVIC_PriorityGroup_0 0 4 16
NVIC_PriorityGroup_1 1 3 8
NVIC_PriorityGroup_2 2 2 4
NVIC_PriorityGroup_3 3 1 2
NVIC_PriorityGroup_4 4 0 1

我个人习惯用分组2。为什么?因为2位抢占优先级(4级)够用,2位子优先级(4级)也够灵活。你想想看,一个系统里真正需要抢占的中断,一般不超过4个。

我的经验:定时器中断、外部中断这种实时性要求高的,给高抢占优先级。UART、I2C这种数据量大的,给低抢占优先级但高子优先级。这样既能保证实时性,又能避免数据丢失。

// 设置分组方式(必须在系统初始化时调用一次)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 定时器中断:抢占优先级0,子优先级0
NVIC_SetPriority(TIM2_IRQn, 0);

// UART中断:抢占优先级2,子优先级0
NVIC_SetPriority(USART1_IRQn, 8);  // 二进制1000,抢占2,子0

4.3 中断服务函数编写

中断服务函数(ISR)的写法,其实有固定套路。我总结了三条铁律:

  • 短小精悍:ISR里不要做复杂运算,不要调用printf,不要用延时。我见过有人在ISR里做浮点运算,结果中断延迟直接飙到几百微秒。
  • 清除标志位:进入ISR第一件事,清除中断标志。否则会反复触发,形成死循环。
  • 传递数据用全局变量:ISR和主循环之间,用volatile修饰的全局变量传递数据。
volatile uint8_t rx_data;
volatile uint8_t rx_flag = 0;

void USART1_IRQHandler(void)
{
    // 检查是不是接收中断
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        // 读数据,顺便清除标志位
        rx_data = USART_ReceiveData(USART1);
        rx_flag = 1;
    }
}

我曾经犯过一个错:在ISR里调用了HAL_Delay()。结果系统直接卡死。为什么?因为HAL_Delay()依赖SysTick中断,而SysTick中断优先级可能比当前中断低,导致死锁。

警告:不要在ISR中使用任何可能引起阻塞的函数。包括但不限于:延时函数、互斥锁、动态内存分配(malloc)、以及任何依赖其他中断的函数。

4.4 中断延迟优化

中断延迟,就是从硬件触发中断,到CPU开始执行ISR第一条指令的时间。这个时间越短越好。

影响中断延迟的因素有三个:

  1. 中断屏蔽:如果当前正在执行高优先级ISR,或者CPU手动关闭了全局中断,低优先级中断就得等着。
  2. 指令执行时间:CPU正在执行一条长指令(比如除法、浮点运算),得等它执行完才能响应中断。
  3. 压栈时间:Cortex-M硬件会自动压栈8个寄存器(R0-R3, R12, LR, PC, xPSR),这个时间固定,但可以优化。

我常用的优化手段:

  • 减少中断屏蔽时间:只在必要的时候关中断,而且关中断的代码块要尽量短。
  • 使用尾链技术:Cortex-M3/M4支持中断尾链。当两个中断连续触发时,硬件会跳过多余的出栈/入栈操作。这个不用我们操心,硬件自动完成。
  • 提高时钟频率:这个最直接,但也最耗电。
  • 使用FIQ(快速中断):有些MCU有FIQ,它有自己的寄存器组,不需要压栈。但Cortex-M没有FIQ,只有NMI(不可屏蔽中断)。

实测数据:在STM32F407上,72MHz主频,从中断触发到ISR执行,大约需要12个时钟周期(约167ns)。如果开启了FPU,并且ISR里用了浮点运算,这个时间会增加到30个周期以上。

嗯,这里要注意:中断延迟优化不是万能的。有时候为了降低延迟,反而增加了系统复杂度。我个人的原则是:先测量,再优化。用逻辑分析仪抓一下GPIO翻转的时间,就知道实际延迟是多少了。

// 测量中断延迟的简单方法
// 在ISR入口翻转一个GPIO
void EXTI0_IRQHandler(void)
{
    GPIO_ToggleBits(GPIOA, GPIO_Pin_0);  // 用示波器看这个翻转
    // ... 实际处理代码
    EXTI_ClearITPendingBit(EXTI_Line0);
}

用示波器看GPIOA.0的波形,从外部触发信号到GPIO翻转的时间差,就是中断延迟。这个方法我用了好多年,简单粗暴有效。

避坑指南:我曾经在优化中断延迟时,把中断优先级设得特别高,结果导致系统滴答定时器(SysTick)无法正常工作。后来才发现,SysTick的优先级默认是15(最低),被我的高优先级中断一直抢占。解决办法:把SysTick优先级也调高,或者确保高优先级ISR执行时间足够短。

最后总结一下:中断系统是MCU的“神经系统”。NVIC管优先级,分组管策略,ISR管执行,延迟优化管性能。把这四块吃透了,你写的中断代码就不会再出幺蛾子。

下一章,我们会讲定时器。定时器和中断是孪生兄弟,到时候你会看到更多有意思的应用场景。