4. 睡眠模式(Sleep Mode):WFI与WFE指令、唤醒源配置、实测功耗数据

好,咱们今天聊聊睡眠模式。说实话,这是STM32低功耗设计里最基础、也最常用的一种模式。我刚开始做低功耗项目那会儿,第一个接触的就是它。为什么?因为它简单、可靠,而且功耗下降的效果立竿见影。

4.1 睡眠模式到底是个啥?

说白了,睡眠模式就是让CPU核心停下来歇着,但外设该干活还干活。你想想看,CPU是芯片里最耗电的模块之一,它一停,电流立马就降下来了。

在睡眠模式下,系统时钟还在跑,SRAM和寄存器里的数据也全部保留。外设像UART、SPI、定时器、ADC这些,你想让它们继续工作,它们就能继续工作。嗯,这里要注意:CPU虽然睡了,但一旦有中断或者事件发生,它马上就能醒过来,响应速度极快。

核心特点:

  • CPU时钟停止,但外设时钟继续
  • SRAM和寄存器内容全部保留
  • 唤醒延迟极低(通常几个时钟周期)
  • 唤醒后立即执行中断服务程序或后续代码

4.2 WFI与WFE指令:两种睡觉姿势

进入睡眠模式有两种指令:WFI和WFE。我经常跟团队里的新人说,这俩就像你睡觉的两种姿势——一个等着被叫醒,一个等着被推醒。

WFI(Wait For Interrupt)

执行WFI后,CPU立刻进入睡眠状态。只有当中断发生时,它才会醒来。我个人习惯在需要精确控制唤醒时机的地方用WFI,比如等待某个外设中断完成。

// 进入睡眠模式(WFI方式)
void enter_sleep_mode_wfi(void)
{
    // 清除唤醒标志
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
    
    // 设置SLEEPDEEP位为0,进入睡眠模式
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    
    // 醒来后从这里继续执行
    // 注意:如果中断服务程序没有返回,这里不会执行
}

WFE(Wait For Event)

WFE的唤醒条件更丰富一些。除了中断,它还能被事件唤醒。事件可以是外部事件线,也可以是SEV指令。我在项目中遇到过一种场景:多个外设需要同步唤醒,用WFE配合事件就特别方便。

// 进入睡眠模式(WFE方式)
void enter_sleep_mode_wfe(void)
{
    // 清除事件标志
    __SEV();
    __WFE();
    
    // 进入睡眠
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFE);
    
    // 醒来后继续执行
    // WFE方式醒来后,程序会直接从这里继续
}

我的经验:WFI和WFE的选择其实有讲究。如果你希望中断服务程序执行完后自动回到主循环,用WFI。如果你希望事件发生后程序继续往下走,用WFE。我曾经在一个数据采集项目里用错了,结果程序逻辑全乱了,排查了半天才发现是唤醒方式的问题。

4.3 唤醒源配置

睡眠模式的唤醒源其实挺多的。我整理了一下,常用的有这些:

唤醒源 说明 典型应用
外部中断(EXTI) GPIO上升沿/下降沿触发 按键唤醒、传感器触发
RTC闹钟 定时唤醒 周期性数据采集
USART接收中断 串口收到数据 通信唤醒
定时器中断 定时器溢出或比较匹配 PWM控制、定时任务
ADC转换完成 模拟量采集完成 传感器数据读取

配置唤醒源其实不复杂,关键是要把对应的中断使能打开。我举个例子,用外部中断唤醒:

// 配置PA0作为外部中断唤醒源
void configure_wakeup_source(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能GPIOA时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 配置PA0为输入,上拉
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 使能EXTI0中断
    HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

// 中断服务程序
void EXTI0_IRQHandler(void)
{
    // 检查中断标志
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        // 清除中断标志
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        
        // 处理唤醒事件
        // 比如设置一个标志位,主循环里判断
    }
}

注意:我曾经踩过一个坑——配置了外部中断唤醒,但忘了在进入睡眠前清除中断标志。结果一执行WFI,CPU立刻被同一个中断唤醒,根本睡不进去。排查了整整一个下午才发现。所以,进入睡眠前一定要确保所有中断标志都被清除了。

4.4 实测功耗数据

光说不练假把式。我拿STM32F103C8T6做了个实测,用万用表串在电源线上测的。环境是:主频72MHz,所有外设时钟默认开启。

模式 典型电流 说明
正常运行(72MHz) 约50mA 所有外设开启,跑while循环
睡眠模式(WFI) 约6mA CPU停止,外设继续运行
睡眠模式(WFE) 约6mA 和WFI基本一致
睡眠模式+关闭不用的外设时钟 约2mA 只保留必要的UART和定时器

看到没?从50mA降到6mA,降了将近90%。如果你再把不用的外设时钟关掉,还能再降。我有个项目,电池供电的,原来续航只有3天,优化了睡眠模式后直接撑到2周。

优化建议:进入睡眠前,把用不到的外设时钟关掉。比如你只用UART1,那就把UART2、UART3、SPI、I2C这些的时钟全关了。别小看这几毫安,积少成多。

4.5 知识体系图

我画了张图,把睡眠模式的核心逻辑串起来,方便你理解:

睡眠模式知识体系 睡眠模式 进入方式 WFI指令 WFE指令 唤醒源 外部中断 RTC闹钟 UART接收 定时器 功耗优化 关闭外设时钟 降低系统时钟 核心:CPU停止 + 外设继续 + 快速唤醒

4.6 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了能少走弯路:

  • 中断优先级:睡眠模式下,只有优先级足够高的中断才能唤醒CPU。我遇到过配置了中断但优先级设得太低,结果CPU一直睡不醒。
  • DMA传输:睡眠模式下DMA可以继续工作,但要注意DMA传输完成中断的优先级。我曾经用DMA+UART接收数据,结果DMA传完了CPU没醒,因为中断优先级没配好。
  • 调试器连接:进入睡眠模式后,调试器可能会断开连接。我建议你在调试阶段先别进睡眠,或者加个延时,等调试器连上了再睡。
  • 唤醒后的状态:WFI唤醒后会执行中断服务程序,然后返回主循环。WFE唤醒后直接继续执行下一条指令。这个区别很重要,别搞混了。

好了,睡眠模式就聊到这儿。你想想看,其实它并不复杂,关键是把进入方式、唤醒源和功耗优化这三块搞清楚。下一节咱们聊停止模式,那个功耗更低,但要注意的东西也更多。


公众号:蓝海资料掘金营,微信deep3321