3. 睡眠模式(Sleep Mode):进入与退出机制、事件与中断唤醒、WFI与WFE指令区别

睡眠模式,说白了就是让CPU先歇着,但外设还得继续干活。这是STM32所有低功耗模式里最轻量级的一种,也是我项目里用得最多的模式。为什么?因为它进得快、出得快,功耗降得也明显。

我记得刚做低功耗设计那会儿,总觉得睡眠模式太简单,不就是个WFI指令嘛。后来踩了坑才发现,这里面的门道还真不少。今天咱们就把睡眠模式的进入、退出、以及WFI和WFE这两个指令的区别,一次性讲透。

3.1 睡眠模式的本质

先看一张图,帮你快速建立整体认知:

睡眠模式核心逻辑 CPU 内核 系统时钟 (SYSCLK) 外设时钟 (HCLK/PCLK) CPU 内核时钟 睡眠模式:CPU时钟停止 中断唤醒 (IRQ) 事件唤醒 (Event) 唤醒引脚 (WKUP) 核心要点:CPU休眠,外设继续工作;唤醒后立即从休眠处继续执行

这张图其实就讲了三件事:

  • 谁停了?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();  /* 真正进入睡眠 */
}
我的习惯:如果只是简单等待某个中断,我用WFI。如果需要在中断和轮询之间灵活切换,我用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 退出睡眠机制

退出睡眠模式,说白了就是唤醒。唤醒源主要有三类:

  1. 中断唤醒(WFI/WFE均可):任何使能的中断都能唤醒。比如UART收到数据、定时器溢出、GPIO外部中断。
  2. 事件唤醒(仅WFE):包括外设事件、软件事件(SEV指令)、以及配置为事件模式的中断。
  3. 唤醒引脚(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真正休眠。这是正常的,量产时就好了。

嗯,睡眠模式的内容就这些。说白了就是两条指令、两种唤醒方式、几个注意事项。但就是这几个简单的点,用好了能让你的产品功耗降一个数量级。下次咱们聊停止模式,那个比睡眠模式复杂多了,但功耗也更诱人。