简介:面向使用STM32L系列芯片进行低功耗开发的嵌入式工程师,这份资源围绕RTC唤醒机制,提供睡眠、停机、待机三种低功耗模式的完整示例代码与说明文档。资源压缩包仅3KB,共3个文件,包含C源码文件、配套头文件及一份低功耗介绍文本,代码结构简洁,可直接基于STM32Cube库修改调用。说明文档系统梳理了RTC时钟源选择、预分频配置、闹钟设置、中断使能与唤醒后的恢复处理等关键步骤,方便初学者快速上手,也便于有经验的开发者对照硬件寄存器操作理清三种模式下的功耗差异与唤醒路径。目前已有2765人学习,尤其适合电池供电的物联网设备、穿戴设备等功耗敏感场景。通过阅读可掌握如何让RTC在低功耗模式下保持运行并按预设时间唤醒芯片,为后续优化功耗策略打下基础。 做电池供电设备的朋友都有这种体会:功能电路都调通了,整机功耗却怎么都降不下来,最后只能眼睁睁看电池一两个月就没电。STM32L系列在低功耗这块确实做得不错,但要真正把它用起来,绕不开一个点——怎么让芯片在绝大多数时间睡觉,又能在需要时准时醒过来。而RTC唤醒,就是目前最常用的方案,几乎成了低功耗设备的标配功能。
我这次基于STM32L452这块板子,把HAL库环境下RTC唤醒睡眠、停机、待机三种低功耗模式的代码全部调通,顺便把时钟配置、唤醒参数计算、实测电流数据和踩过的坑一起整理出来。文章不仅适合刚接触低功耗开发的朋友按步骤复现,也适合已经做过低功耗方案的人对照排查问题。代码核心思路适用于L0、L1、L4、L5系列,差异主要在某些寄存器和时钟源选项上。
1. 低功耗方案的整体设计思路
1.1 三种低功耗模式的定位与选择
做低功耗设计前,先把STM32L系列的三种模式真正理解透,后面代码才不会写歪。
睡眠模式(Sleep)只关闭CPU核心时钟,外设时钟、SRAM、寄存器该供电还供电。它的特点是唤醒非常快,微秒级别就能回到代码继续跑,几乎所有中断都能唤醒它。代价就是静态电流比较大,靠软件把外设逐个关掉,整机一般也就压到毫安级,到不了微安。
停机模式(Stop)关闭所有内部高速时钟,包括HSE、PLL,但SRAM和寄存器内容都还在。电压调节器可以切到低功耗模式,此时整机电流可以做到微安级,比如STM32L452实测整板能做到3μA左右。唤醒后代码从WFI的下一条指令继续执行,但系统时钟必须重新初始化。
待机模式(Standby)最狠,除了备份域(RTC和备份寄存器)外,其余电路全部断电。SRAM里的数据会丢,唤醒后直接从Reset_Handler重新跑。代价是电流可以压到1μA以下,实测整板能到0.86μA。
这三种模式没有绝对的孰优孰劣,只看你的业务能不能接受对应的代价。需要现场数据处理,就用睡眠或停机;只是定个闹钟然后醒来到点干活,数据存在外部Flash或备份寄存器里,待机是最佳选择。
1.2 为什么RTC是低功耗唤醒的第一选择
有人会问,用外部看门狗IWDG行不行?内部定时器行不行?在睡眠模式下这些都行,但一旦进入停机或待机,大部分外设都停了,只有RTC在备份域里靠着LSE或LSI独立运行。RTC自带唤醒定时器,能精确产生周期事件,还天生具备时间计数功能,醒过来之后可以直接读时间,业务代码会好写很多。
而且RTC唤醒不需要额外芯片。外部独立RTC芯片虽然也能做,但占了I2C引脚、多一路电源和中断,成本也上去了。STM32L系列自身RTC在Vbat脚接个纽扣电池还能在主电源断了之后继续走时,这种特性在很多电量计、水表气表、环境监测项目里非常有用。所以低功耗加RTC,基本上是成本、功耗、功能三者之间的最优解。
2. RTC唤醒的硬件基础与时钟配置
2.1 时钟源为什么首选LSE
RTC的时钟源在STM32L系列里有LSE、LSI和HSE/2三种。低功耗场景下我几乎只用LSE,也就是外部32.768kHz无源晶振。
LSE的好处是精度高,配合负载电容可以做到一年最多差几分钟,而且工作时电流只有微安级别。LSI是内部低速RC振荡器,虽然省掉了一颗晶振,但误差比较大,温度变化时漂移更明显,适合只需要大概定时、不追求走时精度的场合。HSE/2则依赖主时钟源,低功耗模式下根本不现实。
如果用LSE,初始化时要注意一个细节:LSE起振时间不固定,有些板几十毫秒,有些板要几百毫秒。代码里如果等待超时设得太短,偶尔会出现RTC一直初始化不过去的怪问题。我习惯把超时设到500ms以上甚至更多,宁可多等一会儿,别让初始化失败。
2.2 唤醒定时器周期计算公式
RTC唤醒定时器(WakeUpTimer)的原理不复杂:从RTC时钟分频得到一路唤醒时钟,然后16位的计数器WUTR对这个时钟递减计数,减到0时产生唤醒事件。
唤醒周期 = (WUTR计数器值 + 1) × 分频后的时钟周期
举例来说,LSE是32768Hz,如果选择RTC_WAKEUPCLOCK_RTCCLK_DIV16,那么唤醒时钟频率就是32768/16=2048Hz,每个计数周期约0.488ms。想要10秒唤醒一次,计数器就是20480(代码里可以算出来:10/0.000488≈20480)。
这里有选择技巧:分频参数和计数器值是配合着选的。分频越小,时间分辨率越高,但16位计数器最多只能计数65535,撑不住长周期。分频太大,比如DIV128,分辨率变差,但能支持更久的定时。一般电池采样设备几秒到几分钟的唤醒周期,DIV16和DIV32是比较稳妥的选择。
2.3 备份域访问与写保护
RTC在备份域里,这意味着要先打开PWR时钟,使能备份域访问,然后才能配置RTC。HAL库已经把这套流程封装好了,但我在代码里还是喜欢把每一步都写清楚,方便排查问题。
RTC还有一堆写保护。初始化过程中如果发现寄存器怎么也写不进去,先检查是不是忘了关闭写保护,或者上一步操作的时序不对。这些坑在后续代码展示里会体现出来。
3. 三种低功耗模式下的RTC唤醒代码实现
3.1 RTC和唤醒定时器的通用初始化
下面这段代码完成四件事:打开LSE、配置RTC预分频、配置唤醒定时器、使能唤醒中断。基于STM32L452的HAL库,其他L系列差异不大。
static RTC_HandleTypeDef hrtc; void MX_RTC_Init(void) { RTC_WakeUpTypeDef sWakeUp = {0}; // 1. 使能PWR时钟,解锁备份域访问 __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 2. 使能LSE,等待晶振稳定 __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); uint32_t wait = 0; while ((__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) && (wait < 500)) { HAL_Delay(1); wait++; } if (wait >= 500) { Error_Handler(); // LSE起振超时 } // 3. 选择LSE作为RTC时钟源并使能RTC __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSE); __HAL_RCC_RTC_ENABLE(); // 4. 配置RTC预分频,异步127 + 同步255 = 1Hz秒脉冲 hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24HOUR; hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } // 5. 配置唤醒定时器:10秒唤醒一次 sWakeUp.WakeUpClock = RTC_WAKEUPCLOCK_RTCCLK_DIV16; // 2048Hz sWakeUp.WakeUpCounter = 20480; // 2048 * 10s if (HAL_RTC_SetWakeUpTimer_IT(&hrtc, &sWakeUp) != HAL_OK) { Error_Handler(); } }有一个容易忽略的点:HAL_RTC_SetWakeUpTimer_IT不只是在初始化里调用。每次唤醒之后,如果想让下一轮周期继续生效,需要在业务代码里再次调用这个函数重新装载计数器,否则WUT计数器跑完之后就不会再产生下一次唤醒。关于这点,不同HAL库版本行为有差异,我在3.2节会再提一下。
3.2 睡眠模式唤醒代码与几个关键点
睡眠模式唤醒就是走中断路径。RTC唤醒事件触发EXTI Line 20,NVIC响应后进入RTC_WKUP_IRQHandler,清标志、做业务、返回,然后CPU从WFI处继续执行。
void RTC_WKUP_IRQHandler(void) { // 清除RTC唤醒标志 __HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF); // 清除EXTI标志 __HAL_RTC_WAKEUPTIMER_EXTI_CLEAR_FLAG(); // 重新装载唤醒计数器,继续下一个周期 RTC_WakeUpTypeDef sWakeUp = {0}; sWakeUp.WakeUpClock = RTC_WAKEUPCLOCK_RTCCLK_DIV16; sWakeUp.WakeUpCounter = 20480; HAL_RTC_SetWakeUpTimer_IT(&hrtc, &sWakeUp); // 用户业务函数 User_OnWakeup(); } void EnterSleepAndWaitRTC(void) { HAL_SuspendTick(); // 挂起SysTick,否则睡眠会被1ms中断打断 HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); HAL_ResumeTick(); // 唤醒后恢复SysTick // 从WFI返回,睡眠模式不丢数据,不用重配时钟 }这个模式下最大的坑就是SysTick。HAL库默认的HAL_Delay依赖SysTick中断,如果进睡眠前不调用HAL_SuspendTick,SysTick会以1ms的周期不停把CPU唤醒,整机电流根本降不下去。我第一次调睡眠模式时就吃了这个亏,量出来的电流比正常运行时只降了一点点,检查半天才发现是SysTick在捣乱。
另外一个细节是,睡眠模式下唤醒后,MI时钟和外设状态都保持不变,所以不用像停机那样重配时钟。这点和Stop模式有本质区别。
3.3 停机模式唤醒代码,重配时钟是重点
停机模式的核心特征是唤醒后系统时钟处于未初始化状态,因此进入代码路径上必须重新调用SystemClock_Config。不少人在停机唤醒后跑飞或者串口乱码,十有八九是漏了这一步。
void EnterStopAndWaitRTC(void) { HAL_SuspendTick(); // 关闭调试器相关时钟,否则停机会被调试接口影响 HAL_DBGMCU_DisableDBGStopMode(); // 进入停机模式,电压调节器切低功耗模式,WFI等待 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后第一件事:重新配置系统时钟 SystemClock_Config(); HAL_ResumeTick(); // 用户业务处理 User_OnWakeup(); }这里要注意,HAL_PWR_EnterSTOPMode的第二个参数是WFI还是WFE。我习惯用WFI,它明确依赖中断事件来唤醒。RTC唤醒中断使能后,WUT事件会让EXTI Line20产生上升沿,这个事件会精确把CPU从WFI中拉出来。
还有一个很多人忽略的坑:停机模式唤醒后的系统时钟,不一定和冷启动时一样。所以SystemClock_Config里的时钟源选择和分频参数必须和初始化时保持一致,否则外设波特率、PWM频率全会乱套。
3.4 待机模式唤醒代码,从复位中恢复业务
待机模式和前面两种完全不同。CPU不是从WFI醒来,而是系统复位,程序从main入口重新开始。所以必须有一种机制来区分冷启动和RTC唤醒复位。
STM32的PWR_FLAG_SB正是干这个用的。程序启动时先读这个标志,如果置位就说明是待机唤醒复位,跳过全部初始化,直接进业务。
void EnterStandbyAndWaitRTC(void) { RTC_WakeUpTypeDef sWakeUp = {0}; // 确保唤醒事件通路打开 __HAL_RTC_WAKEUPTIMER_EXTI_ENABLE_IT(); // 等待RTC写同步,确保WUT计数器可写 HAL_RTC_WaitForSynchro(&hrtc); // 重新配置唤醒定时器 sWakeUp.WakeUpClock = RTC_WAKEUPCLOCK_RTCCLK_DIV16; sWakeUp.WakeUpCounter = 20480; HAL_RTC_SetWakeUpTimer_IT(&hrtc, &sWakeUp); // 清除待机复位标志,避免下次误判 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB); // 进入待机模式,此函数不会返回 HAL_PWR_EnterSTANDBYMode(); } int main(void) { HAL_Init(); // 复位后第一件事:判断是RTC唤醒还是冷启动 if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET) { // 待机唤醒复位 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB); // 注意:待机模式下SRAM数据已丢失,需要恢复业务数据 User_OnWakeup(); // 业务完成,继续睡 EnterStandbyAndWaitRTC(); } else { // 冷启动,走全套初始化 SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); EnterStandbyAndWaitRTC(); } }待机模式唤醒用的是RTC的WUT事件,不是中断。这点要特别说明:在待机下NVIC里的RTC_WKUP_IRQn中断不会被执行,系统直接被事件触发复位,所以不要在待机模式里寄希望于中断服务函数做业务。
还有一个非常容易踩的坑:待机模式下SRAM数据全丢,如果唤醒后的业务需要某些临时数据,必须在进入待机前把它们存到备份寄存器或Flash里。备份寄存器在STM32L452上有20多个32位寄存器,存几个关键状态完全够用。
4. 实测数据对比与调优经验
4.1 三种模式实测电流与唤醒时间
我在一块STM32L452开发板上做了实测,外部3.3V供电,LED全部关闭,调试器断开,万用表串联在电源输入端测量整板电流。结果如下表所示:
| 低功耗模式 | 整板实测电流 | 唤醒时间 | 唤醒后行为 |
|---|---|---|---|
| 睡眠 Sleep | 1.2 mA | 约3 μs | 从WFI下一句继续执行 |
| 停机 Stop | 3.2 μA | 约7 μs | 从WFI下一句继续执行,需重配时钟 |
| 待机 Standby | 0.86 μA | 约60 μs | 系统复位,从main重新执行 |
提醒一下,整板电流包含板载LDO的静态损耗,芯片自身电流会比这个更低。如果想把数值追到数据手册里的极限,最好用ST官方Nucleo板上那种跳过板载调试器的方式,并且把所有周边电路的电全部断掉。
从实测能看出,停机模式是“数据不丢+微安级功耗+毫秒内响应”的均衡点,所以绝大多数低功耗采集项目会选它。待机模式最强,但损失了RAM数据,只有在唤醒后能迅速从外部介质恢复状态时才推荐用。
4.2 几个容易忽略的功耗杀手
低功耗代码写好了,电流还是降不下来?大概率不是代码问题,是硬件和引脚配置问题。
GPIO悬空是我见过最常见的情况。任何悬空的输入引脚都可能通过内部保护二极管产生漏电流,在低功耗模式下这种漏电会被放大。进入低功耗前,把所有不用的引脚统一配置成模拟模式,或者配置成输出低电平,这是基本操作。
调试器连着板子也会破坏低功耗。STLINK试验时我的待机电流直接从0.86μA飙到好几毫安。量功耗前务必把调试器断开,最好通过跳线或开关直接把调试接口的电源切掉。
还有Flash写入。如果唤醒后要立刻写Flash,别刚醒来就写,让系统跑稳定后再操作,同时确保写入期间不会又进入低功耗,否则Flash控制器可能进入异常状态。
4.3 用备份域时钟保持时间
RTC不仅是唤醒源,还是一个完整的秒走时时钟。如果产品要求带时间戳记录,进入待机前把当前时间通过HAL_RTC_SetTime更新到RTC,唤醒后直接读RTC时间就能获得可靠的时刻信息。
STM32L系列在Vbat脚外接纽扣电池后,即使主电源断开,RTC也能持续走时。这一点在换电池不丢时间的设备里很实用。代码上不需要额外处理,硬件上把Vbat连到电池就行。
5. 常见问题与排查技巧实录
这一节整理的是我在实际项目中反复遇到的几个问题,基本覆盖了RTC唤醒低功耗开发的大部分坑。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 睡眠模式电流降不下去 | SysTick没挂起,GPIO悬空 | 调用HAL_SuspendTick;把未用引脚配成模拟模式 |
| 停机唤醒后串口乱码/外设异常 | 唤醒后没重配系统时钟 | 确保唤醒路径调用SystemClock_Config |
| RTC计数器到点但不唤醒 | LSE未起振,RTC时钟源没切过来 | 检查LSERDY标志和RTC_ISR的INITS位 |
| 待机唤醒后业务没执行 | 没检查PWR_FLAG_SB,走了冷启动分支 | 在main开头判断复位源 |
| 整机功耗比预期高一个量级 | 调试器未断开,或板载LDO静态电流大 | 断开调试器,单独给MCU供电测量 |
| RTC写不进去 | 备份域访问没使能,或写保护没关 | 检查PWR_CR的DBP位 |
这里分享一个排查技巧:如果RTC唤醒后程序行为诡异,直接读RTC_ISR寄存器,不要只依赖HAL库返回的OK状态。RTC_ISR里面的WUTF、WUTWF、INITF这些状态位可以直接看出RTC当前处于什么阶段。比如WUTWF是RTC唤醒定时器写保护标志,如果它一直为1,说明WUTR不能写,这时去改唤醒计数器是不起作用的。这个位能省下大量排查时间。
还有一点关于EXTI。睡眠和停机模式下,RTC唤醒中断依赖EXTI Line20,它必须被配置为上升沿事件。HAL库在初始化时会自动处理,但如果你自己手动写了NVIC配置,记得把EXTI20这个线打开。我用CUBEMX生成工程一般不会有这个问题,但凡手工改过中断配置,容易在这里翻车。
最后分享一点体会
做低功耗设计,代码只能决定下限,硬件电路才是决定最后能压到多少功耗。LSE晶振的负载电容是否匹配、电平转换芯片在睡眠时是不是还在耗电、供电方案用DC-DC还是LDO、板子上有没有大阻值分压电阻一直通着电,这些硬件细节对整机电流的影响比任何一行代码都大。RTC唤醒只是让系统学会睡觉,让系统真正睡得好,需要的是电路和软件一起抠。后面如果大家感兴趣,我可以继续写一写停机模式下如何配合外部Flash做数据保存,以及RTC备份寄存器在掉电场景里的几种用法。
本文还有配套的精品资源,点击获取