第4章 Sleep模式实战:进入与退出、WFI与WFE、外设状态与功耗数据
好,咱们直接进入Sleep模式。这是STM32L4所有低功耗模式里最“温柔”的一个——内核停了,但大部分外设还在干活。我刚开始做低功耗项目时,总觉得Sleep模式太简单,不就是个WFI指令嘛。结果呢?板子功耗死活降不下来。后来才发现,问题出在外设时钟没关干净。
这一章,咱们就把Sleep模式掰开揉碎。从进入退出,到WFI和WFE的区别,再到外设到底该不该关,最后给出一组实测数据。你跟着走一遍,心里就有底了。
4.1 Sleep模式的本质
Sleep模式下,CPU时钟停止,但所有外设时钟仍然运行。SRAM和寄存器内容全部保留。说白了,就是CPU睡大觉,外设继续干活。
我习惯把Sleep模式比作“待机”——手机屏幕关了,但后台还在下载。功耗降了,但响应速度极快。
| 特性 | Sleep模式 | 说明 |
|---|---|---|
| CPU时钟 | 停止 | 内核不再取指执行 |
| 外设时钟 | 运行 | USART、SPI、定时器等继续工作 |
| SRAM/寄存器 | 保持 | 数据不丢失 |
| 唤醒源 | 任意中断/事件 | 包括EXTI、外设中断等 |
| 唤醒时间 | ~5个CPU周期 | 几乎无延迟 |
核心要点:Sleep模式是功耗与响应速度的最佳平衡点。如果你的应用需要频繁唤醒(比如1ms一次),Sleep模式是首选。
4.2 进入与退出Sleep模式
进入Sleep模式,就两条指令:WFI和WFE。但怎么用,差别很大。
4.2.1 标准进入流程
先看代码。这是最基础的Sleep模式进入方式:
// 进入Sleep模式(WFI方式)
void Enter_Sleep_Mode(void)
{
// 1. 清除唤醒标志(如果有)
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
// 2. 选择Sleep模式(默认就是Sleep)
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
// 3. 唤醒后从这里继续执行
// 注意:不会复位,直接执行下一条指令
}
嗯,这里要注意:PWR_SLEEPENTRY_WFI 和 PWR_SLEEPENTRY_WFE 的区别,咱们后面细说。
唤醒后,程序从WFI/WFE的下一条指令继续执行。不会进中断吗?会,但中断服务函数执行完后,就回到这里了。我刚开始总以为唤醒后会复位,其实不会。
4.2.2 唤醒源配置
任何中断都可以唤醒Sleep模式。但实际项目中,我建议你明确只使能需要的唤醒源。否则,一个串口空闲中断就能把芯片叫醒,功耗就上去了。
举个例子,用EXTI线0唤醒:
// 配置PA0为EXTI0唤醒源
void Configure_WakeUp_Source(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_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);
}
个人经验:我习惯在进入Sleep前,先关掉所有不用的外设中断。只留唤醒源对应的中断。这样能避免“误唤醒”——有一次我的设备半夜被串口噪声唤醒,电池直接掉了一半。
4.3 WFI与WFE指令的区别
这是Sleep模式里最容易搞混的地方。WFI和WFE都能让CPU睡觉,但唤醒机制完全不同。
| 特性 | WFI | WFE |
|---|---|---|
| 全称 | Wait For Interrupt | Wait For Event |
| 唤醒条件 | 使能的中断被触发 | 事件(中断或SEV指令) |
| 中断响应 | 唤醒后进入ISR | 唤醒后不进入ISR(事件方式) |
| 适用场景 | 需要处理中断 | 仅需唤醒,不处理中断 |
| 功耗 | 相同 | 相同 |
说白了,WFI是“有中断就醒,醒了就干活”。WFE是“有事件就醒,但我不进中断”。
你想想看,什么时候用WFE?比如你用一个定时器产生事件,只是想让CPU醒来轮询一个标志位,不需要进中断服务函数。这时候WFE就比WFI干净。
避坑指南:我曾经在项目里用WFE配合PendSV做任务切换,结果发现唤醒后事件标志位没清干净,导致CPU刚睡下又被唤醒。后来在循环里加了 __SEV() 和 __WFE() 配对使用才解决。记住:WFE的事件标志位是粘性的,不清掉会一直触发。
4.4 Sleep模式下的外设状态
这是Sleep模式最容易被忽视的地方。CPU睡了,但外设还在跑。哪些外设该关?哪些不能关?
4.4.1 外设时钟管理
我个人的习惯是:进入Sleep前,关闭所有不用的外设时钟。别指望HAL库帮你做这件事。
// 进入Sleep前关闭不用的外设时钟
void Sleep_Preprocess(void)
{
// 关闭SPI、I2C、USART等(如果当前不用)
__HAL_RCC_SPI1_CLK_DISABLE();
__HAL_RCC_I2C1_CLK_DISABLE();
__HAL_RCC_USART2_CLK_DISABLE();
// 保留唤醒源对应的外设时钟
// 比如用RTC唤醒,就保留RTC时钟
// __HAL_RCC_RTC_CLK_ENABLE(); // 如果RTC已经在跑,不用动
// 关闭ADC、DAC等模拟外设
HAL_ADC_DeInit(&hadc1);
HAL_DAC_Stop(&hdac1);
}
为什么要手动关?因为HAL_PWR_EnterSLEEPMode不会帮你关外设时钟。你不关,外设就在那耗电。虽然Sleep模式下外设时钟还在,但你不使能它,它就不工作。
4.4.2 必须保持运行的外设
有些外设在Sleep模式下必须保持运行,否则你没法唤醒:
- RTC:定时唤醒的标配
- LPTIM:低功耗定时器,可以在Sleep模式下产生事件
- USART:如果要用串口唤醒,必须保持时钟
- EXTI:外部中断控制器,依赖系统时钟吗?不依赖,但GPIO模块的时钟要开着
关键点:Sleep模式下,外设的功耗取决于它的时钟源。如果外设用的是HSI16或MSI,功耗会比用LSE高很多。我建议:能用LSE的尽量用LSE,比如RTC和LPTIM。
4.5 典型功耗数据
光说不练假把式。下面是我在STM32L496ZG开发板上实测的数据。环境:VDD=3.3V,25°C,所有外设默认配置。
| 模式 | 配置 | 典型电流 | 说明 |
|---|---|---|---|
| Run模式 | HSI16, 所有外设开 | ~2.5 mA | 正常跑代码 |
| Sleep模式 | HSI16, 外设全关 | ~450 µA | 只保留内核供电 |
| Sleep模式 | MSI 1MHz, 外设全关 | ~180 µA | 降低系统时钟频率 |
| Sleep模式 | MSI 1MHz, RTC运行 | ~220 µA | RTC用LSE |
| Sleep模式 | MSI 1MHz, USART运行 | ~350 µA | USART用HSI16 |
看到没?同样是Sleep模式,电流能从450µA降到180µA。差别在哪?系统时钟和外设时钟。
我建议你:进入Sleep前,把系统时钟切换到低速振荡器。比如从HSI16切到MSI 1MHz。这样即使有外设还在跑,功耗也能降一大截。
实测技巧:用MSI 1MHz跑Sleep模式,配合RTC每秒唤醒一次,平均功耗能做到~200µA。如果再加上Stop模式的配合(后面章节会讲),可以做到10µA以下。但那是后话了。
4.6 知识结构图
下面这张图,把Sleep模式的核心逻辑串起来了。你一看就明白:
4.7 实战建议
最后,给你几条我在项目中踩出来的建议:
- 别偷懒用HAL_Delay:Sleep模式下,SysTick中断会唤醒CPU。如果你用HAL_Delay做延时,CPU会频繁醒来,功耗根本降不下去。
- 测量功耗时用万用表串联:别信仿真器里的电流读数。我吃过亏,仿真器显示的电流比实际低30%。
- 先测裸机,再上RTOS:FreeRTOS的Tickless模式依赖Sleep模式。但如果你连裸机的Sleep功耗都调不好,上了RTOS只会更乱。
- 注意GPIO浮空:进入Sleep前,把不用的GPIO设为模拟模式或上拉/下拉输出。浮空引脚在Sleep模式下会漏电,我见过一个浮空引脚多耗20µA。
重要提醒:Sleep模式不是万能的。如果你的目标功耗是10µA以下,Sleep模式做不到。那是Stop模式和Standby模式的领域。但如果你需要快速响应(唤醒时间<10µs),Sleep模式是唯一选择。