2、中断控制器基础:NVIC(嵌套向量中断控制器)介绍、中断优先级分组、中断向量表
好,咱们接着聊中断。上一章我讲了中断的基本概念,说白了就是CPU被“打断”去干别的事。那谁来管理这些“打断”的请求呢?在ARM Cortex-M内核里,这个管家就是NVIC——嵌套向量中断控制器。
我个人习惯把NVIC理解成“中断调度中心”。它不光要接收中断信号,还得决定先处理哪个、后处理哪个,甚至能嵌套处理。你想想看,如果两个中断同时来了,总得有个先后顺序吧?NVIC就是干这个的。
2.1 NVIC的核心功能
NVIC是Cortex-M内核内置的中断控制器。跟传统单片机的外置中断控制器不同,它直接集成在CPU内部,响应速度极快。我在项目中遇到过不少新手,以为NVIC只是个“开关”,其实它的功能远不止这些。
NVIC主要做三件事:
- 使能和禁止中断——控制每个外设中断的开关
- 设置中断优先级——决定哪个中断更重要
- 处理中断嵌套——高优先级中断可以打断低优先级中断
嗯,这里要注意:NVIC支持最多240个中断源,具体数量取决于芯片型号。比如STM32F103系列只用了60多个,但NVIC的硬件结构是完整的。
核心要点:NVIC的每个中断都有独立的使能位、挂起位和优先级配置。这意味着你可以精细控制每个中断的行为。
2.2 中断优先级分组
优先级分组这个概念,说实话,我刚开始学的时候也绕了一阵子。ARM Cortex-M把优先级分成了抢占优先级和子优先级两部分。
为什么会这样设计?因为实际项目中,有些中断必须立即响应(比如系统滴答定时器),有些可以稍微等等(比如按键扫描)。通过分组,你可以灵活配置。
Cortex-M3/M4支持3位或4位优先级配置(具体看芯片)。以STM32为例,它用4位来表示优先级,这4位又可以拆分成抢占优先级和子优先级。分组方式由寄存器SCB->AIRCR控制:
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 描述 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0位 | 4位 | 全部是子优先级,无抢占 |
| NVIC_PriorityGroup_1 | 1位 | 3位 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2位 | 2位 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3位 | 1位 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4位 | 0位 | 16级抢占,无子优先级 |
我曾经在一个TWS耳机项目中,把音频流中断设成高抢占优先级,把按键扫描设成低抢占优先级。结果发现按键偶尔会丢失。排查了半天,原来是音频中断太频繁,把按键中断的挂起位覆盖了。后来我把按键中断也提了一级抢占优先级,问题就解决了。
我的建议:在TWS这种实时性要求高的场景,我习惯用NVIC_PriorityGroup_2(4级抢占+4级子优先级)。这样既有足够的抢占层级,又能用子优先级做精细调度。
2.3 中断向量表
中断向量表,说白了就是一张“地址映射表”。每个中断源对应一个固定的表项,表项里存的是中断服务函数(ISR)的入口地址。
在Cortex-M中,向量表默认放在起始地址0x00000000。但实际项目中,我们经常把它重映射到SRAM或Flash的其他位置。为什么?因为Bootloader和应用程序可能共用中断,需要动态切换。
向量表的结构是这样的:
// 以STM32为例,向量表前16个是系统异常,后面是外设中断
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
&__initial_sp, // 0: 栈顶指针
Reset_Handler, // 1: 复位中断
NMI_Handler, // 2: NMI
HardFault_Handler, // 3: 硬错误
// ... 省略中间系统异常
// 外设中断从16号开始
WWDG_IRQHandler, // 0: 窗口看门狗
PVD_IRQHandler, // 1: PVD
TAMPER_IRQHandler, // 2: 篡改中断
// ... 更多外设中断
};
嗯,这里有个坑:向量表的第0项必须是栈顶指针,第1项是复位中断。如果搞反了,芯片上电直接跑飞。我曾经在调试一个TWS样机时,因为修改了链接脚本导致向量表偏移,结果芯片死活不进main函数。查了整整一下午,才发现是向量表地址没对齐。
避坑指南:重映射向量表时,必须保证地址按256字节对齐(Cortex-M要求)。我曾经用SCB->VTOR = (uint32_t)MyVectorTable时忘了检查对齐,结果中断响应全乱套了。
2.4 实际配置示例
咱们来看一个TWS项目中常用的NVIC配置代码。假设我们要配置UART中断和定时器中断:
// 配置UART中断,抢占优先级2,子优先级1
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置定时器中断,抢占优先级1,子优先级0(比UART优先级高)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
你想想看,如果定时器中断正在执行,UART中断来了,会发生什么?因为UART的抢占优先级(2)比定时器(1)低,所以UART必须等定时器处理完才能执行。反过来,如果UART正在执行,定时器中断来了,定时器会立即打断UART——这就是嵌套中断。
关键点:只有抢占优先级不同的中断才能嵌套。子优先级只在同抢占优先级时起作用——数值越小,优先级越高。
2.5 总结与经验
NVIC这块,说白了就是三个核心:优先级分组决定了你能用多少级抢占;向量表决定了中断去哪找ISR;配置代码决定了每个中断的具体行为。
我在TWS项目中总结了几条经验:
- 音频相关中断(I2S、DMA)设最高抢占优先级,保证音频不卡顿
- 蓝牙协议栈中断设中等优先级,别让音频断了就行
- 按键、GPIO等慢速中断设低优先级,用子优先级区分
- 系统滴答定时器永远设最高优先级,它是操作系统的“心跳”
嗯,最后提醒一句:中断优先级不是越多越好。优先级层级太多,反而会增加中断延迟和栈开销。我个人习惯在TWS这种资源受限的场景下,只用3-4级抢占优先级就够了。
下一章咱们聊聊中断的响应过程,包括中断延迟、压栈出栈这些细节。到时候我会结合一个实际Bug来讲解,保证让你印象深刻。