4. 中断延迟的组成:硬件延迟与软件延迟

中断延迟,说白了就是从中断信号拉高到CPU真正执行第一行中断服务程序(ISR)之间的那段时间。很多刚入行的朋友觉得这玩意儿就是个固定值,查查芯片手册就完事了。我当年也这么想,直到在一个电机控制项目里,因为中断响应慢了那么几个微秒,电机直接过冲烧了驱动管……嗯,从那以后我再也不敢小看中断延迟的组成细节了。

中断延迟由两部分构成:硬件延迟和软件延迟。这两者叠加,才是你实际测到的总延迟。咱们一个一个拆开看。

4.1 硬件延迟:芯片层面的“硬等待”

硬件延迟是中断信号从外设走到CPU内核所花的时间。这部分你很难通过软件优化,但必须心里有数。

4.1.1 总线仲裁延迟

在多主设备系统中(比如CPU、DMA、以太网MAC都挂在总线上),中断信号要等总线空闲才能送达CPU。为什么?因为中断控制器本身也是总线上的一个设备。

我遇到过最夸张的情况:一个系统里DMA在做高速内存拷贝,占着总线不放,中断信号在总线仲裁器那里排队等了将近200个时钟周期。你想想看,200个周期,对于1GHz的CPU就是200纳秒,但对于低速外设(比如UART),这已经能丢一个字节了。

总线类型 典型仲裁延迟 影响因素
AHB(AMBA 2) 1-2个时钟周期 主设备优先级、当前传输长度
AXI(AMBA 3/4) 2-8个时钟周期 乱序传输、写缓冲深度
OCP 1-4个时钟周期 线程ID、管道深度
私有总线(如CoreLink) 0-1个时钟周期 直连中断线、无仲裁
我的习惯:在项目初期就查清楚总线拓扑。如果中断源和DMA共享总线,我会把中断源的优先级提到DMA之上,或者干脆给中断控制器开一条独立的总线通道。

4.1.2 指令完成延迟

CPU不是任何时候都能响应中断的。它得把手头的指令执行完。这个“手头的指令”可能是单周期的ADD,也可能是几十个周期的DIV或者LDM(多寄存器加载)。

我记得在ARM Cortex-M3上做过测试:如果中断刚好撞上一条LDM指令(加载8个寄存器),硬件要等这条指令完全退休才能进中断。LDM本身要花十几个周期,再加上写回和流水线排空,光指令完成延迟就能吃掉20多个周期。

关键数据:
  • 单周期指令(MOV、ADD):0-1个周期延迟
  • 多周期指令(DIV、MUL):4-32个周期延迟
  • 块传输指令(LDM、STM):N+2个周期(N为寄存器个数)
  • 浮点指令(FDIV、FSQRT):15-60个周期延迟
避坑指南:我曾经在一个项目中,中断服务程序里调用了浮点除法。结果中断嵌套进来时,外层ISR的FDIV还没执行完,内层中断又等了几十个周期。最后两个中断都超时了。我的建议是:ISR里别用浮点,别用块传输指令,尽量用简单指令。

4.2 软件延迟:你写得代码决定了延迟有多长

硬件延迟是“天灾”,软件延迟就是“人祸”了。这部分你完全可控,但也是最容易出问题的地方。

4.2.1 中断嵌套

中断嵌套的意思是:一个中断正在执行ISR,另一个更高优先级的中断来了,CPU暂停当前ISR去响应新的中断。听起来很合理对吧?但问题在于:每次嵌套都要压栈、弹栈,这些操作本身就在消耗时间。

我见过一个系统,中断嵌套深度达到了5层。每次嵌套压栈大约20个周期,5层就是100个周期。更可怕的是,最内层的中断如果是个高频中断(比如1ms一次),它可能永远出不来——因为外层中断一直在打断它。

// 中断嵌套的典型场景
void TIMER_IRQHandler(void) {  // 优先级2
    // 执行到一半,UART中断来了
    process_timer_data();
    // UART_IRQHandler 执行完才回来
}

void UART_IRQHandler(void) {   // 优先级1(更高)
    // 执行到一半,DMA中断来了
    process_uart_byte();
    // DMA_IRQHandler 执行完才回来
}

void DMA_IRQHandler(void) {    // 优先级0(最高)
    // 这里才是真正紧急的事
    process_dma_done();
}
我的建议:中断嵌套深度控制在2层以内。如果超过3层,重新设计优先级。我一般把时间敏感的中断(如PWM周期同步)放在最高优先级,把数据搬运类中断(如DMA完成)放在次高,把日志、调试类中断放在最低甚至关掉。

4.2.2 临界区

临界区是中断延迟的“隐形杀手”。你为了保护共享数据关掉中断,结果关中断的时间太长,其他中断就进不来了。

我曾经在一个项目中,工程师为了保护一个全局变量,在临界区里做了2000次循环的查表操作。关中断时间长达500微秒。结果一个1kHz的中断(周期1ms)直接丢了50%的触发。查了三天才找到原因。

临界区保护方式 关中断时间 适用场景
关全局中断(cli/sti) 整个临界区长度 极短操作(< 10个周期)
关特定中断(mask) 只关目标中断 多中断源、优先级明确
自旋锁(spinlock) 等待锁释放 多核系统、RTOS
无锁编程(lock-free) 0 原子操作、CAS指令
我曾经踩过的坑:在一个多核ARM Cortex-A72项目里,我用关全局中断来保护临界区。结果另一个核的中断完全被堵死了。后来改成用自旋锁 + 关本地中断,才把延迟降下来。记住:关中断是“核级”操作,不是“系统级”操作。

4.3 中断延迟的完整组成图

下面这张图把中断延迟的各个阶段串起来了。你可以看到,从外设发出中断到CPU执行ISR,中间经过了硬件和软件两道关卡。

中断延迟完整组成 外设中断源 总线仲裁延迟 指令完成延迟 中断嵌套延迟 临界区延迟 ISR开始执行 硬件延迟(不可控,但可测量) • 总线仲裁延迟:取决于总线负载和优先级 • 指令完成延迟:取决于当前执行指令类型 • 典型范围:5-100个时钟周期 软件延迟(完全可控) • 中断嵌套:每层嵌套增加20-50个周期压栈时间 • 临界区:关中断时间 = 临界区执行时间 • 典型范围:10-1000+个时钟周期(取决于代码质量)

4.4 实战:如何测量中断延迟的各个组成部分

光说不练假把式。我一般用逻辑分析仪或者示波器来拆解中断延迟。方法很简单:

  1. 测总延迟:把中断源信号和GPIO翻转信号同时接到示波器上。中断来时GPIO拉高,ISR第一行代码拉低。两个边沿之间的时间就是总延迟。
  2. 测硬件延迟:在中断控制器输出端(如果芯片有引出)测信号到CPU响应的时间。没有引出的话,用CPU的调试寄存器读中断响应时间戳。
  3. 测软件延迟:总延迟减去硬件延迟,就是软件延迟。如果软件延迟占比超过50%,说明你的临界区或者嵌套设计有问题。
我的经验数据:
  • 一个优化良好的系统:硬件延迟占30%,软件延迟占70%
  • 一个糟糕的系统:硬件延迟占10%,软件延迟占90%(软件是瓶颈)
  • 目标:总中断延迟控制在100个时钟周期以内(对于1GHz CPU就是100ns)