1、中断的本质:什么是中断、中断向量表、中断优先级、中断嵌套的概念

各位同学,咱们今天聊中断。说实话,我刚入行那会儿,觉得中断就是个「硬件喊CPU一声,CPU就屁颠屁颠跑过去」的东西。后来被坑了几次才明白——中断是整个嵌入式系统的神经反射弧,搞不懂它,你写的代码就是纸糊的。

1.1 什么是中断?

中断,说白了就是CPU正在干一件事,突然被一个紧急事件打断,它把手头的活儿暂存一下,转去处理这个紧急事件,处理完再回来接着干。

我习惯用一个比喻:你正在写代码,突然电话响了。你记下当前写到哪一行(保存现场),接电话(执行中断服务程序),挂电话后看一眼笔记继续写(恢复现场)。这就是中断。

中断的核心三要素:

  • 触发源:谁发起的?外设、定时器、外部引脚、软件指令。
  • 响应机制:CPU怎么知道该去处理?硬件自动跳转到固定地址。
  • 恢复机制:处理完怎么回来?靠压栈和出栈。

我在项目中遇到过最典型的场景:一个串口接收中断,每收到一个字节就触发一次。如果中断处理函数里写了延时或者轮询,整个系统就卡死了。嗯,这就是没理解中断的「短平快」原则。

1.2 中断向量表

中断向量表,你可以把它想象成一个「紧急电话本」。每个中断源都有一个编号,CPU根据这个编号去查表,找到对应的处理函数地址,然后跳过去执行。

举个例子,ARM Cortex-M3 的中断向量表长这样:

; 向量表起始地址 0x00000000
0x00000000: 栈顶地址          ; 初始SP
0x00000004: Reset_Handler    ; 复位中断
0x00000008: NMI_Handler      ; 不可屏蔽中断
0x0000000C: HardFault_Handler; 硬错误中断
0x00000010: MemManage_Handler ; 内存管理中断
...
0x00000040: USART1_IRQHandler ; 串口1中断
0x00000044: USART2_IRQHandler ; 串口2中断

你想想看,如果这个表被破坏了或者没配置对,中断来了CPU就不知道该往哪跳——轻则跑飞,重则死机。我见过一个同事,把向量表地址写错了,结果每次按按键就触发 HardFault,查了两天才找到原因。

个人经验:我建议在项目初期就把向量表放在 RAM 里(通过 VTOR 寄存器重映射),这样调试时可以动态修改中断入口,不用每次烧录 Flash。尤其是做 Bootloader 的时候,这招特别管用。

1.3 中断优先级

中断优先级,就是给中断排个「谁更紧急」的序。高优先级的中断可以打断低优先级的中断,反之不行。

常见的优先级模型有两种:

模型 特点 典型芯片
固定优先级 中断号越小优先级越高,不可配置 8051、AVR
可编程优先级 每个中断可以独立设置优先级分组 ARM Cortex-M、RISC-V

ARM Cortex-M 的优先级设计比较巧妙——它把优先级分成「抢占优先级」和「子优先级」。抢占优先级决定能否打断,子优先级决定同时到达时谁先执行。

// STM32 中断优先级配置示例
// 分组:4位抢占优先级,0位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

// 配置两个中断
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);

NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  // 抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
// 结果:TIM2 可以打断 USART1

避坑指南:我曾经在一个项目中,把两个中断设成了相同的抢占优先级,结果它们同时触发时,系统行为变得不可预测。后来查手册才发现——同优先级的中断,硬件按中断号顺序执行,但软件层面不能依赖这个顺序。所以,我建议你给每个中断分配唯一的抢占优先级,除非你非常清楚自己在做什么。

1.4 中断嵌套

中断嵌套,就是「中断里套中断」。高优先级的中断正在执行,又来一个更高优先级的,CPU会再次保存现场,去处理更紧急的那个。

嵌套的深度取决于两个因素:

  • 优先级配置:只有更高优先级的中断才能嵌套。
  • 硬件栈空间:每嵌套一层,就要压一次现场(寄存器、返回地址)。栈溢出就崩了。

我画了一张图,帮你理解中断嵌套的流程:

中断嵌套执行流程 主程序(main) 低优先级中断 ISR 高优先级中断 ISR 高优先级执行完毕,返回低优先级 低优先级执行完毕,返回主程序 栈空间变化(嵌套时) 主程序 → 低优先级ISR(压栈) → 高优先级ISR(再压栈) 高优先级ISR(出栈) → 低优先级ISR(出栈) → 主程序

从图上你能看到:主程序先被低优先级中断打断,低优先级ISR还没执行完,又被高优先级中断打断。高优先级ISR执行完返回低优先级,低优先级执行完再返回主程序。每一层嵌套,栈上就多压一份现场数据。

注意:中断嵌套不是越多越好。我见过一个系统,嵌套深度到了5层,结果栈空间不够,直接 HardFault。我建议你给每个中断ISR分配独立的栈空间(如果硬件支持),或者严格控制嵌套深度不超过3层。另外,中断ISR里绝对不要调用 printf、malloc 这类不可重入函数——我曾经因为这个原因,在调试串口中断时反复死机,最后发现是 printf 内部用了全局缓冲区。

1.5 小结

中断的本质,就是CPU的「多任务应急处理机制」。向量表告诉CPU「去哪处理」,优先级告诉CPU「谁先处理」,嵌套告诉CPU「怎么处理更紧急的」。这三个概念是中断系统的基石,后面的升级中断恢复机制和容错设计,全都建立在这个基础上。