2. 核心概念:什么是休眠模式?什么是唤醒源?

好,咱们正式开始啃核心概念。这一章我打算把两个最基础、也最容易混淆的东西讲透——休眠模式和唤醒源。

你可能会觉得:“休眠不就是睡觉吗?唤醒不就是闹钟响吗?”
嗯,大体没错。但在嵌入式无线系统里,这里的门道比你想象的多得多。
我当年刚入行时,就因为在“休眠模式”的理解上栽了个跟头,导致产品待机电流死活降不下去。后来才发现,我用的根本不是真正的休眠,只是把CPU时钟降慢了而已。

2.1 休眠模式:不只是“睡觉”那么简单

先问一个问题:设备休眠时,到底谁在“睡”?

很多人以为休眠就是CPU停了。其实不对。
休眠模式,本质上是系统有选择地关闭或降频部分模块,以达到降低功耗的目的。

我习惯把休眠模式分成三个层级,这样好理解:

层级 名称 典型功耗 保留状态 唤醒延迟
L1 浅度休眠(Idle/Sleep) mA级 CPU寄存器、RAM、部分外设时钟 μs级
L2 深度休眠(Deep Sleep) μA级 仅RAM保持、RTC运行 ms级
L3 关断模式(Shutdown/Hibernate) nA级 仅备份寄存器或完全不保留 10ms~100ms

关键点:休眠越深,功耗越低,但唤醒后恢复现场的时间也越长。这不是一个“越深越好”的选择题,而是要根据应用场景来权衡。

举个例子。我在做一款蓝牙信标时,需要每秒广播一次。如果每次都用深度休眠,唤醒后重新初始化射频模块,那光初始化时间就占了半个广播周期,反而更费电。最后我选了浅度休眠,虽然待机电流多了几十微安,但整体平均功耗反而更低。

所以你看,选哪种休眠模式,得看你的唤醒频率和唤醒后要干什么。

2.2 唤醒源:谁把设备叫醒的?

设备睡着了,总得有人叫它起来干活吧?
这个“叫醒它”的东西,就是唤醒源。

唤醒源说白了就是一个能产生中断或复位信号的硬件事件。它必须满足两个条件:

  1. 在休眠期间保持工作(不能被关掉)
  2. 能产生一个有效的唤醒信号给电源管理单元

常见的唤醒源有这些:

  • GPIO中断:按键、传感器触发、外部信号跳变
  • RTC闹钟:定时唤醒,最常用
  • 定时器:比RTC精度更高,但功耗也高一些
  • 通信接口:UART的RX引脚、SPI的片选信号、I2C的地址匹配
  • 比较器/ADC阈值:电压或电流超过设定值
  • 看门狗:系统异常复位后重新启动
  • 无线信号:比如WiFi的Beacon、BLE的广播包、LoRa的前导码

我的经验:设计唤醒源时,一定要确认该外设在目标休眠模式下是否还能工作。我见过有人把UART唤醒挂在DMA上,结果深度休眠时DMA时钟被关了,信号来了根本叫不醒。嗯,这种坑踩一次就够了。

2.3 休眠与唤醒的完整流程

光说概念太干,咱们画个图看看整个流程长什么样。

休眠唤醒核心流程 休眠唤醒核心流程 正常运行 进入休眠决策 配置唤醒源 执行休眠 唤醒事件触发 唤醒源类型 • GPIO中断 • RTC闹钟 • 通信接口 休眠模式选择 • 浅度休眠 • 深度休眠 • 关断模式 唤醒后恢复现场

这个流程里有个容易被忽略的环节——配置唤醒源。很多人直接调用休眠函数,忘了先配置好哪个外设能唤醒。结果设备一睡不醒,只能按复位键。我早期一个项目就因为这个,被客户投诉“设备死机”,其实是唤醒源没配对。

2.4 唤醒源的设计原则

设计唤醒源时,我一般会遵循这几个原则:

  1. 最小化唤醒源数量:每个唤醒源在休眠期间都要耗电,能用一个别用两个
  2. 优先使用硬件唤醒:软件轮询唤醒会破坏休眠的连续性
  3. 考虑唤醒延迟:从唤醒到CPU开始执行第一行代码,中间有几十到几百微秒的启动时间
  4. 注意唤醒后的中断优先级:如果多个唤醒源同时触发,要确保高优先级事件被优先处理

避坑指南:我曾经在一个低功耗项目中,同时用了RTC和GPIO两个唤醒源。结果发现GPIO唤醒后,RTC中断标志位没清干净,导致设备刚唤醒又立刻进入休眠,形成了“假死”循环。排查了两天才找到原因——唤醒后一定要先清除所有挂起的中断标志,再执行任务。

2.5 一个简单的代码示例

拿STM32L系列举个例子,配置RTC唤醒和GPIO唤醒的典型代码:

// 配置RTC每10秒唤醒一次
void RTC_Wakeup_Config(void)
{
    // 使能RTC时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_BackupAccessCmd(ENABLE);
    
    // 配置RTC唤醒定时器
    RTC_WakeUpCmd(DISABLE);
    RTC_WakeUpClockConfig(RTC_WakeUpClock_RTCCLK_Div16);
    RTC_SetWakeUpCounter(0x4E20);  // 10秒唤醒一次
    
    // 使能唤醒中断
    RTC_ITConfig(RTC_IT_WUT, ENABLE);
    RTC_WakeUpCmd(ENABLE);
}

// 配置GPIO下降沿唤醒
void GPIO_Wakeup_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    
    // 使能GPIO时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    
    // 配置PA0为输入,上拉
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 使能EXTI0中断
    SYSCFG_EXTILineConfig(EXTI_PortSource_GPIOA, EXTI_PinSource0);
    EXTI_InitTypeDef EXTI_InitStruct;
    EXTI_InitStruct.EXTI_Line = EXTI_Line0;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);
}

// 进入深度休眠
void Enter_DeepSleep(void)
{
    // 清除所有挂起的中断标志
    EXTI_ClearITPendingBit(EXTI_Line0);
    RTC_ClearITPendingBit(RTC_IT_WUT);
    
    // 设置休眠模式为深度休眠
    PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
    
    // 唤醒后从这里继续执行
    // 需要重新配置系统时钟
    SystemClock_Config();
}

这段代码里有个细节:PWR_EnterSTOPMode 调用后,CPU就停了。唤醒后从下一行继续执行。但系统时钟会被复位,所以必须重新配置。这个坑我踩过——第一次唤醒后,外设时钟没恢复,串口打印全是乱码。

2.6 小结一下

休眠模式和唤醒源,说白了就是一对“搭档”。
休眠模式决定了省多少电,唤醒源决定了什么时候醒。
两者必须配合好,否则要么费电,要么醒不来。

我个人习惯在设计初期就把这两个东西画成状态机,明确每个状态下哪些模块开着、哪些关着、哪些能唤醒。这样后期调试时能省不少时间。

嗯,这一章就到这里。概念清楚了,后面咱们才能聊具体的实现技巧。