☰
STM32 RTC掉电不走的原因与排查:VBAT供电、LSE晶振和初始化陷阱
2026/10/20 6:28:36 网站建设 项目流程

做单片机产品这几年,遇到过一个特别容易出现却又特别容易误判的现象:设备明明接了电池,断电后再上电,RTC时间却纹丝不动,就停在断电那一刻。很多人的第一反应是“纽扣电池没电了”,换上新电池再测,还是不走。再往后就开始怀疑晶振、怀疑备份电容、怀疑芯片本身。我可以很肯定地说,STM32Fxx的RTC掉电不走,绝大多数时候不是芯片坏了,而是供电、时钟源和初始化逻辑三个环节里至少有一个踩了坑。

这篇文章把我排查这类问题时的完整思路写下来:先从芯片内部原理上搞清楚备份域和VBAT的关系,再讲最常见的软件配置陷阱,然后给出一条能直接照着做的排查链路,最后附上标准外设库和HAL库两版能落地的初始化代码。不管你是第一次做带RTC的产品,还是板子已经在客户手里出了问题,这篇应该都能帮上忙。

1. RTC掉电不走的物理前提:备份域到底由谁供电

1.1 VDD掉电后,备份域靠VBAT维持

很多人没有意识到,STM32内部有一块独立的“备份域”,它和主电源域是隔离的。RTC的计数器、预分频器、后备寄存器,以及备份域控制寄存器RCC_BDCR里的相关配置位,全部放在这个域里。主电源VDD正常时,备份域由VDD供电;一旦VDD跌到阈值以下,芯片内部会自动切换,改由VBAT引脚供电。这个切换过程是硬件完成的,不需要代码介入。

所以,掉电后RTC能不能继续走时,第一个硬性前提就是VBAT引脚上必须有有效电压。不同系列的最低值略有差异,但一般都在1.65V到3.6V这个范围内。这里最常见的错误有两类:一是VBAT引脚悬空,觉得“反正主电一直在,不用接电池”;二是把VBAT直接接到GND,或者只放了一个滤波电容,掉电瞬间备份域立刻失去供电。

还有一个容易被忽略的设计:有些人为了省成本,把VBAT直接短接到VDD。这样做主电在线时没问题,但掉电保持功能就名存实亡了。只要VDD出现跌落毛刺,备份域就可能复位,RTC时间一样会丢。如果产品确实不需要掉电保持,那直接接VDD可以接受;如果需要真正的“掉电不走”,VBAT必须接能独立供电的电池或大容量电容,而且不能和VDD简单地直接并在一起。

1.2 LSE、LSI、HSE:三个时钟源的供电域完全不同

就算VBAT有电,RTC还得有一个能持续工作的时钟源。STM32给RTC提供的时钟源有三个:LSE、LSI、HSE分频。它们的区别不只在精度,更在供电域。

LSE是外部低速晶振,频率32.768kHz。这个振荡器属于备份域的一部分,VDD掉电后由VBAT继续供电,所以掉电后它还能继续输出时钟脉冲。这是掉电保持场景下唯一正确的选择。

LSI是内部低速RC振荡器,不需要外部晶振,上电即振,成本低。但它的供电域在主电源那边,VDD一消失,LSI立刻停振。RTC一旦失去时钟源,计数器自然冻结。可以说,只要代码里把RTC时钟源配成了LSI,掉电不走就是必然结果,跟电池有没有电没关系。

HSE分频同样在主电源域,精度不错,但同样不具备掉电保持能力。所以设计掉电保持功能时,时钟源必须是LSE。

我把三个时钟源的特性整理成了对比表:

时钟源频率供电域VDD掉电后的状态适用场景
LSE32.768kHz备份域由VBAT继续供电,继续运行掉电保持
LSI约32kHz主电源域停振,计数器冻结低成本、无掉电保持需求
HSE分频由HSE决定主电源域停振,计数器冻结不需掉电保持的场合

还有一个关键点需要理解:RCC_BDCR里的LSEON、RTCSEL、RTCEN这些配置位本身也在备份域里。只要备份域没有被复位,这些配置在掉电后仍然保持。这也是为什么下一章要重点讲“初始化逻辑”的原因——很多时候RTC硬件根本没有问题,是软件在每次上电后把备份域“重新初始化”了一遍,自己毁了掉电保持的成果。

2. 最容易被忽略的三个软件配置陷阱

2.1 无脑初始化:时间不是“没走”,而是每次上电都被清零

我见过非常多项目,main函数里直接调用RTC_Config,然后在配置结束后再调一次RTC_SetCounter或者HAL_RTC_SetTime,把计数器清零或者设置成固定基准。每次上电都执行这一套。在仿真器里调试时一切正常,一旦真断电再上电,时间非常“准时”地回到某个固定值。

这种情况下的真相是:断电期间RTC其实一直由VBAT供电在走,备份域配置也完好。问题出在上电瞬间软件又把计数器重新写了一遍,前面的“走”全白费了。表现出来就是掉电不走。

判断这种问题特别简单:如果每次开机时间都回到同一个固定值,比如1970年1月1日、2000年1月1日,或者一个自己定义的时间基准,大概率就是“无脑初始化”造成的。在代码里搜一下RTC_SetCounter、HAL_RTC_SetTime、HAL_RTC_SetDate这几个函数,看它们是不是在每次主复位后都会被无条件调用,就能确认。

2.2 BKP_DeInit把备份域复位,等于自己把电“断”了

比无脑初始化更隐蔽的是在配置流程里调用BKP_DeInit。这个函数执行的是备份域复位,它的作用范围是整个备份域,不只是后备寄存器。RCC_BDCR里的LSEON、RTCSEL、RTCEN,以及RTC的分频配置,全部会被清掉。

很多人喜欢把例程整段拷过来,而不少官方旧例程在第一次初始化RTC时确实会调用BKP_DeInit。如果这段代码每次开机都执行,哪怕VBAT有电,备份域也会被软件复位,RTC照样不走。更麻烦的是,这种问题在调试器在线运行时不明显,只有断电上电才复现,排查起来很费劲。

正确的做法是:BKP_DeInit只应该在“确认备份域还没被配置过”的时候执行,而且必须放在标志判断的保护范围内。一旦备份域有效,就绝不能再碰它。

2.3 备份寄存器标志被Tamper清掉,让程序误判“首次上电”

用后备寄存器保存“已完成初始化”的标志,这个思路本身没问题,这是标准做法。但有个前提:不要打开入侵检测功能,或者说要确保Tamper功能不会误触发。

有些系列的芯片只要使能了Tamper,当检测引脚上出现电平跳变时,硬件会自动清除部分或全部备份寄存器。很多板子把Tamper检测引脚悬空,上电瞬间电位抖动、外部干扰、甚至人手摸到附近走线,都可能误触发。一旦备份寄存器被清零,下次上电时程序看到标志不对,就会走进“首次配置”分支,重新初始化并重置时间。

除了Tamper之外,还需要注意:备份寄存器在某些低功耗模式下访问受限,或者RTC没有使能备份域访问权限时读写无效。我遇到过想给标志赋值,结果写了个寂寞的情况。标准外设库中,配置备份域寄存器之前必须调用PWR_BackupAccessCmd(ENABLE),否则写操作会被忽略。

3. 硬件上的重灾区:VBAT供电和32.768kHz晶振

3.1 VBAT侧五个检查点

软件层面排查干净之后,如果问题还在,就得老老实实回到硬件。VBAT这一路看似简单,实际上最容易在量产出货时出幺蛾子。

第一,测电压。方法很直接:断开主电源,用万用表量VBAT引脚和GND之间的电压。如果是0V,先怀疑电池座虚焊、电池没装、电池耗尽。我见过一批板子全部掉电不走,拆开一看,电池座弹片氧化,装上电池也不通。

第二,看电压是否低于工作阈值。CR2032刚出厂时电压在3.0V以上,长时间存放可能掉到2.2V甚至更低。如果低于备份域最低工作电压,RTC当然会停。不少产品对电池电压没有监控,客户拿到手时电池其实已经接近失电。

第三,确认VBAT有没有被错误地接到GND或者悬空。原理图看着没问题,到了PCB上因为走线、过孔问题把VBAT网络和GND网络短路的情况,我确实碰到过。

第四,如果用了防反接二极管或者限流电阻,一定要算压降。二极管压降0.3V到0.7V,再叠加电池电压下降,可能把有效电压压到备份域阈值以下。

第五,如果用的是超级电容方案,容量要留足余量。估算公式是t约等于C乘以允许压降,再除以备份域平均电流。假设超级电容1F,从3.3V放到2.0V,备份域耗电按3微安算,理论时间约5天。但超级电容自放电比较明显,实际维持时间往往打对折还要低。这个公式适合做方案估算,真正定容值得留几倍余量。

3.2 LSE起振是另一个隐蔽问题

32.768kHz晶振是RTC最“娇贵”的部分。它不起振或者起振特别慢,会导致RTC根本没有时钟,掉电后自然不走。

晶振选型时要注意三点:负载电容要和芯片匹配,一般选6pF到12.5pF的型号;ESR不能太大,很多资料建议控制在70kΩ以内;晶振本身要适合低温环境,否则在低温下起振会变得更慢。

PCB布局影响也很大。晶振和两个外部电容要紧挨着OSC32_IN和OSC32_OUT引脚,走线尽量短,不要和高速信号交叉,晶振下方不要走其他电源线。LSE引脚在部分封装上和某些GPIO复用,如果被配置成普通GPIO,也会导致晶振无法工作。

起振时间容易被忽视。LSE的起振不像HSE那么快,常温下几百毫秒到几秒都很常见,低温下可能更长。如果代码里等待LSERDY标志是死等,一旦起振慢,程序就卡在初始化里。如果设置了很短的超时,LSE还没起来就被判定失败,后续RTC配置全都会出问题。我建议等待LSE就绪的超时至少放到5秒,并且超时后要有明确的错误处理,而不是直接往下跑。

顺便说一个经验:不要用示波器探头直接去量晶振引脚,探头电容会改变振荡条件,很容易把本来起振的晶振“按停”。想判断LSE是否真的在工作,用软件读LSERDY标志,或者开启RTC校准输出引脚,用示波器量512Hz或1Hz脉冲,都靠谱得多。

4. 从“掉电不走”到根因定位:我常用的五步排查链路

4.1 五步逐级排查

遇到掉电不走的问题,我习惯按固定顺序排查,不跳步,因为跳过任何一步都可能浪费时间在错误的方向上。

第一步,验证VBAT供电。断开主电源,量VBAT到GND的电压。这里要注意,必须在掉电状态下量,因为VDD存在的时候VBAT可能被内部供电,测出来是假电压。

第二步,读取备份寄存器标志。这个标志如果还在,说明断电期间备份域没有被复位,硬件保持链路大概率是正常的。如果标志丢了但VBAT电压正常,说明备份域在掉电过程中被复位过,重点查Tamper误触发和程序里有没有执行备份域复位操作。

第三步,确认时钟源。读出RCC_BDCR的RTCSEL位,确认是不是LSE。再用RCC_FLAG_LSERDY确认LSE是否已经起振。这一步能快速排除LSI配置错误和LSE故障。

第四步,审查初始化代码流程。搜RTC初始化函数,搜BKP_DeInit、RTC_DeInit、RTC_SetCounter、HAL_RTC_SetTime这几个函数,再搜备份寄存器标志的判断位置,把程序在每次上电后到底做了什么事完整梳理一遍。

第五步,断电实测。断开VDD,保持VBAT供电,等几秒再接上电。上电后通过串口打印RTC计数器的值,隔几秒再读一次。如果连续两次的值递增,说明掉电走时硬件链路本身没问题,问题一定在初始化逻辑;如果值不变,问题大概率在供电或晶振。

这套流程能收敛绝大多数问题。如果走到第五步发现RTC在“裸奔”状态下也不递增,那就要回到第一步和第三步,重点查晶振匹配和电池供电链路。

4.2 症状与可能原因对照表

现象可能原因优先处理方向
每次上电时间都回到固定基准值无条件调用时间设置函数加备份寄存器标志,只在首次配置时设时间
掉电后时间完全停止,上电后立刻恢复VBAT供电丢失或时钟源不是LSE测量VBAT电压,核对时钟源配置
偶尔时间丢,但硬件检查正常Tamper误触发、备份域被软件复位禁用Tamper,检查BKP_DeInit调用位置
冷启动卡死在初始化阶段LSE起振慢,死等或超时太短增加超时时间,使用非阻塞等待
上电后时间能走,但偏差越来越大LSE晶振负载电容不匹配核对晶振规格,调整外部电容
批量部分失灵电池座虚焊、物料不良重点检查焊接和电池接触

5. 可直接复用的掉电保持初始化方案

5.1 标准外设库版本的写法

下面这段是我在F1系列上常用的写法,核心思路是用备份寄存器保存一个标志。标志匹配就直接跳过初始化,只做一次RTC同步;标志不匹配才执行完整配置。

#define RTC_CHECK_KEY 0xA55A void RTC_InitWithBackup(void) { // 使能PWR和BKP时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 开放备份域访问权限,这步不写,后续读写备份寄存器无效 PWR_BackupAccessCmd(ENABLE); if (BKP_ReadBackupRegister(BKP_DR0) != RTC_CHECK_KEY) { // 备份域未初始化,执行完整配置流程 // 备份域复位,只有确认走首次配置这里才能调用 BKP_DeInit(); // 配置LSE并等待起振 RCC_LSEConfig(RCC_LSE_ON); uint32_t timeout = 0; while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { timeout++; if (timeout > 10000) { // LSE没有就绪,不要继续,否则RTC会失去时钟源 return; } } // 选择LSE为RTC时钟源并使能RTC时钟 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); // 等待RTC同步和写操作完成 RTC_WaitForSynchro(); RTC_WaitForLastTask(); // 使用标准推荐的预分频配置,输出1Hz秒信号 RTC_InitTypeDef RTC_InitStruct; RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStruct.RTC_AsynchPrediv = 0x7F; RTC_InitStruct.RTC_SynchPrediv = 0xFF; RTC_Init(&RTC_InitStruct); RTC_WaitForLastTask(); RTC_SetCounter(0); RTC_WaitForLastTask(); // 写入初始化完成标志 BKP_WriteBackupRegister(BKP_DR0, RTC_CHECK_KEY); } else { // 备份域有效,RTC配置仍然保留,只需要等待同步即可读取时间 RTC_WaitForSynchro(); RTC_WaitForLastTask(); } }

几个细节值得再强调一下。

第一,PWR_BackupAccessCmd必须放在BKP_DeInit之前。备份域访问权限没打开,后面的复位和写寄存器操作都不会生效。

第二,BKP_DeInit只在标志不匹配的时候执行。这是掉电保持的命门,绝对不能移到初始化函数外面无条件执行。

第三,等待LSE就绪不要死等。我这里用的是计数器超时,实际项目中建议配合定时器或者毫秒延时实现超时,超时后要给出错误提示,比如点亮故障灯或者打印日志。

不同的系列函数名稍有差异,比如F4某些型号没有独立的BKP外设,备份寄存器访问函数变成了RTC_WriteBackupRegister和RTC_ReadBackupRegister,但思路完全一样。

5.2 HAL库/CubeMX环境的写法

在HAL库和CubeMX环境下,思路也一样,只是函数名变了。需要注意,CubeMX自动生成的MX_RTC_Init里,即使备份域是好的,也会重新执行HAL_RTC_Init。这本身不会重置时间计数器,但前提是预分频值必须保持相同,并且不要调用HAL_RTC_SetTime和HAL_RTC_SetDate。

#define RTC_CHECK_KEY 0xA55A RTC_HandleTypeDef hrtc; static void MX_RTC_Init(void) { RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef sDate = {0}; __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 使能LSE并等待起振,注意超时 __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); uint32_t lseTimeout = 0; while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) { lseTimeout++; if (lseTimeout > 1000000) { // LSE起振失败,需要处理,不能直接继续 return; } } __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSE); __HAL_RCC_RTC_ENABLE(); hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; hrtc.Init.SynchPrediv = 0xFF; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != RTC_CHECK_KEY) { // 首次上电或备份域丢失,才需要完整初始化 if (HAL_RTC_Init(&hrtc) != HAL_OK) { return; } sTime.Hours = 0; sTime.Minutes = 0; sTime.Seconds = 0; HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); sDate.Date = 1; sDate.Month = 1; sDate.Year = 0; HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, RTC_CHECK_KEY); } else { // 备份域有效,调用一次HAL_RTC_Init保证句柄和分频配置就位 // 但绝对不要调用SetTime/SetDate把时间覆盖掉 if (HAL_RTC_Init(&hrtc) != HAL_OK) { return; } } }

在CubeMX界面上,记得在RTC配置里把时钟源选成LSE,并关闭Tamper相关的可选功能。生成的代码里,如果LSE起振失败,HAL_RTC_Init会返回错误,很多时候会直接进Error_Handler卡死。这也是掉电不走的一个隐蔽表现,而且比死等在启动阶段更难看。

5.3 进阶玩法:PVD掉电瞬间保存关键数据

掉电保持的实战场景里,单纯让RTC继续走往往不够,很多产品还希望在掉电瞬间把一些状态数据记录下来。这时候可以利用PVD,也就是可编程电压检测器。

配置一个阈值电压,当VDD下降到阈值附近时,芯片会产生中断。在这个中断里,备份域依然由VBAT供电,写备份寄存器非常可靠,比在掉电瞬间写Flash安全得多。可以记录掉电原因、累计掉电次数、当前工作状态等信息,下次上电时从备份寄存器读出来。

PVD中断里动作越简单越好,不要做复杂运算、不要延时,把该保存的寄存器写进去就退出。配合RTC的自带备份寄存器,足够应对大部分掉电记录需求。

6. 踩坑记录与实操建议

6.1 三个真实教训

第一个教训来自一个早期项目。当时为了省一颗晶振,RTC时钟源选了LSI,常温下功能验证都能过。后来做断电测试,无一例外全部掉电不走。最初还以为是VBAT供电问题,换了电池也没用。后来对照芯片手册才意识到,时钟源选错是根本原因。这个坑,原理没吃透之前最容易犯。

第二个教训和晶振起振有关。有一块板子常温下RTC完全正常,放进低温箱做试验,掉电后再上电就不走了。排查了很久,最后发现是LSE起振时间在低温下明显变长,而初始化等待逻辑超时设置得太短。程序直接判定LSE失败,没有完成RTC配置。这个经历让我养成了一个习惯:所有LSE等待都必须带合理超时,而不是无限死等。

第三个教训是Tamper误触发。某款设备偶尔出现RTC时间丢失,频率不高,但客户那边特别头疼。查了两轮发现,备份寄存器标志经常在掉电期间被人为清掉,罪魁祸首就是板子上一个没接任何东西的Tamper检测引脚。之后在代码里关闭了Tamper功能,现象彻底消失。很多“偶发性时间复位”问题,根源就在这里。

6.2 量产前别忘了断电保持测试

最后给一个非常实用的建议:带RTC的板子,在研发验证阶段就把“断电保持测试”写进测试用例。方法很简单,给板子通电,设置好时间,断开主电源,保持VBAT供电,等5分钟或者更长时间,重新上电,读取RTC时间确认在正常递增。

这个测试看起来简单,却能非常有效地暴露VBAT虚焊、电池座接触不良、LSE低温起振失败、初始化逻辑重复覆盖时间这一类问题。

我现在的习惯是在样板阶段就把每个板子都做一遍断电保持测试,而不是只测一两台。因为这类问题往往和焊接、物料相关,连着测几台都正常,不代表量产批次没问题。个人经验是,批量回来后先随机抽几台断电放一晚上,第二天看时间是否正常,这一步能省下大量售后时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询