3. 外部中断唤醒:GPIO外部中断配置、上升沿/下降沿触发、中断服务函数编写、实际项目中的按键唤醒案例

好,咱们接着聊低功耗唤醒。上一章讲了RTC定时唤醒,那是按时间点醒。这一章我们聊聊外部中断唤醒——说白了,就是让外部的一个电平变化把芯片叫醒。

我个人觉得,外部中断唤醒是实际项目里用得最多的方式。为什么?因为它的响应快,而且非常灵活。你想想看,一个按键按下去,一个传感器输出跳变,甚至是一根线被拔插,都能触发唤醒。我做过一个手持设备,用户按一下电源键,系统要在50毫秒内从休眠状态恢复到正常工作。这种场景,用外部中断是最合适的。

3.1 GPIO外部中断配置

配置外部中断,第一步就是把GPIO口设置成中断模式。不同的MCU,配置方式略有差异,但核心思路是一样的。

我以STM32为例,讲一下典型的配置流程。嗯,这里要注意,不是所有GPIO都能做外部中断。通常只有特定的几个引脚,比如PA0、PB0、PC0这些,才能映射到外部中断线。

核心配置步骤:

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO为输入模式,设置上下拉
  3. 将GPIO引脚连接到对应的EXTI线
  4. 配置EXTI中断触发方式(上升沿/下降沿/双边沿)
  5. 使能EXTI中断线
  6. 配置NVIC中断优先级并使能

这里有个坑,我刚开始做的时候踩过。配置完GPIO后,一定要记得把SYSCFG时钟打开,否则EXTI映射不生效。我当时查了两天,最后发现是时钟没开,气得不行。

// 以STM32L4为例,配置PA0为外部中断
void EXTI_Config(void)
{
    // 1. 使能时钟
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;    // GPIOA时钟
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;   // SYSCFG时钟
    
    // 2. 配置PA0为输入,浮空模式
    GPIOA->MODER &= ~(0x03 << 0);           // 00: 输入模式
    GPIOA->PUPDR &= ~(0x03 << 0);          // 00: 无上下拉
    
    // 3. 将PA0连接到EXTI0
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
    
    // 4. 配置下降沿触发(按键按下)
    EXTI->FTSR1 |= EXTI_FTSR1_FT0;          // 下降沿触发
    EXTI->RTSR1 &= ~EXTI_RTSR1_RT0;         // 禁止上升沿
    
    // 5. 使能EXTI0中断线
    EXTI->IMR1 |= EXTI_IMR1_IM0;
    
    // 6. 配置NVIC
    NVIC_SetPriority(EXTI0_IRQn, 2);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

3.2 上升沿/下降沿触发选择

触发方式的选择,说白了就是看你想要什么信号变化来唤醒系统。

触发方式 触发条件 典型应用场景
上升沿触发 信号从低电平变为高电平 传感器输出高电平有效、按键释放检测
下降沿触发 信号从高电平变为低电平 按键按下检测、低电平有效的中断信号
双边沿触发 电平变化即触发 脉冲计数、编码器信号

我个人习惯,按键唤醒一般用下降沿触发。为什么?因为按键按下时,电平从高变低,这个跳变很干脆。而且下降沿触发可以避免按键释放时的误触发。

不过,我在一个项目中遇到过一个问题。某个传感器输出的是开漏信号,平时是高阻态,靠外部上拉电阻拉到高电平。当传感器检测到事件时,输出低电平。按理说用下降沿触发没问题。但实际测试发现,传感器上电瞬间,输出会有一个不确定的抖动,导致系统刚唤醒又被误触发。后来我改成了双边沿触发,然后在中断服务函数里加了一个软件去抖,才解决。

注意: 在低功耗模式下,有些MCU的GPIO内部上下拉电阻可能被禁用。如果你依赖外部上拉电阻来维持电平,一定要确认在休眠状态下这个上拉是否有效。我曾经因为没注意这个,导致芯片在休眠时引脚电平浮空,频繁误唤醒。

3.3 中断服务函数编写

中断服务函数,是唤醒后的第一段代码。写得好不好,直接影响系统的响应速度和稳定性。

我总结了几条原则:

  • 快进快出:中断里只做最必要的事,比如清除中断标志、设置一个唤醒标志。复杂的处理放到主循环里做。
  • 清除标志位:进入中断后,第一件事就是清除中断标志。否则会反复进入中断,系统根本跑不起来。
  • 避免延时:不要在中断里用delay或者while等待。这会阻塞其他中断,甚至导致系统看门狗超时。
  • 考虑重入:如果中断可能被更高优先级的中断打断,要注意共享资源的保护。
// 外部中断0的中断服务函数
void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志
    if(EXTI->PR1 & EXTI_PR1_PIF0)
    {
        // 2. 清除中断标志(必须!)
        EXTI->PR1 = EXTI_PR1_PIF0;
        
        // 3. 设置唤醒标志
        g_wakeup_flag = 1;
        
        // 4. 记录唤醒源(方便调试)
        g_wakeup_source = WAKEUP_SOURCE_KEY;
        
        // 5. 可以在这里做简单的去抖
        // 但更推荐在主循环里做
    }
}

小技巧: 我习惯在中断服务函数里加一个计数器,记录中断发生的次数。调试的时候,通过查看这个计数器的值,可以判断是否有误触发。比如按键按下一次,计数器却增加了10次,那肯定是有抖动。

3.4 实际项目中的按键唤醒案例

好了,理论讲完了,咱们看一个实际案例。这是一个手持设备,用户通过按键唤醒系统,然后执行一些操作,再自动进入休眠。

硬件连接很简单:一个按键,一端接地,另一端接MCU的PA0。内部上拉使能,平时PA0是高电平。按键按下,PA0被拉低,产生下降沿中断。

这里有个细节,我特别想强调:按键去抖。机械按键按下和释放的瞬间,会有几毫秒到十几毫秒的抖动。如果不处理,一次按键可能会触发多次中断。

我的做法是:在中断里只设置一个标志,然后在主循环里做延时去抖。这样既不影响中断的响应速度,又能可靠地检测按键。

// 主循环中的按键处理
void Key_Process(void)
{
    if(g_wakeup_flag)
    {
        // 延时去抖,等待电平稳定
        Delay_ms(20);
        
        // 再次读取按键电平
        if(GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0)
        {
            // 确认按键确实被按下
            // 执行唤醒后的操作
            System_Wakeup_Handler();
            
            // 等待按键释放
            while(GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0);
            
            // 释放后再次去抖
            Delay_ms(20);
        }
        
        // 清除唤醒标志
        g_wakeup_flag = 0;
    }
}

这个案例看起来简单,但实际项目中要考虑的东西不少。比如:

  • 如果按键按下时间很长,系统会不会反复唤醒?——我一般会在唤醒后加一个超时判断,如果按键一直按着,只处理一次。
  • 休眠前要不要把GPIO配置恢复?——有些MCU在休眠时会改变GPIO状态,需要重新配置。
  • 多个按键怎么处理?——每个按键分配一个中断线,或者用矩阵键盘扫描。

我记得有一次,客户反馈说设备偶尔会自己唤醒。排查了很久,发现是电源纹波太大,导致GPIO电平抖动,触发了中断。后来在硬件上加了一个RC滤波,软件上也做了去抖,问题才解决。所以,硬件和软件要配合好,不能只靠一头。

总结一下: 外部中断唤醒,核心就是配置好GPIO、选对触发方式、写好中断服务函数。实际项目中,按键去抖和误触发防护是重点。多留个心眼,多测试几种场景,你的系统就会更可靠。