3. 睡眠模式(Sleep Mode):进入与退出机制、事件与中断唤醒、WFI与WFE指令区别
睡眠模式,说白了就是让CPU先歇着,但外设还得继续干活。这是STM32所有低功耗模式里最轻量级的一种,也是我项目里用得最多的模式。为什么?因为它进得快、出得快,功耗降得也明显。
我记得刚做低功耗设计那会儿,总觉得睡眠模式太简单,不就是个WFI指令嘛。后来踩了坑才发现,这里面的门道还真不少。今天咱们就把睡眠模式的进入、退出、以及WFI和WFE这两个指令的区别,一次性讲透。
3.1 睡眠模式的本质
先看一张图,帮你快速建立整体认知:
这张图其实就讲了三件事:
- 谁停了?CPU内核的时钟停了。注意,只是CPU自己的时钟停了。
- 谁还在跑?系统时钟、外设时钟、所有外设模块,全都在正常工作。
- 怎么醒?中断、事件、或者特定的唤醒引脚。
我个人的理解是:睡眠模式就像让CPU打个盹,但整个系统还在全速运转。外设该采样的采样,该通信的通信,只是CPU暂时不参与处理了。
3.2 进入睡眠模式
进入睡眠模式,STM32给了我们两条指令:WFI 和 WFE。先看代码,再讲区别。
/* 进入睡眠模式 - 使用WFI */
void enter_sleep_mode_wfi(void)
{
/* 清除SLEEPDEEP位,确保进入的是睡眠模式而非停止/待机 */
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
/* 选择睡眠模式进入方式:立即进入 */
__WFI(); /* Wait For Interrupt */
}
/* 进入睡眠模式 - 使用WFE */
void enter_sleep_mode_wfe(void)
{
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
/* 先清除事件标志,防止立即唤醒 */
__SEV(); /* 发送事件 */
__WFE(); /* 再等待事件,清除挂起标志 */
__WFE(); /* 真正进入睡眠 */
}
3.3 WFI与WFE的核心区别
这两个指令,很多初学者觉得差不多。其实差别大了去了。我直接给你一张对比表:
| 对比项 | WFI (Wait For Interrupt) | WFE (Wait For Event) |
|---|---|---|
| 唤醒条件 | 任何使能的中断 | 事件(包括中断转事件、SEV指令、外部事件) |
| 唤醒后行为 | 进入中断服务函数,执行完后返回休眠处 | 直接继续执行下一条指令,不进入中断 |
| 事件挂起标志 | 无 | 有。如果之前有事件挂起,WFE可能不睡眠 |
| 典型场景 | 等待外设中断(如UART接收、定时器) | 等待外设事件、轮询与休眠混合场景 |
| 功耗 | 相同 | 相同 |
| 中断响应延迟 | 正常中断延迟 | 无中断延迟(不进入ISR) |
你想想看,WFI和WFE最大的区别是什么?不是功耗,而是唤醒后的执行路径。
WFI唤醒后,CPU会先跳到中断向量表,执行对应的中断服务函数,然后再回到休眠的地方继续跑。而WFE唤醒后,CPU直接执行WFE的下一条指令,根本不进中断。
关键点:WFE可以配合中断使用,但需要先把中断配置成"事件模式"。在NVIC里,每个中断都可以设置成"中断模式"或"事件模式"。事件模式下,中断信号到达时不会触发ISR,但会唤醒WFE。
3.4 退出睡眠机制
退出睡眠模式,说白了就是唤醒。唤醒源主要有三类:
- 中断唤醒(WFI/WFE均可):任何使能的中断都能唤醒。比如UART收到数据、定时器溢出、GPIO外部中断。
- 事件唤醒(仅WFE):包括外设事件、软件事件(SEV指令)、以及配置为事件模式的中断。
- 唤醒引脚(WKUP):部分STM32系列支持专用的唤醒引脚,可以在最低功耗模式下唤醒。
这里有个坑,我必须要说:
我曾经踩过的坑:用WFE等待UART接收,结果第一次调用WFE直接就返回了,根本没睡眠。查了半天才发现,UART在初始化时产生了接收中断,事件挂起标志被置位了。所以WFE之前一定要先清事件标志,就是代码里那个两次__WFE()的写法。
3.5 实战:两种模式的代码对比
咱们直接看两个实际例子,你就明白什么时候用WFI,什么时候用WFE了。
场景一:等待UART接收数据(用WFI)
/* UART接收中断唤醒 */
void uart_rx_demo(void)
{
uint8_t data;
printf("进入睡眠,等待UART数据...\n");
/* 进入睡眠模式 */
enter_sleep_mode_wfi();
/* 醒来后,已经在UART中断里处理完数据了 */
/* 这里直接读取接收缓冲区 */
data = uart_get_byte();
printf("收到数据: 0x%02X\n", data);
}
/* UART中断服务函数 */
void USART1_IRQHandler(void)
{
if(USART1->SR & USART_SR_RXNE)
{
/* 读取数据,存入缓冲区 */
uart_rx_buf[uart_rx_idx++] = USART1->DR;
}
}
这个场景用WFI很自然。为什么?因为UART数据到了,我们需要进中断去读数据、存缓冲区。WFI唤醒后自动进ISR,流程很顺畅。
场景二:等待定时器标志(用WFE)
/* 定时器事件唤醒 - 不进入中断 */
void timer_event_demo(void)
{
/* 配置定时器更新事件,但不使能中断 */
TIM2->DIER |= TIM_DIER_UIE; /* 使能更新中断 */
NVIC_EnableIRQ(TIM2_IRQn);
/* 关键:在NVIC里把TIM2中断设为事件模式 */
EXTI->EMR |= EXTI_EMR_MR18; /* 假设TIM2映射到EXTI18 */
while(1)
{
/* 等待定时器更新事件 */
__WFE();
/* 直接在这里处理,不进中断 */
/* 检查标志位 */
if(TIM2->SR & TIM_SR_UIF)
{
TIM2->SR = ~TIM_SR_UIF; /* 清除标志 */
/* 执行定时任务 */
do_periodic_task();
}
}
}
我建议:如果你的应用里中断已经很多了,不想再增加中断负担,就用WFE+事件模式。这样既省了中断上下文切换的开销,又能让CPU在空闲时休眠。我在一个多传感器采集项目里就是这么干的,中断负载降了30%。
3.6 睡眠模式下的功耗实测
光说不练假把式。我拿STM32F103实测过一组数据,给你参考:
| 模式 | 典型电流 | 唤醒时间 | 外设状态 |
|---|---|---|---|
| 运行模式 (72MHz) | ~50mA | - | 全部使能 |
| 睡眠模式 (WFI) | ~10mA | ~1.2μs | 全部使能 |
| 睡眠模式 (WFE) | ~10mA | ~1.0μs | 全部使能 |
| 停止模式 | ~20μA | ~5μs | 部分关闭 |
看到没?睡眠模式比运行模式省了80%的电流,但唤醒时间只有1微秒左右。这就是为什么我说它是"性价比最高"的低功耗模式。
实际经验:在一个电池供电的温湿度采集项目中,我把主循环改成"干活→睡眠→干活→睡眠"的模式,用定时器中断唤醒。电池续航从原来的3天直接干到了2周。代价?代码就多了三行:一行清SLEEPDEEP,一行WFI,一行处理唤醒后的逻辑。
3.7 避坑指南
最后,把我这些年踩过的坑总结一下:
- 坑1:WFE被事件挂起标志坑了。前面说过,初始化时产生的中断/事件会让WFE立即返回。解决办法:调用两次WFE,或者先清事件标志。
- 坑2:中断优先级配置不当导致无法唤醒。如果中断优先级被屏蔽了(比如BASEPRI设得过高),中断来了也唤醒不了CPU。检查你的中断优先级设置。
- 坑3:SLEEPDEEP位没清。如果你之前用过停止模式或待机模式,SLEEPDEEP位可能还是1。不清掉的话,执行WFI会直接进停止模式,功耗更低但唤醒逻辑完全不同。
- 坑4:调试器影响睡眠。接上调试器(ST-LINK/J-Link)时,睡眠模式可能无法正常工作。因为调试器会阻止CPU真正休眠。这是正常的,量产时就好了。
嗯,睡眠模式的内容就这些。说白了就是两条指令、两种唤醒方式、几个注意事项。但就是这几个简单的点,用好了能让你的产品功耗降一个数量级。下次咱们聊停止模式,那个比睡眠模式复杂多了,但功耗也更诱人。