2、中断基础概念:ARM Cortex-M4/M7中断向量表、NVIC配置、中断优先级与抢占

各位同学,咱们今天聊聊中断。说实话,搞嵌入式开发,中断是绕不开的坎。你想想看,一个飞控系统要同时处理传感器数据、控制电机、接收遥控信号,没有中断机制,CPU得忙死。我刚开始做PX4驱动时,就吃过中断配置不对的亏——电机转着转着突然卡死,查了两天才发现是优先级设错了。

2.1 中断向量表:CPU的“紧急通讯录”

ARM Cortex-M4/M7内核启动时,第一件事就是找中断向量表。这玩意儿说白了就是一张地址列表,告诉CPU每种中断来了该跳到哪里去执行。

向量表通常放在Flash的起始地址(0x00000000),但也可以重定位到SRAM。我个人习惯把向量表放在SRAM里,这样可以在运行时动态修改——虽然实际项目中很少这么干,但调试时特别方便。

向量表结构(以STM32F4为例):

  • 偏移0x00:栈顶指针(MSP初始值)
  • 偏移0x04:复位向量(Reset_Handler)
  • 偏移0x08:NMI异常
  • 偏移0x0C:硬错误(HardFault)
  • ... 其他系统异常 ...
  • 偏移0x40之后:外部中断(IRQ0~IRQn)
// 典型的向量表定义(汇编启动文件)
__Vectors       DCD     __initial_sp          ; 栈顶指针
                DCD     Reset_Handler         ; 复位
                DCD     NMI_Handler           ; NMI
                DCD     HardFault_Handler     ; 硬错误
                ; ... 省略中间 ...
                DCD     TIM2_IRQHandler       ; TIM2中断
                DCD     TIM3_IRQHandler       ; TIM3中断
                ; ... 更多中断 ...

嗯,这里要注意:向量表里每个条目占4字节,顺序绝对不能乱。我曾经见过有人把TIM2和TIM3的中断处理函数写反了,结果定时器中断触发时跑到了错误的函数里——飞控直接炸机。

2.2 NVIC配置:中断的“总调度中心”

NVIC(嵌套向量中断控制器)是ARM Cortex-M系列的核心外设。它负责管理所有中断的使能、挂起、优先级和抢占逻辑。

说白了,NVIC就是个交通警察。它决定哪个中断能插队,哪个得排队等着。

2.2.1 中断使能与屏蔽

每个中断都有独立的使能位。在NVIC中,通过ISER(中断设置使能寄存器)和ICER(中断清除使能寄存器)来控制。

// 使能TIM2中断
NVIC_EnableIRQ(TIM2_IRQn);

// 禁用TIM2中断
NVIC_DisableIRQ(TIM2_IRQn);

// 设置挂起位(软件触发中断)
NVIC_SetPendingIRQ(TIM2_IRQn);

// 清除挂起位
NVIC_ClearPendingIRQ(TIM2_IRQn);

我建议你在初始化外设时,先配置好所有中断参数,最后再使能。顺序搞反了容易产生意外中断——比如你还没设置好中断处理函数,中断就来了,直接跑飞。

2.2.2 中断优先级分组

Cortex-M4/M7支持最多256级优先级(实际芯片会裁剪,比如STM32只用了4位,共16级)。优先级分为抢占优先级和子优先级,通过PRIGROUP寄存器来分配位数。

分组 抢占优先级位数 子优先级位数 抢占优先级等级数 子优先级等级数
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

我的经验:在PX4飞控中,我通常使用NVIC_PriorityGroup_4(16级抢占优先级,无子优先级)。这样每个中断的优先级都是独立的,逻辑清晰。如果你用子优先级,嵌套逻辑会变得复杂,容易出bug。

2.3 中断优先级与抢占:谁说了算?

中断优先级决定了中断能否打断另一个中断。规则很简单:

  • 抢占优先级高的中断可以打断抢占优先级低的中断
  • 抢占优先级相同的中断,不能互相打断(按子优先级排队)
  • 子优先级只在同抢占优先级的中断同时挂起时,决定谁先执行

为什么会这样?因为ARM Cortex-M内核的硬件设计就是这样——它只允许更高抢占优先级的中断进行嵌套。你想想看,如果两个同优先级的中断互相嵌套,那不就成死循环了?

避坑指南:我曾经在PX4的SPI驱动中,把DMA中断和定时器中断设成了相同抢占优先级。结果DMA传输完成时,定时器正好触发,两个中断同时挂起。由于优先级相同,NVIC按硬件编号顺序处理——先处理编号小的中断。这导致DMA数据读取延迟了几个微秒,传感器数据偶尔跳变。查了三天才找到原因。

2.3.1 中断优先级设置示例

// 设置中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

// 设置TIM2中断优先级为2(抢占优先级2,无子优先级)
NVIC_SetPriority(TIM2_IRQn, 2);

// 设置DMA1中断优先级为1(抢占优先级1,高于TIM2)
NVIC_SetPriority(DMA1_Stream0_IRQn, 1);

// 设置UART中断优先级为3(抢占优先级3,低于TIM2)
NVIC_SetPriority(USART1_IRQn, 3);

数值越小,优先级越高。这是ARM的规定,别记反了。我刚开始做时总搞混,后来在代码里加了个注释才记住。

2.4 中断处理流程:从触发到返回

一个完整的中断处理流程包括:

  1. 中断触发:外设产生中断信号,NVIC检测到挂起位被置1
  2. 优先级判断:NVIC比较当前中断与正在执行的中断的优先级
  3. 压栈:如果允许抢占,CPU自动将xPSR、PC、LR、R0-R3、R12压入当前栈
  4. 取向量:从向量表中读取中断处理函数地址
  5. 执行ISR:跳转到中断服务函数执行
  6. 出栈:执行完ISR后,CPU自动恢复寄存器,返回被中断的代码

关键点:压栈和出栈是硬件自动完成的,不需要软件干预。但ISR中如果使用了R4-R11这些寄存器,需要手动保存和恢复——编译器通常会自动处理,但如果你写汇编ISR,就得自己操心。

2.5 中断延迟:你永远躲不开的问题

中断延迟是指从中断触发到ISR开始执行的时间。影响因素包括:

  • 当前是否在执行更高优先级的中断
  • CPU是否在执行不可中断的指令(如多周期乘法)
  • 内存访问等待周期
  • 中断向量表是否在SRAM(Flash有等待周期)

在PX4中,我要求所有中断延迟不超过1微秒。对于高优先级中断(如电机控制PWM),我会把向量表放在SRAM中,并禁用所有可屏蔽的中断嵌套——说白了就是让这个中断独占CPU,处理完再说。

小技巧:如果你发现中断响应慢,可以检查一下是否在ISR中调用了printf或malloc这类耗时函数。我见过有人把日志打印放在中断里,结果中断延迟从0.5us飙升到50us——飞控直接失控。

2.6 本章知识体系图

ARM Cortex-M4/M7 中断体系 中断向量表 NVIC配置 优先级与抢占 栈顶指针 + 异常向量 + IRQ向量 可重定位到SRAM ISER/ICER 使能控制 优先级分组 (PRIGROUP) 挂起/清除挂起 抢占优先级(高优先级打断低) 子优先级(同抢占时排队) 数值越小优先级越高 核心原则 中断延迟 < 1us | 高优先级不被打断 | ISR中避免耗时操作

这张图把中断体系的三个核心模块串起来了。你想想看,向量表是入口,NVIC是调度器,优先级是规则——三者缺一不可。我在PX4中调试中断时,总是先检查向量表地址对不对,再看NVIC配置,最后确认优先级。按这个顺序排查,90%的问题都能解决。

最后提醒:中断服务函数要短小精悍。我见过有人把整个传感器数据处理放在中断里,结果中断执行时间超过100us,导致其他中断丢失。记住:中断里只做最紧急的事,比如读取数据寄存器、设置标志位。耗时的处理放到任务或主循环中。


公众号:蓝海资料掘金营,微信deep3321