4、待机模式实战:待机模式特性、RTC唤醒配置、备份域寄存器操作

待机模式,说白了就是让芯片进入一种「几乎完全断电」的状态。这时候内核、大部分外设、甚至 RAM 都停了,只剩下几个关键的唤醒源还在工作。我刚开始接触这个模式时,总觉得它跟停机模式差不多,后来才发现——差远了。

4.1 待机模式的核心特性

待机模式是 STM32 低功耗三兄弟里最「狠」的一个。它跟睡眠、停机模式最大的区别在于:RAM 内容会丢失。你想想看,连 SRAM 都掉电了,那程序执行状态自然也就没了。

特性 待机模式 停机模式 睡眠模式
内核时钟 关闭 关闭 关闭(WFI)/ 可选
SRAM 保持 丢失 保持 保持
寄存器状态 大部分丢失 保持 保持
唤醒源 WKUP 引脚、RTC、NRST 任意中断 任意中断/事件
唤醒后行为 复位 继续执行 继续执行

唤醒后芯片会执行一次完整的复位流程,从 Reset_Handler 重新跑。这一点很多人会踩坑——我当年第一次用待机模式时,以为唤醒后能接着睡前的代码继续跑,结果发现变量全没了,程序直接从头开始。

重要提醒:进入待机模式前,一定要把关键数据保存到备份域寄存器或外部 EEPROM 里。否则唤醒后数据就丢了。

4.2 进入待机模式的步骤

进入待机模式其实就三步,但每一步都有细节要注意。我习惯按这个流程来写代码:

  1. 清除 WUF 标志位——PWR 寄存器里的唤醒标志,不清掉的话可能进不去待机。
  2. 设置 PDDS 位——告诉系统我们要进的是待机模式,不是停机模式。
  3. 执行 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 的闹钟或唤醒定时器。下面是一个典型的配置流程:

  1. 使能 PWR 和 RTC 时钟
  2. 配置 RTC 时钟源(通常用 LSE,32.768 kHz)
  3. 设置唤醒定时器的重装载值
  4. 使能 RTC 唤醒中断(虽然待机模式下中断不响应,但标志位会置起)
  5. 进入待机模式
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;
}
经验之谈:RTC 唤醒定时器的重装载值范围是 0x0000 ~ 0xFFFF,配合不同的预分频器,可以实现从几微秒到几十秒的唤醒周期。我一般用 16 位 + 异步预分频,这样精度和范围都能兼顾。

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 再写回去。这样即使主电源断了,计数也不会丢。后来客户要求统计设备运行时长,这个数据帮了大忙。

关键点:备份域寄存器在系统复位后仍然保持数据,但如果是 POR(上电复位)或 BOR(掉电复位),数据可能会丢失。所以设计时要注意区分复位类型。

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 待机模式的知识体系

下面这张图把待机模式的核心流程串起来了,从进入条件到唤醒后的处理,一目了然:

待机模式核心流程 待机模式核心流程 正常运行 配置 RTC / WKUP 引脚 保存关键数据到备份域 执行 WFI 进入待机 唤醒 → 系统复位 注意点 • 唤醒后从 Reset_Handler 执行 • RAM 内容全部丢失 • 需通过备份域恢复数据 唤醒源 • RTC 闹钟 / 唤醒定时器 • WKUP 引脚上升沿 • NRST 复位引脚

4.7 避坑指南

待机模式看起来简单,实际用起来坑不少。我把自己踩过的坑列出来,你遇到了可以少走弯路:

  • 备份域访问权限:操作备份域寄存器前,必须先设置 PWR->CR |= PWR_CR_DBP。我刚开始忘了这一步,写进去的数据读出来全是 0。
  • RTC 时钟源选择:如果用了 LSI 作为 RTC 时钟源,待机模式下 LSI 可能会停振。我建议用 LSE,32.768 kHz 晶振在待机模式下依然稳定。
  • 唤醒标志清除:进入待机模式前一定要清除 WUF 标志。不清的话,有些型号的芯片会直接跳过待机,唤醒后立刻又睡过去。
  • 调试器干扰:连接调试器时,待机模式可能无法正常进入。我一般用串口打印来调试,或者加一个延时让调试器先断开。
曾经踩过的坑:有一次我在产品中用了待机模式,RTC 唤醒定时配置的是 10 秒。结果实际测试发现,有时候 10 秒唤醒,有时候 20 秒才唤醒。查了半天,发现是 RTC 预分频器的配置跟时钟源不匹配。后来统一用 LSE + 异步预分频,问题就解决了。

待机模式虽然「狠」,但只要把备份域用好、唤醒源配对,它其实是低功耗场景下最省电的方案。我个人在电池供电的产品中,待机电流能做到 1.5 µA 左右,配合 RTC 定时唤醒,一节 CR2032 电池能撑一年多。