3. 中断与异常系统:NVIC嵌套向量中断控制器
中断系统,说白了就是CPU的“紧急通道”。没有它,你只能轮询——CPU像个傻保安一样不停问“有事吗?有事吗?”。有了中断,CPU就能安心干正事,等外设主动喊它。
Cortex-M的中断系统比老架构ARM7、ARM9强太多了。硬件自动压栈、自动跳转向量表、自动判断优先级。我刚开始从ARM9切到Cortex-M时,第一反应是“这也太省心了吧”。
3.1 NVIC的核心职责
NVIC(Nested Vectored Interrupt Controller)是Cortex-M内核内置的中断控制器。它管三件事:
- 使能/禁止中断——每个外设中断独立控制
- 优先级管理——支持抢占优先级和子优先级
- 中断响应——自动查找向量表,跳转到对应的ISR
注意,NVIC不是外设,它是内核的一部分。所以它的寄存器映射在系统控制空间(SCS)内,地址从0xE000E100开始。你不需要像配置外设那样先开启时钟,NVIC上电就能用。
关键认知:Cortex-M的中断响应是硬件自动完成的。CPU检测到中断请求后,硬件自动压栈(xPSR、PC、LR、R12、R3-R0),然后从向量表取出ISR地址跳转。整个过程8-12个时钟周期,比软件模拟快一个数量级。
3.2 中断优先级配置
Cortex-M的优先级寄存器是8位的,但具体用多少位由芯片厂商决定。比如STM32只用高4位,低4位固定为0。这就意味着你最多配置16级优先级。
优先级分两部分:抢占优先级和子优先级。怎么切分?靠一个叫PRIGROUP的寄存器。
| PRIGROUP值 | 抢占优先级位数 | 子优先级位数 | 抢占级数 | 子优先级级数 |
|---|---|---|---|---|
| 0 | 7 | 1 | 128 | 2 |
| 1 | 6 | 2 | 64 | 4 |
| 2 | 5 | 3 | 32 | 8 |
| 3 | 4 | 4 | 16 | 16 |
| 4 | 3 | 5 | 8 | 32 |
| 5 | 2 | 6 | 4 | 64 |
| 6 | 1 | 7 | 2 | 128 |
| 7 | 0 | 8 | 1 | 256 |
实际项目中,我习惯用PRIGROUP=3,也就是4位抢占优先级+4位子优先级。这样抢占级有16级,子优先级也有16级,够用且直观。
我的习惯:抢占优先级决定能否打断。高抢占优先级的中断可以打断低抢占优先级的中断。子优先级只在同抢占优先级时起作用——谁子优先级高谁先执行,但不能打断。
3.3 中断向量表
中断向量表就是一张地址表,每个中断源对应一个32位的入口地址。Cortex-M的向量表起始地址默认在0x00000000,但可以通过VTOR寄存器重映射。
向量表的前16个是系统异常,从第16个开始是外部中断。以STM32F103为例:
; 向量表示例(汇编)
__Vectors DCD __initial_sp ; 0: 栈顶指针
DCD Reset_Handler ; 1: 复位
DCD NMI_Handler ; 2: NMI
DCD HardFault_Handler ; 3: 硬错误
DCD MemManage_Handler ; 4: 内存管理
DCD BusFault_Handler ; 5: 总线错误
DCD UsageFault_Handler ; 6: 用法错误
; ... 省略中间 ...
DCD SysTick_Handler ; 15: SysTick
DCD WWDG_IRQHandler ; 16: 窗口看门狗
DCD PVD_IRQHandler ; 17: PVD
; ... 外设中断依次排列 ...
写中断服务函数时,函数名必须和向量表中的名字完全一致。否则链接器找不到入口,中断来了直接跑飞。我曾经在这个坑里摔过一次——手滑把USART1_IRQHandler写成了USART1_IRQ_Handler,多了一个下划线,结果串口中断死活不进。
避坑指南:我曾经在移植代码时,把向量表重映射到了SRAM,但忘了在启动文件里初始化VTOR。结果中断一触发,CPU去0x00000000找向量表,而那里是Flash的默认映射,跟SRAM里的新表对不上。现象就是“中断偶尔响应,偶尔死机”。排查了两天才发现VTOR没写。
3.4 中断优先级配置实战
配置优先级分两步:先设分组,再设每个中断的具体优先级值。
// 第一步:设置优先级分组(PRIGROUP=3,4位抢占+4位子优先级)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);
// 第二步:配置具体中断的优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 再配一个定时器中断,抢占优先级低一些
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
这里有个细节:数值越小优先级越高。抢占优先级1比2高,所以USART1中断可以打断TIM2中断。如果两个中断抢占优先级相同,再看子优先级——子优先级0比1高,但不会打断,只是排队时先执行。
3.5 中断响应流程
一个完整的中断响应流程是这样的:
- 外设触发中断请求——比如串口收到一个字节,RXNE标志置位
- NVIC判断优先级——如果当前没有更高优先级中断在执行,就响应
- 硬件自动压栈——把当前任务的xPSR、PC、LR、R12、R3-R0压入当前栈
- 取向量表地址——从向量表取出对应中断的ISR地址
- 更新PC和LR——PC指向ISR,LR被设置成EXC_RETURN特殊值
- 执行ISR——用户写的中断服务函数开始执行
- 中断返回——执行BX LR(LR=EXC_RETURN),硬件自动出栈恢复现场
整个过程,除了ISR里的用户代码,其他全是硬件自动完成的。这也是Cortex-M实时性好的原因之一。
中断延迟:Cortex-M3/M4的中断延迟通常为12个时钟周期(从外设请求到ISR第一条指令执行)。如果开启了中断优先级屏蔽(BASEPRI),延迟会稍微增加。但相比传统ARM7的30-40个周期,已经快很多了。
3.6 中断优先级分组与NVIC结构图
下面这张图展示了NVIC如何管理中断优先级,以及中断响应时的数据流:
3.7 常见问题与调试技巧
调试中断系统时,我遇到过几个典型问题:
- 中断不进:先查外设中断标志是否使能,再查NVIC是否使能,最后查向量表名字是否匹配。三步排查法,屡试不爽。
- 中断响应慢:检查是否有高优先级中断长时间占用CPU。我曾经遇到一个定时器中断里做了浮点运算,导致低优先级串口丢数据。解决办法是把浮点运算挪到主循环,中断里只做标志位。
- 中断嵌套死机:子优先级配置不当,导致同抢占优先级的中断互相打断。记住:子优先级只决定排队顺序,不能打断。
调试小技巧:在中断服务函数入口和出口各加一个GPIO翻转,用示波器看波形。高电平宽度就是ISR执行时间,低电平宽度就是主循环时间。一眼就能看出中断是否频繁、是否超时。
中断系统是Cortex-M的基石。搞懂了NVIC的优先级分组、向量表映射和硬件响应流程,后面调试ADC采集、定时器、通信外设时,你就能快速定位到底是外设没配置对,还是中断没响应。嗯,这部分值得多花点时间吃透。