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 唤醒源:谁把设备叫醒的?
设备睡着了,总得有人叫它起来干活吧?
这个“叫醒它”的东西,就是唤醒源。
唤醒源说白了就是一个能产生中断或复位信号的硬件事件。它必须满足两个条件:
- 在休眠期间保持工作(不能被关掉)
- 能产生一个有效的唤醒信号给电源管理单元
常见的唤醒源有这些:
- GPIO中断:按键、传感器触发、外部信号跳变
- RTC闹钟:定时唤醒,最常用
- 定时器:比RTC精度更高,但功耗也高一些
- 通信接口:UART的RX引脚、SPI的片选信号、I2C的地址匹配
- 比较器/ADC阈值:电压或电流超过设定值
- 看门狗:系统异常复位后重新启动
- 无线信号:比如WiFi的Beacon、BLE的广播包、LoRa的前导码
我的经验:设计唤醒源时,一定要确认该外设在目标休眠模式下是否还能工作。我见过有人把UART唤醒挂在DMA上,结果深度休眠时DMA时钟被关了,信号来了根本叫不醒。嗯,这种坑踩一次就够了。
2.3 休眠与唤醒的完整流程
光说概念太干,咱们画个图看看整个流程长什么样。
这个流程里有个容易被忽略的环节——配置唤醒源。很多人直接调用休眠函数,忘了先配置好哪个外设能唤醒。结果设备一睡不醒,只能按复位键。我早期一个项目就因为这个,被客户投诉“设备死机”,其实是唤醒源没配对。
2.4 唤醒源的设计原则
设计唤醒源时,我一般会遵循这几个原则:
- 最小化唤醒源数量:每个唤醒源在休眠期间都要耗电,能用一个别用两个
- 优先使用硬件唤醒:软件轮询唤醒会破坏休眠的连续性
- 考虑唤醒延迟:从唤醒到CPU开始执行第一行代码,中间有几十到几百微秒的启动时间
- 注意唤醒后的中断优先级:如果多个唤醒源同时触发,要确保高优先级事件被优先处理
避坑指南:我曾经在一个低功耗项目中,同时用了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 小结一下
休眠模式和唤醒源,说白了就是一对“搭档”。
休眠模式决定了省多少电,唤醒源决定了什么时候醒。
两者必须配合好,否则要么费电,要么醒不来。
我个人习惯在设计初期就把这两个东西画成状态机,明确每个状态下哪些模块开着、哪些关着、哪些能唤醒。这样后期调试时能省不少时间。
嗯,这一章就到这里。概念清楚了,后面咱们才能聊具体的实现技巧。