1. 中断基础概念:中断的定义、中断向量表、中断优先级与嵌套

大家好,欢迎来到这门实战课。今天咱们先聊聊中断——这个嵌入式系统里最基础、也最容易出坑的概念。

我刚开始做嵌入式那会儿,总觉得中断就是个“后台任务”。后来在一个电机控制项目里,中断响应慢了 3 微秒,电机直接抖成了筛子。嗯,从那以后我再也不敢小看它了。

1.1 中断到底是什么?

说白了,中断就是 CPU 正在干一件事,突然被一个紧急信号打断,转去处理更紧急的事,处理完再回来接着干。

你想想看,这就像你正在写代码,领导突然喊你开会。你保存当前进度(压栈),去开会(执行 ISR),开完会回来继续写(出栈)。

中断的核心三要素:

  • 触发源:谁发的中断?定时器、GPIO、UART、DMA……
  • 中断服务函数(ISR):CPU 跳过去执行的那段代码
  • 返回机制:执行完 ISR 后,CPU 怎么回到原来的位置

我在项目中遇到过一种情况:有人把 ISR 写得特别长,里面还调了 printf。结果中断一触发,CPU 半天出不来,主循环直接“假死”。记住,ISR 要短、快、狠——进去就干活,干完就出来。

1.2 中断向量表——CPU 的“紧急通讯录”

中断向量表,其实就是一张表。CPU 收到中断信号后,查这张表,找到对应的 ISR 入口地址,然后跳过去执行。

这张表通常放在 Flash 或 RAM 的固定位置。比如 ARM Cortex-M 系列,向量表默认从地址 0x00000000 开始。

向量偏移 中断源 ISR 入口地址
0x0000 复位 Reset_Handler
0x0004 NMI NMI_Handler
0x0008 硬错误 HardFault_Handler
0x002C SysTick SysTick_Handler
0x0040 UART1 UART1_IRQHandler

小技巧: 调试阶段,我习惯把未使用的中断向量指向一个“死循环”函数。这样一旦有意外中断触发,CPU 会卡住,方便定位问题。我曾经靠这个抓到一个 GPIO 引脚虚焊导致的误触发中断。

来看一段典型的向量表定义代码(以 STM32 为例):

__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    &__StackTop,               // 0x0000: 栈顶指针
    Reset_Handler,             // 0x0004: 复位
    NMI_Handler,               // 0x0008: NMI
    HardFault_Handler,         // 0x000C: 硬错误
    // ... 其他中断
    SysTick_Handler,           // SysTick 中断
    UART1_IRQHandler,          // UART1 中断
    // ...
};

注意看,向量表第一个条目不是中断函数,而是栈顶指针。这是 CPU 启动时用来初始化 SP 寄存器的。很多人刚学的时候会忽略这个,结果程序一跑就飞了。

1.3 中断优先级与嵌套——谁更急?谁先来?

多个中断同时来了怎么办?谁先执行?这就涉及到优先级了。

优先级高的中断可以打断优先级低的中断——这叫嵌套。但优先级相同的中断,通常不能互相打断。

注意: 中断嵌套不是越多越好。嵌套层数太多,栈空间可能溢出。我见过一个项目,嵌套了 5 层中断,结果栈溢出导致系统随机崩溃,查了整整一周才定位到。

常见的优先级模型有两种:

  • 固定优先级:每个中断的优先级是固定的,不能改。比如 8051 系列。
  • 可编程优先级:软件可以配置每个中断的优先级。ARM Cortex-M 的 NVIC 就是典型。

Cortex-M 的优先级配置很灵活。它把优先级分为抢占优先级和子优先级:

优先级分组 抢占优先级位数 子优先级位数 说明
NVIC_PriorityGroup_0 0 4 无抢占,只有子优先级
NVIC_PriorityGroup_1 1 3 1 位抢占,3 位子优先级
NVIC_PriorityGroup_2 2 2 常用配置,2 位抢占,2 位子优先级
NVIC_PriorityGroup_3 3 1 3 位抢占,1 位子优先级
NVIC_PriorityGroup_4 4 0 4 位抢占,无子优先级

配置代码也很简单:

// 设置优先级分组:2 位抢占优先级,2 位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 配置 UART1 中断:抢占优先级 1,子优先级 0
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

这里有个坑:优先级数值越小,优先级越高。也就是说,抢占优先级 0 比 1 更急。我刚开始总搞反,把最重要的中断配成了 15,结果它老被别的打断。

1.4 中断处理流程——一张图说清楚

下面这张图展示了从中断触发到返回的完整流程:

中断处理完整流程 主程序执行 中断发生 CPU 自动压栈 (PC, LR, xPSR, R0-R3, R12) 查向量表 → 跳转 ISR 执行 ISR 执行完 ISR 出栈 → 返回主程序

流程其实就五步:

  1. 主程序正常执行——CPU 正在跑你的 while(1) 循环
  2. 中断发生——外设拉高了中断请求线
  3. CPU 自动压栈——保存当前任务的上下文(PC、LR、寄存器等)
  4. 查向量表,跳转 ISR——找到对应的中断服务函数入口
  5. 执行 ISR,出栈返回——恢复现场,继续执行主程序

避坑指南: 我曾经在一个项目中,ISR 里修改了全局变量,但没加 volatile 修饰。结果编译器优化后,主循环永远读不到更新后的值。记住:中断和主循环共享的变量,一定要加 volatile。

1.5 中断延迟——你躲不开的性能指标

中断延迟,就是从中断触发到 CPU 开始执行 ISR 第一条指令的时间。这个时间越短,系统的实时性越好。

中断延迟的主要来源:

  • CPU 正在执行高优先级中断——得等它执行完
  • CPU 正在执行关中断的临界区代码——得等临界区退出
  • CPU 正在访问不可打断的指令——比如多周期乘法指令
  • 压栈时间——保存现场需要几个时钟周期

拿 Cortex-M4 来说,从中断触发到执行 ISR 第一条指令,通常需要 12 个时钟周期左右。如果你跑 100MHz,那就是 120ns。嗯,这个数字你要记在心里——它是你评估系统实时性的基准。

注意: 不要在 ISR 里做浮点运算!Cortex-M4 虽然支持硬件浮点,但浮点寄存器的压栈/出栈开销很大。我见过有人把 PID 计算放在 ISR 里,结果中断延迟直接飙到几十微秒。把浮点运算放到主循环或任务里去。

好了,中断的基础概念就聊到这儿。记住三个关键词:向量表、优先级、嵌套。后面几章我们会深入分析中断延迟的测量方法和优化技巧。