3、中断系统深入解析:中断向量表、优先级配置、中断嵌套与抢占、中断延迟分析

中断系统,说白了就是单片机的“紧急事务处理机制”。没有它,CPU就得像个傻子一样轮询每个外设——你想想看,那得多浪费算力?我做了十几年嵌入式,见过太多因为中断配置不当导致的“灵异现象”:按键按下去没反应、串口丢数据、电机突然抽风……十有八九都是中断没玩明白。

这一章,咱们就把中断系统扒开揉碎了讲。从最底层的向量表,到优先级怎么配,再到嵌套抢占的坑,最后聊聊中断延迟这个硬指标。嗯,都是实战里天天要面对的东西。

3.1 中断向量表:CPU的“紧急联络簿”

中断向量表,你可以把它想象成一本电话簿。每个中断源都有一个专属的“电话号码”——也就是中断服务函数(ISR)的入口地址。CPU收到中断信号后,就翻这本电话簿,找到对应的号码,直接跳过去执行。

这张表通常放在Flash或RAM的起始地址。以Cortex-M3为例,向量表从0x00000000开始,前16个是系统异常,后面才是外设中断。

关键点: 向量表的地址必须4字节对齐。每个表项占4字节,存放的是函数的地址,而不是指令本身。

我记得有一次调试一个STM32F103的项目,程序死活不进定时器中断。查了半天,发现是向量表偏移没设置对——默认在0x08000000,但我把代码下载到了0x08010000。嗯,这种低级错误,犯过一次就再也不会忘了。

3.1.1 向量表重定位

现代MCU都支持向量表重定位。什么意思呢?就是你可以把向量表从Flash搬到RAM里,运行时动态修改中断入口。这在Bootloader+App架构里特别常见:Bootloader在Flash里,App也在Flash里,但App启动时得把向量表地址改到自己的起始地址。

// Cortex-M3 向量表偏移寄存器设置示例
#define SCB_VTOR    (*(volatile uint32_t *)0xE000ED08)

// 假设App起始地址为0x08010000
SCB_VTOR = 0x08010000 | 0x00000000;  // 设置向量表偏移
注意: 修改向量表偏移后,要确保新的向量表已经正确初始化。否则第一个中断进来,CPU跳到一个空指针——直接HardFault,连个招呼都不打。

3.2 优先级配置:谁更重要,谁先处理

中断优先级,就是给中断排个座次。高优先级的中断可以打断低优先级的,低优先级的只能等着。但这里有个坑:不同MCU的优先级模型不一样。

MCU架构 优先级模型 特点
Cortex-M0/M0+ 固定4级优先级 不可嵌套,简单粗暴
Cortex-M3/M4 可配置抢占优先级+子优先级 支持嵌套,灵活但容易配错
AVR 固定优先级(向量表顺序) 向量号越小优先级越高
PIC 可配置高低两级 简单,但不够灵活

我个人习惯用Cortex-M3/M4时,把抢占优先级分成3位,子优先级1位。这样就有8级抢占优先级,每级下面有2个子优先级。够用,而且不容易搞混。

3.2.1 优先级分组

Cortex-M的优先级分组由SCB->AIRCR寄存器控制。分组方式决定了抢占优先级和子优先级各占多少位。

// STM32 HAL库中的优先级分组配置
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
// 分组4:4位抢占优先级,0位子优先级
// 也就是16级抢占优先级,不支持子优先级

// 设置具体中断的优先级
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);  // 抢占优先级2,子优先级0
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0); // 抢占优先级1,子优先级0
// USART1优先级高于TIM2,可以打断TIM2的中断服务函数
实战建议: 我一般只用抢占优先级,子优先级全设成0。为什么?因为子优先级的逻辑太绕了——两个中断抢占优先级相同,子优先级高的先执行,但不能打断对方。这种“半嵌套”状态,调试起来特别容易晕。不如直接全用抢占优先级,谁高谁说了算,简单明了。

3.3 中断嵌套与抢占:谁可以打断谁

中断嵌套,就是高优先级中断打断低优先级中断的执行。CPU会先保存当前中断的现场(压栈),然后去处理高优先级中断,处理完再回来继续执行低优先级中断。

这个过程,硬件自动完成了大部分工作。但有个关键点:中断服务函数要尽量短。你想想看,如果一个中断ISR执行了100微秒,这期间所有比它优先级低的中断都得等着。要是这个ISR里还调了个延时函数……嗯,系统基本就废了。

我曾经接手过一个项目,UART接收中断的ISR里居然做了协议解析和CRC校验,一次中断处理耗时2毫秒。结果波特率一高,数据就疯狂丢失。后来我把协议解析挪到主循环里做,中断里只做FIFO缓冲,问题立刻解决。

3.3.1 抢占的硬件行为

当高优先级中断抢占时,硬件自动完成以下步骤:

  1. 保存当前中断的上下文(R0-R3, R12, LR, PC, xPSR)到栈中
  2. 更新栈指针SP
  3. 从向量表加载新中断的入口地址
  4. 跳转到新中断的ISR执行
  5. 新ISR执行完毕后,执行异常返回指令(EXC_RETURN)
  6. 硬件自动恢复之前中断的上下文
  7. 继续执行之前中断的ISR

整个过程,硬件大概需要12个时钟周期。嗯,比你手写任何软件调度都快得多。

核心原则: 中断嵌套是硬件特性,不是软件功能。你只需要配好优先级,硬件会自动处理嵌套。但你要保证ISR足够短,否则嵌套层数一多,栈空间可能爆掉。

3.4 中断延迟分析:从触发到执行到底有多快

中断延迟,就是从中断信号触发到CPU开始执行ISR第一条指令的时间。这个指标直接决定了系统的实时性。

中断延迟由以下几个部分组成:

延迟来源 典型时间 说明
硬件响应延迟 3-12个时钟周期 CPU识别中断、压栈、取向量
中断屏蔽时间 取决于当前代码 如果CPU正在执行关中断的代码,必须等开中断后才能响应
高优先级中断阻塞 取决于高优先级ISR长度 如果有更高优先级的中断正在执行,必须等它完成
指令完成延迟 1-2个时钟周期 CPU必须完成当前指令才能响应中断

对于Cortex-M3跑在72MHz,硬件响应延迟大概在0.1-0.2微秒。但如果你在某个关中断的临界区里待了10微秒,那中断延迟就至少是10微秒。

避坑指南: 我曾经在一个电机控制项目里,为了做临界区保护,把中断关了整整50微秒。结果编码器的Z相脉冲丢了,位置计算全乱套。后来我把临界区拆成多个小段,每段不超过5微秒,问题才解决。记住:关中断的时间越短越好,能不用关中断就别关。

3.4.1 测量中断延迟的方法

想知道你的系统中断延迟到底多大?别猜,直接量。我常用的方法:

  1. 在中断触发引脚上接一个GPIO,中断ISR里立刻翻转另一个GPIO
  2. 用示波器同时抓这两个信号
  3. 测量触发信号上升沿到响应信号上升沿的时间差

这个方法虽然粗糙,但足够实用。我见过有人用逻辑分析仪配合DMA来精确测量,但说实话,对于大多数嵌入式项目,示波器量一下就够了。

3.5 中断系统核心逻辑图

下面这张图展示了中断从触发到响应的完整流程,以及优先级、嵌套、延迟之间的关系。我建议你把它存下来,配优先级的时候对照着看。

中断系统核心逻辑图 中断系统核心逻辑:从触发到响应 外设中断源 中断控制器(NVIC) 优先级判断 中断向量表 CPU执行ISR 嵌套/抢占判断 中断延迟分析 硬件响应延迟:3-12个时钟周期 中断屏蔽时间:取决于临界区长度 高优先级阻塞:取决于高优先级ISR长度 指令完成延迟:1-2个时钟周期 关键原则:ISR要短、关中断时间要短、优先级要清晰 中断嵌套是硬件特性,不是软件功能——你只需要配好优先级

这张图把中断的整个链路串起来了。你配置优先级的时候,心里要清楚:这个中断的延迟,取决于它前面所有环节的耗时。尤其是那个“中断屏蔽时间”——很多人忽略了这个,结果系统实时性怎么调都上不去。

我的习惯: 每次写完中断相关的代码,我都会用示波器量一下关键中断的延迟。如果超过预期,就逐段排查:是不是临界区太长了?是不是有更高优先级的中断在捣乱?是不是ISR里干了不该干的活?嗯,量一量,心里就有底了。