目录
一:STM32 中断优先级分组
1. 硬件基础
2. 优先级分组(中断优先级分组的核心)
3. 抢占优先级 vs 子优先级
4. 完整配置流程
5. 一个直观例子
6.FreeRTOS 下的特殊约束
1. 分组建议:全部设为抢占优先级
2. API 调用门槛:
3. 优先级数值方向的"颠倒"
二:系统异常优先级设置寄存器(SHPR1/2/3)
1:简介
2:为什么需要它们?
3:寄存器与系统异常的对应关系
4:FreeRTOS 中的典型配置
5:和外设中断的关系
6:一句话总结
三:FreeRTOS 配置 PendSV 和 SysTick 优先级
1:配置做了什么
2:写入 SHPR3 配置PendSV和SysTick 的步骤
1:第一步
2:第二步
3:第三步
4:总结
四:三个中断屏蔽寄存器
1:区别
2:cotex内核ARM系列优先级分布
3:FreeRTOS 为什么选 BASEPRI
4:FreeRTOS 临界区实际操作
5:BASEPRI = 0x50的移位过程
一:STM32 中断优先级分组
1. 硬件基础
ARM Cortex-M 优先级寄存器: 8 位 STM32 实际使用: 高 4 位 [7:4] → 共 16 级优先级 Bit7 Bit6 Bit5 Bit4 | Bit3 Bit2 Bit1 Bit0 有效 有效 有效 有效 | 保留 保留 保留 保留
2. 优先级分组(中断优先级分组的核心)
4 个有效位通过分组配置切分为抢占优先级和子优先级。STM32 通过NVIC_PriorityGroupConfig()设置,共 5 种分组:
| 分组 | 抢占优先级位数 | 子优先级位数 | 抢占级数 | 子优先级级数 |
|---|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 bit | 4 bit | 1 级 | 16 级 |
| NVIC_PriorityGroup_1 | 1 bit | 3 bit | 2 级 | 8 级 |
| NVIC_PriorityGroup_2 | 2 bit | 2 bit | 4 级 | 4 级 |
| NVIC_PriorityGroup_3 | 3 bit | 1 bit | 8 级 | 2 级 |
| NVIC_PriorityGroup_4 | 4 bit | 0 bit | 16 级 | 0 级(无子优先级) |
关键约束:分组只需设置一次,必须在 main 开头、任何中断使能之前执行。
3. 抢占优先级 vs 子优先级
抢占优先级(Preemption Priority) └─ 高抢占优先级的中断 → 可打断低抢占优先级的中断正在执行 └─ 数值越小 → 优先级越高 子优先级(Sub Priority) └─ 两中断抢占优先级相同、同时到来时 → 子优先级数值小的先执行 └─ 子优先级不同不能相互打断(只决定排队顺序)
打断规则:
抢占优先级不同 → 高抢占的可以打断低抢占的
抢占优先级相同 →不能相互打断,同时到来时看子优先级排队
4. 完整配置流程
// ① 设置优先级分组(全局一次) NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占, 2位子优先级 // ② 配置具体中断 NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; // 中断源 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 子优先级 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);
5. 一个直观例子
假设分组设为NVIC_PriorityGroup_2(抢占 2 bit,子优先级 2 bit):
| 中断 A | 中断 B | 行为 |
|---|---|---|
| 抢占=0, 子=0 | 抢占=1, 子=0 | A 可打断 B(A 抢占更高) |
| 抢占=1, 子=0 | 抢占=1, 子=3 | 互不打断,同时到来时 A 先执行 |
| 抢占=1, 子=2 | 抢占=1, 子=2 | 在向量表中的位置靠前的先执行 |
最后一层兜底:抢占和子优先级都相同时,硬件按中断向量表中的顺序决定(IRQ 编号小的优先)。
6.FreeRTOS 下的特殊约束
1. 分组建议:全部设为抢占优先级
HAL_INit 默认配置 HAL_StatusTypeDef HAL_Init(void) { /* Configure Flash prefetch, Instruction cache, Data cache */ #if (INSTRUCTION_CACHE_ENABLE != 0U) __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); #endif /* INSTRUCTION_CACHE_ENABLE */ #if (DATA_CACHE_ENABLE != 0U) __HAL_FLASH_DATA_CACHE_ENABLE(); #endif /* DATA_CACHE_ENABLE */ #if (PREFETCH_ENABLE != 0U) __HAL_FLASH_PREFETCH_BUFFER_ENABLE(); #endif /* PREFETCH_ENABLE */ /* Set Interrupt Group Priority */ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); ⭐ /* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */ HAL_InitTick(TICK_INT_PRIORITY); /* Init the low level hardware */ HAL_MspInit(); /* Return function status */ return HAL_OK; }原因:去掉子优先级后,只有抢占优先级一维,FreeRTOS 管理起来更简单——中断能否互相打断只看一个数值。
2. API 调用门槛:
configMAX_SYSCALL_INTERRUPT_PRIORITY
中断优先级 0 ~ configMAX_SYSCALL_INTERRUPT_PRIORITY-1: └─ 不能调用 FreeRTOS API(FromISR 函数) └─ 这些是"不受 RTOS 管理"的极高优先级中断 中断优先级 configMAX_SYSCALL_INTERRUPT_PRIORITY ~ 15: └─ 可以调用 FreeRTOS API(如 xQueueSendFromISR 等) └─ 这些中断受 RTOS 管理,不会破坏内核数据结构
典型配置:configMAX_SYSCALL_INTERRUPT_PRIORITY = 5,则优先级 5~15 的中断可以安全调用 FreeRTOS API。
3. 优先级数值方向的"颠倒"
这是最容易搞混的地方,你表格总结得很到位:
中断优先级 任务优先级 (数值小→优先) (数值大→优先) ──────────── ──────────── 高 ← 0 31 1 30 ... ... 14 1 低 ← 15 0
记忆口诀:中断看"排名"(第0名最优先),任务看"分数"(分越高越优先)。
二:系统异常优先级设置寄存器(SHPR1/2/3)
1:简介
这三个寄存器专门用来配置Cortex-M 内核自带系统异常的优先级,跟之前讲的外设中断(如 USART、TIM)走的是两套不同的寄存器。
2:为什么需要它们?
之前我们配置的都是外设中断(USART1_IRQn 等等),那些通过NVIC_Init()配置。但 Cortex-M 内核自己还有 15 个系统异常(Memory Fault、Bus Fault、SysTick 等),它们的优先级不归 NVIC 寄存器管,而是由这三个 SHPR 寄存器负责。
3:寄存器与系统异常的对应关系
SHPR1 (0xE000ED18) SHPR2 (0xE000ED1C) SHPR3 (0xE000ED20) ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ PRI_4 MemManage │ │ 保留 │ │ PRI_12 调试监视器 │ │ PRI_5 BusFault │ │ 保留 │ │ 保留 │ │ PRI_6 UsageFault │ │ 保留 │ │ PRI_14 PendSV │ │ 保留 │ │ PRI_11 SVC │ │ PRI_15 SysTick │ └──────────────────┘ └──────────────────┘ └──────────────────┘
| 寄存器 | 配置谁 | 常见用途 |
|---|---|---|
| SHPR1 | MemManage、BusFault、UsageFault | 这三个是硬件异常,一般不改,保持最高优先级(0) |
| SHPR2 | SVC | FreeRTOS 用SVC 指令触发任务切换,优先级通常设得较低 |
| SHPR3 | PendSV + SysTick | FreeRTOS 核心:PendSV 做上下文切换(设最低),SysTick 做时钟节拍 |
4:FreeRTOS 中的典型配置
// FreeRTOS 在 port.c 中设置这三个寄存器: // SysTick 和 PendSV 都设为最低优先级(15) // 确保任务切换"等所有中断处理完"再执行 NVIC_SetPriority(SysTick_IRQn, 0x0F); // 最低 NVIC_SetPriority(PendSV_IRQn, 0x0F); // 最低 // SVC 留一点余地 NVIC_SetPriority(SVCall_IRQn, 0x0E); // 次低,仅在启动调度时用
为什么 PendSV 和 SysTick 优先级设为最低?如果它们在某个外设中断服务中途就切走了任务,会破坏 ISR 的完整性。设为最低后,它们会等所有 ISR 都跑完再执行任务切换。
5:和外设中断的关系
优先级数值: 0 ←──── 最高 ────→ 15 ←── 最低 ──→ 系统异常 ─────┬─────────────────────────────┬────── Memory Fault, Bus Fault 等 PendSV, SysTick (SHPR1/2/3 配置) (SHPR3 配置) 外设中断 ─────┬─────────────────────────────┬────── USART, TIM, DMA 等 (NVIC 寄存器配置)
两者共用同一套优先级数值空间(0~15),只是配置用的寄存器不同。NVIC_PriorityGroupConfig()的分组设置,可以设置外设也可以设置内核的PendSV 和SysTick ,都生效。
6:一句话总结
SHPR1/2/3 = 系统异常的"NVIC"。普通项目不需要深入,只有写 RTOS 移植层(port.c)才会直接操作它们。理解了"PendSV 和 SysTick 设为最低优先级是为了不打断 ISR",这个概念就够了。
三:FreeRTOS 配置 PendSV 和 SysTick 优先级
核心逻辑非常简洁,就一句话:把 PendSV 和 SysTick 的优先级写到 SHPR3 寄存器里。
1:配置做了什么
SHPR3 寄存器 (0xE000ED20),32 位: [31:24] SysTick 优先级 ← 写入 15 [23:16] PendSV 优先级 ← 写入 15 [15:8] 保留 [7:0] 调试监视器 优先级
2:写入 SHPR3 配置PendSV和SysTick 的步骤
1:第一步
ARM Cortex-M 的优先级寄存器是 8 位,但 STM32 只实现了高 4 位 [7:4]:因此第一步就是先移位设置好优先级
Bit: 7 6 5 4 | 3 2 1 0 有效 有效 有效 有效 | 保留 保留 保留 保留(读出为 0)
C 语言里写15是0x0F(二进制0000 1111),但硬件读的是[7:4],所以必须把值挪到高 4 位才生效。
定义一句宏:#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 中断最低优先级
然后再定义另外一个宏:其中configPRIO_BITS = 4
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
这样实现了 0000 1111 到1111 0000 的移位。
得到了设置中断优先级的宏configKERNEL_INTERRUPT_PRIORITY=15 ,也即0xF0 (1111 0000 )
2:第二步
configKERNEL_INTERRUPT_PRIORITY只是把数据位准备好了(0xF0),还没真正写进硬件寄存器。
真正写入是这两行:
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; // 或进 SHPR3 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; // 或进 SHPR3
因此我们要先得到 portNVIC_PENDSV_PRI和portNVIC_SYSTICK_PRI
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL ) #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
移位结果
configKERNEL_INTERRUPT_PRIORITY = 0xF0 = 0000 0000 1111 0000 (16位视角) 强转 uint32_t 后: 0000 0000 0000 0000 0000 0000 1111 0000 << 16 → portNVIC_PENDSV_PRI: 0000 0000 1111 0000 0000 0000 0000 0000 = 0x00F0_0000 ✓ << 24 → portNVIC_SYSTICK_PRI: 1111 0000 0000 0000 0000 0000 0000 0000 = 0xF000_0000 ✓
3:第三步
`portNVIC_SHPR3_REG是什么
#define portNVIC_SHPR3_REG ( *( ( volatile uint32_t * ) 0xe000ed20 ) )
拆开看:
0xe000ed20 ← 一个数字,即 SHPR3 寄存器的地址 (volatile uint32_t *) 0xe000ed20 ← 把数字强转成"指向 uint32_t 的指针" (告诉编译器:这个地址存着一个 32 位寄存器) *( ... ) ← 解引用,读写这个地址的内容 (直接操作硬件寄存器)
所以它不是一个普通变量,而是一个指向硬编码地址0xE000ED20的指针解引用。读它就等于读 SHPR3 寄存器,写它就等于写 SHPR3 寄存器。
最后进行或运算
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; // 或进 SHPR3 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; // 或进 SHPR3
4:总结
三行代码的分工总结
| 代码 | 做的事情 | 阶段 |
|---|---|---|
configKERNEL_INTERRUPT_PRIORITY | 算出0xF0(把 15 对齐到 [7:4]) | 准备数据 |
portNVIC_PENDSV_PRI/portNVIC_SYSTICK_PRI | 把0xF0移到寄存器对应 bit 段 | 定位位置 |
portNVIC_SHPR3_REG |= ... | 写入硬件寄存器0xE000ED20 | 生效 |
三步走:算好数据 → 移对位置 → 写进硬件。
四:三个中断屏蔽寄存器
1:区别
优先级设定”寄存器(SHPRx):
作用:决定谁更重要。它负责给各种中断源(比如定时器、串口、系统异常等)排座次,规定哪个先执行,哪个后执行。
通俗理解:这就是在“选秀”,决定谁能上台表演。
“中断屏蔽”寄存器(PRIMASK、FAULTMASK、BASEPRI):
作用:决定谁能上台。它负责管理“大门”,控制哪些中断被允许进入 CPU 核心去请求处理。
通俗理解:这就是在“安检”,符合条件(优先级高于阈值或未被屏蔽)的中断才能放行进去找 CPU。
2:cotex内核ARM系列优先级分布
ARM 优先级体系的完整分布: -3 Reset ← 最高的,不可屏蔽 -2 NMI ← 最高可屏蔽?不,NMI 字面意思就是"不可屏蔽中断" -1 HardFault ← 固定负数,比所有可配优先级都高 ──────────────────────── BASEPRI 的管辖上限(只管 0~15) 0 (最高可配优先级) 1 ... 15 (最低可配优先级)
PRIMASK:只屏蔽优先级 0~15 的可配中断,优先级为负数的(NMI、HardFault)它管不着。
FAULTMASK:更狠,把优先级提升到了仅低于 NMI 的层级,连 HardFault 也给你按住了。
BASEPRI:只在 0~15 范围内按阈值屏蔽,负数优先级的异常根本不在它的管辖范围内。
PRIMASK=1 FAULTMASK=1 BASEPRI=阈值 ───────── ─────────── ─────────── NMI (优先级固定 -2) ✗ 关不掉 ✗ 关不掉 ✗ 关不掉(永远最高) HardFault (优先级固定 -1) ✗ 关不掉 ✓ 关掉了 ✗ 管不了(优先级是负的) MemManage/BusFault/ ✓ 关掉了 ✓ 关掉了 ✗ 管不了(这些是异常,不是中断) UsageFault (优先级可配) SysTick/PendSV ✓ 关掉了 ✓ 关掉了 ✓ 按阈值关 (优先级 0~15) USART/TIM/DMA 等外设中断 ✓ 关掉了 ✓ 关掉了 ✓ 按阈值关 (优先级 0~15)
3:FreeRTOS 为什么选 BASEPRI
BASEPRI = 0x50 (即优先级阈值设为 5) 优先级: 0 ─── 不受屏蔽,可打断一切 ───→ 这些是"极紧急"中断(不能调 RTOS API) 1 2 3 4 ──────────────────────── threshold = 5 5 ─── 被 BASEPRI 屏蔽 ────→ FreeRTOS 的临界区期间,这些中断老实等着 ... 15
好处:FreeRTOS 进入临界区时,只需要把 BASEPRI 设为configMAX_SYSCALL_INTERRUPT_PRIORITY(比如 5),就可以只屏蔽"能用 RTOS API"的中断,而不影响那些极高优先级的紧急中断。出临界区时把 BASEPRI 恢复为 0,不屏蔽0~15之间的任1中断响应。
FreeRTOS 的临界区不应该耽误优先级 0~4 的紧急中断(比如电机控制、生命维持),所以用 BASEPRI 而不是 PRIMASK。
4:FreeRTOS 临界区实际操作
进入临界区: taskENTER_CRITICAL() → 读当前 BASEPRI → 写 BASEPRI = 0x50 ← 屏蔽优先级 ≥ 5 的中断 → 此时能调 RTOS API 的中断全部暂停,内核数据结构安全 操作共享数据(比如链表插入)... 退出临界区: taskEXIT_CRITICAL() → 恢复 BASEPRI = 原来的值(通常是 0) → 被屏蔽的中断恢复
整条知识链到这儿就完整了——从硬件寄存器位分布,到优先级分组,到 SHPR 配置两个内核异常,再到 BASEPRI 做运行时屏蔽。
5:BASEPRI = 0x50的移位过程
BASEPRI 的情况和 SHPR3不一样——SHPR3 是一个 32 位寄存器里挤了 4 个不同的优先级,所以需要把数据挪到对应的 byte lane。BASEPRI 没那么复杂。
BASEPRI 地址0xE000ED04,它只存一个东西:阈值。
所以 0x50 直接写在 **bit[7:0]**,不需要再往高处挪:BASEPRI (0xE000ED04), 32 位寄存器: [31:8] 保留(读出为 0) [7:0] 阈值,STM32 只用了高 4 位 [7:4] 所以 0x50 直接写在 **bit[7:0]**,不需要再往高处挪: BASEPRI = 0x00000050 [31:24] [23:16] [15:8] [7:0] ┌──────┬──────┬──────┬──────────┐ │ 00000000│ 00000000│ 00000000│ 01010000│ └──────┴──────┴──────┴──────────┘ ↑ [7:4] = 0101 = 5 有效阈值 = 5 和 SHPR3 的对比:为什么一个要移位、一个不用 SHPR3 (0xE000ED20) — 一个寄存器塞了 4 个优先级: [31:24] [23:16] [15:8] [7:0] SysTick PendSV 保留 调试监视器 ↑ ↑ 需要 <<24 需要 <<16 ← 因为每个字段在不同的 byte 位置 BASEPRI (0xE000ED04) — 一个寄存器只放一个阈值: [31:8] 全部保留,[7:0] 就它自己 不需要在寄存器内部再移位,0x50 直接进 [7:0] ✓
configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 │ << (8 - 4) 即 << 4 ▼ 0x50 ← 数据已对齐 [7:4],可以直接写进 BASEPRI │ 直接赋值给 BASEPRI 寄存器 (0xE000ED04) ▼ BASEPRI = 0x00000050 [7:4]=0101 → 阈值=5 优先级 5~15 的中断被屏蔽 ✓