欢迎光临
我们一直在努力

【STM32】 中断与异常深度详解:从概念到 NVIC 寄存器


  STM32 中断与异常深度详解:从概念到 NVIC 寄存器   

1. 什么是中断

1.1 生活类比

你正在厨房炒菜,门铃响了——放下锅铲去开门,处理完回来继续炒菜。

这就是中断:

  • 主程序 = 炒菜
  • 中断事件 = 门铃响
  • 中断服务程序(ISR) = 去开门
  • 中断返回 = 回来继续炒菜

定义:

        CPU 正在执行主程序,突然收到紧急信号,暂停当前工作,转而去处理这个紧急事件,处理完后回到原来暂停的地方继续执行。


1.2 完整执行流程

① 中断请求(IRQ) → 外设/事件发出中断信号
② 中断响应 → CPU 判断优先级,决定是否响应
③ 保存现场 → 硬件自动压栈:xPSR、PC、LR、R0~R3、R12
④ 跳转到 ISR → 查向量表,找到中断服务函数入口
⑤ 执行 ISR → 用户自己写的中断处理代码
⑥ 恢复现场 → 硬件自动出栈
⑦ 中断返回 → 通过 EXC_RETURN 回到主程序断点


1.3 中断 vs 轮询

对比项轮询中断
CPU 占用 一直查,浪费 事件来了才处理
实时性 取决于轮询周期 几乎实时响应
功耗 一直跑,高 可休眠等待唤醒
代码复杂度 简单但臃肿 需要理解中断机制

2. STM32 中,中断的意义

2.1 轮询的问题

// 轮询:CPU 死等在 while 里
while(1) {
if (按键按下) { 处理按键; }
// 这里干不了别的事
}


2.2 中断的优势

// 中断:主程序干自己的事
int main(void) {
while(1) {
__WFI(); // 休眠,等中断唤醒
}
}

void EXTI0_IRQHandler(void) { // 硬件自动调用
if (EXTI_GetITStatus(EXTI_Line0)) {
处理按键;
EXTI_ClearITPendingBit(EXTI_Line0);
}
}

三大价值:

  • 实时响应:几个时钟周期内响应外设事件
  • CPU 效率:主循环做别的事,甚至进入低功耗模式
  • 事件驱动架构:RTOS、事件框架的基础

  • 3. 谁会产生中断

    3.1 内核异常(Exception)

    Cortex-M 内核自身产生,编号 1~15:

    编号名称优先级说明
    1 Reset -3(最高) 复位
    2 NMI -2 不可屏蔽中断
    3 HardFault -1 硬件错误(除零、非法访问)
    4 MemManage 可编程 MPU 违规
    5 BusFault 可编程 总线错误
    6 UsageFault 可编程 未对齐、未定义指令
    11 SVCall 可编程 系统调用(RTOS 用)
    14 PendSV 可编程 可挂起系统调用(RTOS 上下文切换)
    15 SysTick 可编程 系统滴答定时器

    3.2 外部中断(IRQ)

    外设产生,编号从 16 开始(IRQn = 中断号 – 16):

    • GPIO:EXTI0~EXTI15
    • 定时器:TIM1~TIM14
    • 串口:USART1~UART5
    • ADC、DMA、I2C、SPI、RTC、USB、CAN 等

    区分:异常是内核级(1~15),IRQ 是外设级(16 起),都由 NVIC 统一管理。


    4. 中断向量表

    4.1 什么是向量表

            向量表 = 函数指针数组,每项对应一个中断的服务函数入口地址。

            中断发生时,CPU 根据中断号查表跳转,不需要自己判断"该找谁"。

    偏移内容说明
    0x00 初始 MSP 值 栈顶指针(不是中断!)
    0x04 Reset_Handler 复位入口
    0x08 NMI_Handler NMI
    0x0C HardFault_Handler 硬件错误
    0x40 IRQ0_Handler 外部中断 0(WWDG)

    注意:

            第 0 项是初始栈指针,不是中断向量

            CPU 上电先从 0x00 取栈顶,从 0x04 取复位地址。


    4.2 C 源文件和汇编混合链接

    4.2.1 启动文件 startup_stm32f103xb.s 核心结构

    ; 1. 栈空间
    Stack_Size EQU 0x00000400
    AREA STACK, NOINIT, READWRITE, ALIGN=3
    Stack_Mem SPACE Stack_Size
    __initial_sp

    ; 2. 向量表(放在 Flash 最开头)
    AREA RESET, DATA, READONLY
    EXPORT __Vectors
    __Vectors DCD __initial_sp ; 0x00: 栈顶
    DCD Reset_Handler ; 0x04: 复位
    DCD NMI_Handler ; 0x08
    DCD HardFault_Handler ; 0x0C
    ; … 更多异常 …
    DCD WWDG_IRQHandler ; IRQ0
    DCD PVD_IRQHandler ; IRQ1
    ; … 更多外设中断 …

    ; 3. Reset_Handler:复位后第一个执行
    Reset_Handler PROC
    EXPORT Reset_Handler [WEAK]
    IMPORT __main
    IMPORT SystemInit
    LDR R0, =SystemInit
    BLX R0 ; 先配时钟
    LDR R0, =__main
    BX R0 ; 跳 C 库,最终进 main()
    ENDP

    ; 4. 默认中断函数:弱定义,死循环
    NMI_Handler PROC
    EXPORT NMI_Handler [WEAK]
    B . ; 死循环
    ENDP

    4.2.2 关键机制:[WEAK] 弱符号
    • 启动文件用 [WEAK] 定义了所有中断函数的默认实现(死循环)
    • 你在 C 文件里写同名函数(如 USART1_IRQHandler()),链接器优先用你的版本,覆盖弱定义
    • 不写就用默认死循环版本

    这就是为什么写个函数名完全匹配的 ISR 就能自动被调用——不需要注册。


    4.2.3 内存布局

    Flash (0x08000000) RAM (0x20000000)
    ┌──────────────┐ ┌──────────────┐
    │ 中断向量表 │ │ .data 段 │
    ├──────────────┤ ├──────────────┤
    │ .text 代码 │ │ .bss 段 │
    ├──────────────┤ ├──────────────┤
    │ .data 初值 │ │ 堆 Heap │
    └──────────────┘ ├──────────────┤
    │ 栈 Stack │
    │ ↓ 向下生长 │
    └──────────────┘


    4.3 STM32 工程源文件提供者

    文件类型举例提供者作用
    启动文件 startup_stm32f103xb.s ST 向量表、栈堆初始化
    系统文件 system_stm32f1xx.c ST SystemInit() 配时钟
    库文件 stm32f1xx_gpio.c 等 ST(标准库/HAL) 外设操作封装
    中断模板 stm32f1xx_it.c ST ISR 集中存放点
    用户文件 main.c 等 应用逻辑


    4.4 中断向量表在哪里

    4.4.1 默认位置

    上电后 CPU 从 0x00000000 取向量表,通过 BOOT 引脚映射到不同区域:

    BOOT0BOOT1启动区域向量表位置
    0 X 主 Flash 0x08000000
    1 0 系统存储器(ISP) 0x1FFFF000
    1 1 内置 SRAM 0x20000000

    4.4.2 向量表偏移寄存器 VTOR

    // 把向量表搬到 SRAM(Bootloader 常用)
    SCB->VTOR = 0x20000000;

    地址必须对齐(向量表大小向上取整到 2 的幂)。


    5. 中断号

    5.1 中断号到底是什么?

            一句话:中断号就是向量表这个"数组"的下标。

            你把中断向量表想象成一个超大的数组 vector_table[]:

    vector_table[0] = __initial_sp ← 初始栈顶(不是中断!)
    vector_table[1] = Reset_Handler ← 中断号 1
    vector_table[2] = NMI_Handler ← 中断号 2
    vector_table[3] = HardFault_Handler ← 中断号 3
    vector_table[4] = MemManage_Handler ← 中断号 4

    vector_table[16] = WWDG_IRQHandler ← 中断号 16(第一个外设中断)
    vector_table[17] = PVD_IRQHandler ← 中断号 17

    vector_table[53] = USART1_IRQHandler ← 中断号 53

            中断号 = 这个数组的下标

            就这么简单。CPU 不知道"串口1中断"叫什么名字,它只认编号。发生中断时,硬件给 CPU 一个编号,CPU 就拿这个编号去数组里查表,找到函数地址跳过去。


    5.2 为什么下标从 1 开始?0 去哪了?

    这是最容易困惑的点。

    Cortex-M 内核规定:

            向量表第 0 项(偏移 0x00)不是中断向量,而是初始栈指针(MSP)。

    为什么这么设计?

            因为 CPU 上电复位后,做的第一件事不是执行代码,而是要初始化栈

            栈指针从哪来?就从向量表第 0 项取。

    CPU 上电启动流程:
    1. 从地址 0x00 读 4 字节 → 当作初始栈顶 MSP
    2. 从地址 0x04 读 4 字节 → 当作复位 Reset_Handler 地址
    3. 跳过去执行 Reset_Handler → 进入 main()

            所以地址 0x00 这个位置被"占用"了,只能放栈顶,不能放中断向量。

            于是中断号只能从 1 开始编号。

            就像你家大楼,一楼是大堂(放门禁的),真正的住户从二楼开始。

            地址 0 是大堂,住户编号从 1 开始。


    5.3 为什么 1~15 是"内核异常",16 起才是"外设中断"?

            这是 Cortex-M 内核设计者定死的编号规则,不是 ST 定的。

            Cortex-M3/M4 内核自己需要一些"内部中断"来处理错误、系统调用这些事,这些编号被内核预留了:

            前 15 个编号是 Cortex-M 内核"自留地",所有 Cortex-M3/M4 芯片都一样。

            从 16 开始,才是 ST(或其他芯片厂商)自己加的外设中断。

            ST 加了 WWDG、PVD、EXTI、USART、TIM……每家厂商加的不一样,数量也不一样。

            就像手机系统:前几个按键(电源、音量)是所有手机都有的"系统键",后面的快捷键是各品牌自定义的。


    5.4 IRQn_Type 枚举:代码里看到的是什么?

    打开 STM32 的头文件 stm32f10x.h,你会看到这样一段:

    typedef enum IRQn
    {
    /***** Cortex-M3 内核异常(编号就是绝对值,负数) *****/
    NonMaskableInt_IRQn = -14, // NMI,中断号 = 16 + (-14) = 2
    MemoryManagement_IRQn = -12, // MemManage,中断号 = 16 + (-12) = 4
    BusFault_IRQn = -11, // BusFault,中断号 = 16 + (-11) = 5
    UsageFault_IRQn = -10, // UsageFault,中断号 = 16 + (-10) = 6
    SVCall_IRQn = -5, // SVCall,中断号 = 16 + (-5) = 11
    PendSV_IRQn = -2, // PendSV,中断号 = 16 + (-2) = 14
    SysTick_IRQn = -1, // SysTick,中断号 = 16 + (-1) = 15

    /***** STM32 外设中断(从 0 开始正数) *****/
    WWDG_IRQn = 0, // 中断号 = 16 + 0 = 16
    PVD_IRQn = 1, // 中断号 = 17
    TAMPER_IRQn = 2, // 中断号 = 18
    RTC_IRQn = 3, // 中断号 = 19
    EXTI0_IRQn = 16, // 中断号 = 16 + 16 = 32
    USART1_IRQn = 37, // 中断号 = 16 + 37 = 53
    // …
    } IRQn_Type;

    重点理解这里的"负数设计":

            外设中断从 0 开始数(WWDG=0, PVD=1…)

            内核异常用负数表示(NMI=-14, HardFault 没在这里列,它是 -13 对应的编号 3…)

    CPU 真正用来查表的"绝对中断号"怎么算?

    绝对中断号 = IRQn_Type 的值 + 16

    为什么要 +16?

            因为外设 IRQn 从 0 开始,对应的绝对中断号是 16。这个 +16 的偏移是 Cortex-M 内核定死的。

    IRQn_Type 值含义绝对中断号(= 值 + 16)
    -14 NMI 2
    -12 MemManage 4
    0 WWDG 16
    16 EXTI0 32
    37 USART1 53

    这样设计的好处:

            库函数里只需要传 IRQn(比如 NVIC_EnableIRQ(USART1_IRQn)),

            库函数内部自动 +16 算出绝对中断号去操作寄存器。你不用记绝对编号。


    5.5 中断号怎么用?完整链路走一遍

            假设 USART1 收到一个字节,触发了中断。整个过程是:

    第 1 步:硬件标记"挂起"

            USART1 外设收到数据,硬件把 NVIC 里 ISPR(中断挂起寄存器)的第 37 位置 1(因为 USART1 的 IRQn 是 37)。

    ISPR[0] 寄存器的 bit 37 = 1 → 表示 USART1 中断正在等待处理

    第 2 步:NVIC 判断优先级

    NVIC 看一下:

  • 这个中断使能了吗?(ISER 第 37 位是否为 1)
  • 优先级够高吗?(和当前正在执行的中断比)
  • 全局中断开了吗?(PRIMASK 是否为 0)
  • 都满足 → 决定响应。

    第 3 步:CPU 用中断号查表

    CPU 拿到绝对中断号 53,开始算向量表地址:

    向量表基地址(VTOR) = 0x08000000(默认在 Flash)

    USART1 的向量地址 = VTOR + 绝对中断号 × 4
    = 0x08000000 + 53 × 4
    = 0x08000000 + 212
    = 0x080000D4

            CPU 从 0x080000D4 这个地址读 4 字节,读到的就是 USART1_IRQHandler 这个函数的地址。

    第 4 步:跳过去执行

            CPU 把当前现场压栈,然后 PC 寄存器改成 USART1_IRQHandler 的地址,开始执行你的中断函数。


    5.6 用一个例子彻底搞懂

    为什么 USART1 的中断函数必须叫 USART1_IRQHandler,而不能叫 My_Uart_Handler?

            答案:因为启动文件的向量表里写死了:

    __Vectors DCD __initial_sp ; 偏移 0x00
    DCD Reset_Handler ; 偏移 0x04(中断号 1)
    DCD NMI_Handler ; 偏移 0x08(中断号 2)

    DCD USART1_IRQHandler ; 偏移 0xD4(中断号 53)

            当 USART1 中断发生,CPU 去 0xD4 读地址,读到的就是 USART1_IRQHandler。

            如果你把函数名写成 My_Uart_Handler,向量表里那个位置还是弱定义的默认死循环函数——CPU 跳过去就死循环了,根本不会调到你的函数。

            函数名必须和向量表里的名字一致,链接器才能把你的函数地址填到那个位置上。


    5.7 一张图总结

    绝对中断号 向量表偏移 内容
    ───────── ───────── ──────────────────
    0 0x00 初始栈顶 MSP(不是中断!)
    1 0x04 Reset_Handler
    2 0x08 NMI_Handler
    3 0x0C HardFault_Handler
    … … …
    15 0x3C SysTick_Handler
    16 0x40 WWDG_IRQHandler ← 外设开始
    17 0x44 PVD_IRQHandler
    … … …
    32 0x80 EXTI0_IRQHandler
    … … …
    53 0xD4 USART1_IRQHandler

    核心公式:

  • 向量表偏移 = 绝对中断号 × 4
  • 绝对中断号 = IRQn_Type 值 + 16
  • 向量地址 = VTOR + 绝对中断号 × 4
  • 这就是中断号的全部本质——它就是向量表这个数组的下标,CPU 靠它找到该执行哪个函数。


  • 6. NVIC

    6.1 谁来管理中断 — NVIC

    NVIC(Nested Vectored Interrupt Controller) 是 Cortex-M 内核的一部分:

    • 中断使能/失能
    • 优先级设置
    • 挂起状态管理
    • 嵌套判断
    • 响应与返回控制

    NVIC 是内核紧耦合的,响应极快——从中断请求到进入 ISR 仅需约 12 个时钟周期。


    6.2 中断嵌套

    6.2.1 什么是嵌套

    高优先级中断可以打断低优先级中断的执行。

    主程序: ──────█~~~~~~~~~~~~~~~~~~~~~~~~────
    ↓ 低优先级中断 A
    中断 A: █~~~~~~~~~~~~~~~~~█
    ↓ 高优先级中断 B 来了
    中断 B: █~~~~~█
    中断 A: █~~~~~█
    主程序: ──────█~~~~~~~~~~~~~~~~~~~~~~~~────

    6.2.2 嵌套条件
  • 新中断的抢占优先级严格高于当前中断
  • 当前中断没被屏蔽(PRIMASK/FAULTMASK 没关)
  • 对比 ARM7:

            ARM7 默认不能嵌套,要手动在 ISR 里开中断,很麻烦。Cortex-M 把嵌套硬件化了。


    6.3 中断优先级

    6.3.1 抢占优先级 + 子优先级

    4 个 bit 分成两组:

    • 抢占优先级:决定能不能嵌套——数值越小优先级越高
    • 子优先级:同时挂起时谁先执行——不能嵌套

    6.3.2 优先级分组
    分组抢占位数子优先级位数抢占数子优先级数
    Group_0 0 4 1 16
    Group_1 1 3 2 8
    Group_2 2 2 4 4
    Group_3 3 1 8 2
    Group_4 4 0 16 1

    推荐 Group_2:4 级抢占 + 4 级子优先级,兼顾嵌套深度和并发处理。


    6.3.3 判断规则
  • 先看抢占优先级:高的先执行,且能打断低的
  • 抢占相同看子优先级:高的先执行(但不嵌套)
  • 都相同看中断号:号小的先执行
  • 注意:优先级数值越小,优先级越高。0 是最高,15 是最低。


    6.3.4 代码示例

    // 分组:2 bit 抢占 + 2 bit 子优先级
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    // USART1:抢占 1,子 0
    NVIC_InitTypeDef n;
    n.NVIC_IRQChannel = USART1_IRQn;
    n.NVIC_IRQChannelPreemptionPriority = 1;
    n.NVIC_IRQChannelSubPriority = 0;
    n.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&n);

    // TIM2:抢占 0(比 USART1 高,能打断它)
    n.NVIC_IRQChannel = TIM2_IRQn;
    n.NVIC_IRQChannelPreemptionPriority = 0;
    n.NVIC_IRQChannelSubPriority = 0;
    NVIC_Init(&n);


    6.4 理解 NVIC

    6.4.1 工作流程

    外设中断请求 → 挂起(Pending)

    优先级判断

    响应?
    ├─ 是 → 硬件压栈 → 跳向量表 → 执行 ISR → 硬件出栈 → 返回
    └─ 否 → 保持挂起,等当前中断结束


    6.4.2 Tail-Chaining(尾链)

    中断 A 刚结束,中断 B 已挂起 → 直接从 ISR A 跳到 ISR B,不用先恢复再保存,省约 12 个周期。


    6.4.3 Late Arriving(迟到中断)

    压栈过程中来了更高优先级的中断 → 硬件先响应高优先级的,压栈只做一次。


    6.5 NVIC 内部寄存器

    NVIC 基地址 0xE000E100:

    寄存器偏移作用
    ISER0~7 0x000 中断使能(写 1 使能)
    ICER0~7 0x080 清除使能(写 1 失能)
    ISPR0~7 0x100 挂起置位
    ICPR0~7 0x180 挂起清除
    IABR0~7 0x200 活动位(看是否正在执行)
    IPR0~59 0x300 优先级(每个 8 bit,用高 4 bit)

    6.5.1 优先级寄存器位域

    IPR[x] 每个中断 1 字节:
    bit 7 6 5 4 3 2 1 0
    ┌──┬──┬──┬──┬──┬──┬──┬──┐
    │ 抢占优先级 │ 子优先级 │0│
    └──┴──┴──┴──┴──┴──┴──┴──┘
    ← 有效 → ←保留→

    低 4 bit 保留,直接写寄存器时优先级值要左移 4 位。


    6.5.2 内核辅助寄存器
    寄存器作用
    PRIMASK 写 1 屏蔽所有可屏蔽中断(只剩 NMI/HardFault)
    FAULTMASK 屏蔽所有异常(包括 HardFault)
    BASEPRI 设阈值,低于该优先级的全屏蔽

    __disable_irq(); // PRIMASK=1,关中断
    __enable_irq(); // PRIMASK=0,开中断
    __set_BASEPRI(0x60); // 屏蔽优先级数值 ≥ 6 的中断

            RTOS 临界区常用 BASEPRI 而不是 PRIMASK——保留高优先级中断(SysTick、NMI)的响应能力。


    总结

    知识点核心结论
    中断 硬件自动暂停主程序 → 跳 ISR → 返回
    意义 实时响应、CPU 高效、低功耗唤醒
    中断源 内核异常(1~15)+ 外设 IRQ(16 起)
    向量表 函数指针数组,在 Flash 开头
    启动文件 汇编写向量表,[WEAK] 让用户函数覆盖
    中断号 偏移 = 中断号 × 4
    NVIC 管理使能、优先级、嵌套
    优先级 抢占决定嵌套,子优先级决定并发顺序
    嵌套 高抢占打断低抢占,硬件自动完成
    优化 Tail-Chaining + Late Arriving

    面试题精选

    一、基础概念题

    1. 什么是中断?中断和轮询有什么区别?

            答:中断是 CPU 在正常执行程序时,收到硬件/软件的紧急信号,暂停当前程序,转而去处理紧急事件,处理完后回到断点继续执行的机制。

    对比项轮询中断
    CPU 占用 一直查询,空转浪费 事件驱动,按需处理
    实时性 取决于轮询周期,最差延迟大 纳秒~微秒级响应
    功耗 CPU 一直全速运行 可进入低功耗休眠,中断唤醒
    代码 简单,但主循环臃肿 需理解中断机制,架构清晰

    2. 中断向量表是什么?放在哪里?

            答:中断向量表是一张"函数指针数组",每个表项对应一个中断/异常的服务程序入口地址。

    • 默认位置:Flash 起始地址 0x08000000(由 BOOT 引脚决定映射到 0x00000000)
    • 第 0 项:不是中断向量,而是初始栈指针 MSP(CPU 上电第一件事就是取它初始化栈)
    • 第 1 项起:Reset_Handler、NMI_Handler、HardFault_Handler……
    • 可重定位:通过 SCB->VTOR 寄存器把向量表搬到 SRAM 中(Bootloader 场景常用)

    3. 中断号是什么?异常编号和 IRQ 编号有什么关系?

            答:中断号就是向量表这个数组的下标,CPU 靠它查表找到对应的 ISR 地址。

    • 1~15:Cortex-M 内核异常(Reset、NMI、HardFault、SysTick 等),所有 Cortex-M3/M4 芯片都一样
    • 16 起:外部外设中断(IRQ),各厂商自定义,ST 的 STM32 从 WWDG(IRQ0) 开始

            代码里用 IRQn_Type 枚举:外设 IRQn 从 0 开始,内核异常用负数。

            绝对中断号 = IRQn_Type 值 + 16(因为外设 IRQ0 对应绝对编号 16)。

            公式:向量地址 = VTOR + 绝对中断号 × 4


    4. Cortex-M 的异常和外部中断有什么区别?

    答:

    对比项异常(Exception)外部中断(IRQ)
    来源 Cortex-M 内核自身 片上外设
    编号 1~15 16 起
    例子 Reset、HardFault、SysTick、PendSV USART1、EXTI0、TIM2
    谁管理 NVIC NVIC

            本质上两者都走同一个 NVIC,只是来源不同——异常是内核内部事件,IRQ 是外设发来的信号。


    二、NVIC 与优先级

    5. NVIC 是什么?有什么作用?

            答:NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)

                    是 Cortex-M 内核紧耦合的中断管理单元,功能包括:

  • 中断使能/失能(ISER/ICER 寄存器)
  • 中断优先级设置(IPR 寄存器)
  • 挂起状态管理(ISPR/ICPR)
  • 中断嵌套判断
  • 自动现场保护与恢复
  • 关键特点:和内核紧耦合,响应极快——从中断请求到进入 ISR 仅需约 12 个时钟周期。


    6. STM32 的中断优先级是怎么分组的?

            答:STM32 用 4 bit 表示优先级,分成两组:

    • 抢占优先级:决定能不能嵌套——数值越小优先级越高,高抢占能打断低抢占
    • 子优先级:同时挂起时谁先执行——不能嵌套

    通过 NVIC_PriorityGroupConfig() 选择分组方式:

    分组抢占位数子优先级位数抢占级数子优先级级数
    Group_0 0 4 1 16
    Group_1 1 3 2 8
    Group_2 2 2 4 4
    Group_3 3 1 8 2
    Group_4 4 0 16 1

    推荐 Group_2:兼顾嵌套深度和并发处理能力。


    7. 中断嵌套是什么?什么条件下发生?

            答:高优先级中断可以打断低优先级中断的执行,形成嵌套。

    主程序 ────────────────
    ↓ 低优先级中断 A
    中断 A ────────────────
    ↓ 高优先级中断 B 来了
    中断 B ──────
    中断 A ──────
    主程序 ────────────────

    嵌套条件:

  • 新中断的抢占优先级严格高于当前正在执行的中断
  • 当前中断没有被全局屏蔽(PRIMASK/FAULTMASK 没开)
  • 新中断没有被 BASEPRI 阈值屏蔽
  •         子优先级相同的两个中断不能互相嵌套——哪怕子优先级不同也不行,只有抢占优先级才能决定嵌套。


    8. 优先级数值越大优先级越高,还是越小越高?

            答:数值越小,优先级越高

    • 0 是最高(除了内核的 -3、-2、-1)
    • 15 是最低

    这是 Cortex-M 的设计——硬件判断时直接做无符号数比较,小的赢,效率最高。


    三、现场保护与返回

    9. 中断发生时,硬件自动做了哪些事?

            答:进入中断时硬件自动压栈 8 个寄存器:

    压栈顺序(从高地址到低地址):
    xPSR ← 程序状态寄存器
    PC ← 程序计数器(断点地址)
    LR ← 链接寄存器
    R12
    R3
    R2
    R1
    R0 ← 当前 SP 指向这里

            这就是为什么中断响应快——8 个寄存器的入栈是硬件一条指令完成的,软件不用管。

            退出中断时:硬件自动出栈恢复这 8 个寄存器,然后根据 EXC_RETURN 值判断回到哪个模式(线程模式/ Handler 模式,用 MSP/PSP)。


    10. EXC_RETURN 是什么?

            答:中断返回时,LR 寄存器会被硬件自动填入一个特殊值叫 EXC_RETURN(比如 0xFFFFFFF9 / 0xFFFFFFFD)。

            它告诉内核:

    • 退出后回到线程模式还是 Handler 模式
    • 用 MSP 还是 PSP
    • 是否发生了浮点上下文

            CPU 执行 BX LR 时,看到 LR 是 EXC_RETURN 值,就知道这是中断返回,自动走出栈流程。


    四、启动文件与弱符号

    11. 为什么中断服务函数的名字必须完全匹配?

            答:因为启动文件的向量表里用函数名做"索引"。

            启动文件里写了:

    __Vectors DCD USART1_IRQHandler ; 中断号 53 对应的位置

            当 USART1 中断发生,CPU 去向量表第 53 项读地址,读到的就是 USART1_IRQHandler 的地址。

            如果你写的函数叫 My_Uart_Handler,链接器不会自动替换——向量表里还是启动文件里的弱定义死循环,CPU 跳过去就卡死了。


    12. 什么是 [WEAK] 弱符号?

            答:启动文件用 [WEAK] 关键字定义了所有中断函数的默认实现、

            (通常是死循环 B .):

    USART1_IRQHandler PROC
    EXPORT USART1_IRQHandler [WEAK]
    B .
    ENDP

    链接规则:

    • 你在 C 文件里写了同名函数 → 链接器优先用你的版本,覆盖弱定义
    • 你没写 → 用弱定义的默认死循环版本

            这就是为什么你不需要在任何地方"注册"中断函数——只要函数名对,链接器自动帮你填进向量表。


    五、进阶原理题

    13. 什么是 Tail-Chaining(尾链)?

            答:Cortex-M 的中断优化机制——如果中断 A 刚执行完,中断 B 已经挂起,CPU 不需要先恢复现场再保存现场,而是直接从 ISR A 跳到 ISR B

            节省了两次出栈/入栈的时间(约 12 个时钟周期),中断响应更快。


    14. 什么是 Late Arriving(迟到中断)?

            答:在压栈过程中,如果来了一个优先级更高的中断,CPU 会先响应这个更高优先级的中断,压栈只做一次。

            服务完高优先级中断后,再服务那个低优先级的。

            这是硬件自动处理的,软件不需要干预。


    15. PRIMASK、FAULTMASK、BASEPRI 有什么区别?

            答:这三个是 Cortex-M 的中断屏蔽寄存器:

    寄存器作用常用场景
    PRIMASK 写 1 → 屏蔽所有可屏蔽中断(只剩 NMI、HardFault) 极短临界区
    FAULTMASK 写 1 → 屏蔽所有异常(包括 HardFault) 极少用
    BASEPRI 设置优先级阈值 → 优先级数值 ≥ 阈值的全屏蔽 RTOS 临界区

    为什么 RTOS 用 BASEPRI 而不是 PRIMASK?

             因为 BASEPRI 可以保留高优先级中断的响应能力(比如 SysTick 节拍中断、NMI),只屏蔽低优先级的——临界区更短,实时性更好。

    __disable_irq(); // PRIMASK=1,关所有中断
    __set_BASEPRI(0x60); // 屏蔽优先级数值 ≥ 6 的,高优先级还能响应


    16. 中断延迟是什么?受哪些因素影响?

            答:从中断请求发出,到 ISR 第一条指令执行的时间,叫中断延迟。

                   Cortex-M3 典型中断延迟是 12 个时钟周期(包括压栈 8 个寄存器 + 向量取指)。

    影响因素:

  • 当前正在执行的指令:如果当前指令没执行完,要等它结束(最多十几个周期)
  • 中断屏蔽:PRIMASK/FAULTMASK/BASEPRI 屏蔽了
  • 总线延迟:向量表在 Flash 中,如果开了 Flash 预取,可能有等待周期
  • 如果是 Tail-Chaining:延迟更短(约 6 周期)

  • 六、实战题

    17. 中断服务函数里能做什么?不能做什么?

    答:

    ✅ 可以做:

    • 读寄存器清中断标志
    • 简单的数据处理
    • 置标志位/发信号量通知主循环
    • 短时间的延时(短!)

    ❌ 不能做:

    • 不要做长延时(HAL_Delay() 这种)——会卡住低优先级中断
    • 不要打印串口(printf)——太慢,可能丢数据
    • 不要做复杂计算——会影响实时性
    • 不要在 ISR 里调用会阻塞的函数(RTOS 的 vTaskDelay())

    原则:中断服务函数要短、快、只做必要的事,复杂工作交给主循环或任务去做。


    18. 怎么设计一个好的中断处理?

            答:标准模式——"中断只置标志,主循环做处理":

    volatile uint8_t uart_rx_flag = 0;
    uint8_t uart_rx_data;

    void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
    uart_rx_data = USART_ReceiveData(USART1); // 读数据
    uart_rx_flag = 1; // 置标志
    // 不在这里做复杂处理!
    }
    }

    int main(void) {
    while(1) {
    if (uart_rx_flag) {
    uart_rx_flag = 0;
    // 这里慢慢处理数据,打印、解析都行
    process_uart_data(uart_rx_data);
    }
    }
    }

       volatile 关键字必须加——因为这个变量在 ISR 和主循环里都用,编译器优化可能会把它缓存到寄存器,导致主循环读不到 ISR 更新的值。


    19. 为什么中断里的标志位变量要加 volatile?

            答:编译器优化时,会把频繁访问的变量缓存到 CPU 寄存器里。

            如果主循环里读 flag,编译器可能优化成"只读一次,后面一直用寄存器里的值"——但 ISR 里改了内存里的 flag,主循环根本看不到。

            加 volatile 告诉编译器:这个变量随时可能被改变,每次都必须从内存重新读,不许优化缓存。


    20. 写中断服务函数时,为什么要手动清中断标志?

            答:绝大多数 STM32 外设中断,硬件不会自动清标志——如果你不写                                 EXTI_ClearITPendingBit() 或 USART_ClearITPendingBit(),

            中断返回后会立刻再次触发同一个中断,CPU 就卡在 ISR 里出不来了,主程序完全不跑。

            例外:

                    有些中断标志是读/写清零的(比如 USART 的 RXNE,读 DR 寄存器就自动清),但最好还是手动显式清,养成习惯,避免不同外设行为不一致踩坑。


    高频考点速记

    考点一句话答案
    中断号本质 向量表数组下标,偏移 = 中断号 × 4
    向量表第 0 项 初始栈指针 MSP,不是中断
    优先级分组 抢占决定嵌套,子优先级决定并发顺序
    优先级数值 越小越高
    嵌套条件 新中断抢占优先级严格更高
    [WEAK] 作用 用户写同名函数自动覆盖默认死循环
    硬件自动压栈 8 个寄存器:xPSR/PC/LR/R12/R0-R3
    中断延迟 Cortex-M3 典型 12 个时钟周期
    volatile 防止编译器优化缓存 ISR 修改的变量
    ISR 原则 短、快、清标志、复杂工作交给主循环
    Tail-Chaining 连续中断直接尾接,省去恢复/保存现场
    BASEPRI vs PRIMASK BASEPRI 保留高优先级,RTOS 临界区用它

    赞(0)
    未经允许不得转载:171主机测评 » 【STM32】 中断与异常深度详解:从概念到 NVIC 寄存器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址