💡 本文是《STM32内核精讲》栏目的第六篇。前五篇我们学习了家族图谱、编程模型、存储器模型、指令集基础和双堆栈机制。从本篇开始,我们将进入 Cortex-M 最核心、最强大的机制——异常与中断系统。这是理解实时性、中断优先级、RTOS 调度的基石。
本文只讲解内核侧的 NVIC 原理与寄存器操作,不涉及任何外设(GPIO、EXTI 等)配置。完整的可运行示例将在第三阶段《实战解析》中单独呈现。


📌 一、引言:什么是异常?什么是中断?

在嵌入式系统中,“中断”这个词几乎天天见。但 Cortex-M 的术语更宽泛:异常(Exception) 包括所有需要暂停当前程序流、转去执行专门处理程序的事件。

  • 中断(Interrupt):由外部硬件(如 GPIO、定时器、UART)触发,是异常的子集
  • 系统异常(System Exception):由内核内部事件触发(如复位、硬故障、SVCall、PendSV、SysTick)。

Cortex-M 通过一个强大的 NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器) 来管理所有这些异常。它支持:

  • 多达 240 个外部中断(具体数量由芯片厂商决定)
  • 可编程的优先级(最多 256 级,实际芯片常用 16 级)
  • 中断嵌套(高优先级可抢占低优先级)
  • 尾链(Tail‑Chaining)和晚到(Late‑Arriving)机制(下篇详述)

理解 NVIC,是掌握 Cortex-M 实时响应能力的核心。


📌 二、异常类型清单

Cortex-M 为异常分配了编号(IRQ number),从 1 到 15 为系统异常,16 及以上为外部中断。编号越小,默认优先级越高(但优先级可重新配置)。

2.1 系统异常列表(编号 1~15)

编号 异常名称 功能描述
1 Reset 复位。优先级最高(-3),不可配置
2 NMI 不可屏蔽中断。通常用于硬件紧急事件(如掉电检测)
3 HardFault 硬故障。当其他故障无法处理时触发,优先级 -1
4 MemManage 存储器管理故障(MPU 访问违例、未对齐访问等)
5 BusFault 总线故障(取指/数据访问时总线返回错误)
6 UsageFault 用法故障(未定义指令、非对齐访问(若未开启捕获)、除零等)
7-10 保留
11 SVCall 系统服务调用。由 SVC 指令触发,用于用户程序请求内核服务
12 DebugMonitor 调试监视器(调试时使用)
13 保留
14 PendSV 可挂起的系统服务调用。可软件触发,优先级低,常用于 RTOS 任务切换
15 SysTick 系统滴答定时器中断。用于 OS 时间片或裸机延时
16+ IRQ0…IRQn 外部中断(如 GPIO、UART、TIM 等)

注意

  • MemManage、BusFault、UsageFault 统称为“可配置故障”,它们可以被单独使能/禁止,且可以触发硬故障(如果对应故障处理未使能或优先级不足)。
  • Cortex-M0/M0+ 不支持 MemManage、BusFault、UsageFault,只有 HardFault。

2.2 外部中断编号与厂商映射

外部中断的编号从 16 开始,对应 IRQ0。具体哪些外设映射到哪个 IRQ 编号,由芯片厂商定义(在参考手册的 NVIC 章节会给出列表)。例如:

  • STM32F103 中,IRQ0 对应 WWDG,IRQ1 对应 PVD,IRQ2 对应 TAMPER……
  • 不同的芯片系列,中断线数量和外设映射差异很大。

在编程时,CMSIS 提供了 IRQn_Type 枚举(如 TIM2_IRQnUSART1_IRQn),你不需要直接使用数字编号。


📌 三、中断向量表:异常的“地址簿”

3.1 向量表结构

向量表(Vector Table)是存放在内存起始地址(默认 0x00000000)的一个数组,每个表项占 4 字节,存储对应异常处理函数的入口地址

偏移(字节) 异常编号 内容
0x000 初始主堆栈指针(MSP)
0x004 1 复位处理程序地址
0x008 2 NMI 处理程序地址
0x00C 3 HardFault 处理程序地址
0x010 4 MemManage 处理程序地址
0x014 5 BusFault 处理程序地址
0x018 6 UsageFault 处理程序地址
0x03C 14 PendSV 处理程序地址
0x040 15 SysTick 处理程序地址
0x044 16 IRQ0 处理程序地址
0x048 17 IRQ1 处理程序地址

注意:第一个表项不是异常处理函数,而是初始 MSP 值。复位后,处理器从该地址取出 SP,再取第二个表项作为 PC。

3.2 向量表重定位(VTOR)

有些情况下,需要将向量表从 Flash 搬移到 RAM(例如动态修改中断处理函数、实现 bootloader)。Cortex-M 提供了一个向量表偏移寄存器(VTOR,地址 0xE000ED08

  • 写入 VTOR 的值必须满足对齐要求:对于 Cortex-M3/M4,要求 bit[7:0] 为 0(即 256 字节对齐);对于 Cortex-M7,要求 bit[8:0] 为 0(512 字节对齐)。这是因为向量表大小必须是 2 的幂,且起始地址必须对齐到该大小。
  • 设置 VTOR 后,处理器会自动从新的基址读取向量表。

示例:将向量表重定位到 RAM 中的 0x20000000(假设已对齐)

#define VTOR  (*(volatile uint32_t *)0xE000ED08)

// 确保 0x20000000 处已复制好向量表(通常通过链接脚本或 memcpy)
VTOR = 0x20000000;
__DSB();
__ISB();   // 确保后续取指使用新向量表

在 bootloader 跳转到应用程序前,通常需要重定位向量表。


📌 四、NVIC 寄存器级控制(纯内核)

NVIC 提供了一系列寄存器,用于使能/禁止中断、设置挂起/清除、配置优先级。这些寄存器只能特权级访问。

4.1 中断使能与禁止

每个外部中断有两个对应的寄存器:

  • NVIC_ISERx(Set-Enable Register):写 1 使能中断。写 0 无效。
  • NVIC_ICERx(Clear-Enable Register):写 1 禁止中断。写 0 无效。

为什么需要两个寄存器?为了原子操作:不需要先读后写,直接写 1 即可,不会影响其他位。

CMSIS 函数

NVIC_EnableIRQ(IRQn);   // 使能
NVIC_DisableIRQ(IRQn);  // 禁止

4.2 中断挂起与清除

  • NVIC_ISPRx(Set-Pending Register):写 1 将中断置为挂起状态(软件触发中断)。
  • NVIC_ICPRx(Clear-Pending Register):写 1 清除挂起状态。

如果中断已经被挂起但还未响应,清除挂起可以取消这次中断请求。

CMSIS 函数

NVIC_SetPendingIRQ(IRQn);    // 设置挂起
NVIC_ClearPendingIRQ(IRQn);  // 清除挂起

4.3 中断优先级配置

每个中断都有自己的优先级寄存器 NVIC_IPRx(8 位,但实际使用的位数由芯片决定,如 STM32 使用高 4 位)。

  • 数值越小,优先级越高。
  • 优先级分为抢占优先级子优先级,由优先级分组寄存器(AIRCR)配置(将在进阶篇详述)。

CMSIS 函数

NVIC_SetPriority(IRQn, priority);   // priority 为 0~255
NVIC_GetPriority(IRQn);

重要priority 是直接写入 IPR 寄存器的 8 位原始值。抢占和子优先级的划分由优先级分组决定,写入的值并不直接等于“抢占值 + 子值”。具体如何计算请参考下篇,或者使用厂商库中的 NVIC_Init() 函数。

4.4 系统异常控制

系统异常(如 MemManage、BusFault、UsageFault、PendSV、SysTick)的使能、优先级等不在 NVIC 寄存器中,而是位于 SCB(系统控制块) 的相关寄存器。

例如:

  • 使能 MemManage、BusFault、UsageFault:SCB->SHCSR 寄存器。
  • 设置 PendSV 优先级:NVIC_SetPriority(PendSV_IRQn, priority)(其实 PendSV 的优先级寄存器也位于 NVIC 区域,但 CMSIS 统一处理)。

📌 五、纯内核示例:软件触发 PendSV 中断

以下示例不涉及任何外设,仅使用内核寄存器演示 NVIC 的中断使能、优先级设置和软件触发。

#include "core_cm3.h"   // 或 core_cm4.h,根据芯片选择

int main(void) {
    // 1. 设置 PendSV 优先级为最低(0xFF)
    NVIC_SetPriority(PendSV_IRQn, 0xFF);
    
    // 2. 使能 PendSV 中断
    NVIC_EnableIRQ(PendSV_IRQn);
    
    // 3. 软件触发 PendSV(写 ICSR 寄存器)
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
    
    // 4. 主循环空转,等待 PendSV 响应
    while (1);
}

// PendSV 中断服务函数
void PendSV_Handler(void) {
    // 在这里设置断点,或翻转一个内存变量(不涉外设)
    static volatile uint32_t count = 0;
    count++;
}

验证方法(无需外设):

  • PendSV_Handler 中设置断点,全速运行后应能停在断点处。
  • 观察 count 变量会递增。

备注:如果需要我像上一篇那样出一篇实验讲解文章,可以在评论区留言。有人需要我再出


📌 六、常见陷阱与注意事项

  1. 中断优先级设置无效:检查是否设置了优先级分组(AIRCR)。没有分组,优先级比较行为不确定。
  2. 中断无法触发
    • 检查是否调用了 NVIC_EnableIRQ()
    • 检查全局中断是否使能(__enable_irq())。
  3. HardFault 进入后无法定位:优先检查是否使能了 MemManage、BusFault、UsageFault(默认关闭),并配置这些故障的优先级。下篇将深入故障分析。
  4. 向量表重定位后 HardFault:检查新向量表地址的对齐要求(256 字节或 512 字节对齐),以及是否完整复制了向量表(至少到所使用的最大中断号)。
  5. 优先级数值的计算:不要凭感觉移位,请使用厂商库的 NVIC_Init(),或者按照分组明确计算(详见下篇)。

📌 七、总结与下篇预告

7.1 本篇核心要点

  1. 异常类型:系统异常(编号 1~15)和外部中断(编号 16+)。复位优先级最高,硬故障次之,PendSV 和 SysTick 通常设为最低。
  2. 向量表:存放在 0x00000000(可重定位),第一项是初始 MSP,第二项是复位向量。VTOR 寄存器用于重定位,注意对齐。
  3. NVIC 控制:通过 ISER/ICER 使能/禁止,ISPR/ICPR 挂起/清除,IPR 配置优先级(数值越小优先级越高)。
  4. CMSIS 封装:使用 NVIC_EnableIRQ()NVIC_SetPriority() 等函数,但优先级数值需结合分组正确计算。

7.2 下篇预告:《异常与中断系统(NVIC)—— 进阶篇》

下一篇我们将深入 NVIC 的高级特性:

  • 抢占优先级与子优先级:优先级分组寄存器 AIRCR 的配置与影响
  • 晚到(Late‑Arriving)与尾链(Tail‑Chaining)机制:如何减少中断延迟
  • 异常返回值 EXC_RETURN 的位含义:从异常返回时的栈恢复决策
  • 异常返回的硬件序列:出栈、恢复 PC 和 SP 的完整流程

这些机制是 Cortex-M 实时性冠绝群雄的秘密武器。


💬 读者问题专栏 · 问题征集

本篇我们学习了异常类型、向量表和 NVIC 基础控制。

你在使用中断时,是否遇到过这些问题:

  • 为什么我设置的中断优先级似乎不生效?
  • 同一个中断被触发多次,但只响应了一次,挂起位是怎么工作的?
  • 如何软件触发一个中断?PendSV 和 SVC 有什么区别?
  • 向量表重定位后程序跑飞,可能是什么原因?

欢迎在评论区留下你的疑问,我会选取典型问题,在 《Cortex‑M 有问必答》 专栏中专题解答。

提供相关代码片段或寄存器截图,能让我更准确地定位问题。


📢 关于作者与更多内容

我是 BackCatK Chen,长期关注嵌入式底层、国产半导体与 AI 算力芯片。

如果你对芯片架构、行业趋势感兴趣,欢迎关注我的公众号,获取更多宏观技术观察。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐