2. 中断控制器详解:NVIC(嵌套向量中断控制器)工作原理、中断使能与挂起

好,咱们接着聊中断。上一章我讲了中断的基本概念,这一章咱们深入看看NVIC这个核心部件。说白了,NVIC就是ARM Cortex-M系列内核里专门管中断的“大管家”。你想想看,一个MCU里可能有好几十个中断源,谁先响应、谁后响应、能不能打断别人,这些都得有个规矩。NVIC就是制定和执行这些规矩的硬件模块。

2.1 NVIC的核心工作原理

NVIC的全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。这个名字里每个词都有含义:

  • 嵌套:高优先级中断可以打断低优先级中断
  • 向量:每个中断都有自己独立的入口地址(向量表)
  • 控制器:统一管理所有中断的使能、挂起、优先级

我个人习惯把NVIC理解成一个“智能调度器”。它不像传统的单片机那样只有一个总中断标志,而是给每个中断都配了独立的控制位。这样做的好处很明显——你可以精细控制每个中断的行为。

关键点:NVIC支持最多240个中断源(具体看芯片型号),每个中断都有独立的使能位、挂起位和优先级配置。这在Cortex-M3/M4/M7上都是标配。

2.2 中断使能与屏蔽

中断使能,说白了就是决定哪个中断能“说话”。在NVIC里,每个中断对应一个使能位。你把它置1,这个中断就被激活了;清零,它就闭嘴。

我刚开始做项目时犯过一个低级错误:配置完外设后忘了使能NVIC的中断,结果折腾了半天中断就是不触发。嗯,这里要注意——外设的中断使能和NVIC的中断使能是两回事,得都打开才行。

代码示例:使能一个外部中断

// 使能EXTI0中断(假设中断号为6)
NVIC_EnableIRQ(EXTI0_IRQn);  // 设置NVIC的使能位

// 对应的底层操作其实是写寄存器
// NVIC->ISER[0] |= (1 << 6);  // ISER: Interrupt Set-Enable Register

对应的还有屏蔽操作:

// 禁用EXTI0中断
NVIC_DisableIRQ(EXTI0_IRQn);

// 底层:NVIC->ICER[0] |= (1 << 6);  // ICER: Interrupt Clear-Enable Register

小技巧:我个人习惯在初始化函数末尾统一使能所有需要的中断,而不是分散在各处。这样排查问题时一目了然。

2.3 中断挂起与清除

挂起(Pending)是个很有意思的概念。当一个中断条件满足但CPU正在处理更高优先级的中断时,这个中断不会丢失,而是被“挂起”了——相当于在NVIC里记了一笔:“嘿,这个中断在等你”。

为什么会这样?因为中断信号可能是脉冲式的,如果CPU当时没空处理,信号就没了。挂起机制保证了中断不会丢失。

我曾经在一个电机控制项目里遇到过一个问题:两个编码器中断几乎同时到达,结果第二个中断的脉冲太短,CPU处理第一个中断时第二个信号已经消失了。后来查手册才发现,NVIC的挂起寄存器会锁存这个中断请求,直到CPU处理完前一个中断。

挂起相关的操作:

// 手动挂起一个中断(用于软件触发)
NVIC_SetPendingIRQ(EXTI0_IRQn);
// 底层:NVIC->ISPR[0] |= (1 << 6);

// 清除挂起状态
NVIC_ClearPendingIRQ(EXTI0_IRQn);
// 底层:NVIC->ICPR[0] |= (1 << 6);

// 查询挂起状态
uint32_t pending = NVIC_GetPendingIRQ(EXTI0_IRQn);

避坑指南:我曾经在中断服务函数里手动清除挂起位,结果导致同一个中断被触发了两次。原因是硬件自动清除挂起位和软件清除产生了竞争。记住:大多数情况下,硬件会在进入中断服务函数时自动清除挂起位,你不需要手动操作。

2.4 优先级管理

NVIC支持中断优先级分组。Cortex-M内核允许你配置最多8级(3位)或16级(4位)优先级。但更关键的是,优先级还分成了抢占优先级和子优先级。

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

配置优先级分组:

// 设置分组方式:3位抢占优先级,1位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);

// 设置具体中断的优先级
NVIC_SetPriority(EXTI0_IRQn, 2);  // 抢占优先级2,子优先级0
NVIC_SetPriority(EXTI1_IRQn, 3);  // 抢占优先级3,子优先级0

实际经验:我建议在系统初始化时就确定好分组方式,之后不要再改。因为一旦改了分组,所有中断的优先级含义都会变,容易出问题。另外,数值越小优先级越高,这个和直觉相反,我刚接触时也搞反过。

2.5 中断向量表

每个中断都有一个固定的向量表入口。当NVIC决定响应某个中断时,它会从向量表中取出对应的地址,直接跳转过去。这就是“向量”的含义——不需要软件查询中断源,硬件直接帮你跳转。

向量表通常放在Flash的起始地址(0x00000000),但有些场景下需要重定位,比如Bootloader跳转到应用程序时:

// 重定位向量表到SRAM(假设地址0x20000000)
SCB->VTOR = 0x20000000;

嗯,这里要注意:重定位后,新的向量表里必须包含正确的堆栈指针和复位向量,否则程序会跑飞。我见过有人直接复制Flash里的向量表到SRAM,结果忘了更新堆栈指针,调试了一整天。

2.6 实战中的NVIC配置流程

总结一下,我在项目中配置NVIC的典型步骤:

  1. 确定优先级分组:根据系统实时性要求选择分组方式
  2. 配置外设中断:使能外设自身的中断产生条件
  3. 设置NVIC优先级:为每个中断分配抢占优先级和子优先级
  4. 使能NVIC中断:调用NVIC_EnableIRQ()
  5. 编写中断服务函数:注意函数名要和启动文件里的向量表一致

个人建议:对于时间敏感的中断(比如定时器、通信),我习惯给最高的抢占优先级。对于不那么紧急的中断(比如按键扫描),给低优先级就行。这样高优先级中断可以随时打断低优先级,保证实时性。

好了,NVIC的核心内容就这些。下一章咱们聊聊中断延迟和响应时间的优化,那才是真正考验功力的地方。