STM32 低功耗三兄弟实测:SLEEP、STOP、STANDBY 一次讲透(附 6 个实机坑)
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 SLEEP | 42 STOP | 43 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 基础上做了同样的改造,这套模板本身就是可复用的经验:
- OLED + USART1 双通道日志:屏幕 4 行(0/2/4/6),串口每次启动和每次唤醒各打一行;
- 唤醒源可识别:不是笼统地"醒了",而是能说出是谁叫醒的;
- 所有等待带超时:官方库函数里大量
while(标志位==RESET);是无限等待,低功耗场景下等待对象可能永远不来(坑三),全部换成带超时的版本; - 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) 配下降沿外部中断。
改造版实测(板上验证通过):
- 屏幕显示
CPU STOP...、LED 点亮后进入停机; - 按 KEY1,屏幕瞬间跳到
WAKE UP!——秒醒,和待机模式"等 5 秒闹钟"形成鲜明对比; - 串口/屏幕给出这次实验最有价值的读数:
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。
它有两点和前两位兄弟完全不同:
- 唤醒 = 复位重启。程序从
main第一行重新跑,所以这个文件的末尾没有while(1)——写在PWR_EnterSTANDBYMode()之后的代码永远不会执行; - 唤醒源是硬件白名单: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。
#六、三兄弟同台对照(实测口径)
| 对照项 | SLEEP | STOP | STANDBY |
|---|---|---|---|
| 按 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 串下来,沉淀出的不是知识本身,而是一套做法:
- 同一块板、同一把尺子:统一的界面/日志/计数模板,三种模式的差异自己浮出水面(KEY1 唤不醒、时钟回落、LED 熄灭都是"对比"出来的,不是背出来的);
- 唤醒源要可解释:
LPWRRST、ALRF、INT_MARK这些标志组合起来,让每次唤醒都有"案底"; - 永远给等待加超时、永远留调试逃生通道、永远把状态画在屏幕上。
遗留事项:41_SLEEP 的改造版已编译通过、待上机走一遍现象;PA0 外接 10k 上拉让 KEY1 唤醒待机的实验还没做;RTC 用 HSE 标定 LSI 的进阶玩法可以留给下个项目。
#九、相关阅读
- OLED 字库与中文点阵:16×16 汉字取模——坑五"屏幕阶段号"的显示底座
- 定时器与中断:1 秒心跳与五阶段状态机——SLEEP 的 TIM3 自动唤醒与"中断置标志"写法
- 六轴姿态传感器(MPU6050,I2C)——坑三"等待无超时假死"的同类病(I2C 版)
- 温湿度传感器(DHT11,单总线)——printf 半主机坑的出处
- 洋桃1号下载模式速查——固件失联时的 BOOT0 救援与 SWD 排查
#十、更新日志
| 日期 | 更新内容 |
|---|---|
| 2026-09-20 | 初稿发布:同一套固件模板实测 SLEEP/STOP/STANDBY 三模式,三兄弟同台对照 + 6 个实机坑(WKUP 极性、LSI 被偷偷清零、库函数死等、OLED 残影、阶段号定位法、KEY2 逃生通道) |