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 的流程写成标准步骤,每次照着做,不容易漏。
- 关掉不用的外设时钟——RCC 寄存器里一个个清。
- 配置唤醒源——比如使能 RTC 闹钟中断。
- 设置引脚状态——不用的引脚设成模拟输入或高阻。
- 配置电源寄存器——选择电压调节器模式(正常/低功耗)。
- 设置 SLEEPDEEP 位——SCR 寄存器。
- 执行 WFI——进入 DeepSleep。
- 醒来后恢复——重新配置时钟、外设、引脚。
// 伪代码:进入 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 知识结构图:睡眠模式全景
下面这张图把三种睡眠模式、唤醒源和切换路径串起来了。你可以把它当作设计时的参考地图。
3.7 常见问题与避坑
- 睡下去醒不来?——检查中断是否使能,NVIC 里有没有配好。WFI 必须等中断,中断没开就永远睡死。
- 醒来后外设不工作?——DeepSleep 模式下,有些外设的时钟会被关掉。醒来后要重新使能时钟,再初始化外设。
- 功耗比预期高?——用电流表逐项排查:引脚漏电、未使用的内部上拉、调试接口(SWD)没关,这些都是常见元凶。
重要提醒:调试期间别直接进 Standby!我曾经在调试时进了 Standby,结果调试器连不上了,只能按复位键。建议先用 Sleep 模式调通唤醒逻辑,再逐步加深睡眠等级。
好了,关于 ARM 睡眠模式的核心内容就这些。记住一句话:没有最好的模式,只有最合适的模式。根据你的功耗预算和唤醒延迟要求,选对模式,配好唤醒源,你的产品续航就能上一个台阶。