1. 中断系统的引入
在嵌入式开发中,CPU 需要及时响应外部事件,例如按键按下、定时器溢出、串口收到数据等。如果 CPU 一直忙于查询这些事件是否发生,就会浪费大量时间在空转上。中断系统的引入,正是为了解决这一问题:它让 CPU 在事件发生时被主动打断,转而执行对应的处理程序,处理完成后再回到原来的任务继续执行。
中断机制的核心价值在于:提高 CPU 利用率、保证事件响应的实时性,以及简化程序逻辑。有了中断,主程序可以专注于核心业务,而把外部事件的响应交给中断服务函数去完成。
2. 传统轮询的缺陷
在引入中断之前,嵌入式程序通常采用轮询(Polling)方式处理外部事件。轮询的基本思路是:主循环不断读取各个外设的状态寄存器,判断是否有事件发生,如果有则执行相应处理。
轮询方式存在以下明显缺陷:
- CPU 利用率低:大部分时间 CPU 都在空转查询,真正用于业务处理的时间很少。
- 实时性差:事件发生后,必须等到主循环轮询到对应标志位才能处理,响应延迟不可控。
- 程序结构混乱:多个事件交织在一起,主循环代码越来越复杂,难以维护。
- 功耗高:CPU 持续运行查询,无法进入低功耗模式。
相比之下,中断方式让 CPU 在无事可做时可以休眠或执行其他任务,事件到来时再被唤醒,既省电又高效。
3. 中断的定义
中断(Interrupt)是指 CPU 在执行程序的过程中,遇到某些紧急事件需要处理时,暂时停止当前程序的执行,转而去执行处理该事件的服务程序,处理完毕后再返回原程序继续执行的过程。
从触发来源来看,中断可以分为两大类:
3.1 硬件中断
硬件中断由外部硬件信号触发,例如 GPIO 引脚电平变化、定时器溢出、串口接收完成、ADC 转换结束等。硬件中断是异步的,CPU 无法预知它何时到来,因此需要中断控制器来统一管理和分发。
3.2 编程错误(异常)
编程错误引发的异常属于同步事件,由 CPU 在执行指令时检测到异常情况而触发,例如:
- 除零错误:执行除法指令时除数为 0。
- 未对齐访问:访问未对齐的内存地址。
- 未定义指令:执行了 CPU 不认识的指令码。
- 总线错误:访问了不存在的地址空间。
- 硬故障(HardFault):上述故障无法处理时进入的最终异常。
在 ARM Cortex-M 体系中,异常(Exception)是更广义的概念,中断(Interrupt)是异常的一种特例,通常指来自外设的异步请求。
4. Cortex-M4 中断向量表
Cortex-M4 处理器支持两种工作模式:
- 线程模式(Thread Mode):正常运行应用程序代码时处于该模式,可以运行在特权级或非特权级。
- 处理模式(Handler Mode):执行异常或中断服务函数时进入该模式,始终运行在特权级。
两种模式配合两种特权等级(特权级、非特权级),构成了 Cortex-M4 的完整运行模型。当异常发生时,处理器自动从线程模式切换到处理模式,执行完中断服务函数后自动返回线程模式。
中断向量表
中断向量表(Vector Table)是存放在 Flash 起始地址处的一张表,表中每一项对应一个异常或中断的入口地址。Cortex-M4 的向量表结构如下:
| 向量位置 | 异常类型 | 说明 |
|---|---|---|
| 0x00000000 | 初始栈指针 | 主栈指针 MSP 的初始值 |
| 0x00000004 | Reset | 复位后第一条指令地址 |
| 0x00000008 | NMI | 不可屏蔽中断 |
| 0x0000000C | HardFault | 硬故障 |
| 0x00000010 | MemManage | 内存管理异常 |
| 0x00000014 | BusFault | 总线故障 |
| 0x00000018 | UsageFault | 用法故障 |
| ... | ... | ... |
| 0x00000040 | SVCall | 系统服务调用 |
| 0x00000044 | DebugMonitor | 调试监控 |
| 0x00000048 | PendSV | 可挂起系统服务 |
| 0x0000004C | SysTick | 系统节拍定时器 |
| 0x00000050 起 | 外部中断 IRQ0~IRQn | 由芯片厂商定义的具体外设中断 |
当异常或中断发生时,处理器根据中断号从向量表中取出对应的入口地址,自动压栈(保存现场),跳转到服务函数执行,执行完毕后出栈(恢复现场)并返回。
向量表的存放位置:在 Cortex-M4 中,向量表默认存放在 Flash 的起始地址(0x00000000),也就是代码区的开头。芯片上电复位后,处理器会从该地址读取初始栈指针和复位入口地址。向量表也可以被重定位到 SRAM 或其他地址,这需要通过向量表偏移寄存器(VTOR)来配置。
向量表偏移寄存器(VTOR):VTOR(Vector Table Offset Register)是系统控制块(SCB)中的一个 32 位寄存器,用于设置向量表的基地址。通过修改 VTOR,可以将向量表从默认的 Flash 起始地址搬移到其他位置,常见应用场景包括:
- Bootloader 跳转:Bootloader 引导应用程序时,需要将向量表偏移到应用程序的起始地址,否则中断无法正确响应。
- RAM 中运行:将向量表放到 SRAM 中,可以动态修改中断入口,常用于在线升级或调试场景。
- 多固件切换:在多个固件镜像之间切换时,通过 VTOR 指向不同固件的向量表。
在 STM32F4 中,VTOR 的地址为 0xE000ED08,属于 SCB 寄存器区域。配置 VTOR 时需要注意:向量表地址必须按 512 字节对齐(Cortex-M4 要求),并且向量表所在区域必须可读。使用标准库时,可以通过以下方式设置 VTOR:
// 将向量表偏移到应用程序起始地址(例如 0x08010000) SCB->VTOR = 0x08010000;注:"中断向量表"==>详情请参考《STM32F4xx中文参考手册》234页
5. STM32F4 中断总逻辑
STM32F4 系列基于 Cortex-M4 内核,其中断系统由内核异常和外设中断两部分组成。外设中断通过嵌套向量中断控制器(NVIC)统一管理。
STM32F4 中断系统的整体逻辑如下:
- 外设事件源:GPIO、定时器、串口、ADC、DMA 等外设产生中断请求。
- 中断请求线:每个外设中断对应一条请求线,连接到 NVIC。
- NVIC 控制器:负责中断使能、挂起、优先级管理和中断分发。
- 内核异常:SysTick、PendSV、SVCall 等内核异常直接由内核管理。
- CPU 响应:NVIC 将最高优先级的中断请求送给 CPU 内核,CPU 自动进入处理模式执行中断服务函数。
在 STM32F4 中,NVIC 支持多达 82 个可屏蔽中断通道,支持 16 级可编程优先级(4 位优先级位,分为抢占优先级和子优先级)。中断优先级分组由 SCB->AIRCR 寄存器配置,常见分组方式如下:
| 分组 | 抢占优先级位数 | 子优先级位数 |
|---|---|---|
| NVIC_PriorityGroup_0 | 0 位 | 4 位 |
| NVIC_PriorityGroup_1 | 1 位 | 3 位 |
| NVIC_PriorityGroup_2 | 2 位 | 2 位 |
| NVIC_PriorityGroup_3 | 3 位 | 1 位 |
| NVIC_PriorityGroup_4 | 4 位 | 0 位 |
抢占优先级决定中断是否可以打断当前正在执行的中断,子优先级用于同抢占优先级下的响应顺序。
6. 外部中断
6.1 概念
外部中断(EXTI)是指由 GPIO 引脚上的电平变化(上升沿、下降沿或双边沿)触发的中断。STM32F4 的 EXTI 控制器支持最多 16 条外部中断线(EXTI0~EXTI15),每条线可以独立配置触发方式和中断使能。
6.2 EXTI 与 GPIO 映射关系
STM32F4 的 GPIO 引脚通过 AFIO(复用功能 IO)映射到 EXTI 线。例如:
- EXTI0 可以映射到 PA0、PB0、PC0、PD0、PE0、PF0、PG0、PH0、PI0 中的任意一个。
- EXTI1 可以映射到 PA1、PB1、PC1 等对应编号的引脚。
- 同一时刻,一条 EXTI 线只能连接一个 GPIO 引脚,通过 SYSCFG_EXTICRx 寄存器选择。
7. STM32F4 函数中断配置
7.1 中断配置步骤
以 STM32F4 标准库为例,配置一个外部中断通常需要以下步骤:
- 使能 GPIO 时钟:调用
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE),开启 GPIOA 端口的时钟。STM32F4 的 GPIO 挂在 AHB1 总线上,使用前必须先使能对应端口时钟,否则寄存器读写无效。 - 配置 GPIO 引脚:填充
GPIO_InitTypeDef结构体,设置GPIO_Pin(引脚号)、GPIO_Mode(输入模式GPIO_Mode_IN)、GPIO_PuPd(上下拉,如GPIO_PuPd_UP),然后调用GPIO_Init(GPIOA, &GPIO_InitStructure)完成初始化。 - 使能 SYSCFG 时钟:调用
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE)。SYSCFG 挂在 APB2 总线上,负责 GPIO 到 EXTI 的映射配置,必须先使能其时钟。 - 配置 EXTI 映射:调用
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0),将 PA0 引脚连接到 EXTI0 线。该函数内部操作 SYSCFG_EXTICR1 寄存器,完成引脚到中断线的映射。 - 配置 EXTI 触发方式:填充
EXTI_InitTypeDef结构体,设置EXTI_Line(EXTI_Line0)、EXTI_Mode(中断模式EXTI_Mode_Interrupt)、EXTI_Trigger(下降沿EXTI_Trigger_Falling)、EXTI_LineCmd(使能ENABLE),然后调用EXTI_Init(&EXTI_InitStructure)。 - 使能 EXTI 中断:上一步中
EXTI_LineCmd = ENABLE即完成了 EXTI 中断的使能,对应操作 EXTI_IMR 寄存器,允许该中断线向 NVIC 发出请求。 - 配置 NVIC:填充
NVIC_InitTypeDef结构体,设置NVIC_IRQChannel(EXTI0_IRQn)、NVIC_IRQChannelPreemptionPriority(抢占优先级)、NVIC_IRQChannelSubPriority(子优先级)、NVIC_IRQChannelCmd(使能),然后调用NVIC_Init(&NVIC_InitStructure)。 - 编写中断服务函数:实现
EXTI0_IRQHandler,在函数内先调用EXTI_GetITStatus(EXTI_Line0)判断中断是否发生,处理事件后调用EXTI_ClearITPendingBit(EXTI_Line0)清除挂起标志位。
7.2 外部中断配置代码
下面给出一个使用标准库配置 PA0 引脚外部中断的示例:
void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 处理按键事件 EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 使能 GPIOA 和 SYSCFG 时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // 2. 配置 PA0 为输入模式 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 将 PA0 映射到 EXTI0 SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0); // 4. 配置 EXTI0 下降沿触发 EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 5. 配置 NVIC NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } // 主函数中调用 EXTI_Config() 完成初始化 int main(void) { EXTI_Config(); while (1) { // 主循环处理其他任务 } }中断服务程序的完整编写示例:下面给出一个完整的中断服务程序示例,演示如何在中断中置标志位、在主循环中处理业务逻辑:
// 全局标志位,供主循环查询 volatile uint8_t g_key_pressed = 0; // EXTI0 中断服务函数 void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 置标志位,通知主循环有按键事件发生 g_key_pressed = 1; // 清除挂起标志位,防止反复进入中断 EXTI_ClearITPendingBit(EXTI_Line0); } } // 主函数 int main(void) { EXTI_Config(); // 完成 GPIO、EXTI、NVIC 的初始化配置 while (1) { // 主循环中查询标志位,处理具体业务 if (g_key_pressed) { g_key_pressed = 0; // 清除标志位 // 在这里执行按键对应的业务逻辑,例如切换 LED 状态 GPIO_ToggleBits(GPIOD, GPIO_Pin_12); } // 其他主循环任务 } }这个示例体现了中断服务程序编写的核心思想:中断服务函数只负责置标志位和清除中断标志,具体的业务处理(如切换 LED)放到主循环中完成,从而保证中断响应足够快,也避免在中断里做耗时操作导致系统卡顿。
中断服务程序的编写要点:
- 函数名必须与中断向量表一致:EXTI0 中断的服务函数名必须为
EXTI0_IRQHandler,该名称由启动文件(startup_stm32f40x.s)中的向量表预先定义,写错名称会导致中断无法响应。 - 先判断中断标志位:进入服务函数后,先调用
EXTI_GetITStatus(EXTI_Line0)确认确实是本中断线触发的,避免误处理。 - 处理完必须清除标志位:调用
EXTI_ClearITPendingBit(EXTI_Line0)清除挂起标志,否则中断会反复触发,导致程序卡死在服务函数中。 - 保持函数短小精悍:中断服务函数中不要做延时、打印等耗时操作,建议只置一个标志位,把具体业务逻辑放到主循环中处理。
7.3 外部中断注意事项
- 中断标志位必须清除:中断服务函数执行完毕后,必须清除对应的挂起标志位,否则会反复进入中断。
- 中断服务函数要短小精悍:不要在中断里做耗时操作,尽量只置标志位,把具体处理放到主循环中。
- 注意按键抖动:机械按键在按下和释放时会产生抖动,需要在硬件或软件上做消抖处理。
- 优先级配置要合理:实时性要求高的中断应设置较高的抢占优先级。
8. 总结
本文从嵌入式开发的实际需求出发,系统梳理了 STM32F4 中断系统的完整脉络。首先通过对比传统轮询的缺陷,引出中断机制在提高 CPU 利用率、保证实时性和简化程序逻辑方面的核心价值;随后介绍了中断与异常的定义、Cortex-M4 的线程模式与处理模式,以及中断向量表的结构和 VTOR 偏移寄存器的配置方法。在此基础上,进一步讲解了 STM32F4 中断总逻辑中 NVIC 的优先级分组机制,并以外部中断 EXTI 为例,详细说明了 GPIO 到 EXTI 线的映射关系、标准库中断配置的完整步骤,以及中断服务程序的编写要点和注意事项。
掌握 STM32F4 中断编程的关键在于:理解从轮询到中断的演进逻辑,熟悉 Cortex-M4 的向量表与 NVIC 优先级机制,并遵循中断服务函数短小精悍、正确清除中断标志位、合理配置优先级等实践原则。在实际项目中,建议先通过按键触发外部中断的简单实验跑通完整流程,再逐步扩展到定时器、串口、ADC 等多外设协同的中断场景,从而真正建立起稳定可靠的中断驱动开发能力。