3. 外部中断唤醒:GPIO外部中断配置、上升沿/下降沿触发、中断服务函数编写、实际项目中的按键唤醒案例
好,咱们接着聊低功耗唤醒。上一章讲了RTC定时唤醒,那是按时间点醒。这一章我们聊聊外部中断唤醒——说白了,就是让外部的一个电平变化把芯片叫醒。
我个人觉得,外部中断唤醒是实际项目里用得最多的方式。为什么?因为它的响应快,而且非常灵活。你想想看,一个按键按下去,一个传感器输出跳变,甚至是一根线被拔插,都能触发唤醒。我做过一个手持设备,用户按一下电源键,系统要在50毫秒内从休眠状态恢复到正常工作。这种场景,用外部中断是最合适的。
3.1 GPIO外部中断配置
配置外部中断,第一步就是把GPIO口设置成中断模式。不同的MCU,配置方式略有差异,但核心思路是一样的。
我以STM32为例,讲一下典型的配置流程。嗯,这里要注意,不是所有GPIO都能做外部中断。通常只有特定的几个引脚,比如PA0、PB0、PC0这些,才能映射到外部中断线。
核心配置步骤:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式,设置上下拉
- 将GPIO引脚连接到对应的EXTI线
- 配置EXTI中断触发方式(上升沿/下降沿/双边沿)
- 使能EXTI中断线
- 配置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、选对触发方式、写好中断服务函数。实际项目中,按键去抖和误触发防护是重点。多留个心眼,多测试几种场景,你的系统就会更可靠。