中断管理基础:NVIC配置、中断优先级分组、中断嵌套与抢占

中断管理,说白了就是CPU的“紧急事务处理机制”。你正在跑主循环,突然来了个紧急事件——按键按下、定时器溢出、串口收到数据——CPU得停下手里的事,先去处理这个紧急请求。谁先处理、谁后处理、能不能打断正在处理的中断,这些规则就是中断管理的核心。

我刚开始做STM32项目时,对中断优先级这块理解得不够深。有一次产品在现场频繁死机,查了三天才发现是中断嵌套导致栈溢出。嗯,从那以后我再也不敢轻视NVIC的配置了。

NVIC是个什么角色?

NVIC(Nested Vectored Interrupt Controller),嵌套向量中断控制器。它是Cortex-M内核里专门管中断的硬件模块。你不需要自己写中断调度算法,NVIC全帮你干了。

它的核心能力有三点:

  • 中断使能与禁止——哪些中断能用,哪些不能用,由你说了算
  • 优先级管理——谁更重要,谁先执行
  • 中断嵌套——高优先级中断可以打断低优先级中断

STM32的NVIC最多支持240个中断(具体看型号),每个中断都可以独立配置优先级。你想想看,这么多中断如果全靠软件轮询,那效率得多低?

中断优先级分组:你得先定好规则

这是新手最容易懵的地方。STM32的优先级分两段:抢占优先级和子优先级(也叫响应优先级)。

抢占优先级决定能不能嵌套。A中断的抢占优先级比B高,那A就能打断B。子优先级呢?当两个中断的抢占优先级相同时,子优先级高的先执行,但不能嵌套。

那怎么分配这两段的位数?靠一个叫NVIC_PriorityGroupConfig()的函数。

优先级分组表

分组 抢占优先级位数 子优先级位数 抢占优先级取值范围 子优先级取值范围
NVIC_PriorityGroup_0 0 4 — 0~15
NVIC_PriorityGroup_1 1 3 0~1 0~7
NVIC_PriorityGroup_2 2 2 0~3 0~3
NVIC_PriorityGroup_3 3 1 0~7 0~1
NVIC_PriorityGroup_4 4 0 0~15 —

我个人习惯用Group_2。为什么?2位给抢占,2位给子优先级,各4级,够用且均衡。你想想,如果选Group_4,所有中断都能互相嵌套,栈压力会很大。如果选Group_0,那就没有嵌套了,高优先级中断也得排队等。

我的建议:整个工程只调用一次NVIC_PriorityGroupConfig(),放在系统初始化最前面。中途改分组?那所有已配置的中断优先级含义全变了,调试起来会疯掉。

中断嵌套与抢占:谁打断谁?

中断嵌套的规则其实就三条:

  1. 抢占优先级高的可以打断抢占优先级低的——这是嵌套的核心
  2. 抢占优先级相同,子优先级高的先执行——但不能嵌套,得等当前中断处理完
  3. 抢占优先级和子优先级都相同——那就看中断向量表中的硬件编号,编号小的先执行

我在项目中遇到过一个问题:两个外设的中断抢占优先级设成了相同的值,但子优先级没仔细配。结果一个高频中断总是被另一个低频中断堵住,导致数据丢包。后来把高频中断的抢占优先级提高一级,问题就解决了。

来看一个具体的配置例子:

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

// 配置USART1中断:抢占优先级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);

// 配置TIM2中断:抢占优先级0,子优先级3
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

这个例子里,TIM2的抢占优先级是0,USART1是1。数值越小优先级越高。所以TIM2可以打断USART1。如果两个中断同时来,TIM2先执行。

注意:优先级数值越小,优先级越高。这个和直觉相反,我刚学的时候也搞反过。0是最高优先级,15是最低。

中断嵌套的栈风险

中断嵌套不是越多越好。每嵌套一层,CPU就要压栈保存当前上下文。如果嵌套层数太多,栈空间不够,程序就崩了。

我曾经在一个项目里,把5个中断的抢占优先级都设成了不同值,结果最极端情况下嵌套了4层。栈从默认的512字节涨到了近2KB,还好当时RAM够用。如果用的是STM32F103C8T6这种只有20KB RAM的芯片,栈溢出就是分分钟的事。

我的建议:

  • 中断服务函数里尽量少做事,把耗时操作放到主循环或任务里
  • 嵌套深度控制在3层以内
  • 如果必须多层嵌套,手动算一下栈使用量,或者把栈设大一点

NVIC配置的完整流程

配置一个中断,其实就四步:

  1. 设置优先级分组——全局只调一次
  2. 使能外设的中断请求——在外设初始化里配
  3. 配置NVIC——设置优先级、使能中断通道
  4. 编写中断服务函数——函数名必须用启动文件里的名字

第三步最容易漏。我见过有人配好了外设,也开了中断使能,但忘了调NVIC_Init()。结果中断死活不触发,查了半天才发现NVIC根本没使能那个通道。

下面这张图帮你理清整个中断管理的知识结构:

中断管理知识结构 NVIC 嵌套向量中断控制器 中断使能 / 禁止 优先级管理 中断嵌套 优先级分组配置 抢占 vs 子优先级 嵌套规则 NVIC_PriorityGroupConfig NVIC_Init 配置通道 编写中断服务函数 注意:优先级数值越小优先级越高 / 嵌套深度控制 / 栈空间评估 配置顺序:分组 → 外设中断使能 → NVIC通道配置 → 中断服务函数 分组全局只调一次,中途修改会导致所有优先级含义错乱

几个容易踩的坑

最后分享几个我踩过的坑,你遇到了能少走弯路:

  • 中断服务函数名写错——启动文件里叫什么,你就得叫什么。比如USART1的中断函数是USART1_IRQHandler,少写一个字母都不行。我见过有人写成USART1_Handler,编译不报错,但中断永远进不去。
  • 忘了清中断标志——很多外设的中断标志不会自动清除。你不手动清,中断服务函数退出后马上又进来,CPU就卡死在那了。
  • 在中断里调printf——printf通常很慢,而且可能用了同一个串口。中断里调printf,轻则丢数据,重则死锁。
  • 优先级分组改来改去——同一个工程里,分组只能设一次。中途改分组,之前配的优先级数值含义全变了,调试起来非常痛苦。

中断管理这块,说白了就是定好规则、配好参数、写好服务函数。规则定清楚了,后面出问题的概率就小很多。嗯,这部分内容就到这,你先把NVIC的配置流程跑通,后面讲中断与状态机的配合时,你会用得更顺手。