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中断,分四步走:

  1. 使能GPIO时钟:GPIOA和GPIOC的时钟要打开
  2. 配置GPIO模式:PA0设为输入,PC13设为推挽输出
  3. 配置EXTI:选择中断线、触发方式、使能中断
  4. 配置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翻转,每一步都清清楚楚。

EXTI中断完整流程 ① 按键按下 PA0电平变化 ② EXTI检测 下降沿触发 ③ NVIC调度 中断优先级判断 ④ 进入ISR EXTI0_IRQHandler ⑤ 回调函数 HAL_GPIO_EXTI_Callback ⑥ 清除标志位 __HAL_GPIO_EXTI_CLEAR_IT ⑦ 翻转LED PC13电平取反 中断返回,继续执行主程序

4.8 常见问题与避坑

最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:

问题 现象 原因 解决办法
中断不触发 按键按了没反应 GPIO模式没配成中断模式,或NVIC没使能 检查Mode参数是否为GPIO_MODE_IT_FALLING,检查HAL_NVIC_EnableIRQ
中断反复触发 按一次LED闪多次 按键抖动,或标志位未清除 加软件消抖,检查标志位清除代码
中断里卡死 程序跑飞或死机 中断里调用了HAL_Delay等阻塞函数 中断里只设标志位,主循环处理
多个中断冲突 优先级低的无法响应 NVIC优先级配置不当 合理分配抢占优先级和子优先级

嗯,这一节的内容就到这儿。EXTI驱动其实不难,关键是理解中断的整个链路——从硬件触发,到NVIC调度,再到ISR执行,最后清标志位返回。每一步都环环相扣,漏掉一个就可能出问题。

你写代码的时候,建议先拿一个按键、一个LED练手。跑通了再往项目里加。别一上来就搞复杂的外设,容易把自己绕晕。


公众号:蓝海资料掘金营,微信deep3321