4、待机模式实战:待机模式特性、RTC唤醒配置、备份域寄存器操作
待机模式,说白了就是让芯片进入一种「几乎完全断电」的状态。这时候内核、大部分外设、甚至 RAM 都停了,只剩下几个关键的唤醒源还在工作。我刚开始接触这个模式时,总觉得它跟停机模式差不多,后来才发现——差远了。
4.1 待机模式的核心特性
待机模式是 STM32 低功耗三兄弟里最「狠」的一个。它跟睡眠、停机模式最大的区别在于:RAM 内容会丢失。你想想看,连 SRAM 都掉电了,那程序执行状态自然也就没了。
| 特性 | 待机模式 | 停机模式 | 睡眠模式 |
|---|---|---|---|
| 内核时钟 | 关闭 | 关闭 | 关闭(WFI)/ 可选 |
| SRAM 保持 | 丢失 | 保持 | 保持 |
| 寄存器状态 | 大部分丢失 | 保持 | 保持 |
| 唤醒源 | WKUP 引脚、RTC、NRST | 任意中断 | 任意中断/事件 |
| 唤醒后行为 | 复位 | 继续执行 | 继续执行 |
唤醒后芯片会执行一次完整的复位流程,从 Reset_Handler 重新跑。这一点很多人会踩坑——我当年第一次用待机模式时,以为唤醒后能接着睡前的代码继续跑,结果发现变量全没了,程序直接从头开始。
4.2 进入待机模式的步骤
进入待机模式其实就三步,但每一步都有细节要注意。我习惯按这个流程来写代码:
- 清除 WUF 标志位——PWR 寄存器里的唤醒标志,不清掉的话可能进不去待机。
- 设置 PDDS 位——告诉系统我们要进的是待机模式,不是停机模式。
- 执行 WFI/WFE 指令——触发实际的状态切换。
代码实现大概是这样的:
void Enter_StandbyMode(void)
{
// 1. 清除唤醒标志
PWR->CR |= PWR_CR_CWUF;
// 2. 设置 PDDS 位,选择待机模式
PWR->CR |= PWR_CR_PDDS;
// 3. 执行 WFI 指令进入待机
__WFI();
}
嗯,这里要注意:__WFI() 执行后,芯片会立刻进入待机模式。如果调试器还连着,有时候会卡住——我遇到过好几次,后来养成习惯:调试待机模式时,先断开调试器,用串口打印来观察行为。
4.3 RTC 唤醒配置
RTC 是待机模式下最常用的唤醒源。它本身有独立的电源域(VBAT),即使主电源断了也能跑。我个人觉得,用 RTC 做定时唤醒是最优雅的方案——不需要外部引脚,精度也够。
配置 RTC 唤醒待机模式,核心是设置 RTC 的闹钟或唤醒定时器。下面是一个典型的配置流程:
- 使能 PWR 和 RTC 时钟
- 配置 RTC 时钟源(通常用 LSE,32.768 kHz)
- 设置唤醒定时器的重装载值
- 使能 RTC 唤醒中断(虽然待机模式下中断不响应,但标志位会置起)
- 进入待机模式
void RTC_Wakeup_Config(uint32_t seconds)
{
// 使能 PWR 时钟
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
// 允许访问备份域
PWR->CR |= PWR_CR_DBP;
// 使能 RTC 时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
// 选择 LSE 作为 RTC 时钟源
RCC->BDCR |= RCC_BDCR_RTCSEL_0;
// 等待 RTC 寄存器同步
RTC->ISR |= RTC_ISR_INIT;
while (!(RTC->ISR & RTC_ISR_INITF));
// 设置唤醒定时器
RTC->CR &= ~RTC_CR_WUTE;
while (!(RTC->ISR & RTC_ISR_WUTWF));
RTC->WUTR = seconds - 1; // 重装载值
RTC->CR |= RTC_CR_WUTE; // 使能唤醒定时器
RTC->CR |= RTC_CR_WUTIE; // 使能唤醒中断
// 退出初始化模式
RTC->ISR &= ~RTC_ISR_INIT;
}
4.4 备份域寄存器操作
备份域寄存器(Backup Registers)是待机模式下的「救命稻草」。它们位于 VBAT 供电区域,主电源掉电也不会丢失数据。我习惯把系统状态、校准参数、运行计数这些关键信息存到这里。
备份域有 20 个 32 位寄存器(BKPxR,x=1~20),操作前需要先解锁:
void BackupReg_Write(uint32_t reg_index, uint32_t data)
{
// 使能 PWR 时钟
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
// 允许访问备份域
PWR->CR |= PWR_CR_DBP;
// 写入备份寄存器
BKP->REG[reg_index] = data;
}
uint32_t BackupReg_Read(uint32_t reg_index)
{
// 使能 PWR 时钟
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
// 允许访问备份域
PWR->CR |= PWR_CR_DBP;
// 读取备份寄存器
return BKP->REG[reg_index];
}
我曾经在一个项目中用备份域来保存「设备唤醒次数」。每次待机唤醒后,读取备份域的值加 1 再写回去。这样即使主电源断了,计数也不会丢。后来客户要求统计设备运行时长,这个数据帮了大忙。
4.5 待机模式唤醒后的程序流程
唤醒后芯片从 Reset_Handler 开始执行。这时候需要判断:是上电复位还是待机唤醒复位?
判断方法很简单——检查 RCC 的复位状态寄存器:
void Check_Reset_Source(void)
{
if (RCC->CSR & RCC_CSR_SBF) {
// 待机唤醒复位
// 从备份域恢复数据
uint32_t wakeup_count = BackupReg_Read(0);
wakeup_count++;
BackupReg_Write(0, wakeup_count);
// 清除标志
RCC->CSR |= RCC_CSR_RMVF;
} else {
// 上电复位,初始化所有数据
BackupReg_Write(0, 0);
}
}
这个判断逻辑我建议放在 main() 函数的最前面。这样一上电就能区分场景,避免把备份域里的「旧数据」当成「新数据」来用。
4.6 待机模式的知识体系
下面这张图把待机模式的核心流程串起来了,从进入条件到唤醒后的处理,一目了然:
4.7 避坑指南
待机模式看起来简单,实际用起来坑不少。我把自己踩过的坑列出来,你遇到了可以少走弯路:
- 备份域访问权限:操作备份域寄存器前,必须先设置
PWR->CR |= PWR_CR_DBP。我刚开始忘了这一步,写进去的数据读出来全是 0。 - RTC 时钟源选择:如果用了 LSI 作为 RTC 时钟源,待机模式下 LSI 可能会停振。我建议用 LSE,32.768 kHz 晶振在待机模式下依然稳定。
- 唤醒标志清除:进入待机模式前一定要清除 WUF 标志。不清的话,有些型号的芯片会直接跳过待机,唤醒后立刻又睡过去。
- 调试器干扰:连接调试器时,待机模式可能无法正常进入。我一般用串口打印来调试,或者加一个延时让调试器先断开。
待机模式虽然「狠」,但只要把备份域用好、唤醒源配对,它其实是低功耗场景下最省电的方案。我个人在电池供电的产品中,待机电流能做到 1.5 µA 左右,配合 RTC 定时唤醒,一节 CR2032 电池能撑一年多。