4. 中断优先级配置策略:抢占优先级与子优先级的权衡、优先级分组模型
中断优先级配置,说白了就是给每个中断排个队,决定谁先处理谁后处理。但这里面的门道,比表面看起来要深得多。我见过不少项目,中断响应乱套了,查到最后都是优先级配置出了问题。
咱们先搞清楚两个核心概念:抢占优先级和子优先级。这两个东西,决定了中断嵌套的规则。
4.1 抢占优先级 vs 子优先级:到底谁听谁的?
抢占优先级,决定了一个中断能不能打断另一个正在执行的中断。数值越小,优先级越高。高抢占优先级的中断,可以打断低抢占优先级的中断服务程序。
子优先级,也叫响应优先级,它只在抢占优先级相同的时候才起作用。当两个中断同时到达,抢占优先级一样,那就看子优先级,数值小的先执行。
举个例子你就明白了:
| 中断源 | 抢占优先级 | 子优先级 |
|---|---|---|
| 定时器A | 0 | 1 |
| UART接收 | 1 | 0 |
| 外部中断 | 1 | 1 |
定时器A的抢占优先级是0,比UART和外部中断都高。所以定时器A可以打断它们俩。UART和外部中断的抢占优先级都是1,它俩之间不能互相打断。但如果它俩同时来了,UART的子优先级是0,外部中断是1,那就先处理UART。
关键点:抢占优先级决定嵌套能力,子优先级只决定同优先级下的排队顺序。别搞混了。
4.2 优先级分组模型:你手里的分配权
ARM Cortex-M内核提供了优先级分组机制。说白了,就是让你决定用多少位来表示抢占优先级,多少位来表示子优先级。这个分组,通过一个叫 PRIGROUP 的寄存器来配置。
以STM32为例,它支持4位优先级配置,分组方式如下:
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 抢占优先级等级数 | 子优先级等级数 |
|---|---|---|---|---|
| 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 |
你想想看,这个分组模型给了你很大的灵活性。如果你的系统里中断类型很多,需要多层嵌套,那就选Group_4,16个抢占等级够你分的。如果中断少,但每个中断的优先级需要精细区分,那就选Group_0,16个子优先级慢慢排。
我的习惯:一般项目我选Group_2,4个抢占等级+4个子优先级。这个组合比较均衡,既保证了嵌套层次,又留了排队空间。除非有特殊需求,否则我不轻易动这个分组。
4.3 配置代码示例
下面这段代码,展示了如何在STM32上配置优先级分组和中断优先级:
// 设置优先级分组为Group_2:2位抢占优先级,2位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置定时器中断:抢占优先级0,子优先级0
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置UART中断:抢占优先级1,子优先级0
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
// 配置外部中断:抢占优先级1,子优先级1
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
注意,优先级分组必须在系统初始化时一次性设置好,中途不能改。这是硬性规定,改了系统行为就不可预测了。
4.4 优先级配置策略:实战中的权衡
配置优先级,不是简单地把重要中断设成高优先级就完事了。这里有几个原则,是我这些年踩坑踩出来的:
- 实时性要求高的中断,抢占优先级要高。比如电机控制、PWM生成,这些中断响应慢了系统就出问题。
- 数据量大的中断,抢占优先级要低。比如DMA传输完成中断,让它慢慢排队,别影响其他关键中断。
- 共享资源的中断,抢占优先级要一致。如果两个中断都操作同一个缓冲区,最好让它们抢占优先级相同,避免嵌套访问导致数据错乱。
- 中断服务程序要短。优先级再高,ISR里跑了几百行代码,其他中断也得等着。我一般要求ISR里只做标志位设置和数据搬运,具体处理扔到任务里。
我曾经犯过的错:有一次做数据采集系统,我把ADC中断的抢占优先级设成了最高,结果ADC中断里做了浮点运算,执行时间太长,导致低优先级的通信中断一直得不到响应,系统频繁超时。后来把ADC中断的ISR精简到只读数据、设标志,问题就解决了。
4.5 优先级分组模型选择指南
不同场景,适合不同的分组模型。我整理了一个对照表,方便你快速决策:
| 应用场景 | 推荐分组 | 理由 |
|---|---|---|
| 强实时控制(电机、飞控) | Group_4 | 需要多层嵌套,抢占等级越多越好 |
| 数据采集与通信混合 | Group_2 | 平衡嵌套深度和排队粒度 |
| 简单系统(中断少于5个) | Group_0 | 不需要嵌套,子优先级排队就够了 |
| 高安全要求(汽车、医疗) | Group_3 | 8个抢占等级,配合子优先级做冗余 |
嗯,这里要注意一点:分组模型一旦选定,整个系统的中断优先级都要按这个模型来配置。不能这个中断用Group_2的规则,那个中断用Group_3的规则,那不乱套了嘛。
4.6 优先级配置的避坑指南
最后,分享几个我实际项目中遇到的坑,你写代码的时候多留个心眼:
- 不要依赖默认优先级。很多芯片复位后优先级分组是Group_0,所有中断抢占优先级都一样。你不显式配置,就等于没优先级。
- 小心中断优先级反转。低优先级中断持有锁,高优先级中断等锁,结果谁也跑不了。解决办法是让共享资源的中断优先级一致,或者用互斥信号量。
- 优先级数值和优先级高低是反的。0是最高优先级,15是最低。我刚开始做的时候老搞反,后来养成了习惯,配置完都拿笔算一遍。
- 中断嵌套层数不要太多。理论上可以无限嵌套,但实际中超过3层就容易出问题。栈空间、寄存器保存都是开销。
总结一下:优先级配置的核心是分组模型的选择和每个中断的精细分配。分组模型决定了你的分配空间,中断优先级决定了系统的实时行为。这两步走对了,中断系统就稳了一半。