第3章 中断系统与NVIC配置:外部中断EXTI、中断优先级分组、中断服务函数编写、中断嵌套实战
中断系统,说白了就是CPU的“紧急响应机制”。没有中断,你的STM32就得不停地轮询——就像一个人每隔一秒就去检查一下门铃有没有响,效率极低。而有了中断,CPU可以安心干自己的活,门铃一响,立刻放下手头的事去开门。
我刚开始做嵌入式开发时,对中断的理解就停留在“按个按钮触发一下”的层面。直到有一次做无线通信项目,数据包频繁丢失,查了三天才发现是中断优先级没配好,高优先级的中断把低优先级的给“饿死”了。从那以后,我对NVIC的配置再也不敢马虎。
3.1 中断系统的整体架构
STM32的中断系统由两部分组成:NVIC(嵌套向量中断控制器)和EXTI(外部中断/事件控制器)。NVIC是ARM Cortex-M内核自带的,负责中断优先级管理和向量表调度;EXTI则是ST外设,负责管理GPIO引脚的中断触发。
核心关系:EXTI负责“检测到中断事件”,NVIC负责“决定要不要响应以及何时响应”。两者配合,缺一不可。
3.2 EXTI外部中断配置
EXTI支持最多16条外部中断线(EXTI0~EXTI15),每条线对应一组GPIO引脚。比如EXTI0可以映射到PA0、PB0、PC0……但同一时刻只能选一个。
个人经验:我习惯把按键中断都放在EXTI0上,因为EXTI0的中断向量在向量表中排最前面,响应速度最快。虽然实际差异微乎其微,但心理上觉得踏实。
3.2.1 EXTI配置步骤
- 使能GPIO时钟——没有时钟,外设就是一块死硅。
- 配置GPIO为输入模式——上拉还是下拉?看你的电路设计。
- 将GPIO连接到EXTI线——通过SYSCFG_EXTICR寄存器。
- 配置EXTI触发边沿——上升沿、下降沿或双边沿。
- 使能EXTI中断线——不使能,触发信号进不来。
- 配置NVIC并使能中断——最后一步,让CPU知道这个中断的存在。
// 以PA0为例,配置EXTI0中断
void EXTI0_Config(void)
{
// 1. 使能GPIOA时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. 配置PA0为输入,上拉
GPIOA->MODER &= ~(0x03 << 0); // 00: 输入模式
GPIOA->PUPDR |= (0x01 << 0); // 01: 上拉
// 3. 将PA0连接到EXTI0
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
// 4. 配置下降沿触发(按键按下)
EXTI->FTSR |= EXTI_FTSR_TR0;
// 5. 使能EXTI0中断线
EXTI->IMR |= EXTI_IMR_MR0;
// 6. 配置NVIC
NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_SetPriority(EXTI0_IRQn, 0x02); // 优先级设为2
}
注意:配置SYSCFG之前,一定要先使能SYSCFG时钟(RCC->APB2ENR)。我曾经在这个坑里摔过一次——代码逻辑完全正确,但中断就是不触发,查了半天才发现SYSCFG时钟没开。
3.3 中断优先级分组
这是整个中断系统里最容易出问题的地方。STM32的NVIC支持最多16级抢占优先级和16级子优先级,但具体怎么分配,取决于优先级分组。
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 抢占优先级范围 | 子优先级范围 |
|---|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 无 | 0~15 |
| NVIC_PriorityGroup_1 | 1 | 3 | 0~1 | 0~7 |
| NVIC_PriorityGroup_2 | 2 | 2 | 0~3 | 0~3 |
| NVIC_PriorityGroup_3 | 3 | 1 | 0~7 | 0~1 |
| NVIC_PriorityGroup_4 | 4 | 0 | 0~15 | 无 |
我个人习惯使用NVIC_PriorityGroup_2,也就是2位抢占优先级、2位子优先级。为什么?因为大多数项目里,中断的层级关系不需要太复杂,4级抢占优先级足够用了,而且2位子优先级还能做点精细调整。
关键规则:
- 抢占优先级高的中断可以打断抢占优先级低的中断。
- 子优先级只在抢占优先级相同时起作用——谁子优先级高,谁先执行。
- 整个系统只能调用一次NVIC_PriorityGroupConfig(),中途不能改。
3.4 中断服务函数编写
中断服务函数(ISR)的写法有固定套路。函数名必须和启动文件里的向量表一致,比如EXTI0的中断服务函数就叫EXTI0_IRQHandler。
void EXTI0_IRQHandler(void)
{
// 1. 检查中断标志位
if (EXTI->PR & EXTI_PR_PR0)
{
// 2. 清除中断标志位(必须!)
EXTI->PR = EXTI_PR_PR0;
// 3. 执行用户逻辑
// 注意:ISR里不要做复杂操作,尽量短小精悍
LED_Toggle();
}
}
我曾经犯过的错:在ISR里调用了printf(),结果程序一进中断就卡死。后来才想起来,printf内部用了互斥锁,而中断里不能使用任何阻塞操作。记住:ISR里只做标志位设置、数据拷贝、简单IO操作,复杂逻辑放到主循环里处理。
3.4.1 ISR编写黄金法则
- 快进快出——ISR执行时间越短越好,别在里面做延时。
- 清除标志位——不清除标志位,中断会反复触发,系统直接死机。
- 避免浮点运算——除非你开了FPU并且做了上下文保存。
- 不要调用不可重入函数——比如malloc、printf等。
3.5 中断嵌套实战
中断嵌套,说白了就是一个中断正在执行,另一个更高优先级的中断来了,CPU先跑去处理高优先级的,处理完再回来继续执行原来的中断。
你想想看,如果两个中断的抢占优先级相同,那它们之间不会发生嵌套——必须等当前ISR执行完,下一个才能进来。这就是为什么抢占优先级的设计如此重要。
3.5.1 实战场景:按键中断 + 定时器中断
假设我们有两个中断源:
- EXTI0(按键):抢占优先级1,子优先级0
- TIM2(定时器):抢占优先级0,子优先级0
因为TIM2的抢占优先级(0)高于EXTI0(1),所以当TIM2中断正在执行时,按键中断无法打断它。反过来,如果按键中断正在执行,TIM2中断来了,按键中断会被立刻打断,CPU跳去执行TIM2的ISR。
// 配置两个中断的优先级
NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_PriorityGroup_2, 1, 0));
NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_PriorityGroup_2, 0, 0));
// TIM2中断服务函数
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR = ~TIM_SR_UIF;
// 模拟耗时操作
for (volatile uint32_t i = 0; i < 100000; i++);
}
}
// EXTI0中断服务函数
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0)
{
EXTI->PR = EXTI_PR_PR0;
// 模拟耗时操作
for (volatile uint32_t i = 0; i < 50000; i++);
}
}
调试技巧:我习惯在ISR里翻转一个GPIO引脚,用示波器观察中断的实际响应时间和嵌套情况。比如TIM2中断里拉高PB0,EXTI0中断里拉高PB1,一看波形就知道谁打断了谁。
3.6 常见问题与避坑指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 中断不触发 | SYSCFG时钟未使能 | 检查RCC->APB2ENR |
| 中断反复触发 | 未清除标志位 | 在ISR开头清除EXTI->PR |
| 低优先级中断被饿死 | 高优先级中断频繁触发 | 调整优先级分组或降低高优先级中断频率 |
| 中断响应延迟大 | ISR内执行了耗时操作 | 将耗时操作移到主循环 |
| 系统进入HardFault | ISR内调用了不可重入函数 | 检查ISR中的函数调用 |
我曾经踩过的坑:有一次做多中断项目,EXTI中断和USART中断同时工作,结果USART数据总是丢字节。查到最后发现,EXTI中断的抢占优先级设得太高,每次按键按下都打断USART的接收过程,导致数据溢出。解决办法很简单——把USART的抢占优先级设得比EXTI高一级,保证数据接收优先。
嗯,中断系统这块内容确实有点绕,但只要你理解了NVIC的优先级分组机制,再配合EXTI的配置流程,大部分项目的中断需求都能搞定。记住:中断优先级的设计一定要从系统整体角度考虑,哪个中断最紧急、哪个中断不能丢数据、哪个中断可以等一等——想清楚了再动手配置。