【STM32】 NVIC 与 EXTI 中断 · 超详细源码级解析


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 的写操作都长这样:

cpp 复制代码
SCB->AIRCR = 旧值清场 | (新值) | SCB_AIRCR_VECTKEY_Msk;

SCB_AIRCR_VECTKEY_Msk = (0x05FAUL << 16),也就是 0x05FA0000。


1.3 SetPriorityGrouping 源码逐段再拆

cpp 复制代码
void 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):

cpp 复制代码
HAL_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 索引公式推导(系统异常那支)

cpp 复制代码
SCB->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 四个核心寄存器 + 位映射公式

cpp 复制代码
NVIC->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 源码:

cpp 复制代码
void 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 源码:

cpp 复制代码
void 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 分支源码

cpp 复制代码
void 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 层(前文已拆)

cpp 复制代码
HAL_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(拼错=链接到弱默认=不触发)

六、陷阱清单(加量版)

  1. 时钟没开:GPIO/AFIO 时钟没使能,配置全部静默失效,中断不触发。
  2. 改了分组忘了重配:Group4→Group2 后,所有旧优先级被"重新拆位",行为全变;且 HAL 的 SetPriority 不按分组重组,非 Group4 下子优先级形同虚设(需手动合成 4 位值传给 Preempt)。
  3. 优先级数值方向搞反:0 最大,15 最小,不是越大越紧急。
  4. 只开 EXTI 没开 NVIC:IMR 放行≠中断会响应,还差 EnableIRQ。
  5. 共线 IRQHandler 不判 Pin:EXTI9_5 / EXTI15_10 共用函数,回调里必须 if (GPIO_Pin == GPIO_PIN_x)。
  6. 中断函数名拼错:EXTI0_IRQHandler 写错字母 → 链接到 startup 的弱默认(死循环),中断"像没使能一样"。
  7. 写优先级后立即使能:慢速写可能未落地,靠读回/__DSB 保证,HAL 已内置,别自己删。
  8. ISR 里调 HAL_Delay / printf:SysTick 15 最低抢不进 + 串口忙等 → 死锁(呼应上一章)。
  9. 改 SYSRESETREQ 忘钥匙:任何 AIRCR 写操作不带 VECTKEY 都会被忽略。
  10. F1 与 F4 寄存器体系混用:CRL/CRH vs MODER、AFIO vs SYSCFG,移植时对照系列差异表改。

相关推荐
iCxhust2 小时前
51单片机电力载波应用开发(4)单片机发PC端接收
单片机·嵌入式硬件·51单片机
国科安芯5 小时前
断电也隔离、听得真:ASL3159S 单通道 SPDT 模拟开关的“断电保护 + 音频保真“账
单片机·嵌入式硬件·安全·商业航天·抗辐射
自小吃多5 小时前
【Verilog/FPGA】存储器(数组)
嵌入式硬件·fpga开发
szxinmai主板定制专家5 小时前
RK3588+FPGA异构架构|高速数据采集+边缘AI落地应用全解析
人工智能·嵌入式硬件·fpga开发·架构·zynq
jianqiang.xue5 小时前
【CStackGUI 实战】深色主题终端:app.dark 默认深色 + 顶层命名样式表 + 运行时切换
单片机·嵌入式·cstackgui·c语言gui·可视化拖拽
M78佐菲5 小时前
ARM学习笔记(11)
linux·arm开发·笔记·嵌入式硬件·学习
M78佐菲5 小时前
ARM学习笔记(10)
linux·arm开发·笔记·嵌入式硬件·学习
小程序设计5 小时前
基于STM32的智慧厨房多参数安全监测与预警系统设计
stm32·嵌入式硬件·安全
新晨单片机设计6 小时前
S008C-基于STM32单片机无线抢答器【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·无线抢答器
Zw-awa6 小时前
Delay_us 与 Delay_ms:用 SysTick 做一把可靠的时间尺
stm32