《STM32 NVIC 优先级分组配错的代价:抢占优先级与子优先级混淆,高优先级中断为什么反而等不到响应》
做 STM32 项目时,最让人困惑的一类故障是:明明把某个中断的优先级设得很高,关键时刻它却迟迟不响应,反而是低优先级的中断先跑了。查代码,优先级数字没写错;查手册,中断确实使能了。问题往往不在某一个优先级数值上,而在优先级分组(Priority Grouping)上——分组一旦配错,所有优先级数字的含义都会变。
一、一个典型故障场景
一块 STM32F4 板子上有两个中断:串口接收中断负责收不定长数据帧,要求尽快响应;定时器中断每 10ms 做一次状态机刷新,耗时稍长。开发者把串口中断设为优先级 1,定时器设为优先级 2,以为串口一定能打断定时器。实测却是:定时器中断处理函数执行期间,串口数据照样丢。改成串口 0、定时器 3 还是不行。最后发现,工程里在不同地方调用了两次 HAL_NVIC_SetPriorityGrouping,一次设成分组 2,一次设成分组 4,优先级数字在两种分组下的解释完全不同,所谓的“1 比 2 高”根本不成立。
二、NVIC 优先级到底由什么决定
Cortex-M 的 NVIC 用一个 8 位优先级寄存器表示每个中断的优先级,STM32 实际只用高 4 位,取值范围是 0~15,数值越小优先级越高。这 4 位又被优先级分组切成两段:抢占优先级(Preemption Priority)和子优先级(Subpriority,也叫响应优先级)。分组决定了 4 位里有几位分给抢占优先级、几位分给子优先级,共有 5 种分组:
分组 0:抢占 0 位,子优先级 4 位——所有中断互相不能抢占,只能按子优先级排队;
分组 1:抢占 1 位,子优先级 3 位;
分组 2:抢占 2 位,子优先级 2 位;
分组 3:抢占 3 位,子优先级 1 位;
分组 4:抢占 4 位,子优先级 0 位——只有抢占优先级,没有子优先级。
关键规则只有两条。第一,只有抢占优先级高的中断才能打断正在执行的中断,子优先级再高也不能抢占,只能决定同时挂起时谁先执行。第二,抢占优先级相同、子优先级不同的两个中断,先挂起的先执行,后到的必须等前一个执行完。把这两条记牢,很多“优先级失灵”就有了头绪。
三、三个最常见的配错
第一,分组在中断配置之后才设置。HAL_NVIC_SetPriority 会把传入的抢占优先级和子优先级按当前分组编码进优先级寄存器。如果先调 HAL_NVIC_SetPriority 配置了所有中断,后面又改了分组,寄存器里已经编码好的值不会自动重算,优先级就全乱了。正确顺序是:系统初始化一开始就只调一次分组,之后再配置各个中断。
第二,把子优先级当成抢占优先级用。分组 0 下所有中断的抢占优先级都是 0,设成 0 和 15 没有任何抢占差别,只能排队。有人在分组 0 下把串口设为 0、定时器设为 15,以为串口能抢占,结果定时器处理函数一旦开始执行,串口只能等它跑完,数据自然会丢。需要抢占,就必须选分组 1~4,给抢占优先级留出位数。
第三,忽略系统中断的优先级。SysTick、PendSV、SVC 这些系统异常也有优先级,而且它们的默认值可能高于外设中断。做延时或 RTOS 切换时,如果 SysTick 优先级配得比串口还高,串口中断会被 SysTick 频繁打断,时序抖动就变大。工程里应当把系统异常和外设中断放在同一张优先级表里统一规划,而不是各配各的。
四、一段可直接用的配置代码
下面的顺序是我在项目里固定下来的写法:分组只设一次,放在 HAL 初始化之后、外设初始化之前;每个中断的抢占优先级和子优先级都写注释说明用途。
/* 只调用一次:4 位全部作为抢占优先级,不用子优先级。
抢占关系最直观,适合“关键中断必须打断一切”的场景。 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* 串口接收:抢占优先级 1,关键数据,允许打断定时器。 */
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* 定时器:抢占优先级 3,周期任务,可被串口打断。 */
HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
/* SysTick 保持默认的最低抢占优先级,避免打断外设中断。 */
HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);
如果项目确实需要“同级中断按顺序排队”,可以选分组 2:抢占优先级 2 位、子优先级 2 位,同一抢占等级内的中断用子优先级定先后。但一旦选了分组 2,就不要再用 0~15 的全范围去理解抢占优先级——抢占优先级只剩 0~3,写 5 和写 15 在编码时会被截断,结果出乎意料。
五、怎么验证优先级真的生效了
只看代码不够,优先级问题必须用实验验证。我的做法分三步。第一步,在两个中断处理函数入口各置一个 GPIO 翻转,用逻辑分析仪或示波器看波形:让低优先级中断先进入处理函数并故意延时,在延时期间触发高优先级中断,看高优先级对应的 GPIO 是否在低优先级处理函数返回前翻转。翻转了,说明抢占生效;没翻转,说明分组或优先级配错了。
第二步,读寄存器核对。用调试器看 SCB->AIRCR 的 PRIGROUP 字段,确认分组确实是期望值;再看 NVIC->IPR 里对应中断的优先级字节,确认编码后的值与预期一致。只看 HAL 调用记录不看寄存器,等于只验了一半。
第三步,做边界测试。把高优先级中断的触发源调到最频繁的状态,连续跑半小时,看低优先级任务是否饿死、数据是否丢失。优先级配错的系统在轻载时往往表现正常,重载时才暴露,这一步不能省。
六、检查清单
新建工程或接手别人的工程时,我会按这张清单过一遍:分组是否只设置了一次、是否在所有中断配置之前;每个中断的抢占优先级和子优先级是否有注释、是否和分组位数匹配;系统异常的优先级是否统一规划;有没有用 GPIO 或寄存器实测过抢占关系;高优先级中断处理函数里有没有耗时操作——抢占优先级再高,处理函数里做延时、打印、Flash 擦写,照样会拖垮整个系统,优先级救不了结构问题。
优先级分组是个很小的配置,配错的代价却很大:它不报错、不死机,只是让中断响应时序悄悄偏离预期,等到数据丢失、控制抖动时再回头查,往往已经绕了很远。把分组写死在一处、写进注释、用实测验证一次,比事后排查要便宜得多。
- 点赞
- 收藏
- 关注作者
评论(0)