3. 中断响应流程:从请求到返回的完整闭环
中断,说白了就是 CPU 正在干活,突然被一个紧急事件叫停,处理完再回来接着干。这个流程看起来简单,但每个环节都有坑。我做了十几年嵌入式,见过太多因为中断流程没理清导致的诡异 bug。
今天咱们就把中断的完整流程掰开揉碎,从请求到返回,一个环节都不放过。
3.1 中断请求:谁在敲门?
中断请求是流程的起点。外设或内部模块需要 CPU 服务时,会拉高一个信号线——这就是中断请求信号。
请求信号有两种类型:
| 类型 | 特点 | 典型场景 |
|---|---|---|
| 电平触发 | 信号保持高/低电平,直到 CPU 响应 | 按键中断、外部传感器 |
| 边沿触发 | 信号上升沿或下降沿触发一次 | 定时器溢出、UART 接收 |
每个中断源都有一个对应的中断标志位。当请求发生时,硬件自动置位这个标志。CPU 在每执行完一条指令后,都会检查这些标志位——这就是中断采样。
3.2 中断判优:谁更紧急?
多个中断同时来了怎么办?CPU 需要决定先处理谁。这就是中断判优。
判优机制分两种:
- 固定优先级:硬件写死的,中断号越小优先级越高。比如 STM32 的 NVIC 里,IRQ0 永远比 IRQ1 优先。
- 可编程优先级:你可以自己设置每个中断的优先级。Cortex-M 系列支持抢占优先级和子优先级,灵活度很高。
判优的逻辑其实很简单:
- CPU 检查当前是否有中断请求
- 如果有多个,比较它们的优先级
- 选优先级最高的那个
- 如果优先级相同,比较中断号(编号小的优先)
核心原则:中断判优只在中断响应前做一次。一旦 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 需要回到被中断的地方继续执行。这个过程叫中断返回。
返回流程:
- 执行中断返回指令(如 Cortex-M 的
BX LR,8051 的RETI) - 硬件自动从堆栈恢复之前保存的现场(PC、PSR 等)
- 开中断(如果硬件自动关了的话)
- CPU 继续执行被中断的程序
重要提醒:中断返回指令和普通函数返回指令不一样。RETI 除了恢复 PC,还会重新使能中断。如果你用 RET 代替 RETI,中断会被永久关闭——我当年在 8051 项目上犯过这个错,排查了整整两天。
还有一个容易被忽略的点——堆栈平衡。如果在 ISR 里手动压栈了,返回前一定要手动出栈,否则返回地址会错乱,程序直接跑飞。
3.6 完整流程可视化
下面这张图把整个中断流程串起来了。你可以把它当作调试时的检查清单。
3.7 常见问题与避坑指南
流程讲完了,咱们聊聊实际项目中容易踩的坑。
中断延迟:从中断请求到 ISR 第一条指令执行,中间有时间差。影响因素包括:关中断时间、高优先级中断执行时间、CPU 正在执行的指令(比如除法、多周期指令)。
我曾经在一个电机控制项目里,中断延迟从 2μs 飙到 50μs,导致电机抖动。最后发现是某个外设驱动里关了中断做 SPI 通信,一关就是 40μs。
调试技巧:用 GPIO 翻转法测量中断延迟。在 ISR 入口拉高一个 GPIO,出口拉低。用示波器看这个 GPIO 的波形,就能知道中断响应时间和 ISR 执行时间。
其他常见问题:
- 中断丢失:边沿触发的中断,如果 CPU 没来得及采样,信号就消失了。解决办法是用电平触发,或者在硬件上加锁存器。
- 中断嵌套过深:高优先级中断不断打断低优先级中断,堆栈溢出。我建议限制嵌套层数,最多 3 层。
- 中断标志未清除:ISR 退出后,同一个中断又立即触发,形成死循环。每次进 ISR 第一件事就是清标志。
嗯,中断流程就这些。记住一个原则——中断是紧急通道,不是高速公路。让它干最少的活,然后尽快放行。这样你的系统才能稳定、高效。