低功耗模式基础(一):睡眠模式

各位同学,今天咱们来啃低功耗模式的第一块硬骨头——睡眠模式。说实话,我刚接触STM32L4的时候,觉得低功耗不就是个“睡觉”功能嘛,有啥好学的?结果第一次做产品,电池续航死活达不到指标,才发现这里面的门道深着呢。

睡眠模式是STM32L4所有低功耗模式里最“温柔”的一种。它只停了CPU的时钟,外设和内存都还在跑。你想想看,这就像你躺在床上闭着眼,但耳朵还能听,心脏还在跳——随时可以醒过来干活。

睡眠模式的核心原理

先看一张图,把睡眠模式的整体逻辑理清楚:

STM32L4 睡眠模式核心流程 正常运行 CPU + 外设全速 执行 WFI/WFE 暂停CPU时钟 睡眠状态 外设继续工作 中断/事件触发 唤醒CPU 关键说明: • WFI(Wait For Interrupt):等待中断唤醒,执行后立即进入睡眠 • WFE(Wait For Event):等待事件唤醒,可被SEV指令或硬件事件触发 • 睡眠模式下,SRAM和寄存器内容全部保持,唤醒后从断点继续执行

这张图把睡眠模式的完整生命周期画出来了。从正常运行到执行WFI/WFE指令,再到睡眠状态,最后被中断或事件唤醒——整个过程就像按了个暂停键。

WFI 与 WFE 指令的区别

这两个指令是进入睡眠模式的“开关”。我刚开始学的时候,总觉得它们差不多,直到有一次在项目里用错了,导致设备死活唤不醒……

对比项 WFI(Wait For Interrupt) WFE(Wait For Event)
唤醒条件 任何中断触发 事件(中断或SEV指令)
执行后行为 立即进入睡眠 检查事件寄存器,有挂起事件则跳过睡眠
典型场景 串口接收唤醒、定时器唤醒 外设事件同步、多核通信
功耗 相同 相同
唤醒延迟 固定几个时钟周期 固定几个时钟周期

我的经验:如果你只是想让CPU在等待串口数据时睡觉,用WFI就够了。WFE更适合那种“有事件就处理,没事件就睡”的轮询场景——比如ADC采样完成事件触发CPU处理数据。

进入睡眠模式的代码实现

代码其实很简单,但有几个坑得注意。先看基础版:

/* 进入睡眠模式 - 使用WFI */
void enter_sleep_mode_wfi(void)
{
    /* 清除唤醒标志 */
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
    
    /* 设置睡眠模式(默认就是Sleep Mode) */
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    
    /* 唤醒后从这里继续执行 */
    /* 注意:唤醒后先执行中断服务函数,再回到这里 */
}

嗯,这里要注意一个细节:PWR_MAINREGULATOR_ON 表示主稳压器保持开启,这样唤醒速度最快。如果你用 PWR_LOWPOWERREGULATOR_ON,功耗更低但唤醒会慢一些。

再看WFE版本:

/* 进入睡眠模式 - 使用WFE */
void enter_sleep_mode_wfe(void)
{
    /* 清除事件寄存器 */
    __SEV();
    __WFE();
    
    /* 进入睡眠 */
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFE);
    
    /* 唤醒后继续 */
}

注意:WFE有个“陷阱”——如果事件寄存器里已经有挂起事件,执行WFE会直接跳过睡眠!所以标准做法是先SEV再WFE,把事件寄存器清干净,然后再执行一次WFE进入睡眠。我曾经在这个坑里趴了一整天……

串口唤醒实战

这是睡眠模式最常用的场景:CPU睡觉,串口等着收数据,收到就醒。我当年做的一个智能传感器项目就是这么干的——平时功耗只有1.2mA,串口一发命令就醒。

先配置串口中断:

/* 配置USART2中断唤醒 */
void uart_wakeup_config(void)
{
    USART_TypeDef *USARTx = USART2;
    
    /* 使能USART2时钟 */
    __HAL_RCC_USART2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    /* 配置TX(PA2)和RX(PA3) */
    /* ... GPIO配置代码省略 ... */
    
    /* 使能接收中断 */
    USARTx->CR1 |= USART_CR1_RXNEIE;
    
    /* 使能USART2中断 */
    HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART2_IRQn);
}

然后在主循环里这样用:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    uart_wakeup_config();
    
    uint8_t rx_byte = 0;
    
    while (1)
    {
        /* 进入睡眠,等待串口数据 */
        printf("Entering sleep mode...\r\n");
        enter_sleep_mode_wfi();
        
        /* 被串口中断唤醒后执行 */
        printf("Woke up!\r\n");
        
        /* 处理接收到的数据 */
        if (HAL_UART_Receive(&huart2, &rx_byte, 1, 100) == HAL_OK)
        {
            printf("Received: 0x%02X\r\n", rx_byte);
        }
        
        /* 处理完继续睡 */
        HAL_Delay(10);
    }
}

关键点:串口中断唤醒后,CPU会先执行中断服务函数(把收到的数据存到缓冲区),然后返回主循环继续执行。所以你在主循环里调用HAL_UART_Receive时,数据已经在缓冲区里了。

功耗测量对比

光说不练假把式。我拿STM32L476RG-NUCLEO开发板实测了一组数据,用万用表串在供电回路里测的:

工作模式 典型电流 说明
正常运行(HSE 80MHz) 约 8.5 mA CPU全速跑,外设全开
正常运行(MSI 2MHz) 约 1.8 mA 降频运行,功耗明显降低
睡眠模式(WFI) 约 1.2 mA CPU停,外设继续
睡眠模式(WFE) 约 1.2 mA 与WFI功耗一致
停止模式(Stop Mode) 约 8 μA 下一章会讲,先对比一下

看到没?从8.5mA降到1.2mA,降了7倍多。但说实话,1.2mA对于电池供电的产品还是太高了——这就是为什么后面还要学停止模式和待机模式。

测量小技巧:测低功耗电流时,记得把调试器的SWD接口断开!因为调试器本身会从目标板取电,而且调试接口的时钟也会阻止CPU进入真正的低功耗状态。我刚开始测的时候,怎么测都是3mA,后来才发现是调试器在“捣乱”。

避坑指南

最后总结几个我踩过的坑,你们记一下:

  • 中断优先级:如果唤醒中断的优先级被屏蔽了,CPU就醒不过来。检查NVIC的优先级分组和中断使能位。
  • 外设时钟:睡眠模式下外设时钟还在跑,但如果你在进睡眠前关了某个外设的时钟,它就不能产生唤醒中断了。
  • GPIO状态:进睡眠前最好把不用的GPIO设成模拟模式或下拉,否则浮空引脚会产生额外的漏电流。
  • 调试器影响:接调试器时,睡眠模式可能无法真正进入。断开调试器再测功耗。