STM32 低功耗模式(STOP2)下 RTC 唤醒源与 GPIO 状态保持的边界条件排查

一、STOP2 模式的核心机制

STOP2 模式(对应 Cortex-M4 的深度睡眠)关闭了大部分时钟,但保留 SRAM 和寄存器内容,唤醒延迟约 10µs(相比 Standby 的 3ms 快得多)。其关键特性:

  • 电压域:VDD 保持,但内部 LDO 处于低功耗状态,数字外设时钟全部停止。
  • GPIO 状态:默认保持进入 STOP2 前的输出电平,但若引脚配置为模拟模式或浮空输入,则状态不可控。
  • 唤醒源:支持 RTC 闹钟、外部中断(EXTI)、串口等,但需注意 EXTI 线必须配置为允许唤醒。

二、RTC 唤醒源的边界条件

1. RTC 时钟源选择

RTC 必须使用 LSE(32.768kHz)或 LSI(约 32kHz),不能使用 HSE 或 HSI,因为 STOP2 模式下 HSE/HSI 被关闭。若使用 LSI,其精度较差(±5%),适合短定时;若需精确时间,必须用 LSE 并确保外部晶振已起振。

// 启用 LSE 并等待就绪
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

2. 唤醒中断标志清除顺序

RTC 闹钟唤醒后,必须先清除 RTC 中断标志,再清除 EXTI 线路挂起位。若顺序颠倒,会导致 EXTI 无法正确触发唤醒,或唤醒后立即再次进入中断。

void RTC_Alarm_IRQHandler(void) {
    if (RTC_GetITStatus(RTC_IT_ALRA) != RESET) {
        RTC_ClearITPendingBit(RTC_IT_ALRA);  // 先清 RTC 标志
        EXTI_ClearITPendingBit(EXTI_Line17); // 再清 EXTI 挂起位
    }
}

3. EXTI 线配置

RTC 闹钟唤醒通常映射到 EXTI 线 17(对于 ALRA),需使能 EXTI 中断并配置为上升沿触发(RTC 闹钟事件为脉冲)。

EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line17;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);

三、GPIO 状态保持的边界条件

1. 进入 STOP2 前的 GPIO 配置

GPIO 在 STOP2 下保持输出状态,但若引脚配置为开漏输出且外部无上拉,则电平可能漂移。建议:

  • 对于需要保持高电平的引脚,配置为推挽输出,并设置输出高。
  • 对于需要保持低电平的引脚,配置为推挽输出,并设置输出低。
  • 若引脚外接上拉/下拉电阻,可配置为输入模式,但需注意浮空输入会导致不确定状态。
// 示例:保持 PA5 为高电平
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_5);

2. 模拟引脚的陷阱

若某些引脚被复用为 ADC 输入,进入 STOP2 前必须将其配置为模拟模式,否则会引入漏电流。但模拟模式下 GPIO 状态不保持,若该引脚同时用于唤醒(如外部中断),则需在进入 STOP2 前重新配置为输入模式。

四、完整代码示例

以下代码演示了进入 STOP2、RTC 定时唤醒、GPIO 状态保持的完整流程。

#include "stm32l4xx.h"

void SystemClock_Config(void);
void RTC_Config(void);
void GPIO_Config(void);
void EnterStop2(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    GPIO_Config();
    RTC_Config();

    while (1) {
        // 业务代码
        printf("Running...\n");
        HAL_Delay(1000);

        // 进入 STOP2 前配置 GPIO 状态
        GPIO_SetBits(GPIOA, GPIO_Pin_5); // PA5 保持高
        GPIO_ResetBits(GPIOB, GPIO_Pin_3); // PB3 保持低

        EnterStop2();

        // 唤醒后恢复时钟
        SystemClock_Config();
        printf("Woken up!\n");
    }
}

void EnterStop2(void) {
    // 确保 RTC 中断已使能
    HAL_RTC_AlarmA_IRQHandler(&hrtc);
    // 进入 STOP2
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
    // 唤醒后重新配置系统时钟(因为 STOP2 会关闭 HSI/HSE)
    SystemClock_Config();
}

void RTC_Config(void) {
    __HAL_RCC_RTC_ENABLE();
    // 配置 RTC 闹钟为 10 秒后
    RTC_AlarmTypeDef sAlarm = {0};
    sAlarm.AlarmTime.Seconds = 10;
    sAlarm.AlarmMask = RTC_ALARMMASK_DATE_WEEKDAY | RTC_ALARMMASK_HOURS | RTC_ALARMMASK_MINUTES;
    sAlarm.Alarm = RTC_ALARM_A;
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}

void GPIO_Config(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    // 配置 PA5 为推挽输出,PB3 为推挽输出
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_3;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

五、常见问题排查清单

  • 现象:无法唤醒。检查 RTC 闹钟是否配置正确,LSE 是否起振,EXTI 线是否使能,以及中断优先级是否被屏蔽。
  • 现象:唤醒后立即再次进入 STOP2。检查中断标志清除顺序,以及是否有其他 EXTI 线误触发。
  • 现象:GPIO 状态丢失。确认引脚未配置为模拟模式,且输出类型为推挽。
  • 现象:功耗异常高。检查未使用的 GPIO 是否配置为模拟模式,以及 RTC 时钟源是否选择 LSI(功耗略高)。

六、总结

STOP2 模式下的 RTC 唤醒与 GPIO 保持,本质是时钟域与电源域的协同问题。开发者需牢记:RTC 时钟源必须为 LSE/LSI,中断标志清除顺序不可颠倒,GPIO 配置需避免模拟模式。通过上述配置与排查方法,可有效避免低功耗设计中的“隐性”错误,实现稳定可靠的唤醒机制。