中断延迟:硬件、总线、上下文的三层博弈

做 DMA 传输时,大家最关心的是什么?吞吐量?数据完整性?其实还有一个容易被忽略的痛点——中断延迟。你说你每次传输完成,CPU 要多久才能拿到那个“活儿干完了”的信号?我刚开始干这行时,总觉得差个几微秒无所谓。直到有一次在视频采集卡项目里,丢帧丢得客户直接拍桌子… 嗯,从那以后我才认真把中断延迟的每个环节拆开来看。

说白了,中断延迟就是从硬件拉高中断请求线,到CPU 开始执行 ISR 的第一条指令之间的那段时间。它不是一个单一数字,而是三个环节的叠加:

  • 硬件延迟(信号传递 + 电平建立)
  • 总线仲裁(谁拿到总线控制权)
  • 上下文切换(保存现场 + 跳转)

我们一个一个拆。

中断延迟三大组成结构 硬件延迟 信号建立 + 电平毛刺 总线仲裁 请求 → 授权 → 释放 上下文切换 保护现场 + 向量跳转 核心公式: 总中断延迟 = 硬件延迟 + 总线仲裁时间 + 上下文切换时间 实际项目中,总线仲裁往往是最大的“黑盒”,容易超过预期。 IO 管脚延迟 AHB/AXI 仲裁 PUSH/POP 开销

硬件延迟:第一个不可压缩的“物理税”

硬件延迟指的是中断请求信号从外设引脚传到中断控制器所花的时间。这包括:IO 管脚的电平建立时间、信号的毛刺滤波时间、以及中断控制器内部的同步处理。我自己遇到过最离谱的案例,是在一个温度传感器项目里,中断信号经过 3 级电平转换,硬生生多了 0.5 µs——而这还只是纯硬件延迟。

要注意几个典型来源:

  • 电平毛刺滤波:很多 MCU 内部会加一个去抖滤波器,比如 3 个时钟周期的滤波。如果你用 10 MHz 时钟,那就是 300 ns。对于紧急中断,可以尝试用边沿触发绕过滤波器。
  • 时钟域同步:当外设时钟域和 CPU 时钟域不同时,需要 2~3 个周期的同步。我记得在一个多时钟的 FPGA 加速卡上,同步级数甚至达到了 4 级,直接吞掉 80 ns。
  • 中断控制器优先级处理:NVIC 或 GIC 内部要先判断当前是否有更高优先级的中断在服务,这又是一层硬件逻辑延迟。
小技巧:如果你使用的是 ARM Cortex-M 系列,可以手动关闭 PendSV 和 SysTick 的中断,避免它们和 DMA 中断抢优先级。我习惯在 DMA 传输前先把 PendSV 优先级调到最低或直接屏蔽。

总线仲裁:最容易被忽视的“堵车”

中断信号到了 CPU 门口,CPU 就得去读中断向量表或者响应中断?没那么简单——CPU 得先拿到内部总线的控制权。如果此时 DMA 正在做 bulk 传输占着 AHB/AXI 总线,CPU 就得等 DMA 释放。

为什么会这样?因为 DMA 传输完成中断的触发条件就是“传输完了”,而传输完那瞬间,DMA 很可能还占着总线在做最后的写回操作。你想想看,总线仲裁器要等当前传输 beat 结束,才能把总线交给 CPU。这个时间取决于:

  1. 当前总线 burst 长度(比如 AHB burst 可以是 1、4、8、16 beat)
  2. 总线时钟频率
  3. 是否有多主设备竞争(比如另一个 DMA 通道也在抢总线)

我曾经在一个多路视频采集项目中,8 个 DMA 通道同时工作,中断延迟从 0.8 µs 直接飙到了 4.2 µs。抓波形一看,总线仲裁占了将近 2/3 的时间。最后怎么解决的?我把高优先级的中断对应的 DMA 通道优先级也调高,并且在 DMA 配置里把 burst 长度从 16 beat 降到了 4 beat——虽然降低了单次传输效率,但中断响应快了很多。

总线类型 典型仲裁延迟(1 beat 传输) 最佳优化方案
AHB-Lite 2~3 时钟周期 减少 burst 长度
AXI4(无 out-of-order) 1~2 时钟周期 + 写响应延迟 使用 low-latency 端口
多主 AXI 互连 5~15 时钟周期 优先级固定 + 预留带宽
警告:不要盲目降低 DMA burst 长度。如果你的应用对吞吐量要求很高,牺牲总线效率可能得不偿失。我的做法是对关键中断通道单独用一个小 burst(比如 1 或 4),其他非实时通道继续开大 burst。

上下文切换:CPU 的“换人”代价

终于,CPU 拿到了总线,开始响应中断。但现在还有一个流程:保护当前任务的寄存器和状态,跳转到中断向量表,然后进入 ISR。这部分时间也叫中断进入延迟。

不同架构差别很大:

  • Cortex-M3/M4:硬件自动压栈 R0-R3、R12、LR、PC、xPSR,总共 8 个 32 位寄存器,约 12 个时钟周期(取决于闪存等待和总线状态)。
  • Cortex-A 系列:需要软件手动压栈更多寄存器,通常要 30~80 个时钟周期。
  • RISC-V:类似 Cortex-M,但部分实现需要 2 级跳转(先跳转 trap handler,再跳转 ISR),多出 3~5 个周期。

我之前在一个基于 RISC-V 的 AI 加速器项目中,发现每次中断进 ISR 都要多花 1.2 µs。查了半天,原来是向量表被放在了外部 QSPI Flash 里,每次都要等 Flash 读。后来把向量表搬到内部 SRAM,延迟直接降到 0.4 µs。嗯,这个坑我踩过,大家千万别学我。

关键优化点:中断向量表是否在零等待内存中?ISR 函数是否用 __attribute__((interrupt)) 避免不必要的压栈?这些细节决定了上下文切换是否能跑在理论值。

延迟拆解:一张表看清各环节占比

下面是一个典型的 100 MHz Cortex-M4 + AHB DMA 系统的延迟分配表(实测数据,单位 ns):

延迟环节 典型值 优化后 优化手段
硬件信号建立 40 20 跳过去抖滤波
总线仲裁(DMA 正在传输) 180 60 减小 burst,优先级提升
中断控制器处理 50 30 固定优先级,不嵌套
上下文切换(压栈 + 跳转) 120 80 向量表放 SRAM,使用尾链
总延迟 390 190 —

你看,优化后差不多砍掉一半。而最难啃的骨头总是总线仲裁——我做过十几个项目,大部分总延迟超标的根因都在这块。

其实中断延迟的优化没有银弹。你只能针对硬件延迟、总线仲裁、上下文切换各自下功夫,一个环节一个环节压。我个人习惯先把所有环节的理论下限算出来,然后拿示波器或逻辑分析仪抓实际波形对比——差距在哪,答案就在哪。