待机模式(Standby)详解:进入条件与唤醒源
待机模式,是 STM32 所有低功耗模式里最省电的那个。说白了,就是让芯片几乎完全断电——只剩下几个关键的唤醒电路还在工作。我刚开始接触这个模式时,总觉得它跟停机模式差不多,结果测电流才发现,差了不止一个数量级。
这一章,咱们就把待机模式掰开揉碎。重点看三个问题:怎么进去?怎么醒来?醒来后程序从哪跑?
进入待机模式的条件
想进待机模式,不是随便调个函数就行的。你得先满足几个前置条件,否则芯片根本不搭理你。
核心条件清单:
- 清除 WUF 标志位——PWR 寄存器里的唤醒标志,必须在进入前清零
- 设置 PDDS 位——在 PWR_CR 寄存器中,告诉芯片你要进的是待机,不是停机
- 执行 WFI 或 WFE 指令——这是真正的"关门"动作
- 所有唤醒源配置完毕——否则进去了就出不来
我记得第一次调试时,忘了清 WUF 标志,结果芯片刚进待机就立刻被唤醒。反复试了好几次,电流波形就像心跳一样一高一低。后来才反应过来——嗯,标志位没清干净。
标准进入流程,我一般这么写:
// 1. 清除唤醒标志
PWR->CR |= PWR_CR_CWUF;
// 2. 设置待机模式
PWR->CR |= PWR_CR_PDDS;
// 3. 执行WFI指令进入
__WFI();
这里有个细节:PDDS 位必须在执行 WFI 之前设置。如果你先睡了再设,那设的是下回的事,这次进的是停机模式。电流能差几十倍。
唤醒源全景图
待机模式下,芯片大部分电路都断电了。能唤醒它的,只有下面这几个"特批通道":
| 唤醒源 | 触发方式 | 典型应用 | 注意事项 |
|---|---|---|---|
| WKUP 引脚 | 上升沿 | 按键唤醒、外部触发 | 最多 3 个引脚(PA0/PC13/PE6) |
| RTC 闹钟 | 闹钟匹配 | 定时唤醒、周期性任务 | 需 RTC 时钟保持运行 |
| RTC 唤醒 | 周期性溢出 | 固定间隔唤醒 | 可配置 1Hz~数秒 |
| 入侵事件 | TAMPER 引脚 | 安全检测、防拆 | 需 RTC 备份域供电 |
| NRST 复位 | 外部复位 | 手动复位 | 不是真正唤醒,是复位 |
你想想看,这么多唤醒源,到底用哪个?我个人习惯是:需要精确时间点用 RTC 闹钟,需要固定周期用 RTC 唤醒,需要外部触发用 WKUP 引脚。
WKUP 引脚唤醒
WKUP 引脚,说白了就是专门用来唤醒的 GPIO。它跟普通中断不一样——待机模式下,外部中断控制器已经断电了,只有 WKUP 引脚还活着。
配置 WKUP 唤醒,我一般走这几步:
- 使能 PWR 时钟——RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE)
- 配置 WKUP 引脚——设置为输入模式,带上拉或下拉
- 使能 WKUP 功能——PWR_WakeUpPinCmd(ENABLE)
- 进入待机——按前面流程走
小技巧:WKUP 引脚默认是上升沿触发。如果你需要下降沿唤醒,可以在引脚外部加一个反相器。我在一个门禁项目里就这么干过——用 NPN 三极管把按键的下降沿翻成上升沿,完美解决。
RTC 闹钟唤醒
RTC 闹钟唤醒,是待机模式里最灵活的唤醒方式。你可以设定一个绝对时间,比如"明天早上 8 点整",到了那个时刻,RTC 就会拉高 WKUP 信号,把芯片从待机里拽出来。
配置要点:
// 设置闹钟时间为 10:30:00
RTC_AlarmTypeDef RTC_AlarmStruct;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Hours = 10;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Minutes = 30;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Seconds = 0;
RTC_AlarmStruct.RTC_AlarmMask = RTC_AlarmMask_None;
RTC_SetAlarm(RTC_Format_BIN, RTC_AlarmStruct);
// 使能闹钟中断(待机唤醒不需要中断服务函数,但必须使能)
RTC_ITConfig(RTC_IT_ALRA, ENABLE);
这里有个坑:闹钟中断必须使能,但中断服务函数可以留空。因为待机模式下 CPU 根本不跑代码,中断向量表都访问不到。唤醒是靠硬件直接拉高 WKUP 信号完成的。
我曾经踩过的坑:闹钟时间设成了 24 小时制,但 RTC 初始化时配的是 12 小时制。结果闹钟永远不触发。查了两天才发现——RTC 的时间格式必须跟闹钟格式一致。后来我统一用 RTC_Format_BIN,省心。
RTC 唤醒(周期性唤醒)
RTC 唤醒,也叫 RTC 唤醒定时器。它跟闹钟不一样——闹钟是"到点就响",唤醒是"每隔一段时间响一次"。适合做周期性数据采集、传感器轮询这类场景。
唤醒定时器的计数器是一个 16 位或 17 位的递减计数器,时钟源可以选 RTC 时钟的 2、4、8、16 分频。我常用的配置:
// 配置唤醒定时器,每 1 秒唤醒一次
// 时钟源:RTC 时钟 32.768kHz,分频 16 = 2048Hz
// 计数值:2048 - 1 = 2047
RTC_WakeUpCmd(DISABLE);
RTC_WakeUpClockConfig(RTC_WakeUpClock_CK_SPRE_16bits);
RTC_SetWakeUpCounter(2047);
RTC_WakeUpCmd(ENABLE);
你想想看,这个 2047 怎么来的?32.768kHz 除以 16 等于 2048Hz,倒数就是约 0.488ms。要凑 1 秒,需要 2048 个周期,计数器从 0 开始,所以设 2047。
我个人习惯把唤醒间隔做成宏定义,方便调试时改:
#define WAKEUP_INTERVAL_MS 1000
#define WAKEUP_COUNTER ((32768 / 16 * WAKEUP_INTERVAL_MS / 1000) - 1)
入侵事件唤醒
入侵事件,也叫 TAMPER 事件。它原本是用来检测设备被拆开的——比如机箱盖被打开,TAMPER 引脚电平变化,RTC 备份寄存器会被自动清除,起到防篡改的作用。
在待机模式下,入侵事件同样可以唤醒芯片。配置方式跟 WKUP 类似,但多了一步:需要使能 RTC 的 TAMPER 功能。
应用场景:我在一个工业采集器上用过这个功能。设备装在户外机柜里,机柜门有一个磁簧开关,接到 TAMPER 引脚。门一打开,芯片立即唤醒,通过 4G 模块上报"非法开门"事件。功耗控制得很好——待机时只有 1.6μA。
唤醒后的程序执行流程
待机模式唤醒后,芯片的行为跟复位几乎一样:
- 系统从复位向量重新启动——main 函数从头执行
- SRAM 内容全部丢失——所有变量都需要重新初始化
- RTC 备份寄存器保留——可以用来判断是"冷启动"还是"待机唤醒"
怎么区分是上电复位还是待机唤醒?看 PWR 状态寄存器:
if (PWR->SR & PWR_SR_WUF) {
// 待机唤醒
// 清除标志
PWR->CR |= PWR_CR_CWUF;
} else {
// 上电复位
}
这个判断很重要。我见过有人把外设初始化放在 main 开头,每次唤醒都重新配一遍,结果唤醒后外设工作异常——因为有些外设的寄存器在待机模式下并没有完全复位,重复配置反而会冲突。
知识体系结构图
下面这张图,把待机模式的进入条件、唤醒源和唤醒后流程串起来了:
避坑指南
做待机模式调试,有几个坑我几乎每次都会遇到。列出来,你遇到了直接对照排查:
坑一:唤醒后外设初始化顺序
待机唤醒后,HSE 和 PLL 需要重新稳定。如果你在时钟还没稳定时就操作外设,大概率会卡死。我习惯在 main 开头先等时钟稳定,再初始化外设。
坑二:RTC 时钟源选择
待机模式下,LSE 和 LSI 都可以给 RTC 供电。但 LSE 精度高、功耗低,LSI 精度差但不需要外部晶振。我建议:对时间精度有要求用 LSE,只做周期唤醒用 LSI。
坑三:调试器干扰
调试时,SWD 接口在待机模式下会断电。如果你用调试器下载完程序后直接跑,芯片进待机后调试器就断开连接了。解决办法:在进入待机前加一个延时,或者用串口打印日志来判断唤醒状态。
待机模式,说白了就是"断舍离"——断掉一切不必要的电路,只留最核心的唤醒通路。理解了进入条件和唤醒源,你就能在功耗和功能之间找到最佳平衡点。