STM32内核精讲 | 第六章:异常与中断系统(NVIC)—— 基础篇
文章摘要 本文深入解析了Cortex-M内核的异常与中断机制。首先区分了异常(包括系统异常和外部中断)的概念,介绍了NVIC控制器的核心功能。详细列出了系统异常类型(如Reset、HardFault等)和外部中断编号规则,并阐述了中断向量表的结构与重定位方法。重点讲解了NVIC寄存器的操作方式,包括中断使能、挂起控制及优先级配置。最后通过STM32的EXTI0中断实例,演示了正确的中断配置流程。全
💡 本文是《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_IRQn、USART1_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变量会递增。
备注:如果需要我像上一篇那样出一篇实验讲解文章,可以在评论区留言。有人需要我再出
📌 六、常见陷阱与注意事项
- 中断优先级设置无效:检查是否设置了优先级分组(AIRCR)。没有分组,优先级比较行为不确定。
- 中断无法触发:
- 检查是否调用了
NVIC_EnableIRQ()。 - 检查全局中断是否使能(
__enable_irq())。
- 检查是否调用了
- HardFault 进入后无法定位:优先检查是否使能了 MemManage、BusFault、UsageFault(默认关闭),并配置这些故障的优先级。下篇将深入故障分析。
- 向量表重定位后 HardFault:检查新向量表地址的对齐要求(256 字节或 512 字节对齐),以及是否完整复制了向量表(至少到所使用的最大中断号)。
- 优先级数值的计算:不要凭感觉移位,请使用厂商库的
NVIC_Init(),或者按照分组明确计算(详见下篇)。
📌 七、总结与下篇预告
7.1 本篇核心要点
- 异常类型:系统异常(编号 1~15)和外部中断(编号 16+)。复位优先级最高,硬故障次之,PendSV 和 SysTick 通常设为最低。
- 向量表:存放在 0x00000000(可重定位),第一项是初始 MSP,第二项是复位向量。VTOR 寄存器用于重定位,注意对齐。
- NVIC 控制:通过 ISER/ICER 使能/禁止,ISPR/ICPR 挂起/清除,IPR 配置优先级(数值越小优先级越高)。
- 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 算力芯片。
如果你对芯片架构、行业趋势感兴趣,欢迎关注我的公众号,获取更多宏观技术观察。
更多推荐




所有评论(0)