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 中断响应流程

一个完整的中断响应流程是这样的:

  1. 外设触发中断请求——比如串口收到一个字节,RXNE标志置位
  2. NVIC判断优先级——如果当前没有更高优先级中断在执行,就响应
  3. 硬件自动压栈——把当前任务的xPSR、PC、LR、R12、R3-R0压入当前栈
  4. 取向量表地址——从向量表取出对应中断的ISR地址
  5. 更新PC和LR——PC指向ISR,LR被设置成EXC_RETURN特殊值
  6. 执行ISR——用户写的中断服务函数开始执行
  7. 中断返回——执行BX LR(LR=EXC_RETURN),硬件自动出栈恢复现场

整个过程,除了ISR里的用户代码,其他全是硬件自动完成的。这也是Cortex-M实时性好的原因之一。

中断延迟:Cortex-M3/M4的中断延迟通常为12个时钟周期(从外设请求到ISR第一条指令执行)。如果开启了中断优先级屏蔽(BASEPRI),延迟会稍微增加。但相比传统ARM7的30-40个周期,已经快很多了。

3.6 中断优先级分组与NVIC结构图

下面这张图展示了NVIC如何管理中断优先级,以及中断响应时的数据流:

NVIC中断优先级与响应流程 NVIC中断优先级管理与响应流程 外设中断源 USART1 RXNE TIM2 更新 EXTI0 外部 NVIC 核心 优先级分组 (PRIGROUP) 抢占优先级比较 子优先级比较 中断使能/挂起控制 CPU 响应 硬件自动压栈 取向量表地址 跳转ISR执行 中断返回出栈 关键点:抢占优先级决定能否打断,子优先级决定同抢占级时的执行顺序 数值越小优先级越高,硬件自动完成压栈和向量表查找

3.7 常见问题与调试技巧

调试中断系统时,我遇到过几个典型问题:

  • 中断不进:先查外设中断标志是否使能,再查NVIC是否使能,最后查向量表名字是否匹配。三步排查法,屡试不爽。
  • 中断响应慢:检查是否有高优先级中断长时间占用CPU。我曾经遇到一个定时器中断里做了浮点运算,导致低优先级串口丢数据。解决办法是把浮点运算挪到主循环,中断里只做标志位。
  • 中断嵌套死机:子优先级配置不当,导致同抢占优先级的中断互相打断。记住:子优先级只决定排队顺序,不能打断。

调试小技巧:在中断服务函数入口和出口各加一个GPIO翻转,用示波器看波形。高电平宽度就是ISR执行时间,低电平宽度就是主循环时间。一眼就能看出中断是否频繁、是否超时。

中断系统是Cortex-M的基石。搞懂了NVIC的优先级分组、向量表映射和硬件响应流程,后面调试ADC采集、定时器、通信外设时,你就能快速定位到底是外设没配置对,还是中断没响应。嗯,这部分值得多花点时间吃透。