2. 外部中断唤醒与按键消抖
任务定义
本案例实现一个基于外部中断的按键唤醒系统:MCU进入深度睡眠模式后,按键产生的下降沿通过EXTI唤醒MCU;唤醒后在中断服务程序中启动软件定时器消抖,消抖确认有效按键后执行一次任务(翻转LED状态),随后重新进入低功耗模式。目标芯片为STM32L0系列,代码基于HAL库。
硬件连接
软件消抖原理
按键按下时会产生约5–20ms的机械抖动,若在EXTI中断中直接读取电平并执行任务,极易误触发。本案例采用“中断唤醒+定时器确认”的两步消抖策略:EXTI下降沿唤醒MCU后,先关闭该引脚中断,启动一个10ms的软件定时器;定时器到期后再次读取引脚电平,若仍为低电平则判定为有效按键,执行任务;否则视为抖动,直接重新进入低功耗。该方案避免了阻塞式延时,使MCU在消抖等待期间仍处于低功耗状态。
代码实现
// main.c — STM32L0 外部中断唤醒按键消抖示例
#include "stm32l0xx_hal.h"
/* 引脚定义 */
#define KEY_PIN GPIO_PIN_0
#define KEY_PORT GPIOA
#define LED_PIN GPIO_PIN_5
#define LED_PORT GPIOB
#define KEY_EXTI_IRQ EXTI0_1_IRQn
/* 状态标志 */
static volatile uint8_t key_pressed = 0; // 消抖确认标志
static volatile uint8_t debounce_started = 0;
/* 软件定时器计数(SysTick 1ms中断累加) */
static volatile uint32_t tick_ms = 0;
static uint32_t debounce_timeout = 0;
void SystemClock_Config(void);
static void GPIO_Init(void);
static void EnterLowPower(void);
static void ExecuteTask(void);
/* SysTick 1ms 中断 */
void SysTick_Handler(void) {
tick_ms++;
}
/* EXTI0中断服务函数 */
void EXTI0_1_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(KEY_PIN) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(KEY_PIN);
if (!debounce_started) {
/* 关闭EXTI中断,防止消抖期间重复触发 */
HAL_NVIC_DisableIRQ(KEY_EXTI_IRQ);
/* 记录消抖起始时间 */
debounce_timeout = tick_ms + 10; // 10ms后确认
debounce_started = 1;
}
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
GPIO_Init();
/* 配置SysTick 1ms中断 */
HAL_SYSTICK_Config(SystemCoreClock / 1000);
while (1) {
/* 消抖确认逻辑(主循环中非阻塞检查) */
if (debounce_started) {
if ((int32_t)(tick_ms - debounce_timeout) >= 0) {
debounce_started = 0;
/* 再次读取引脚电平确认 */
if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) {
key_pressed = 1;
}
/* 重新使能EXTI中断 */
HAL_NVIC_EnableIRQ(KEY_EXTI_IRQ);
}
}
/* 执行任务 */
if (key_pressed) {
key_pressed = 0;
ExecuteTask();
}
/* 进入低功耗模式 */
EnterLowPower();
}
}
static void ExecuteTask(void) {
/* 翻转LED状态 */
HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
}
static void EnterLowPower(void) {
/* 进入STOP模式,仅保留EXTI唤醒 */
HAL_SuspendTick(); // 暂停SysTick,降低功耗
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 唤醒后恢复 */
HAL_ResumeTick();
SystemClock_Config(); // 重新配置时钟
}
static void GPIO_Init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
/* PA0 按键输入,下拉电阻 */
gpio.Pin = KEY_PIN;
gpio.Mode = GPIO_MODE_IT_FALLING;
gpio.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(KEY_PORT, &gpio);
/* PB5 LED输出 */
gpio.Pin = LED_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_PORT, &gpio);
/* 使能EXTI中断 */
HAL_NVIC_SetPriority(KEY_EXTI_IRQ, 2, 0);
HAL_NVIC_EnableIRQ(KEY_EXTI_IRQ);
}
void SystemClock_Config(void) {
/* 使用MSI 2.1MHz,最低功耗配置 */
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};
osc.OscillatorType = RCC_OSCILLATORTYPE_MSI;
osc.MSIState = RCC_MSI_ON;
osc.MSIClockRange = RCC_MSIRANGE_5; // 2.097MHz
osc.MSICalibrationValue = 0;
HAL_RCC_OscConfig(&osc);
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV1;
clk.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_0);
}
编译与烧录
# 使用STM32CubeIDE或Makefile工程编译
# 依赖:STM32CubeL0 HAL库
arm-none-eabi-gcc -mcpu=cortex-m0plus -mthumb \
-DSTM32L072xx -IInc \
-c main.c -o main.o
# 链接生成hex后通过ST-Link烧录
st-flash write firmware.hex 0x08000000
验证方法
测试步骤:
- 烧录程序后,观察LED初始状态(默认灭);
- 用示波器探头连接PA0引脚,按下按键并释放,观察波形存在抖动;
- 快速点击按键(<10ms间隔),LED不应翻转——验证消抖有效;
- 正常按下按键(保持>50ms后释放),LED状态翻转——验证任务执行;
- 用电流表串联供电端,待机时测量电流应处于μA级(STOP模式典型值约3μA);
- 在按键按下瞬间观察电流波形出现短暂上升,随后恢复低电平——验证唤醒后重新入睡。
判定标准:快速抖动不触发LED翻转,正常按键每次可靠翻转一次;待机电流低于5μA;每次按键唤醒后MCU能自动重新进入低功耗状态。