中断管理基础: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(),放在系统初始化最前面。中途改分组?那所有已配置的中断优先级含义全变了,调试起来会疯掉。
中断嵌套与抢占:谁打断谁?
中断嵌套的规则其实就三条:
- 抢占优先级高的可以打断抢占优先级低的——这是嵌套的核心
- 抢占优先级相同,子优先级高的先执行——但不能嵌套,得等当前中断处理完
- 抢占优先级和子优先级都相同——那就看中断向量表中的硬件编号,编号小的先执行
我在项目中遇到过一个问题:两个外设的中断抢占优先级设成了相同的值,但子优先级没仔细配。结果一个高频中断总是被另一个低频中断堵住,导致数据丢包。后来把高频中断的抢占优先级提高一级,问题就解决了。
来看一个具体的配置例子:
// 设置优先级分组: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配置的完整流程
配置一个中断,其实就四步:
- 设置优先级分组——全局只调一次
- 使能外设的中断请求——在外设初始化里配
- 配置NVIC——设置优先级、使能中断通道
- 编写中断服务函数——函数名必须用启动文件里的名字
第三步最容易漏。我见过有人配好了外设,也开了中断使能,但忘了调NVIC_Init()。结果中断死活不触发,查了半天才发现NVIC根本没使能那个通道。
下面这张图帮你理清整个中断管理的知识结构:
几个容易踩的坑
最后分享几个我踩过的坑,你遇到了能少走弯路:
- 中断服务函数名写错——启动文件里叫什么,你就得叫什么。比如USART1的中断函数是
USART1_IRQHandler,少写一个字母都不行。我见过有人写成USART1_Handler,编译不报错,但中断永远进不去。 - 忘了清中断标志——很多外设的中断标志不会自动清除。你不手动清,中断服务函数退出后马上又进来,CPU就卡死在那了。
- 在中断里调printf——printf通常很慢,而且可能用了同一个串口。中断里调printf,轻则丢数据,重则死锁。
- 优先级分组改来改去——同一个工程里,分组只能设一次。中途改分组,之前配的优先级数值含义全变了,调试起来非常痛苦。
中断管理这块,说白了就是定好规则、配好参数、写好服务函数。规则定清楚了,后面出问题的概率就小很多。嗯,这部分内容就到这,你先把NVIC的配置流程跑通,后面讲中断与状态机的配合时,你会用得更顺手。