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 中断使能与除能
使能和除能看起来简单,但实际项目中容易出问题。我总结了几点经验:
使能中断的标准流程:
- 配置外设时钟和参数
- 设置中断优先级(NVIC->IPR)
- 清除挂起标志(NVIC->ICPR)
- 使能中断(NVIC->ISER)
- 编写中断服务函数
// 完整示例:使能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的内容就讲到这里。记住我的一句话:中断优先级分组是整个系统的基石,一定要在项目初期就规划好。别像我当年那样,写到一半才发现优先级不够用,回头改代码改到崩溃。
公众号:蓝海资料掘金营,微信deep3321