1. ISR 基础概念:中断向量表、中断优先级、中断嵌套

各位同学,咱们今天开篇先聊点最基础的东西。中断服务程序(ISR),说白了就是硬件在紧急情况下喊你起来干活的一种机制。我做了十几年嵌入式,见过太多因为中断没处理好导致系统崩溃的案例。嗯,咱们先把地基打牢。

1.1 中断向量表——硬件的“紧急通讯录”

中断向量表是什么?我习惯把它想象成一本电话簿。每个中断源都有一个唯一的编号,叫中断号。CPU 收到中断信号后,就去查这个表,找到对应的处理函数地址,然后跳过去执行。

举个例子,ARM Cortex-M 系列的中断向量表,前 16 个是系统异常,后面才是外设中断。你想想看,如果这个表被破坏了,CPU 就不知道该往哪儿跳,系统直接死给你看。

核心要点:中断向量表必须放在正确的位置,且不能被意外修改。我在项目中遇到过,有人不小心把向量表地址写错了,结果一按按键就死机,查了两天才找到原因。

典型的向量表结构如下:

// ARM Cortex-M 向量表示例
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    &__StackTop,               // 0: 栈顶指针
    Reset_Handler,             // 1: 复位
    NMI_Handler,               // 2: NMI
    HardFault_Handler,         // 3: 硬错误
    // ... 系统异常 ...
    TIM2_IRQHandler,           // 28: 定时器2中断
    USART1_IRQHandler,         // 37: 串口1中断
    // ... 其他外设中断 ...
};

这里要注意,向量表的起始地址通常由链接脚本指定。我个人习惯在链接脚本里加上对齐约束,确保向量表不会跑偏。

警告:千万不要在运行时动态修改向量表!除非你明确知道自己在做什么。我曾经见过有人为了“优化性能”在运行时改向量表,结果触发了一个极其隐蔽的 race condition,调试了整整一周。

1.2 中断优先级——谁更紧急谁先上

中断优先级,说白了就是给中断排个队。高优先级的中断可以打断低优先级的中断。这个机制在实时系统中至关重要。

不同的处理器架构,优先级管理方式不一样。我拿最常见的两种来对比:

特性 ARM Cortex-M (NVIC) RISC-V (PLIC)
优先级位数 4-8 位可配置 通常 3 位
优先级值含义 数值越小优先级越高 数值越大优先级越高
抢占优先级 vs 子优先级 支持分组配置 不支持分组
硬件自动压栈 支持 通常不支持

你可能会问,优先级设多少合适?我的经验是:时间敏感的中断设高优先级,数据量大的中断设低优先级。比如,电机控制的中断必须设最高,因为错过一个周期电机就抖了;而串口接收中断可以设低一些,反正有 FIFO 兜底。

小技巧:在 Cortex-M 上,我习惯把优先级分组设为 4 位抢占 + 4 位子优先级。这样既能保证关键中断的实时性,又能给同优先级的中断留出排队空间。

1.3 中断嵌套——高优先级打断低优先级

中断嵌套,就是高优先级中断在执行过程中,被更高优先级的中断打断。这个机制用好了是神器,用不好是灾难。

咱们画个图来理解这个过程:

中断嵌套执行流程 主程序 (Main) 低优先级 ISR 高优先级 ISR 时间 → 主程序执行中 IRQ_L ISR_L 执行 IRQ_H ISR_H 执行 结束 ISR_L 继续 结束 主程序继续 流程说明: 1. 主程序正常执行 2. 低优先级中断到达,CPU 保存现场,跳转到 ISR_L 3. ISR_L 执行过程中,高优先级中断到达 4. CPU 再次保存现场,跳转到 ISR_H 5. ISR_H 执行完毕后,返回 ISR_L 继续执行

从图上可以看得很清楚:高优先级中断 ISR_H 打断了正在执行的 ISR_L。等 ISR_H 跑完了,ISR_L 才继续。这就是中断嵌套的核心逻辑。

但是,嵌套不是越多越好。我见过有人把中断嵌套搞了 5 层,结果栈空间直接爆了。你想想看,每嵌套一层,CPU 就要压一次栈。如果栈深度不够,系统就挂了。

避坑指南:我曾经在一个项目中,把中断嵌套层数设到了 4 层。结果系统运行几小时后随机死机。最后发现是栈溢出。从那以后,我严格控制嵌套层数不超过 3 层,并且给每个 ISR 单独分配栈空间。

1.4 中断响应的关键时序

中断响应时间,就是从硬件拉高中断信号到 CPU 开始执行 ISR 第一条指令的时间。这个时间由两部分组成:

  • 硬件延迟:CPU 完成当前指令、压栈、查向量表的时间。通常是固定的几个时钟周期。
  • 软件延迟:如果 CPU 正在执行更高优先级的中断,或者全局中断被关闭了,那就得等。

我习惯用逻辑分析仪来测量中断响应时间。具体做法是:在 ISR 入口处拉高一个 GPIO,在 ISR 出口处拉低。然后看中断信号和 GPIO 信号之间的时间差。

// 测量中断响应时间的示例代码
void TIM2_IRQHandler(void) {
    // 清除中断标志
    TIM2->SR = ~TIM_SR_UIF;
    
    // 测量点:拉高 GPIO
    GPIOA->BSRR = GPIO_BSRR_BS0;
    
    // 实际中断处理逻辑
    // ...
    
    // 测量点:拉低 GPIO
    GPIOA->BSRR = GPIO_BSRR_BR0;
}

经验之谈:如果发现中断响应时间不稳定,多半是因为全局中断被长时间关闭了。我建议在关中断的地方加上调试输出,看看是哪个函数在“霸占”CPU。

1.5 中断优先级分组——别让配置坑了你

ARM Cortex-M 的优先级分组是个容易踩坑的地方。NVIC 支持把优先级位拆分成抢占优先级和子优先级两部分。举个例子:

分组方式 抢占优先级位数 子优先级位数 抢占优先级数量 子优先级数量
NVIC_PriorityGroup_0 0 4 1 16
NVIC_PriorityGroup_1 1 3 2 8
NVIC_PriorityGroup_2 2 2 4 4
NVIC_PriorityGroup_3 3 1 8 2
NVIC_PriorityGroup_4 4 0 16 1

这里有个坑:分组必须在系统初始化时设置一次,之后不能再改。我见过有人在不同模块里重复调用分组配置,结果后调用的覆盖了前面的,导致优先级乱套。

我的建议:在系统启动代码的最前面,统一设置优先级分组。比如我习惯用 NVIC_PriorityGroup_4,也就是 16 级抢占优先级,没有子优先级。这样逻辑最清晰,不容易出错。