posts/stm32-lowpower-sleep-stop-standby.md

STM32 低功耗三兄弟实测:SLEEP、STOP、STANDBY 一次讲透(附 6 个实机坑)

📅 发布日期2026-09-19
✏️ 更新日期2026-09-20
STM32 系列 教程 22 分钟阅读 #低功耗#SLEEP#STOP#STANDBY#RTC

STM32 低功耗三兄弟实测:SLEEP、STOP、STANDBY 一次讲透(附 6 个实机坑)

开发板:洋桃1号(STM32F103C8T6)|工程:demo00\41_SLEEP / 42_STOP / 43_STANDBY,同一套固件模板改造

#一、为什么要折腾低功耗

插着 USB 调试的开发板永远不需要省电,所以低功耗是很多人学 STM32 时跳过的章节。但任何一块电池供电的设备——手环、水表、遥测节点——90% 的时间都在"装死",醒来的那一瞬间才干活。装死装得越深、醒得越准,电池活得越久。

STM32F103 提供了三档"装死"深度,官方例程给了三个 demo,但三个 demo 各自为战,看不出它们之间的层次关系。这次我把洋桃 1 号开发板上的三个例程用同一套固件模板改造后逐个实测:统一的 ShaoHua-lab 开机画面、统一的 USART1(PA9,115200)日志、统一的唤醒源识别和计数,让三种模式在同一块板上、用同一把尺子量出来。

先上结论总表,后文逐个展开:

41 SLEEP42 STOP43 STANDBY
一句话CPU 打个盹全板断钟换了一颗芯片
停什么只停 CPU 内核时钟HSI/HSE 全停,1.8V 域时钟全停1.8V 域直接断电
SRAM / 寄存器✅ 保持✅ 保持❌ 全部丢失
备份域(BKP/RTC)✅✅✅(唯一幸存者)
唤醒源任意使能的中断EXTI 线(如 KEY1)WKUP 引脚 / RTC 闹钟 / NRST / IWDG
唤醒后接着跑(WFI 下一行)接着跑,但时钟变 HSI 8MHz复位重启,从 main 第一行重来
实测功耗档位省 RGB 电平(外设还在跑)µA 级~2µA,最深
KEY1(PA0) 能唤醒吗✅✅❌(详见坑一)

#二、统一的固件模板

三个工程都在官方 demo 基础上做了同样的改造,这套模板本身就是可复用的经验:

  1. OLED + USART1 双通道日志:屏幕 4 行(0/2/4/6),串口每次启动和每次唤醒各打一行;
  2. 唤醒源可识别:不是笼统地"醒了",而是能说出是谁叫醒的;
  3. 所有等待带超时:官方库函数里大量 while(标志位==RESET); 是无限等待,低功耗场景下等待对象可能永远不来(坑三),全部换成带超时的版本;
  4. KEY2 调试逃生通道:按住 KEY2(PA1) 复位,程序不进低功耗、LED 慢闪,保证随时可以重烧(坑六)。

工程没勾 MicroLIB,串口输出用 sprintf + USART_SendData 拼装,避开 printf 的半主机坑(这是之前 DHT11 模块踩过的)。

#三、41 SLEEP:CPU 打个盹

原理:__WFI() 执行后 CPU 内核时钟停止,但所有外设时钟照常运行,SRAM、寄存器、GPIO 全部保持。任何使能了的中断都能把它叫醒,唤醒后程序从 __WFI() 的下一行继续跑——连上下文现场都不用恢复,因为它根本没丢。

关键配置只有一行:明确告诉内核"这不是深睡"——

cNVIC_SystemLPConfig(NVIC_LP_SLEEPDEEP, DISABLE);   /* SLEEPDEEP=0: 睡眠而非停机/待机 */

改造版设计(双唤醒源):

  • KEY1(PA0→EXTI0,下降沿):手动唤醒。ISR 里只置 INT_MARK 标志(记得 volatile);
  • TIM3 每 2 秒一次更新中断:自动唤醒。ISR 里只清标志——清标志这个动作本身就是唤醒,主循环根本不用在中断里干活。

主循环醒来后靠 INT_MARK 判断是谁叫的:

c__WFI();                              /* CPU 睡在这,任何使能的 IRQ 都能叫醒 */
wakeCnt++;
if(INT_MARK){ keyCnt++; /* WAKE by KEY */ }
else        { tmrCnt++; /* WAKE by TIMER */ }

一个容易忽略的细节:这套板子的 delay_ms() 是 SysTick 轮询实现(TICKINT=0,不开中断),所以它不会把睡着的 CPU 唤醒——如果换用中断式延时,主循环里的 delay_ms(300) 会变成"每 300µs 把 CPU 踹醒一次",睡眠就名存实亡了。

预期现象:屏幕 CPU SLEEP... 与 WAKE by KEY / WAKE by TIMER2s 交替,不按键时每 2 秒被定时器叫醒一次,按 KEY1 随时插队。

#四、42 STOP:全板断钟,醒来要先"重修时钟"

原理:PWR_EnterSTOPMode() 之后 1.8V 域的所有时钟(HSI、HSE、PLL)全停,SRAM 和寄存器内容保持,功耗进入 µA 级。唤醒源收窄为 EXTI 线——本板用 KEY1(PA0) 配下降沿外部中断。

改造版实测(板上验证通过):

  1. 屏幕显示 CPU STOP...、LED 点亮后进入停机;
  2. 按 KEY1,屏幕瞬间跳到 WAKE UP!——秒醒,和待机模式"等 5 秒闹钟"形成鲜明对比;
  3. 串口/屏幕给出这次实验最有价值的读数:CLK:HSI -> 72M。

第三条是 STOP 模式最经典的坑:唤醒后系统时钟自动回落到 HSI 内部 8MHz(因为 PLL 停了),而程序里所有 delay_us() 都按 72MHz 换算 SysTick 重装值——不重新配置时钟,之后所有延时拉长 9 倍、USART 波特率也全错。改造版在 WFI 返回后第一件事就是抓现行:

cPWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);

sws = (u8)((RCC->CFGR >> 2) & 0x03);   /* 先抓唤醒瞬间的时钟源:0=HSI */
RCC_Configuration();                   /* 立刻恢复 72MHz(HSE+PLL),否则延时/串口全乱 */

CFGR.SWS 这两个 bit 是"当前真正在用的时钟源",读出来是 0 就坐实了"醒来在 HSI 上"。再一个直观证据:STOP 期间 LED 一直是亮的——GPIO 输出寄存器还活着;到了待机模式它就会熄灭,这是"寄存器保持 vs 全丢"肉眼可见的区别。

#五、43 STANDBY:醒来等于换了一颗芯片

原理:待机是 F103 最深的低功耗。PWR_EnterSTANDBYMode() 之后 1.8V 域直接断电——SRAM、所有寄存器、PLL、GPIO 全部清零,只有备份域(备份寄存器 BKP_DRx、RTC、LSE 晶振电路)靠 VDD/VBAT 顽强存活,功耗 ~2µA。

它有两点和前两位兄弟完全不同:

  1. 唤醒 = 复位重启。程序从 main 第一行重新跑,所以这个文件的末尾没有 while(1)——写在 PWR_EnterSTANDBYMode() 之后的代码永远不会执行;
  2. 唤醒源是硬件白名单:WKUP 引脚(PA0)、RTC 闹钟、NRST、IWDG。EXTI 普通引脚中断在这里统统无效。

改造版设计(板上验证通过):

  • 双唤醒源:RTC 闹钟每 5 秒自动唤醒(先试 LSE 晶振,4 秒内没起振自动退回内部 LSI)+ 板载「唤醒」键(PA0=WKUP)。醒来后用 RCC_CSR.LPWRRST(是否从待机醒)+ RTC->CRL 的 ALRF(是否闹钟叫醒)组合判断,屏幕显示 POWER ON / RESET KEY / WKUP KEY / RTC ALARM,停留 4 秒供观察;
  • 备份域可视化:启动次数存 BKP->DR1——冷上电=1、复位/待机唤醒累加、断电丢失。按复位计数继续涨(备份域跨复位存活),拔电重上电归 1。配合 RTC 计数器(同样活在备份域),"CPU 全死了但 RTC 还在走、闹钟还在响"这件事被演示得明明白白;
  • 闹钟装载:RTC_SetAlarm(RTC_GetCounter() + 5),同时打开 RTC_IT_ALR 和 EXTI 线 17(RTC 闹钟在 F103 上硬连到 EXTI17)。

实测循环:醒 6.5 秒(显示原因,LED 亮)→ 睡约 5 秒(LED 灭)→ 闹钟叫醒计数 +1。按复位计数保留,断电归 1。

#六、三兄弟同台对照(实测口径)

对照项SLEEPSTOPSTANDBY
按 KEY1 秒醒✅✅ (本次 B 实验)❌ (怎么按都没反应)
谁能唤醒任意使能中断仅 EXTI 线WKUP/RTC 闹钟/NRST/IWDG
醒来后程序原地继续原地继续从 main 重来
醒来时时钟72MHz 不变HSI 8MHz,必须重配复位,重走初始化
睡着时 LED亮(寄存器保持)亮(寄存器保持)灭(GPIO 断电)
RAM 计数器跨唤醒保持保持丢(只有 BKP 计数还在)
适合场景等中断的空闲循环秒级响应的电池设备天级定时采集/抄表

KEY1 那一行就是本次实验的核心结论:同一个键、同一个引脚,在 SLEEP/STOP 里是"门铃",在 STANDBY 里什么都不是——原因见下一个坑。

#七、踩坑实录(6 个,全部实机复现)

#坑一:KEY1 永远唤不醒待机——按键极性和 WKUP 要求正好相反

现象:43 待机后按 KEY1(文档里写着"PA0 按键(KEY1)唤醒")毫无反应,拔掉共用 PA0 的触摸模块也没用,只有复位键有效。

根因是一对方向相反的电气约定:

  • key.c 里 KEY1 配的是 IPU(内部上拉)+ EXTI 下降沿 → 按下 = 把 PA0 拉低到 GND;
  • 而 WKUP 引脚进入待机后被硬件强制为输入+内部下拉,只认被拉高的上升沿(参考手册原话:引脚应接 VDD 才能唤醒)。

KEY1 按下去只会让 PA0 更接近 0V,永远造不成上升沿。解法:用板载专门接 VDD 的「唤醒」键;或者给 PA0 外接一个 10k 上拉到 3.3V——平时 PA0 为高、按下变低,松手的瞬间产生上升沿,同样能唤醒待机(且不影响其他例程的按键逻辑)。

#坑二:LSI 使能位会被待机唤醒"偷偷"清掉(最隐蔽)

用 LSI(内部 RC,~40kHz)当 RTC 时钟源驱动闹钟唤醒时,第一轮循环正常,之后闹钟永远不响、芯片睡死。

根因:LSION 位于 RCC_CSR——任何系统复位都会把它清零,而待机唤醒恰恰就是一次系统复位。于是每次被闹钟叫醒后,LSI 都处于关闭状态,RTC 停走,下次装载的闹钟自然永远到不了。

解法:用 LSI 作 RTC 时钟时,每次启动都必须重新 RCC_LSICmd(ENABLE) 并等待 LSIRDY/寄存器同步(RSF),不能因为"备份域里记过配置标志"就跳过。顺带一提,LSE 晶振电路整个都在备份域(RCC_BDCR),跨复位存活,焊了 32.768k 晶振的板子永远遇不到这个坑——我们的固件是 LSE 优先、LSI 兜底,正好把两种情况都覆盖了。

另附 LSI 精度实测:标称 40kHz,实际 30~60kHz,本芯片实测约 33kHz,"5 秒"的闹钟实际走成约 6 秒。就算按实测校准分频值,RC 也会随温度漂移——产品级定时唤醒请用 LSE 或用 HSE 标定 LSI。

#坑三:库函数死等假死——RTC 一停,while 永远等下去

坑二的连锁反应才是真正吓人的部分:RTC 时钟停了之后,RTC_SetAlarm() 内部的

cwhile((RTC->CRL & RTC_FLAG_RTOFF) == RESET);   /* 库函数原样,无超时 */

永远等不到 RTOFF(RTC 寄存器写入完成需要 RTC 时钟),整机冻结在唤醒原因那一屏,串口没输出、按键没反应、连烧录都像失联。当时靠给 OLED 加"阶段标记"才把卡点定位到这一行。

解法:涉及低功耗/备份域的等待一律自己写寄存器级版本并加超时,超时就降级(比如退化为"仅唤醒键唤醒")而不是死等:

cstatic u8 rtc_wait_rtoff(void){
    u32 g = 400000;
    while(((RTC->CRL & RTC_CRL_RTOFF) == 0) && (--g)){}
    return (g != 0);          /* 超时返回 0,调用方降级处理 */
}

这条经验值得外推:凡是不由 CPU 主时钟驱动的等待(RTC、I2C 从机应答、外部握手),都不能裸写无限 while——之前 MPU6050 模块那次"I2C 从机不应答导致程序假死"就是同一类病。

#坑四:待机时 OLED 不会灭,别被它骗了

调试时最容易被"屏幕没变"误导。实际上 OLED 由 VDD 供电,待机只断 1.8V 域,屏幕会一直保留断电前最后一帧——看起来像死机,其实芯片睡得很好。判断是否真睡着要看 LED:待机时 GPIO 断电、LED 熄灭;STOP/SLEEP 时寄存器保持、LED 维持原状态。

#坑五:卡死定位法——往屏幕上写阶段号

低功耗固件一旦卡死,串口可能没接、调试口可能断电,"死在哪"非常难查。这次用的土办法意外好用:在初始化流程的每个关键步骤后,往 OLED 第 6 行写 <原因> S<n> 阶段号。程序冻结时,屏幕上最后停留的阶段号直接指出卡点——本模块就是靠 S8(即将进待机)和更早版本的"停在原因屏",把两次不同的卡死分别定位到了待机入口和 RTC 死等。成本几行代码,收益是"黑盒变白盒"。

#坑六:待机固件失联,先试"KEY2+复位"再动 BOOT0

芯片睡着时调试口断电,pyocd 直接连不上,报 Get IDCODE error,非常像锁死。传统解法是 BOOT0 拉高进 ROM bootloader 救援,但更好的做法是在固件里预埋逃生通道:按住 KEY2(PA1) 复位,程序进入"永不进低功耗"的 DEBUG 模式(LED 慢闪),此时调试口常开、随时可烧。三个低功耗工程的固件里都带了这个通道。

#八、模板化的小结

把三个 demo 串下来,沉淀出的不是知识本身,而是一套做法:

  1. 同一块板、同一把尺子:统一的界面/日志/计数模板,三种模式的差异自己浮出水面(KEY1 唤不醒、时钟回落、LED 熄灭都是"对比"出来的,不是背出来的);
  2. 唤醒源要可解释:LPWRRST、ALRF、INT_MARK 这些标志组合起来,让每次唤醒都有"案底";
  3. 永远给等待加超时、永远留调试逃生通道、永远把状态画在屏幕上。

遗留事项:41_SLEEP 的改造版已编译通过、待上机走一遍现象;PA0 外接 10k 上拉让 KEY1 唤醒待机的实验还没做;RTC 用 HSE 标定 LSI 的进阶玩法可以留给下个项目。

#九、相关阅读

#十、更新日志

📝共 1 条记录
日期更新内容
2026-09-20初稿发布:同一套固件模板实测 SLEEP/STOP/STANDBY 三模式,三兄弟同台对照 + 6 个实机坑(WKUP 极性、LSI 被偷偷清零、库函数死等、OLED 残影、阶段号定位法、KEY2 逃生通道)