3. ARM 睡眠模式详解:WFI/WFE 指令、Sleep/DeepSleep/Standby 模式切换与唤醒源配置

低功耗设计,说白了就是让芯片在没事干的时候赶紧歇着。但怎么歇、歇多深、怎么叫醒,这里面的门道可不少。我做了这么多年嵌入式,见过太多因为睡眠模式没用好,导致产品续航翻车的案例。今天咱们就把 ARM 的睡眠机制彻底聊透。

3.1 睡眠的入口:WFI 与 WFE 指令

ARM 提供了两条指令让处理器进入休眠:WFI(Wait For Interrupt)和 WFE(Wait For Event)。

  • WFI:执行后处理器立即停止,直到有中断发生才醒来。这是最常用的休眠指令。
  • WFE:执行后处理器暂停,但可以被事件(Event)唤醒,不一定要中断。事件可以来自外部信号,也可以是其他核发送的。

核心区别:WFI 必须等中断;WFE 可以被事件唤醒,响应更快,适合核间同步场景。

我个人的习惯是:产品中 90% 的场景用 WFI 就够了。WFE 更多用在多核系统中,比如 Cortex-A 系列。在 Cortex-M 上,WFI 是绝对主力。

// 汇编示例:执行 WFI 指令
__asm volatile("wfi");

// CMSIS 封装方式
__WFI();  // 推荐使用,可移植性好

小技巧:执行 WFI 前,建议先关掉不必要的时钟和外设中断,避免被意外唤醒后又马上睡回去,造成“假休眠真耗电”。

3.2 三种睡眠深度:Sleep、DeepSleep、Standby

ARM 的睡眠模式不是一刀切的。根据功耗和唤醒延迟的权衡,通常分为三级。我拿 STM32 的 Cortex-M 系列举例,各家芯片大同小异。

模式 CPU 状态 SRAM 状态 外设状态 典型功耗 唤醒延迟
Sleep 停止 保持 保持 ~mA 级 几 us
DeepSleep 停止 保持 可选保持 ~uA 级 几十 us
Standby 掉电 丢失 大部分掉电 ~nA 级 几 ms

你看这个表就明白了:睡得越深,省电越多,但醒来越慢。选哪个模式,取决于你的应用场景。

3.3 模式切换:怎么从 Sleep 切到 DeepSleep?

ARM Cortex-M 内核通过 SLEEPDEEP 位来控制睡眠深度。这个位在系统控制寄存器(SCR)中。

// 设置 SLEEPDEEP 位,进入 DeepSleep
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;

// 清除 SLEEPDEEP 位,进入普通 Sleep
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;

// 执行 WFI 进入对应模式
__WFI();

嗯,这里要注意:光设 SLEEPDEEP 位还不够。芯片厂商通常还会在电源控制寄存器里再设一级,比如 STM32 的 PWR 寄存器。你得先配置好电压调节器模式,再设 SLEEPDEEP,最后执行 WFI。

我曾经踩过的坑:在进入 DeepSleep 前,没有把外部总线的引脚拉成高阻态,结果漏电流比 DeepSleep 本身的功耗还大。后来用电流表一测,发现多耗了 50uA。记住:引脚状态比内核模式更重要。

3.4 唤醒源配置:谁叫得醒你?

不同睡眠模式,能用的唤醒源不一样。我整理了一张对照表,方便你选型时参考。

唤醒源 Sleep DeepSleep Standby
外部中断 (GPIO) ✓ ✓ ✗
定时器 (RTC) ✓ ✓ ✓
UART 接收 ✓ ✓ ✗
WKUP 引脚 ✗ ✗ ✓
复位 ✓ ✓ ✓

为什么 Standby 模式只能用 RTC 和 WKUP 引脚?因为大部分外设都掉电了,只有最基础的唤醒逻辑还在工作。你想想看,都睡到 nA 级了,还想让 UART 等着收数据?那不可能。

3.5 实战配置流程:以 DeepSleep 为例

我习惯把进入 DeepSleep 的流程写成标准步骤,每次照着做,不容易漏。

  1. 关掉不用的外设时钟——RCC 寄存器里一个个清。
  2. 配置唤醒源——比如使能 RTC 闹钟中断。
  3. 设置引脚状态——不用的引脚设成模拟输入或高阻。
  4. 配置电源寄存器——选择电压调节器模式(正常/低功耗)。
  5. 设置 SLEEPDEEP 位——SCR 寄存器。
  6. 执行 WFI——进入 DeepSleep。
  7. 醒来后恢复——重新配置时钟、外设、引脚。
// 伪代码:进入 DeepSleep 并等待 RTC 唤醒
void enter_deep_sleep_with_rtc(void) {
    // 1. 关闭外设时钟
    RCC->AHBENR = 0;
    RCC->APB1ENR = 0;
    RCC->APB2ENR = 0;

    // 2. 配置 RTC 闹钟
    RTC->ALRMAR = ...;  // 设置闹钟时间
    RTC->CR |= RTC_CR_ALRAIE;  // 使能闹钟中断

    // 3. 引脚处理
    for (int i = 0; i < 16; i++) {
        GPIO->MODER &= ~(3 << (i*2));  // 设成模拟模式
    }

    // 4. 电源配置
    PWR->CR |= PWR_CR_LPDS;  // 低功耗 DeepSleep

    // 5. 设 SLEEPDEEP
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;

    // 6. 进入睡眠
    __WFI();

    // 7. 醒来后恢复
    system_clock_reconfig();
    gpio_reconfig();
    peripheral_reconfig();
}

避坑指南:我曾经在恢复阶段忘了重新配置 SysTick 定时器,结果 RTOS 的调度直接崩了。醒来后第一件事,先把系统时钟和滴答定时器搞定。

3.6 知识结构图:睡眠模式全景

下面这张图把三种睡眠模式、唤醒源和切换路径串起来了。你可以把它当作设计时的参考地图。

ARM 睡眠模式全景图 ARM 睡眠模式全景图 Sleep 模式 DeepSleep 模式 Standby 模式 唤醒源 外部中断 (GPIO) RTC 闹钟 UART 接收 WKUP 设计要点 • Sleep 模式:唤醒快,适合频繁进出休眠的场景(如按键扫描) • DeepSleep 模式:平衡功耗与响应,IoT 设备的主力模式 • Standby 模式:极致省电,但唤醒后相当于复位,需重新初始化

3.7 常见问题与避坑

  • 睡下去醒不来?——检查中断是否使能,NVIC 里有没有配好。WFI 必须等中断,中断没开就永远睡死。
  • 醒来后外设不工作?——DeepSleep 模式下,有些外设的时钟会被关掉。醒来后要重新使能时钟,再初始化外设。
  • 功耗比预期高?——用电流表逐项排查:引脚漏电、未使用的内部上拉、调试接口(SWD)没关,这些都是常见元凶。

重要提醒:调试期间别直接进 Standby!我曾经在调试时进了 Standby,结果调试器连不上了,只能按复位键。建议先用 Sleep 模式调通唤醒逻辑,再逐步加深睡眠等级。

好了,关于 ARM 睡眠模式的核心内容就这些。记住一句话:没有最好的模式,只有最合适的模式。根据你的功耗预算和唤醒延迟要求,选对模式,配好唤醒源,你的产品续航就能上一个台阶。