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()。但直接调它可不够,有几个坑得先填上。
标准流程分三步:
- 配置唤醒源(RTC闹钟或外部中断)
- 设置Stop模式进入参数(电压调节器模式、是否关闭Flash)
- 调用进入函数,并处理唤醒后的时钟恢复
代码骨架长这样:
// 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() 下一行继续执行。
__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模式实战流程,我把它画成了流程图,方便你对照代码理解。
流程看着简单,但实际调试时容易在“恢复时钟”这一步翻车。我遇到过好几次,唤醒后忘了重新配置时钟,结果串口打印出来全是乱码。后来我养成了一个习惯:在 HAL_PWR_EnterSTOPMode() 后面直接跟 SystemClock_Config(),一秒都不耽误。
4.7 避坑指南
实战中还有几个坑,我一个个说给你听。
Stop模式下,大部分外设的寄存器内容会丢失(除了RTC、备份域、部分LPGPIO)。唤醒后需要重新初始化用到的外设。我建议把外设初始化函数封装成一个
Periph_ReInit(),在唤醒后统一调用。
唤醒源的中断优先级必须高于或等于当前正在执行的中断优先级。如果你在某个高优先级中断里调用
HAL_PWR_EnterSTOPMode(),那么低优先级的唤醒源是叫不醒MCU的。解决办法:要么在进入Stop前把优先级设好,要么确保不在中断里进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模式下会漏电。