2. 硬件架构基础:中断控制器与中断响应

中断,是嵌入式系统的灵魂。没有中断,CPU 就得轮询,效率低得吓人。但中断也常常是 bug 的温床——我见过太多项目因为中断延迟超标导致系统崩溃。所以,搞懂硬件怎么处理中断,是调试实时系统的第一步。

这一章,我们聚焦两个主流架构:ARM Cortex-M 系列的 NVIC,和 Cortex-A/R 系列的 GIC。别被名字吓到,核心逻辑是相通的。

2.1 中断控制器:NVIC 与 GIC

中断控制器是 CPU 和外部中断源之间的“总调度员”。它的职责很简单:收中断、判优先级、发信号给 CPU。

2.1.1 NVIC(Nested Vectored Interrupt Controller)

NVIC 是 Cortex-M 系列的内置中断控制器。它最大的特点是:硬件自动压栈和出栈。这意味着中断响应时,CPU 会自动把 R0-R3、R12、LR、PC、xPSR 压入堆栈,然后从向量表取出中断服务函数地址直接跳转。

我个人习惯把 NVIC 的关键参数记在脑子里:

参数 说明 典型值
中断数量 可配置的外部中断线 16~240 条
优先级位数 可配置的优先级分组 3~8 位(Cortex-M3/M4 通常 4 位)
抢占优先级 高优先级可打断低优先级 由分组决定
子优先级 同抢占优先级下的响应顺序 由分组决定
尾链技术 连续中断免去多余压栈 硬件自动支持
避坑指南: 我曾经在一个项目中,把 NVIC 优先级分组设成了 7 位抢占 + 1 位子优先级。结果两个中断同时触发时,子优先级高的反而先响应——因为硬件比较的是整个优先级寄存器的值,不是分组后的逻辑。从那以后,我统一用 4 位抢占 + 0 位子优先级,省心。

2.1.2 GIC(Generic Interrupt Controller)

GIC 用在 Cortex-A/R 系列上,比如手机 SoC 或高端 MCU。它比 NVIC 复杂得多,支持多核、消息中断、虚拟化等。

GIC 的架构分两部分:

  • 分发器(Distributor):管理所有中断源,配置优先级、使能、触发方式。
  • CPU 接口(CPU Interface):每个 CPU 核一个,负责向核发送中断信号。

GIC 支持三种中断类型:

  • PPI(Private Peripheral Interrupt):私有外设中断,每个核独享。
  • SPI(Shared Peripheral Interrupt):共享外设中断,可路由到任意核。
  • SGI(Software Generated Interrupt):软件触发中断,用于核间通信。
注意: GIC 的优先级比较是数值越小优先级越高。这和 NVIC 一样,但很多 RTOS 的优先级定义是数值越大优先级越高。移植时一定要做映射,否则中断响应顺序会乱套。

2.2 中断向量表

中断向量表,说白了就是一张地址表。CPU 收到中断后,根据中断号查表,找到对应的服务函数地址,然后跳过去执行。

Cortex-M 的向量表固定在地址 0x00000000(或通过 VTOR 寄存器重映射)。表的前 16 个是系统异常,后面是外部中断。举个例子:

; Cortex-M3 向量表示例(部分)
__Vectors       DCD     __initial_sp          ; 0: 栈顶指针
                DCD     Reset_Handler         ; 1: 复位
                DCD     NMI_Handler           ; 2: NMI
                DCD     HardFault_Handler     ; 3: 硬错误
                ...
                DCD     TIM2_IRQHandler       ; 28: TIM2 中断
                DCD     TIM3_IRQHandler       ; 29: TIM3 中断

嗯,这里要注意:向量表必须 4 字节对齐。我遇到过有人把向量表放在一个未对齐的地址上,结果一进中断就跑飞了。调试了整整一天才发现是 VTOR 配置错了。

2.3 中断优先级与抢占机制

优先级和抢占,是实时系统的命门。搞不清楚这两个概念,中断延迟就无从谈起。

2.3.1 优先级分组

NVIC 支持优先级分组。通过 SCB->AIRCR 寄存器设置分组,把优先级寄存器拆成抢占优先级和子优先级两部分。

举个例子,4 位优先级寄存器,分组为 2 位抢占 + 2 位子优先级:

  • 抢占优先级:0~3(数值越小优先级越高)
  • 子优先级:0~3(数值越小优先级越高)

抢占优先级高的中断可以打断抢占优先级低的中断。子优先级只在同抢占优先级时决定谁先响应,不能打断。

关键点: 抢占是嵌套中断的基础。如果你的系统要求高实时性,建议把所有中断的抢占优先级设成不同值,避免子优先级带来的不确定性。

2.3.2 抢占与嵌套

当一个高优先级中断发生时,如果 CPU 正在执行低优先级中断,硬件会自动触发抢占。流程是这样的:

  1. 低优先级中断正在执行。
  2. 高优先级中断到达。
  3. 硬件比较优先级,发现高优先级更高。
  4. CPU 暂停低优先级中断,压栈当前上下文。
  5. 跳转到高优先级中断服务函数。
  6. 高优先级中断执行完毕,出栈恢复上下文。
  7. 继续执行低优先级中断。

这个过程,NVIC 是硬件自动完成的。但 GIC 需要软件配合,因为 GIC 不自动压栈——你得在中断入口手动保存上下文。

2.4 CPU 流水线与中断响应

CPU 流水线对中断响应的影响,很多人会忽略。其实,它直接决定了中断延迟的下限。

2.4.1 流水线阶段

以 Cortex-M3 为例,它有一条 3 级流水线:取指、译码、执行。中断发生时,CPU 必须等当前指令执行完才能响应。如果当前指令是多周期指令(比如除法),中断就得等。

更麻烦的是,流水线中的预取指令会被冲刷掉。这意味着中断响应后,CPU 需要重新从向量表取地址,再取中断服务函数的第一条指令。这个过程会额外消耗几个时钟周期。

2.4.2 中断延迟的计算

中断延迟 = 硬件响应时间 + 软件处理时间。硬件响应时间包括:

  • 等待当前指令完成(1~12 个周期)
  • 压栈(12 个周期,Cortex-M3)
  • 取向量表(1 个周期)
  • 取中断服务函数第一条指令(1 个周期)

加起来,Cortex-M3 的硬件中断延迟大约是 12~26 个时钟周期。嗯,这个数字很关键——如果你测出来的中断延迟远大于这个值,那问题一定出在软件上。

实战经验: 我曾经用逻辑分析仪抓一个系统的中断响应时间,发现每次中断延迟都在 100 微秒以上。查了半天,发现是中断服务函数里调用了 printf。printf 会关中断,导致后续中断被阻塞。从那以后,我定了一条铁律:中断服务函数里绝对不允许调用任何可能阻塞的函数。

2.5 知识结构图

下面这张图总结了中断系统的核心流程和关键节点。你可以把它当作调试时的检查清单。

中断系统核心流程 外部中断源 中断控制器 CPU 核 中断向量表 ISR 执行 中断信号 IRQ/FIQ 查表 跳转 关键延迟点: 1. 中断控制器仲裁(优先级比较) 2. CPU 等待当前指令完成(流水线冲刷) 3. 压栈与向量表读取(硬件自动完成)

2.6 本章小结

中断系统的硬件基础,说白了就是三件事:中断控制器怎么收中断、向量表怎么查地址、CPU 怎么响应。NVIC 和 GIC 虽然实现不同,但核心逻辑一致。优先级分组和抢占机制决定了中断的嵌套行为,而流水线则影响了中断延迟的下限。

调试中断问题时,我建议你先确认三件事:

  • 向量表地址是否正确?
  • 优先级分组是否一致?
  • 中断服务函数里有没有阻塞操作?

这三步走完,80% 的中断问题都能定位。剩下的 20%,往往藏在硬件时序和流水线交互里——那才是真正考验功力的地方。