4. Stop模式实战:配置Stop模式,使用RTC或外部中断唤醒,保留SRAM数据

Stop模式,说白了就是让STM32进入一种“浅睡”状态。内核停了,时钟也停了,但SRAM和寄存器里的数据还在。我刚开始做低功耗项目时,总觉得Stop模式比Sleep模式复杂不少,其实摸清门道后也就那么回事。

这一章,咱们就动手把Stop模式跑起来。目标很明确:进入Stop模式,用RTC闹钟或者外部按键中断唤醒,醒来后数据不丢,程序接着跑。

4.1 Stop模式的核心特征

先看一张表,把Stop模式和之前讲的Sleep模式做个对比。你心里就有数了。

特性 Sleep模式 Stop模式
CPU时钟 关闭 关闭
系统时钟(HSE/HSI) 运行 关闭
SRAM与寄存器 保持 保持
唤醒源 任意中断 EXTI(特定线)
典型电流 ~mA级 ~µA级
唤醒延迟 几µs 几十µs

看到没?Stop模式把主时钟都关了,所以功耗能降到微安级别。代价是唤醒稍微慢一点,而且不是所有中断都能叫醒它——只有连接到EXTI的特定事件才行。

4.2 进入Stop模式的正确姿势

进入Stop模式,STM32的HAL库给了我们一个现成的函数:HAL_PWR_EnterSTOPMode()。但直接调它可不够,有几个坑得先填上。

重要提醒: 进入Stop模式前,一定要确保所有外设的DMA传输已经完成。否则数据可能丢,甚至唤醒后外设卡死。我吃过这个亏,后来每次进Stop前都加一个标志位检查。

标准流程分三步:

  1. 配置唤醒源(RTC闹钟或外部中断)
  2. 设置Stop模式进入参数(电压调节器模式、是否关闭Flash)
  3. 调用进入函数,并处理唤醒后的时钟恢复

代码骨架长这样:

// 1. 配置RTC闹钟(假设RTC已初始化)
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, FORMAT_BIN);

// 2. 进入Stop模式
//    PWR_STOPENTRY_WFI:使用WFI指令等待中断
//    PWR_MAINREGULATOR_ON:主调压器保持开启,唤醒更快
HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI);

// 3. 唤醒后第一件事:重新配置系统时钟
//    因为HSE/HSI在Stop模式下被关闭了
SystemClock_Config();

嗯,这里要注意:SystemClock_Config() 必须放在唤醒后的第一行。否则你后面调任何外设,都会因为时钟不对而出问题。

4.3 唤醒源之一:RTC闹钟

RTC闹钟唤醒,是定时唤醒场景的首选。比如传感器每隔10秒采集一次数据,中间时间就睡过去。

配置RTC闹钟的步骤,我习惯这样写:

RTC_AlarmTypeDef sAlarm = {0};

// 设置闹钟时间为10秒后
sAlarm.AlarmTime.Hours = 0;
sAlarm.AlarmTime.Minutes = 0;
sAlarm.AlarmTime.Seconds = 10;
sAlarm.AlarmTime.SubSeconds = 0;
sAlarm.AlarmMask = RTC_ALARMMASK_NONE;  // 精确匹配
sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;
sAlarm.Alarm = RTC_ALARM_A;

// 使能闹钟中断
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, FORMAT_BIN);

闹钟触发后,会进入RTC中断回调。在回调里,你只需要清除闹钟标志位,别的不用做——因为唤醒后程序会从 HAL_PWR_EnterSTOPMode() 下一行继续执行。

个人经验: 我建议在RTC闹钟中断回调里只做一件事:__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF)。别在里面处理业务逻辑,否则容易和主循环的时序打架。

4.4 唤醒源之二:外部中断(EXTI)

外部中断唤醒,适合按键触发、传感器中断信号等场景。比如一个低功耗门磁,平时睡大觉,门一开就通过GPIO中断叫醒MCU。

配置外部中断的要点:

  • GPIO必须配置为中断模式(不是事件模式)
  • 中断线必须连接到EXTI
  • 使能NVIC中对应的中断通道

以PA0引脚为例:

GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();

GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能中断线
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

中断服务函数里,同样只需要清除标志位:

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)
    {
        // 清除挂起位,啥也不干
        // 主循环会处理按键逻辑
    }
}

4.5 保留SRAM数据:一个容易忽略的细节

Stop模式下SRAM默认是保持的,但有个前提:VDD供电不能断。如果你用的是电池供电,电池电压掉到MCU的最低工作电压以下,SRAM数据就保不住了。

另外,有些STM32系列(比如L4、U5)提供了备份SRAM,由VBAT引脚供电。即使主电源掉电,备份SRAM里的数据也能保留。如果你的项目有“掉电不丢数据”的需求,可以考虑把关键变量放到备份SRAM里。

举个例子,用属性定义把变量放在备份SRAM:

// 放在备份SRAM中的计数器
__attribute__((section(".backup_sram"))) uint32_t boot_count = 0;

不过要注意,备份SRAM的访问速度比主SRAM慢一点,而且容量有限。别把大数组往里塞。

4.6 完整实战流程

下面是一个完整的Stop模式实战流程,我把它画成了流程图,方便你对照代码理解。

Stop模式实战流程图 1. 系统初始化 2. 配置唤醒源(RTC闹钟 / EXTI) 3. HAL_PWR_EnterSTOPMode() 4. 等待唤醒(WFI) 5. SystemClock_Config() 恢复时钟 6. 继续执行主循环 SRAM数据全程保持

流程看着简单,但实际调试时容易在“恢复时钟”这一步翻车。我遇到过好几次,唤醒后忘了重新配置时钟,结果串口打印出来全是乱码。后来我养成了一个习惯:在 HAL_PWR_EnterSTOPMode() 后面直接跟 SystemClock_Config(),一秒都不耽误。

4.7 避坑指南

实战中还有几个坑,我一个个说给你听。

坑1:唤醒后外设配置丢失
Stop模式下,大部分外设的寄存器内容会丢失(除了RTC、备份域、部分LPGPIO)。唤醒后需要重新初始化用到的外设。我建议把外设初始化函数封装成一个 Periph_ReInit(),在唤醒后统一调用。
坑2:中断优先级导致无法唤醒
唤醒源的中断优先级必须高于或等于当前正在执行的中断优先级。如果你在某个高优先级中断里调用 HAL_PWR_EnterSTOPMode(),那么低优先级的唤醒源是叫不醒MCU的。解决办法:要么在进入Stop前把优先级设好,要么确保不在中断里进Stop。
坑3:调试器连接时无法进入Stop
这是新手最容易懵的地方。当你用ST-Link调试时,调试器会阻止MCU真正进入Stop模式。你看到的电流可能还是mA级。别慌,拔掉调试器,用万用表测一下,电流就下来了。

4.8 实测数据参考

最后给一组我在STM32F103上实测的数据,供你参考:

条件 典型电流 唤醒时间
Stop模式,主调压器开启 ~35 µA ~10 µs
Stop模式,低功耗调压器 ~8 µA ~50 µs
Stop模式 + RTC运行 ~40 µA ~10 µs

注意,不同型号、不同时钟配置下,数据会有差异。但量级不会差太多。如果你的实测电流比表格里大一个数量级,先检查是不是有GPIO没配置好——浮空引脚在Stop模式下会漏电。