2. NVIC中断控制器:寄存器详解与优先级分组

各位同学,今天我们来聊聊NVIC。说实话,很多初学者一看到NVIC就头大,觉得寄存器又多又乱。其实没那么复杂,我当年刚接触STM32时也这么觉得,后来在项目中踩过几次坑,才真正搞明白。

NVIC全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。说白了,它就是CPU的中断管家。你想想看,一个芯片可能有几十个中断源,谁先响应、谁后响应、能不能打断别人,这些都得有个规矩。NVIC就是定规矩的那个。

2.1 NVIC寄存器详解

NVIC的寄存器不多,但每个都很关键。我习惯把它们分成三类:使能类、挂起类和优先级类。咱们一个一个看。

2.1.1 中断使能寄存器(ISER)

ISER全称Interrupt Set-Enable Register。每个ISER有32位,对应32个中断。写1使能,写0无效。注意,是写1使能,不是写0!

// 使能USART1中断(IRQ 37)
NVIC->ISER[1] = (1 << (37 - 32));  // ISER[1]对应中断32-63

这里有个细节:ISER是数组,ISER[0]管中断0-31,ISER[1]管32-63,以此类推。我在项目中见过有人直接写NVIC->ISER[37],结果程序跑飞了。嗯,这个坑我踩过。

2.1.2 中断除能寄存器(ICER)

ICER全称Interrupt Clear-Enable Register。和ISER对应,写1除能,写0无效。

// 除能USART1中断
NVIC->ICER[1] = (1 << (37 - 32));

为什么要单独搞个除能寄存器?直接写0不行吗?不行。因为读-改-写操作在多中断环境下可能出问题。ARM设计者考虑得很周到,用这种写1有效的方式,避免了竞态条件。

2.1.3 中断挂起寄存器(ISPR/ICPR)

ISPR用于手动挂起中断,ICPR用于清除挂起状态。这两个寄存器在调试时特别有用。

// 手动挂起USART1中断(模拟中断触发)
NVIC->ISPR[1] = (1 << (37 - 32));

// 清除挂起状态
NVIC->ICPR[1] = (1 << (37 - 32));

我曾经在调试一个串口通信问题时,用ISPR手动触发中断,配合逻辑分析仪,很快就定位到了问题。这招很实用,建议大家记住。

2.1.4 中断优先级寄存器(IPR)

IPR是8位寄存器,但STM32只用高4位。每个IPR管理4个中断,每个中断占8位。

// 设置USART1中断优先级为2
NVIC->IPR[9] = (2 << 4);  // USART1是IRQ 37,在IPR[9]中

这里要注意:IPR的索引计算方式是中断号除以4。37/4=9,余数1,所以USART1在IPR[9]的高8位中的第二个字节位置。具体偏移是(37%4)*8=8位。

2.2 中断优先级分组

优先级分组是NVIC最核心的概念,也是最容易搞混的地方。我刚开始学的时候,被这个分组搞得晕头转向。

2.2.1 分组原理

STM32用4位来表示优先级,但这4位可以拆分成抢占优先级和子优先级两部分。怎么拆?由SCB->AIRCR寄存器决定。

分组 抢占优先级位数 子优先级位数 抢占优先级范围 子优先级范围
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

我个人习惯用Group_2,也就是2位抢占+2位子优先级。这样既有足够的抢占层级,又能处理子优先级嵌套。当然,具体用哪个要看项目需求。

2.2.2 配置方法

// 设置优先级分组为Group_2
NVIC_SetPriorityGrouping(2);

// 或者直接操作寄存器
SCB->AIRCR = (0x5FA << 16) | (0x700 | (2 << 8));

注意:AIRCR的写入有密钥保护,高16位必须写0x5FA,否则写入无效。这个设计是为了防止意外修改。我曾经在调试时不小心写错了密钥,结果优先级分组没改过来,中断响应乱套了。嗯,从那以后我每次写AIRCR都会仔细检查密钥。

2.3 中断使能与除能

使能和除能看起来简单,但实际项目中容易出问题。我总结了几点经验:

使能中断的标准流程:

  1. 配置外设时钟和参数
  2. 设置中断优先级(NVIC->IPR)
  3. 清除挂起标志(NVIC->ICPR)
  4. 使能中断(NVIC->ISER)
  5. 编写中断服务函数
// 完整示例:使能USART1接收中断
void USART1_IRQ_Init(void)
{
    // 1. 配置USART1
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    USART1->CR1 |= USART_CR1_RE | USART_CR1_TE;
    USART1->CR1 |= USART_CR1_RXNEIE;  // 使能接收中断
    
    // 2. 设置优先级(抢占2,子优先级1)
    NVIC->IPR[9] = (2 << 4) | (1 << 0);  // 注意字节对齐
    
    // 3. 清除挂起
    NVIC->ICPR[1] = (1 << 5);  // 37-32=5
    
    // 4. 使能中断
    NVIC->ISER[1] = (1 << 5);
}

小技巧:使能中断前先清除挂起标志,可以避免意外触发中断。我在项目中遇到过上电瞬间误触发中断的情况,加上这步后就再也没出过问题。

2.4 避坑指南

我曾经踩过的坑:

  • 优先级分组只设置一次:整个系统只调用一次NVIC_SetPriorityGrouping(),在main函数开头就设好。中途修改会导致优先级混乱。
  • 中断服务函数命名要规范:STM32的中断向量表是固定的,函数名必须和启动文件中的一致。比如USART1的中断服务函数叫USART1_IRQHandler,不能自己乱起名。
  • 中断服务函数要短小精悍:不要在中断里做复杂运算或延时。我见过有人在中断里调用printf,结果系统卡死。中断里只做标志位设置,具体处理放到主循环。
  • 注意中断嵌套深度:虽然NVIC支持嵌套,但嵌套太深会导致栈溢出。一般建议嵌套不超过3层。

2.5 知识体系图

下面这张图展示了NVIC的核心知识结构,我画了好几个项目才总结出来的:

NVIC中断控制器知识体系 NVIC中断控制器 寄存器详解 ISER:中断使能寄存器 ICER:中断除能寄存器 ISPR/ICPR:挂起寄存器 IPR:优先级寄存器 优先级分组 Group 0-4:5种分组方式 抢占优先级 vs 子优先级 AIRCR寄存器配置 0x5FA密钥保护机制 使能与除能 标准使能流程 中断服务函数编写 嵌套深度控制 避坑指南 核心:优先级分组决定中断响应行为 寄存器操作 + 分组配置 + 规范流程 = 稳定可靠的中断系统

这张图把NVIC的知识点串起来了。你想想看,寄存器是基础,分组是核心,使能除能是实践。三者缺一不可。

好了,NVIC的内容就讲到这里。记住我的一句话:中断优先级分组是整个系统的基石,一定要在项目初期就规划好。别像我当年那样,写到一半才发现优先级不够用,回头改代码改到崩溃。


公众号:蓝海资料掘金营,微信deep3321