4、EXTI驱动实战:按键中断控制LED、中断服务函数编写、中断标志位清除
好,咱们今天来聊聊EXTI。说白了,就是外部中断。
我记得刚入行那会儿,第一次用STM32做按键控制LED,用的还是轮询——就是主循环里一直读GPIO电平。当时觉得挺简单,后来产品量产了才发现问题:按键响应慢,CPU还被占得死死的。嗯,从那以后,我基本都用中断了。
这一节,咱们就手把手把EXTI驱动写出来。目标很明确:按一下按键,LED翻转一次。核心就三件事——配置中断、写服务函数、清标志位。
4.1 EXTI是什么?
EXTI全称是External Interrupt,外部中断控制器。它负责把GPIO引脚上的电平变化,转化成中断请求,送给NVIC(嵌套向量中断控制器)。
你想想看,如果没有EXTI,CPU就得一直盯着引脚看,多累啊。有了中断,CPU该干嘛干嘛,引脚一变,它自己就跳过来处理了。
STM32的EXTI支持16条外部中断线(EXTI0~EXTI15),每条线可以独立配置触发方式:
- 上升沿触发:从低到高跳变时触发
- 下降沿触发:从高到低跳变时触发
- 双边沿触发:任意跳变都触发
我在项目中遇到过一种情况:按键按下时会有抖动,如果只用单边沿触发,可能会误判。后来我加了20ms的软件消抖,问题就解决了。这个后面会细说。
4.2 硬件连接与原理
咱们用最经典的板子举例:
- 按键接在 PA0,默认上拉,按下为低电平
- LED接在 PC13(很多开发板上的板载LED),低电平点亮
所以逻辑是:按键按下(PA0变低)→ 触发EXTI0中断 → 在中断里翻转PC13的电平。
核心要点:EXTI0只能映射到GPIO的Pin0,比如PA0、PB0、PC0都可以,但同一时间只能选一个。配置时要用GPIO_AFRL或AFRH选择复用功能。
4.3 配置步骤(HAL库版)
我个人习惯用HAL库,因为可读性好,而且CubeMX生成的代码基本不用改。但咱们得知道底层在干什么。
配置EXTI中断,分四步走:
- 使能GPIO时钟:GPIOA和GPIOC的时钟要打开
- 配置GPIO模式:PA0设为输入,PC13设为推挽输出
- 配置EXTI:选择中断线、触发方式、使能中断
- 配置NVIC:设置中断优先级,使能中断通道
代码长这样:
// 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// 配置PA0为输入,上拉
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PC13为推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 设置NVIC优先级,使能中断
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
小提示:Mode参数用GPIO_MODE_IT_FALLING,HAL库会自动帮我们配置好EXTI和NVIC的关联。如果你用标准库,得手动操作AFIO和EXTI寄存器,麻烦不少。
4.4 中断服务函数怎么写?
中断服务函数,也叫ISR(Interrupt Service Routine)。名字是固定的,不能乱起。EXTI0的中断服务函数叫 EXTI0_IRQHandler。
但咱们一般不直接在里面写业务逻辑。HAL库提供了一个弱函数 HAL_GPIO_EXTI_Callback,我们只需要重写它就行。
为什么?因为HAL库在 EXTI0_IRQHandler 里已经帮我们做了中断标志位的判断和清除,然后才调用回调函数。这样我们就不用操心底层细节了。
代码示例:
// 中断服务函数(HAL库已实现,我们不用动)
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
// 回调函数——我们在这里写业务逻辑
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
// 翻转LED
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
你看,代码是不是很简洁?但这里有个坑——中断里不要做耗时操作。比如延时、打印、复杂计算,统统不要放进去。我见过有人直接在中断里调 HAL_Delay,结果系统直接卡死。为什么?因为 HAL_Delay 依赖SysTick中断,而SysTick中断优先级可能比EXTI低,就死锁了。
警告:中断服务函数要短小精悍。如果确实需要做复杂处理,建议用“中断上半部+下半部”的模式——中断里只设一个标志位,主循环里轮询这个标志位再处理。
4.5 中断标志位清除——这个不能忘
中断触发后,硬件会自动置位一个标志位。如果你不清除它,中断会反复触发,CPU就卡死在中断里了。
HAL库的 HAL_GPIO_EXTI_IRQHandler 函数里,会自动调用 __HAL_GPIO_EXTI_CLEAR_IT 来清除标志位。所以用HAL库基本不用操心。
但如果你用标准库或者直接操作寄存器,就得手动清:
// 标准库方式清除EXTI0标志位
EXTI->PR = EXTI_PR_PR0; // 写1清除
我记得有一次调试,发现按键按一次,LED闪了好几下。查了半天,发现是中断标志位没清干净,导致重复进入中断。从那以后,我每次写中断都会检查标志位清除的代码。
4.6 按键消抖——实战中的必修课
机械按键有个物理特性:按下和松开时,电平会抖动几次,持续约10~20ms。如果不处理,一次按键可能触发多次中断。
消抖有两种方式:
- 硬件消抖:加RC滤波电路,简单粗暴
- 软件消抖:检测到中断后,延时20ms再读一次电平确认
我个人更推荐软件消抖,因为不增加成本。代码实现:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
// 延时20ms消抖
HAL_Delay(20);
// 再次确认按键是否真的按下(低电平)
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
}
注意:上面这个写法有个小问题——HAL_Delay 在中断里用有风险。更稳妥的做法是:在中断里只设一个标志位,主循环里检测到标志位后,再延时消抖并处理。这样既安全又可靠。
4.7 知识体系图
下面这张图,把EXTI中断的完整流程画出来了。从按键按下到LED翻转,每一步都清清楚楚。
4.8 常见问题与避坑
最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 中断不触发 | 按键按了没反应 | GPIO模式没配成中断模式,或NVIC没使能 | 检查Mode参数是否为GPIO_MODE_IT_FALLING,检查HAL_NVIC_EnableIRQ |
| 中断反复触发 | 按一次LED闪多次 | 按键抖动,或标志位未清除 | 加软件消抖,检查标志位清除代码 |
| 中断里卡死 | 程序跑飞或死机 | 中断里调用了HAL_Delay等阻塞函数 | 中断里只设标志位,主循环处理 |
| 多个中断冲突 | 优先级低的无法响应 | NVIC优先级配置不当 | 合理分配抢占优先级和子优先级 |
嗯,这一节的内容就到这儿。EXTI驱动其实不难,关键是理解中断的整个链路——从硬件触发,到NVIC调度,再到ISR执行,最后清标志位返回。每一步都环环相扣,漏掉一个就可能出问题。
你写代码的时候,建议先拿一个按键、一个LED练手。跑通了再往项目里加。别一上来就搞复杂的外设,容易把自己绕晕。
公众号:蓝海资料掘金营,微信deep3321