2. 中断控制器:NVIC的工作原理与配置

说到中断,就绕不开NVIC。NVIC的全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。这名字挺长,但说白了,它就是CPU和外部中断源之间的一个“调度员”。

我刚开始做嵌入式开发时,总觉得中断就是“来了就处理”这么简单。直到有一次,我在一个多中断源的系统中遇到了优先级反转的问题——一个低优先级的中断把高优先级的给堵死了。嗯,从那以后,我才真正开始认真研究NVIC。

2.1 NVIC的核心职责

NVIC到底管什么?我总结了三件事:

  • 中断使能与屏蔽——决定哪个中断能进来,哪个得等着
  • 优先级管理——多个中断同时来了,谁先处理
  • 向量表映射——中断来了,CPU该跳到哪里去执行

你想想看,如果没有NVIC,每个外设都得直接跟CPU“喊话”,那CPU得疯掉。NVIC就像一个秘书,帮你过滤、排序、安排这些“电话”。

2.2 嵌套向量中断——名字里的秘密

“嵌套”这个词很关键。它意味着:一个中断正在处理时,更高优先级的中断可以打断它

举个例子:

  1. 系统正在运行主程序
  2. UART中断来了(优先级3),CPU跳去处理
  3. 处理到一半,定时器中断来了(优先级1,更高)
  4. CPU暂停UART处理,先去处理定时器
  5. 定时器处理完,回来继续处理UART
  6. UART处理完,回到主程序

这就是嵌套。我在项目中遇到过一种情况:如果不小心把两个中断设成相同优先级,那嵌套就乱了,系统行为变得不可预测。所以,优先级分配一定要仔细

2.3 优先级分组——别小看这个配置

ARM Cortex-M系列的NVIC支持优先级分组。什么意思呢?就是把优先级寄存器里的位,分成两组:

  • 抢占优先级(组优先级)——决定能否打断
  • 子优先级——同组内谁先执行

配置方式是通过SCB->AIRCR寄存器。常用的分组方式有:

分组 抢占优先级位数 子优先级位数 典型应用场景
NVIC_PriorityGroup_0 0 4 所有中断同级,按子优先级排队
NVIC_PriorityGroup_1 1 3 只有2级抢占,适合简单系统
NVIC_PriorityGroup_2 2 2 4级抢占,我常用的配置
NVIC_PriorityGroup_3 3 1 8级抢占,复杂系统
NVIC_PriorityGroup_4 4 0 16级抢占,最灵活

我个人习惯:用NVIC_PriorityGroup_2。4级抢占够用,子优先级还能做点微调。别一上来就搞16级,容易把自己绕晕。

2.4 中断使能与屏蔽——该开的开,该关的关

NVIC里每个中断源都有独立的使能位。配置方法很简单:

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

// 禁用USART1中断
NVIC_DisableIRQ(USART1_IRQn);

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

这里有个坑:设置优先级要在使能中断之前做。我曾经在调试一个电机驱动时,先使能了中断再设优先级,结果中断一使能就触发了,优先级还是默认值,整个系统直接卡死。嗯,从那以后我都是先设优先级,再使能。

2.5 向量表——中断来了往哪跳

每个中断源都有一个固定的中断号,对应向量表中的一个入口地址。当NVIC判定某个中断需要处理时,它会告诉CPU:“去这个地址执行代码”。

向量表默认放在Flash的起始地址(0x00000000)。但有时候我们需要重定向它,比如在做Bootloader时:

// 将向量表重定向到SRAM
SCB->VTOR = (uint32_t)SRAM_BASE | 0x200;

注意:重定向向量表时,新地址必须对齐到512字节边界。我见过有人写错了偏移量,结果中断一触发就跑飞了。调试了整整一天才发现是VTOR没对齐。

2.6 中断优先级配置的实战建议

说了这么多理论,来点实际的。我在一个工业控制项目中,总结了这么几条优先级分配原则:

  • 时间敏感的中断给最高优先级——比如电机控制的PWM中断,错过一个周期电机就抖
  • 数据量大的中断给中等优先级——比如DMA传输完成中断,可以稍微等一等
  • 人机交互的中断给低优先级——按键、触摸这些,慢一点用户感觉不到

我曾经犯过一个错:把按键中断设得比定时器中断还高。结果按一下按键,定时器就被打断,PWM输出变得不稳定,电机嗡嗡响。后来把优先级调过来,问题就解决了。

2.7 NVIC配置的完整示例

下面是一个完整的NVIC配置流程,以STM32F4为例:

void NVIC_Configuration(void)
{
    // 1. 设置优先级分组:2位抢占,2位子优先级
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    // 2. 配置USART1中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    // 3. 配置定时器中断(更高优先级)
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  // 更高
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

你看,配置其实不复杂。关键是想清楚:哪个中断更重要?哪个能等?

2.8 中断延迟——一个容易被忽略的问题

NVIC虽然快,但也不是瞬间响应。从中断触发到CPU开始执行中断服务函数,中间有固定的延迟:

  • Cortex-M3/M4:通常12个CPU周期
  • Cortex-M0/M0+:通常16个CPU周期

这个延迟在大多数场景下可以忽略。但如果你在做高速数据采集或实时控制,就得算一算了。我记得有个项目要求中断响应在1微秒以内,当时CPU主频是72MHz,12个周期就是约167纳秒,嗯,够用。

核心要点回顾

  • NVIC是中断的调度中心,管使能、优先级、向量映射
  • 优先级分组决定了抢占和子优先级的位数分配
  • 先设优先级,再使能中断——这个顺序别搞反
  • 向量表重定向要注意对齐
  • 中断延迟在高速场景下需要关注
NVIC中断处理流程图 外设中断源 外设中断源 外设中断源 NVIC 中断使能/屏蔽 优先级判断 嵌套管理 向量表查找 优先级最高的中断 CPU 执行ISR 中断返回 多个中断同时到达时,NVIC选择优先级最高的一个送给CPU

好了,NVIC的核心内容就这些。配置起来其实不复杂,但一定要想清楚优先级分配。我见过太多系统因为优先级没设好,出现各种奇怪的问题。记住一句话:高优先级给时间敏感的任务,低优先级给能等的任务

专注资料整理