1. CMS32 中断系统概述:中断向量表、中断优先级、中断嵌套机制

各位工程师朋友,咱们今天正式进入 CMS32 的中断世界。说实话,中断系统是单片机实时响应的灵魂。我做了这么多年嵌入式,见过太多因为中断配置不当导致的“灵异现象”——程序跑着跑着突然死机,或者响应慢得像蜗牛。其实,问题多半出在中断向量表、优先级和嵌套这三个核心点上。

咱们先搭个框架,看看中断系统到底长什么样。

核心观点: 中断系统 = 中断源 + 中断向量表 + 中断控制器 + CPU 响应机制。CMS32 的中断控制器支持多达 64 个中断源,每个中断源都有独立的向量号和优先级配置位。

1.1 中断向量表——中断的“电话簿”

中断向量表,说白了就是一张地址映射表。每个中断源对应一个唯一的向量号,向量号指向中断服务函数(ISR)的入口地址。CPU 一旦检测到中断请求,就根据向量号查表,跳转到对应的 ISR 去执行。

我个人习惯在工程启动文件(比如 startup_cms32.s)里直接定义向量表。CMS32 的向量表默认放在 0x00000000 起始地址,每个向量占 4 字节,共 64 个中断向量 + 16 个系统异常向量。

向量号 偏移地址 中断源 默认优先级
0 0x0000 复位 最高
1 0x0004 NMI 次高
2~15 0x0008~0x003C 系统异常(HardFault 等) 固定
16~79 0x0040~0x013C 外设中断(TIM、ADC、UART 等) 可配置

嗯,这里要注意:CMS32 支持向量表重定位。你可以通过设置 VTOR 寄存器,把向量表搬到 SRAM 或 Flash 的其他位置。我在做 Bootloader 项目时就用过这个功能——把向量表搬到 RAM 里,运行时动态修改中断入口,实现灵活的固件升级。

小技巧: 如果你的程序使用了 RTOS,建议把向量表重定位到 RAM 区域。这样 OS 可以动态注册和注销中断服务函数,避免 Flash 擦写带来的延迟。

1.2 中断优先级——谁更重要?

中断优先级决定了多个中断同时触发时,CPU 先响应谁。CMS32 支持 4 位可编程优先级,也就是 0~15 共 16 级。数值越小,优先级越高。0 级最高,15 级最低。

我记得有一次调试一个电机控制项目,电流采样中断和通信中断同时触发,结果电机转速波动很大。查了半天才发现,我把电流采样中断的优先级设成了 7,通信中断设成了 3。通信中断频繁打断采样,导致电流环失控。后来我把采样中断提到优先级 1,问题立刻解决。

优先级配置通过 NVIC 的 IPR 寄存器实现。每个中断源占用一个字节的 IPR 寄存器,高 4 位有效,低 4 位保留。

// 设置 TIM2 中断优先级为 3(数值越小优先级越高)
NVIC_SetPriority(TIM2_IRQn, 3);

// 设置 ADC 中断优先级为 1
NVIC_SetPriority(ADC_IRQn, 1);

// 使能中断
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_EnableIRQ(ADC_IRQn);
避坑指南: 我曾经把两个中断设成相同优先级,结果系统随机响应,时好时坏。CMS32 的优先级分组默认是 4 位抢占优先级,没有子优先级。相同优先级的中断,硬件按向量号小的优先响应。但这不是标准行为,千万别依赖它!

1.3 中断嵌套机制——中断里还能有中断?

中断嵌套,就是高优先级中断可以打断低优先级中断的服务函数。CMS32 的 NVIC 支持硬件自动压栈和出栈,嵌套深度最多可达 16 层(受限于堆栈空间)。

你想想看,如果低优先级中断正在执行,高优先级中断来了,CPU 会怎么做?

  1. 保存当前中断的上下文(R0~R3、R12、LR、PC、xPSR)到堆栈
  2. 加载高优先级中断的向量,跳转到对应的 ISR
  3. 高优先级 ISR 执行完毕,执行异常返回指令
  4. CPU 自动恢复低优先级中断的上下文,继续执行

这个过程完全由硬件完成,不需要软件干预。但要注意:如果低优先级中断里调用了不可重入的函数(比如 printf),高优先级中断也调用同一个函数,就会出问题。我踩过这个坑——调试时用串口打印,结果中断嵌套导致打印缓冲区被破坏,数据全乱套了。

经验之谈: 中断服务函数里尽量别调用库函数,尤其是那些带全局锁或静态缓冲区的。如果非要调用,确保函数是可重入的,或者用互斥量保护。

下面这张图展示了 CMS32 中断系统的整体架构,帮你理清中断源、NVIC、CPU 之间的协作关系。

CMS32 中断系统架构图 中断源 TIM0~TIM7 ADC0~ADC3 UART0~UART2 SPI0~SPI1 GPIO EXTI ... 共 64 个 NVIC 中断控制器 中断使能寄存器 (ISER) 中断挂起寄存器 (ISPR) 中断优先级寄存器 (IPR) 中断活跃状态寄存器 (IABR) CPU 内核 向量表查找 自动压栈/出栈 优先级比较 异常返回 中断向量表 0x0000: 复位向量 0x0004: NMI 向量 0x0040: TIM0_IRQHandler 0x0044: ADC_IRQHandler 优先级分组 抢占优先级: 4 位 (0~15) 子优先级: 无 数值越小优先级越高 中断嵌套 高优先级可打断低优先级 硬件自动保存/恢复上下文 最大嵌套深度: 16 层 中断源 → NVIC 仲裁 → 向量表查找 → CPU 响应 → 嵌套/返回

1.4 中断响应流程——从触发到执行

一个完整的中断响应流程,我习惯分成 6 步:

  1. 中断请求: 外设产生中断事件,硬件置位中断挂起位
  2. 优先级仲裁: NVIC 比较当前中断与正在执行中断的优先级
  3. 向量表查找: CPU 根据向量号从向量表中取出 ISR 入口地址
  4. 自动压栈: 硬件自动将 R0~R3、R12、LR、PC、xPSR 压入堆栈
  5. 执行 ISR: CPU 跳转到 ISR 执行,软件处理中断事件
  6. 异常返回: ISR 执行完毕,硬件自动出栈,恢复现场
优化建议: 中断响应时间 = 向量表查找时间 + 压栈时间 + ISR 执行时间。前两项是硬件固定的,我们能优化的只有 ISR 执行时间。所以,ISR 里只做最紧急的事,比如置标志位、读数据,耗时操作放到主循环或任务里处理。

1.5 常见问题与避坑

  • 中断丢失: 如果同一个中断源在 ISR 执行期间再次触发,CMS32 会挂起该中断,但只挂起一次。如果连续触发多次,只会响应一次。解决办法:在 ISR 里尽快清除中断标志位。
  • 优先级反转: 低优先级中断持有某个资源,高优先级中断等待该资源,导致高优先级被阻塞。我曾在多中断共享一个 SPI 总线时遇到过,后来用互斥量 + 关中断解决了。
  • 堆栈溢出: 嵌套深度太大,堆栈空间不够。CMS32 的默认堆栈大小是 512 字节,如果嵌套 5 层以上,建议加大到 1024 或 2048 字节。
我曾经踩过的坑: 在 ISR 里调用 HAL_Delay(),结果系统直接卡死。因为 HAL_Delay() 依赖 SysTick 中断,而 SysTick 中断优先级比当前 ISR 低,导致死锁。记住:ISR 里别用任何阻塞延时函数!