睡眠模式详解:WFI与WFE指令、睡眠模式进入与退出、唤醒源配置

各位同学,咱们接着聊。上一章我们把Cortex-M的几种低功耗模式捋了一遍,知道了睡眠模式是门槛最低、退出最快的一种。今天我就带大家深入睡眠模式内部,把WFI、WFE这两个指令吃透,再聊聊怎么进、怎么出,以及唤醒源怎么配。

说实话,我早年刚接触ARM时,觉得WFI和WFE不就是两条休眠指令嘛,有啥好研究的?直到有一次产品量产前发现电池续航差了一大截,排查了三天才发现是WFE用错了场景。嗯,从那以后我再也不敢小看这两条指令了。

WFI与WFE:两条指令,两种哲学

先看指令本身。WFI全称Wait For Interrupt,WFE全称Wait For Event。名字上就差了一个字,行为上却差了不少。

特性 WFI WFE
触发条件 必须等到中断发生 等待事件(中断或SEV)
退出延迟 固定,约几个时钟周期 略快,可立即响应事件
事件寄存器 不涉及 依赖事件寄存器状态
典型场景 深度睡眠、等待中断 轮询等待、自旋锁
中断挂起 必须使能且挂起才退出 事件寄存器为1即可退出

你想想看,WFI就像在等一个电话——电话不来,你就一直等着。WFE则像在等一个通知——可能是电话,也可能是门铃,甚至可能是你之前收到的一条短信还没看。

为什么会有这种区别?因为Cortex-M内部有一个事件寄存器。WFE执行时,如果事件寄存器已经是1,它会直接清零并继续执行,根本不停。如果事件寄存器是0,它才真正进入睡眠。而WFI不管寄存器状态,直接睡。

核心区别一句话:WFI是"必须被叫醒",WFE是"看看有没有未读消息,没有就睡"。

睡眠模式的进入流程

进入睡眠模式,说白了就三步:配寄存器、选模式、执行指令。但每一步都有坑,我一个个说。

第一步:配置系统控制寄存器

主要操作SCR(System Control Register)的SLEEPDEEP位。睡眠模式下,这一位必须清零。

// 进入睡眠模式,SLEEPDEEP = 0
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;

// 可选:配置SLEEPONEXIT
// 0 = 执行WFI/WFE时立即睡眠
// 1 = 从异常返回时自动睡眠
SCB->SCR &= ~SCB_SCR_SLEEPONEXIT_Msk;  // 默认行为

我个人习惯把SLEEPONEXIT保持为0。为什么?因为SLEEPONEXIT=1时,中断处理完回到主循环又会自动睡,有时候你还没准备好它就睡了,容易出问题。

第二步:配置唤醒源

睡眠模式下,大部分外设时钟还在跑。所以唤醒源的选择比深度睡眠丰富得多。常用的有:

  • GPIO中断:按键、传感器触发
  • 定时器中断:SysTick、LPTIM、RTC闹钟
  • 串口中断:UART接收数据
  • DMA完成中断:数据传输完毕
  • 比较器输出:模拟信号阈值触发

这里要注意:中断必须使能,而且NVIC中对应的中断也要使能。我见过有人配了外设中断,但忘了在NVIC里使能,结果WFI下去就再也醒不来了。嗯,这种问题查起来真的很痛苦。

第三步:执行WFI或WFE

这一步最简单,但也最容易忽略细节。

// 方式一:直接内联汇编
__WFI();   // 或 __WFE();

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

执行完这条指令,内核立即暂停。PC指针停在下一指令,等待唤醒。

小技巧:执行WFI前,建议先执行一条__DSB()(数据同步屏障),确保所有内存访问完成。否则可能出现外设还没准备好,内核就睡了的情况。

睡眠模式的退出机制

退出睡眠模式,比进入要复杂一些。因为涉及中断响应、现场恢复、时钟稳定等多个环节。

WFI的退出路径

  1. 外设产生中断请求
  2. 中断到达NVIC,NVIC判断优先级
  3. 内核被唤醒,恢复时钟(睡眠模式下时钟通常未停,所以这一步很快)
  4. 执行中断服务程序(ISR)
  5. ISR执行完毕,返回主循环

整个过程,从中断触发到ISR第一条指令执行,通常只需要几个到十几个时钟周期。这也是睡眠模式延迟低的原因——时钟没停,PLL没关,唤醒就是一瞬间的事。

WFE的退出路径

WFE的退出有两种情况:

  • 中断唤醒:和WFI一样,但WFE不会因为中断而进入ISR,除非中断使能且优先级足够
  • 事件唤醒:由SEV指令或其他核发送的事件触发,不经过中断系统

我记得有一次做多核通信,用WFE+SEV实现核间同步,比用中断快了不少。但要注意,WFE的事件唤醒不会自动清除中断标志,你得手动处理。

避坑指南:WFE退出后,事件寄存器会被硬件清零。如果你在循环中使用WFE,一定要确保每次进入WFE前事件寄存器是0。否则WFE会直接跳过,导致CPU空转。我曾经在RTOS的Idle任务里踩过这个坑,CPU占用率显示为0,但功耗死活降不下来。

唤醒源配置实战

光说不练假把式。我们拿一个实际场景来演示:用RTC闹钟唤醒睡眠模式,每秒钟唤醒一次,做一次数据采集。

// 1. 配置RTC闹钟(以STM32为例)
RTC->CR |= RTC_CR_ALRAE;          // 使能闹钟A
RTC->ALRMAR = 0x00000001;         // 每秒触发一次

// 2. 使能RTC中断
RTC->CR |= RTC_CR_ALRAIE;         // 闹钟中断使能
NVIC_EnableIRQ(RTC_Alarm_IRQn);   // NVIC使能

// 3. 配置睡眠模式
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;  // 睡眠模式
__DSB();                              // 数据同步屏障

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

// 5. 醒来后,ISR中处理
void RTC_Alarm_IRQHandler(void) {
    if(RTC->ISR & RTC_ISR_ALRAF) {
        RTC->ISR &= ~RTC_ISR_ALRAF;   // 清除标志
        // 采集数据...
    }
}

这段代码看起来简单,但有几个关键点:

  • RTC的时钟源必须是低速时钟(如LSE),不能在睡眠时停掉
  • 闹钟中断标志必须在ISR中清除,否则下次无法触发
  • NVIC使能不能忘,否则中断到了内核也醒不来

睡眠模式与深度睡眠的边界

很多初学者搞不清睡眠模式和深度睡眠的区别。我画了一张图,帮大家理清关系。

Cortex-M 睡眠模式 vs 深度睡眠模式 睡眠模式 CPU 内核暂停 (时钟门控,不执行指令) 外设时钟保持运行 (UART、定时器、DMA等) 唤醒延迟:2~12 时钟周期 (几乎无延迟) 典型功耗:~1mA (取决于外设活动) 唤醒源:所有使能中断 (GPIO、定时器、串口等) 深度睡眠模式 CPU 内核暂停 (同睡眠模式) 大部分外设时钟关闭 (仅保留唤醒所需外设) 唤醒延迟:~100μs (需等待PLL重新锁定) 典型功耗:~10μA (仅保留SRAM和RTC) 唤醒源:有限 (RTC、WKUP引脚、复位) SLEEPDEEP=0 SLEEPDEEP=1 选择哪种模式,取决于你的唤醒延迟要求和功耗目标

从图上可以看得很清楚:睡眠模式是用功耗换速度,深度睡眠是用速度换功耗。选哪个,取决于你的应用场景。

我的建议:如果唤醒频率高(比如每秒几十次),用睡眠模式。如果唤醒频率低(比如几秒甚至几分钟一次),用深度睡眠。我在一个智能门锁项目里,门磁触发用睡眠模式,日常待机用深度睡眠,两者结合,一节电池撑了两年。

常见问题与避坑

最后,我把这些年遇到的坑总结一下,你们遇到类似问题可以直接翻到这里查。

问题现象 可能原因 解决方案
WFI后无法唤醒 中断未使能或NVIC未配置 检查外设中断使能位和NVIC使能寄存器
WFE执行后立即返回 事件寄存器为1 执行SEV或清除事件寄存器
唤醒后外设工作异常 时钟配置被改变 检查RCC寄存器,确保时钟源未切换
功耗比预期高 GPIO浮空或外设未关闭 将未使用的GPIO设为输出低或上拉/下拉
中断响应延迟大 中断优先级配置不当 将唤醒中断设为最高优先级