4. 深入睡眠模式:Sleep模式原理、进入与退出机制、事件与中断唤醒

低功耗设计里,Sleep模式是最基础、也最常用的一招。说白了,就是让CPU停下来歇着,但外设还醒着。我刚开始做低功耗项目时,总觉得Sleep模式太简单,不就是个WFI指令吗?后来踩了坑才发现,这里面的门道比想象中多得多。

4.1 Sleep模式的本质

STM32的Sleep模式,核心动作就一个:关闭CPU内核时钟,但保持所有外设时钟、SRAM和寄存器内容不变。CPU停止取指执行,一旦有中断或事件,立刻恢复运行。

你想想看,这就像你坐在工位上闭目养神——人没离开座位,周围的声音还能听到,随时可以睁眼干活。相比Stop模式和Standby模式,Sleep模式的唤醒延迟最低,通常只需要几个CPU周期。

特性 Sleep模式 Stop模式 Standby模式
CPU时钟 关闭 关闭 关闭
外设时钟 保持 可配置 关闭
SRAM内容 保持 保持 丢失
唤醒延迟 ~几个周期 ~几微秒 ~几毫秒
典型功耗 ~mA级 ~μA级 ~nA级

关键认知:Sleep模式不是万能药。如果你的系统大部分时间都在等待外设事件,Sleep模式很合适。但如果需要长时间休眠(比如传感器每隔几分钟采集一次),Stop或Standby模式更划算。

4.2 进入Sleep模式:WFI与WFE

进入Sleep模式有两种指令:WFI(Wait For Interrupt)和WFE(Wait For Event)。

WFI:执行后CPU立即休眠,直到有中断发生才唤醒。这是最常用的方式。

WFE:执行后检查事件寄存器,如果有挂起事件则不清除直接继续执行;如果没有则休眠,直到事件发生。事件可以来自外设,也可以是软件触发的SEV指令。

我个人习惯在大多数场景下用WFI,因为逻辑更直观。WFE适合那种需要轮询事件标志但又不想浪费CPU的场景。

// 进入Sleep模式的标准写法
void enter_sleep_mode(void)
{
    // 1. 确保所有外设操作完成
    __DSB();  // 数据同步屏障
    __ISB();  // 指令同步屏障
    
    // 2. 清除唤醒标志(某些系列需要)
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
    
    // 3. 设置Sleep模式并进入
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    
    // 4. 唤醒后从这里继续执行
    // 注意:不会复位,所有寄存器保持原值
}

小技巧:进入Sleep前加DSB和ISB指令,能确保之前的所有内存访问和指令都完成。我在一个项目中遇到过进入Sleep后立即被意外唤醒的问题,排查了半天,最后发现就是少了这两条屏障指令。

4.3 退出Sleep模式:中断唤醒

中断唤醒是Sleep模式最常用的退出方式。任何未被屏蔽的中断都能把CPU从Sleep中拉回来。

这里有个容易忽略的点:中断优先级。如果所有中断的抢占优先级都设置为0,那么任何中断都能唤醒CPU。但如果你用了优先级分组,只有优先级高于当前CPU优先级的中断才能唤醒。

我曾经在一个项目中,把定时器中断优先级设得比SysTick还低,结果定时器死活唤不醒CPU。嗯,后来查了参考手册才明白这个机制。

// 定时器中断唤醒示例
void TIM2_IRQHandler(void)
{
    // 清除中断标志
    __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
    
    // 处理唤醒后的业务
    process_sensor_data();
    
    // 注意:不要在中断里做耗时操作
    // 建议只设置标志位,主循环处理
}

// 主循环中的休眠逻辑
while (1)
{
    // 检查是否需要休眠
    if (system_idle())
    {
        // 进入Sleep,等待TIM2中断唤醒
        enter_sleep_mode();
        
        // 唤醒后继续执行
        handle_wakeup_events();
    }
    else
    {
        process_active_tasks();
    }
}

4.4 事件唤醒机制

事件唤醒是STM32的一个特色功能。它和中断唤醒的区别在于:事件不需要CPU进入中断服务程序,直接唤醒后继续执行下一条指令。

这有什么用?我举个例子:你有一个外部按键,按下时唤醒CPU读取传感器。如果用中断唤醒,CPU要进中断、出中断,至少几十个周期。如果用事件唤醒,CPU直接醒来继续跑主循环,省去了中断进出开销。

注意:事件唤醒虽然快,但无法在中断服务程序里做处理。所有唤醒后的逻辑都得在主循环里完成。如果你的唤醒事件需要立即响应,还是用中断唤醒更合适。

// 配置EXTI为事件模式
void configure_event_wakeup(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能GPIO时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    
    // 配置PA0为输入
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_EVT_RISING;  // 事件模式
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 连接到EXTI0
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
    
    // 使能EXTI0事件(注意:不是中断)
    EXTI->IMR &= ~EXTI_IMR_IM0;   // 屏蔽中断
    EXTI->EMR |= EXTI_EMR_EM0;    // 使能事件
}

4.5 核心流程图

下面这张图展示了Sleep模式的完整生命周期,从进入条件判断到唤醒后的处理流程。

Sleep模式生命周期流程图 系统运行中 是否满足休眠条件? (无待处理任务、外设空闲) 否 准备进入Sleep DSB/ISB、清除唤醒标志 执行WFI/WFE Sleep状态(CPU停止) 外设运行、SRAM保持、等待唤醒事件 中断/事件到达 唤醒后处理 检查唤醒源、处理事件 继续主循环

4.6 避坑指南

我这些年做低功耗项目,在Sleep模式上踩过的坑不少。挑几个典型的说说:

  • 外设时钟没关干净:进入Sleep前,把不需要的外设时钟关了。否则外设还在跑,功耗降不下来。我曾经有个项目,进入Sleep后电流还有5mA,查了半天发现是SPI时钟没关。
  • 中断标志没清干净:进入Sleep前,确保所有挂起的中断标志都清除了。否则一进Sleep就被唤醒,形成死循环。
  • GPIO电平没处理好:进入Sleep前,检查所有GPIO的电平状态。浮空输入会漏电,推挽输出如果驱动了外部电路也可能耗电。
  • 调试器干扰:连接调试器时,Sleep模式可能无法正常工作。因为调试器会保持CPU的调试时钟。我建议在调试低功耗时,用串口打印代替在线调试。

重要提醒:Sleep模式下,如果SysTick中断使能,它会周期性唤醒CPU。这在某些场景下是需要的(比如做时间片轮询),但如果你的目标是深度休眠,记得在进入Sleep前禁用SysTick中断。

4.7 实战建议

最后给几个实战中的建议:

  1. 测量实际功耗:别只看数据手册。用万用表或功耗分析仪测一下实际电流。我见过数据手册标1mA,实际跑了3mA的情况。
  2. 分阶段测试:先让系统跑起来,再逐个关闭外设,观察功耗变化。这样能快速定位耗电大户。
  3. 保留调试接口:在代码里留一个调试模式,可以强制禁止进入Sleep。方便开发阶段排查问题。
  4. 注意唤醒后的状态恢复:Sleep模式不丢失寄存器内容,但有些外设可能需要重新配置。比如DMA传输了一半进入Sleep,唤醒后要检查DMA状态。