3. 中断响应流程:从请求到返回的完整闭环

中断,说白了就是 CPU 正在干活,突然被一个紧急事件叫停,处理完再回来接着干。这个流程看起来简单,但每个环节都有坑。我做了十几年嵌入式,见过太多因为中断流程没理清导致的诡异 bug。

今天咱们就把中断的完整流程掰开揉碎,从请求到返回,一个环节都不放过。

3.1 中断请求:谁在敲门?

中断请求是流程的起点。外设或内部模块需要 CPU 服务时,会拉高一个信号线——这就是中断请求信号。

请求信号有两种类型:

类型 特点 典型场景
电平触发 信号保持高/低电平,直到 CPU 响应 按键中断、外部传感器
边沿触发 信号上升沿或下降沿触发一次 定时器溢出、UART 接收
注意:电平触发有个坑——如果中断服务程序没把信号拉低,CPU 会反复响应同一个中断。我曾经在一个电机驱动项目里遇到这个问题,CPU 卡在中断里出不来,主循环直接饿死。

每个中断源都有一个对应的中断标志位。当请求发生时,硬件自动置位这个标志。CPU 在每执行完一条指令后,都会检查这些标志位——这就是中断采样。

3.2 中断判优:谁更紧急?

多个中断同时来了怎么办?CPU 需要决定先处理谁。这就是中断判优。

判优机制分两种:

  • 固定优先级:硬件写死的,中断号越小优先级越高。比如 STM32 的 NVIC 里,IRQ0 永远比 IRQ1 优先。
  • 可编程优先级:你可以自己设置每个中断的优先级。Cortex-M 系列支持抢占优先级和子优先级,灵活度很高。

判优的逻辑其实很简单:

  1. CPU 检查当前是否有中断请求
  2. 如果有多个,比较它们的优先级
  3. 选优先级最高的那个
  4. 如果优先级相同,比较中断号(编号小的优先)

核心原则:中断判优只在中断响应前做一次。一旦 CPU 开始执行某个中断服务程序,除非有更高优先级的中断来抢占,否则不会响应同级或更低的中断。

我个人习惯把实时性要求高的中断(比如定时器、通信接收)设成高优先级,把非关键的中断(比如按键扫描)设成低优先级。这样能保证系统不会因为一个按键按下而错过一个数据包。

3.3 中断响应:CPU 的现场保护

判优结束后,CPU 开始响应中断。这一步最关键,也最容易出问题。

响应过程包含以下步骤:

  • 关中断:硬件自动关闭全局中断,防止被其他中断打断
  • 保存现场:把当前正在执行的程序计数器(PC)、状态寄存器(PSR)等压入堆栈
  • 查找中断向量:从中断向量表中找到对应中断服务程序的入口地址
  • 跳转执行:PC 指向中断服务程序的第一条指令

这里有个细节——不同架构的现场保存方式不一样:

架构 保存方式 保存内容
Cortex-M3/4 硬件自动压栈 R0-R3, R12, LR, PC, PSR
8051 软件手动保存 ACC, B, PSW, DPTR 等
AVR 硬件保存 PC,其余手动 SREG, R0-R31 视情况

经验之谈:用 Cortex-M 系列时,别以为硬件自动压栈就万事大吉。如果你在中断里调用了函数,编译器可能会用 R4-R11 这些寄存器,这些硬件不会帮你保存。我建议在中断服务程序开头用 __asm 或编译器指令显式保存所有可能用到的寄存器。

3.4 中断服务程序:干活要快

中断服务程序(ISR)是真正干活的地方。但这里有个铁律——ISR 要短、要快、不能阻塞。

我见过最离谱的代码,有人在 ISR 里做了 500 毫秒的延时。结果整个系统卡死,连系统滴答定时器都进不去。

ISR 里应该做什么:

  • 读取硬件状态,清除中断标志
  • 把数据搬到缓冲区
  • 设置一个标志位或信号量
  • 尽快退出

ISR 里不应该做什么:

  • 不要做复杂计算
  • 不要调用 printf 或串口打印
  • 不要等待外部事件
  • 不要调用可能引起阻塞的函数
// 好的 ISR 写法
void TIM2_IRQHandler(void) {
    // 清除中断标志
    TIM2->SR &= ~TIM_SR_UIF;
    
    // 把数据搬到缓冲区
    g_uart_rx_buf[g_uart_rx_idx++] = USART1->DR;
    
    // 设置标志位
    g_uart_rx_flag = 1;
}

// 坏的 ISR 写法
void TIM2_IRQHandler(void) {
    // 千万别这么干!
    while(USART1->SR & USART_SR_RXNE) {
        process_data(USART1->DR);  // 处理数据可能很耗时
    }
    HAL_Delay(10);  // 阻塞!
    printf("Received data\n");  // 串口打印!
}

3.5 中断返回:恢复现场

ISR 执行完毕后,CPU 需要回到被中断的地方继续执行。这个过程叫中断返回。

返回流程:

  1. 执行中断返回指令(如 Cortex-M 的 BX LR,8051 的 RETI)
  2. 硬件自动从堆栈恢复之前保存的现场(PC、PSR 等)
  3. 开中断(如果硬件自动关了的话)
  4. CPU 继续执行被中断的程序

重要提醒:中断返回指令和普通函数返回指令不一样。RETI 除了恢复 PC,还会重新使能中断。如果你用 RET 代替 RETI,中断会被永久关闭——我当年在 8051 项目上犯过这个错,排查了整整两天。

还有一个容易被忽略的点——堆栈平衡。如果在 ISR 里手动压栈了,返回前一定要手动出栈,否则返回地址会错乱,程序直接跑飞。

3.6 完整流程可视化

下面这张图把整个中断流程串起来了。你可以把它当作调试时的检查清单。

中断响应完整流程图 主程序执行 中断请求发生 中断判优 中断响应(保存现场) 关中断 → 压栈 PC/PSR → 查向量表 ISR 执行 清标志 → 搬数据 → 设标志 中断返回 恢复现场 → 开中断

3.7 常见问题与避坑指南

流程讲完了,咱们聊聊实际项目中容易踩的坑。

中断延迟:从中断请求到 ISR 第一条指令执行,中间有时间差。影响因素包括:关中断时间、高优先级中断执行时间、CPU 正在执行的指令(比如除法、多周期指令)。

我曾经在一个电机控制项目里,中断延迟从 2μs 飙到 50μs,导致电机抖动。最后发现是某个外设驱动里关了中断做 SPI 通信,一关就是 40μs。

调试技巧:用 GPIO 翻转法测量中断延迟。在 ISR 入口拉高一个 GPIO,出口拉低。用示波器看这个 GPIO 的波形,就能知道中断响应时间和 ISR 执行时间。

其他常见问题:

  • 中断丢失:边沿触发的中断,如果 CPU 没来得及采样,信号就消失了。解决办法是用电平触发,或者在硬件上加锁存器。
  • 中断嵌套过深:高优先级中断不断打断低优先级中断,堆栈溢出。我建议限制嵌套层数,最多 3 层。
  • 中断标志未清除:ISR 退出后,同一个中断又立即触发,形成死循环。每次进 ISR 第一件事就是清标志。

嗯,中断流程就这些。记住一个原则——中断是紧急通道,不是高速公路。让它干最少的活,然后尽快放行。这样你的系统才能稳定、高效。