NVIC 与 EXTI 中断 · 超详细源码级解析
一、AIRCR:优先级分组的"写保护"寄存器
1.1 AIRCR 全位图(SCB->AIRCR,0xE000ED0C)
位 名称 说明 31:16 VECTKEYSTAT/VECTKEY 写钥匙:写入 0x05FA 才允许本次写操作生效; 读时返回 0xFA05 15 ENDIANNESS 端序,只读(F1 为 0 = 小端) 10:8 PRIGROUP 优先级分组,3 位,决定抢占/子优先级怎么拆 2 SYSRESETREQ 写 1 请求系统复位(NVIC_SystemReset 用) 1 VECTCLRACTIVE 清异常活动状态(调试用) 0 VECTRESET 复位内核(调试用)
1.2 为什么必须带 VECTKEY 钥匙
AIRCR 是系统级关键寄存器,硬件规定:bit31:16 必须写入 0x05FA,否则本次写入整体被忽略。所以 HAL 里所有对 AIRCR 的写操作都长这样:
cppSCB->AIRCR = 旧值清场 | (新值) | SCB_AIRCR_VECTKEY_Msk;
SCB_AIRCR_VECTKEY_Msk = (0x05FAUL << 16),也就是0x05FA0000。
1.3 SetPriorityGrouping 源码逐段再拆
cppvoid HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup) { assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup)); /* ① 只收合法宏 */ SCB->AIRCR = ((SCB->AIRCR & ~(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk)) /* ② 清钥匙域+分组域,保其他位 */ | ((PriorityGroup & 0x7UL) << SCB_AIRCR_PRIGROUP_Pos) /* ③ 分组宏低3位 → 放到位[10:8] */ | SCB_AIRCR_VECTKEY_Msk); /* ④ 补钥匙 */ }
- ② 是"读-改-写":先读回 AIRCR,把要动的位清零,其他位(SYSRESETREQ 等)原样保留;
- ③
PriorityGroup & 0x7:分组宏的低 3 位就是 PRIGROUP 值(见下表),左移 8 位到 10:8;- ④ 最后补钥匙,三个操作一次写完。
1.4 PRIGROUP 编码与 F1 截断(分组宏值=PRIGROUP 值)
宏 宏值 PRIGROUP(3位) F1 抢占位 F1 子优先级位 抢占值范围 NVIC_PRIORITYGROUP_0 7 111 0 4 无(全子优先级) NVIC_PRIORITYGROUP_1 6 110 1 3 0~1 NVIC_PRIORITYGROUP_2 5 101 2 2 0~3 NVIC_PRIORITYGROUP_3 4 100 3 1 0~7 NVIC_PRIORITYGROUP_4 3 011 4 0 0~15 关键理解:
PRIGROUP 不改变寄存器里存的 4 位值,只改变 NVIC 仲裁时"把这 4 位拆成抢占部分+子部分"的方式。改分组 = 让所有已配置中断的优先级含义全部重解释。
1.5 两个必须知道的点
HAL_Init()内部已默认调HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4),普通工程不需要再写;- 分组改了,
HAL_NVIC_SetPriority传参语义跟着变(见下节,这是坑)。
二、优先级字节:IP/SHP 的位布局(核心难点)
2.1 一个优先级字节长什么样
2.2 为什么是
<< (8 - __NVIC_PRIO_BITS)
(PreemptPriority << (8U - __NVIC_PRIO_BITS)) | (SubPriority & (uint8_t)(0xFFUL >> __NVIC_PRIO_BITS))F1 的
__NVIC_PRIO_BITS = 4(在
stm32f1xx.h里由芯片头文件定义,CMSIS 据此生成所有寄存器访问):
8 - 4 = 4:把 4 位的抢占值左移 4 位放进高 4 位(bit7:4);0xFF >> 4 = 0x0F:子优先级掩码,只允许低 4 位;- 合成一个字节写入 IP/SHP。
举例(默认 Group4):
cppHAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0); /* Preempt=5 → 5<<4 = 0x50,Sub=0 → IP[6] = 0x50 */ /* 硬件读高4位 = 5 = 抢占优先级 ✓ */
2.3 一个重要事实(面试加分点):HAL 假设你用 Group4
HAL_NVIC_SetPriority的拼接是固定左移 4 位,它不会根据当前分组自动重组。这意味着:
- 默认 Group4 下:Preempt 0~15 直接对应高 4 位,Sub 必须传 0,完全正确;
- 若改成 Group2 想表达"抢占 1、子 2":4 位值应为
0b0110,但 HAL 写的是(1<<4)|2 = 0x12,硬件取高 4 位 = 1 → 被拆成"抢占 0、子 1",子优先级实际不生效。所以实战建议:
保持 HAL 默认的 Group4,只用抢占优先级(Sub 一律传 0);
确实需要子优先级,就手动按分组合成一个 4 位值传给 Preempt 参数。
2.4 SHP 索引公式推导(系统异常那支)
cppSCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL) - 4UL] = ...;负数 IRQn 的补码 & 0xF 正好等于"异常号低 4 位":
IRQn 补码 & 0xF 减 4 SHP 索引 对应异常号 -1 (SysTick) 15 11 SHP11 15 -2 (PendSV) 14 10 SHP10 14 -5 (SVCall) 11 7 SHP7 11 -10 (UsageFault) 6 2 SHP2 6 -11 (BusFault) 5 1 SHP1 5 -12 (MemManage) 4 0 SHP0 4 规律:
SHP 索引 = 异常号 - 4(SHP0 对异常 4 开始),公式用补码 & 0xF 巧妙规避了负数运算。
2.5 为什么写完要读回/
__DSB()NVIC 是"慢速外设",写操作可能被写缓冲延迟。
HAL 在写完优先级后读回一次(或部分版本用
__DSB()数据同步屏障),强制写完成,保证紧跟其后的EnableIRQ生效时优先级已就位。否则可能出现"中断来了但优先级还是旧的"竞态。
三、NVIC 全套控制寄存器与周边 API
3.1 四个核心寄存器 + 位映射公式
cppNVIC->ISER[IRQn >> 5] = 1 << (IRQn & 0x1F); /* 使能 Set-Enable */ NVIC->ICER[IRQn >> 5] = 1 << (IRQn & 0x1F); /* 禁能 Clear-Enable */ NVIC->ISPR[IRQn >> 5] = 1 << (IRQn & 0x1F); /* 置挂起 Set-Pending */ NVIC->ICPR[IRQn >> 5] = 1 << (IRQn & 0x1F); /* 清挂起 Clear-Pending */
IRQn >> 5:每组 32 个中断,ISER0 管 031、ISER1 管 3263;1 << (IRQn & 0x1F):组内位号。- EXTI0_IRQn = 6 →
ISER[0] = (1 << 6),bit6 置 1。
3.2 EXTI 相关中断编号速查(F1/F4 通用)
IRQn 编号 挂哪个 ISER EXTI0_IRQn 6 ISER0 bit6 EXTI1_IRQn 7 ISER0 bit7 EXTI2_IRQn 8 ISER0 bit8 EXTI3_IRQn 9 ISER0 bit9 EXTI4_IRQn 10 ISER0 bit10 EXTI9_5_IRQn 23 ISER0 bit23 EXTI15_10_IRQn 40 ISER1 bit8 注意:
EXTI59 共用 EXTI9_5_IRQHandler,EXTI1015 共用 EXTI15_10_IRQHandler------回调里必须按
GPIO_Pin区分是哪条线。
3.3 周边 API 源码要点速览
API 底层动作 说明 HAL_NVIC_EnableIRQ ISER = 1<<n HAL_NVIC_DisableIRQ ICER = 1<<n HAL_NVIC_SetPendingIRQ ISPR = 1<<n HAL_NVIC_ClearPendingIRQ ICPR = 1<<n HAL_NVIC_GetPendingIRQ 读 ISPR 返回是否挂起 HAL_NVIC_GetPriority 读 IP/SHP 再右移 (8-PRIO_BITS) 读回优先级 HAL_NVIC_GetPriorityGrouping 读 AIRCR PRIGROUP 读当前分组 HAL_NVIC_SystemReset AIRCR = VECTKEY|SYSRESETREQ, 再死等复位 软件复位
HAL_NVIC_SystemReset源码:
cppvoid HAL_NVIC_SystemReset(void) { __DSB(); /* 确保之前写完成 */ SCB->AIRCR = (uint32_t)(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_SYSRESETREQ_Msk); __DSB(); /* 确保复位请求已发出 */ while (1); /* 等硬件复位 */ }
四、HAL_GPIO_Init:从时钟到 EXTI 的完整链路
4.1 第一步:开 GPIO 时钟
__HAL_RCC_GPIOA_CLK_ENABLE();F1 展开后本质是:
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN); /* APB2ENR 的 bit2 = GPIOA 时钟门 */忘了开时钟,读写 GPIO 寄存器全部无效(总线上的寄存器读回 0)------这是"中断完全不触发"的头号原因,优先级比 EXTI/NVIC 配置更靠前。
4.2 GPIO 寄存器:F1 vs F4 差异
功能 F1(CRL/CRH 体系) F4(MODER 体系) 方向/模式 CRL(07)/CRH(815):CNF+MODE MODER:2 位/脚 速度 同上 MODE 位 OSPEEDR 上下拉 同样编码进 CNF PUPDR(独立) 输出类型 CNF(推挽/开漏) OTYPER 置位/清零 BSRR/BRR BSRR 复用 AFIO(+重映射) AFRL/AFRH HAL 的
GPIO_InitTypeDef是跨系列统一的抽象,Pull、Speed、Mode这些字段在 F1 上最终都被编码进 CRL/CRH 的 CNF/MODE 位。
4.3 第二步:EXTI 源选择(这步容易漏)
EXTI0 只能接 PA0/PB0/PC0/PD0/PE0 中的一根,选哪根由"源选择寄存器"决定:
- F1:AFIO->EXTICR4,CubeMX 生成代码长这样:
cpp__HAL_RCC_AFIO_CLK_ENABLE(); /* AFIO 也要时钟! */ HAL_GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
HAL_GPIO_EXTILineConfig源码:
cppvoid HAL_GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource) { uint32_t tmp = 0x00; tmp = AFIO->EXTICR[GPIO_PinSource >> 2]; /* 每 4 根线一组 */ tmp &= ~(0x0FUL << (4 * (GPIO_PinSource & 0x03))); /* 清该线旧源 */ tmp |= ((uint32_t)GPIO_PortSource << (4 * (GPIO_PinSource & 0x03))); /* 写新源 */ AFIO->EXTICR[GPIO_PinSource >> 2] = tmp; }
- F4:没有 AFIO,改用 SYSCFG,且
HAL_GPIO_Init内部自动完成(__HAL_RCC_SYSCFG_CLK_ENABLE()+ 写 SYSCFG->EXTICR),不需要手动调。只在 PA 上用 EXTI 时,EXTICR 默认值 0 = PA,可以省这一步;
但 CubeMX 总是生成,保留无妨。
4.4 第三步:HAL_GPIO_Init 的 EXTI 分支源码
cppvoid HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { /* ...... ① GPIO 方向/速度/上下拉/复用配置(CRL/CRH 或 MODER 等)...... */ if ((GPIO_Init->Mode & EXTI_Mode) != RESET) /* Mode 里带 EXTI 才进 */ { /* ② 上升沿:模式含 RISING → RTSR 对应位置 1 */ if ((GPIO_Init->Mode & GPIO_MODE_IT_RISING) == GPIO_MODE_IT_RISING) SET_BIT(EXTI->RTSR, position); else CLEAR_BIT(EXTI->RTSR, position); /* ③ 下降沿:模式含 FALLING → FTSR 对应位置 1 */ if ((GPIO_Init->Mode & GPIO_MODE_IT_FALLING) == GPIO_MODE_IT_FALLING) SET_BIT(EXTI->FTSR, position); else CLEAR_BIT(EXTI->FTSR, position); /* ④ 中断模式 → 开 IMR 放行;事件模式 → 开 EMR(不打扰 CPU) */ if ((GPIO_Init->Mode & GPIO_MODE_IT) == GPIO_MODE_IT) SET_BIT(EXTI->IMR, position); else SET_BIT(EXTI->EMR, position); } }到这里外设层就绪:
边沿选好、IMR 放行。
·但 NVIC 没开,事件来了只会置 PR 挂起位,不会进 ISR。
4.5 第四步:NVIC 层(前文已拆)
cppHAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0); /* IP[6] = 0x50:抢占 5 */ HAL_NVIC_EnableIRQ(EXTI0_IRQn); /* ISER[0] bit6 = 1 */
五、多中断优先级仲裁实战推演
5.1 场景配置(三个中断抢 CPU)
cpp/* 默认 Group4:4 位全抢占,数值小=紧急 */ HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); /* 抢占 1:按键 */ HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); /* 抢占 2:串口 */ HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0); /* 抢占 15:HAL_Delay 计时(最低) */ HAL_NVIC_EnableIRQ(EXTI0_IRQn); HAL_NVIC_EnableIRQ(USART1_IRQn);
5.2 仲裁推演表
场景 行为 结果 主循环中按键中断 抢占 1 > 主循环(无优先级) 立即打断主循环进 ISR 串口 ISR 中按键来 1 < 2,抢占 1 更紧急 打断串口 ISR,先按键后回来 按键 ISR 中串口来 2 > 1,抢占 2 不敌 1 挂起排队,按键处理完再进串口 按键+串口同时来 NVIC 仲裁,1 先响应 串口挂起,随后自动进入 SysTick vs 一切外设 15 最低 永远被外设打断(所以 ISR 里 Delay 卡死) 这就是嵌入式实时性的根基:
抢占优先级决定"谁打断谁",同抢占下才轮到子优先级和编号排序。
5.3 三层缺一不可检查清单
bash☐ GPIO 时钟开了吗? __HAL_RCC_GPIOx_CLK_ENABLE() ☐ AFIO 时钟开了吗?(F1) __HAL_RCC_AFIO_CLK_ENABLE() ☐ 边沿选对了吗? RTSR/FTSR(HAL_GPIO_Init 的 Mode) ☐ EXTI 源选对了吗? AFIO->EXTICR / SYSCFG->EXTICR ☐ IMR 放行了吗? HAL_GPIO_Init 已开 ☐ 优先级写了吗? HAL_NVIC_SetPriority ☐ NVIC 使能了吗? HAL_NVIC_EnableIRQ ☐ 中断函数名对吗? EXTI0_IRQHandler(拼错=链接到弱默认=不触发)
六、陷阱清单(加量版)
- 时钟没开:GPIO/AFIO 时钟没使能,配置全部静默失效,中断不触发。
- 改了分组忘了重配:Group4→Group2 后,所有旧优先级被"重新拆位",行为全变;且 HAL 的 SetPriority 不按分组重组,非 Group4 下子优先级形同虚设(需手动合成 4 位值传给 Preempt)。
- 优先级数值方向搞反:0 最大,15 最小,不是越大越紧急。
- 只开 EXTI 没开 NVIC:IMR 放行≠中断会响应,还差 EnableIRQ。
- 共线 IRQHandler 不判 Pin:EXTI9_5 / EXTI15_10 共用函数,回调里必须
if (GPIO_Pin == GPIO_PIN_x)。- 中断函数名拼错:
EXTI0_IRQHandler写错字母 → 链接到 startup 的弱默认(死循环),中断"像没使能一样"。- 写优先级后立即使能:慢速写可能未落地,靠读回/__DSB 保证,HAL 已内置,别自己删。
- ISR 里调 HAL_Delay / printf:SysTick 15 最低抢不进 + 串口忙等 → 死锁(呼应上一章)。
- 改 SYSRESETREQ 忘钥匙:任何 AIRCR 写操作不带 VECTKEY 都会被忽略。
- F1 与 F4 寄存器体系混用:CRL/CRH vs MODER、AFIO vs SYSCFG,移植时对照系列差异表改。



