3. 中断系统与NVIC:理解STM32的中断优先级,配置外部中断(EXTI)实现按键中断
中断,说白了就是CPU正在干活,突然被一个紧急事件叫停,转去处理这个事件,处理完再回来接着干。你想想看,如果没有中断,CPU就得不停地轮询按键有没有按下——那得多浪费啊。
我刚开始学STM32时,对中断优先级这块儿特别迷糊。什么抢占优先级、子优先级,一堆概念绕来绕去。后来在做一个工业控制项目时,因为优先级没配好,导致高优先级中断把低优先级的中断饿死了,现场设备直接失控。嗯,从那以后我再也不敢轻视NVIC的配置了。
3.1 中断系统的基本概念
STM32的中断系统由两部分组成:NVIC(嵌套向量中断控制器)和EXTI(外部中断/事件控制器)。NVIC是内核级别的,负责管理所有中断的使能、挂起和优先级;EXTI则是外设级别的,专门处理外部引脚的中断请求。
核心要点:NVIC是中断的"调度中心",EXTI是中断的"触发源"。两者配合,才能实现外部中断。
STM32支持多达16个中断优先级(4位优先级寄存器),但具体能用多少,取决于你配置的优先级分组。我个人习惯用NVIC_PriorityGroup_2,也就是2位抢占优先级、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级子优先级也能在同一个抢占级别内做精细调度。
3.2 EXTI的工作原理
EXTI支持16条外部中断线(EXTI0~EXTI15),每条线对应一个GPIO引脚。但注意,不是每个引脚都有独立的EXTI线——比如PA0、PB0、PC0都共用EXTI0线。所以同一时刻,只能有一个引脚映射到同一条EXTI线上。
我曾经在一个项目里踩过这个坑:同时配置了PA0和PB0的EXTI中断,结果两个引脚的中断互相干扰,按键响应完全乱套。后来查手册才发现,同一EXTI线只能选一个引脚。
EXTI的触发方式有三种:
- 上升沿触发:信号从低到高时触发
- 下降沿触发:信号从高到低时触发
- 双边沿触发:上升沿和下降沿都触发
按键中断一般用下降沿触发,因为按键按下时引脚电平从高变低。但要注意硬件消抖——机械按键按下时会有抖动,如果不做处理,一次按下可能触发多次中断。
避坑指南:我曾经在一个产品中没做软件消抖,结果按键按下一次,串口打印了七八次中断信息。后来在中断服务函数里加了10ms的延时去抖,问题才解决。记住:硬件消抖(RC滤波)和软件消抖(延时判断)至少要选一个。
3.3 配置外部中断的完整步骤
配置一个按键中断,需要走以下流程:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式(上拉或下拉)
- 将GPIO引脚连接到EXTI线(通过SYSCFG)
- 配置EXTI的触发方式和中断使能
- 配置NVIC优先级并使能中断
- 编写中断服务函数
下面是一个完整的代码示例,实现PA0引脚(按键)的下降沿中断:
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 连接EXTI线
SYSCFG_EXTILineConfig(EXTI_PortSource_GPIOA, EXTI_PinSource0);
// 4. 配置EXTI
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 6. 中断服务函数
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 延时消抖(简单实现)
Delay_ms(10);
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
// 按键确实被按下
// 在这里处理按键逻辑
}
// 清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
注意:中断服务函数里一定要清除中断标志位,否则会一直触发中断。我见过有人忘了这步,结果程序卡死在中断里出不来。
3.4 中断优先级配置的实战经验
优先级配置看似简单,但实际项目中很容易出问题。我总结了几条经验:
- 时间敏感的中断给高优先级:比如定时器中断、通信中断,这些需要快速响应。
- 非关键中断给低优先级:比如按键中断、LED闪烁,慢一点没关系。
- 避免中断嵌套过深:中断嵌套超过3层,栈空间可能不够用。
- 中断服务函数要短:尽量只做标志位设置,具体处理放在主循环里。
举个例子,我在一个电机控制项目里,把PWM中断设为抢占优先级0(最高),把按键中断设为抢占优先级2(较低)。这样即使按键中断正在执行,PWM中断也能立即抢占,保证电机控制的实时性。
3.5 中断系统的知识结构
下面这张图展示了中断系统的整体架构和配置流程:
3.6 常见问题与调试技巧
配置外部中断时,我遇到过不少问题,这里列几个最常见的:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 中断不触发 | SYSCFG时钟未使能 | 检查RCC_APB2PeriphClockCmd是否包含SYSCFG |
| 中断触发多次 | 未做消抖处理 | 加软件延时或硬件RC滤波 |
| 中断响应慢 | 优先级配置太低 | 提高抢占优先级 |
| 程序卡死在中断 | 未清除中断标志位 | 在ISR末尾调用EXTI_ClearITPendingBit |
| 多个中断互相干扰 | 同一EXTI线映射了多个引脚 | 确保每个EXTI线只对应一个引脚 |
调试建议:我习惯在中断服务函数里加一个GPIO翻转,用示波器看波形。如果波形正常,说明中断触发了;如果波形不对,再查配置。这比单纯看代码效率高多了。
好了,中断系统和NVIC的内容就讲到这里。记住:中断是嵌入式系统的核心机制,配置得好,系统稳定高效;配置不好,各种奇怪问题都会冒出来。多动手、多调试,慢慢就能掌握其中的门道。