资讯中心

STM32 RTC掉电时钟保持方案:VBAT供电与备份寄存器实现解析

📅 2026/10/23 3:44:45
STM32 RTC掉电时钟保持方案:VBAT供电与备份寄存器实现解析
搞嵌入式的朋友应该都有过这种经历设备断电后重新上电系统时间直接回到初始值日志时间戳全乱了做数据记录、定时开关机的项目更是头疼。我之前在做一个低功耗采集终端时也被这个问题卡了几天后来用STM32内置的RTC模块加一颗CR2032纽扣电池从硬件到固件完整过了一遍总算把“掉电时钟不中断”这个功能稳定跑起来了。这篇就手把手拆解整个方案从VBAT电源设计到RTC代码解析给以后做类似需求的朋友一份能直接抄作业的参考。1. 项目整体设计与思路拆解1.1 为什么需要掉电时钟不中断很多嵌入式项目都需要实时时间戳比如环境监测终端要记录每一次传感器上报的时刻工控设备要保存故障发生的时间智能锁、定时插座要按预定时间执行动作。如果系统一断电时间就归零重新上电后所有记录都会变得不可信依赖时间的逻辑也会错乱。解决思路有两个方向一是每次上电后通过网络或用户手动校准时间二是让时钟在系统掉电后继续走。前者显然依赖外部条件不稳定后者才是工业级产品的常规做法。STM32内部集成RTC模块并且有专门的VBAT引脚为备份域供电只要在硬件上接一颗电池主电源断开后RTC照常运行主控醒来一读时间就是连续准确的。这个方案适合大多数基于STM32的项目尤其是采集终端、仪表、控制器这类需要长期无人值守的设备。相比外挂实时时钟芯片它节省了PCB面积和物料成本代码也简单只要把电源设计和初始化逻辑做对整体可靠性非常高。1.2 方案选型片内RTC加外部电池还是专用RTC芯片我在实际选型时对比过三条路线。第一条是STM32内部RTC加VBAT电池也就是本文的最终方案。它的优势是成本低一颗32.768kHz晶振加一颗纽扣电池就搞定代码用HAL库或标准库很快就能调通。缺点是内部RTC的走时精度取决于外部晶振温度漂移时误差会稍大但对常规日志和时间戳应用完全够用。第二条是内部RTC使用LSI低速内部时钟。LSI频率约32kHz不需要外部晶振成本进一步降低但精度太差常温下误差可能每天几十秒只能用在要求不高的场合。第三条是外挂专用RTC芯片比如DS3231、PCF8563。这类芯片精度更高还能带温度补偿但价格高、体积大还需要多占用I2C或SPI接口。对于普通项目来说有点杀鸡用牛刀。最终选择内部RTC加VBAT的方案核心原因是这颗芯片已经集成了完整的电源切换机制系统掉电时自动切换到VBAT供电用户不需要额外设计复杂的电源切换电路只要把VBAT引脚接好电池就行。这让我少走了很多弯路。1.3 整体架构与工作逻辑整个系统的供电关系可以这样理解正常工作时STM32的VDD由主电源供电RTC模块和备份寄存器由VDD供电此时VBAT引脚上的电池处于待机状态电流消耗极低当主电源掉电后芯片内部的电源切换电路自动让备份域从VBAT引脚取电RTC继续计数备份寄存器里的数据也不丢失。要让这套机制可靠运行需要保证三件事外部低速晶振能稳定起振VBAT引脚上确实有合格的电池电压软件在每次上电时能判断时间是已经初始化过的还是需要重新设置。后面我会分别从硬件和软件两个层面展开。2. 核心硬件连接与电路解析2.1 STM32的备份域与VBAT引脚STM32的电源体系里有一块独立区域叫备份域它包含了RTC核心、备份寄存器和部分唤醒逻辑。备份域可以由VDD供电也可以由VBAT引脚供电芯片内部有自动切换开关不需要外部继电器或MOS管来切换。不同型号的STM32引脚定义略有差异但VBAT引脚基本都是独立引出的通常标记为VBAT有的芯片叫VBAT/VDD。对于LQFP封装VBAT一般在角落位置参考原理图库连线时一定要确认物理引脚位置别画错封装。在设计时还要注意一点如果项目中根本不需要RTC掉电保持VBAT引脚不能悬空不管。手册里通常会要求把VBAT直接接到VDD否则备份域没有供电芯片内部逻辑可能出现奇怪问题。我在早期设计时就吃过这个亏VBAT悬空导致RTC初始化不稳后来老老实实连到3.3V才正常。如果需要电池备份VBAT引脚必须接电池正极电池负极接GND。同时建议VBAT引脚对地并联一颗0.1uF陶瓷电容放在引脚附近可以滤除电源噪声防止掉电切换瞬间的电压抖动造成RTC复位。2.2 外部32.768kHz晶振电路设计RTC要稳定走秒必须给它提供准确的时钟源。STM32有两个RTC时钟源可选外部低速晶振LSE和内部低速RC振荡器LSI。我使用的是外部32.768kHz晶振因为32.768kHz经过2的15次方分频后正好等于1Hz非常适合做秒计数。晶振的连接方式比较简单晶振两端分别接在STM32的OSC32_IN和OSC32_OUT引脚上两端各接一个负载电容到地。负载电容的大小要看晶振厂家给的规格常见的32.768kHz晶振匹配6pF到12.5pF。我一般先用9pF到12pF做初始值然后根据实际走时误差微调。有一些新手容易忽略的点晶振引脚附近的走线要短电容尽量靠近晶振摆放晶振下方不要走高速信号线最好铺地隔离。另外有些STM32型号的内部振荡电路已经内置了反馈电阻外部不需要再并联1M欧姆电阻但部分参考设计会建议加具体看芯片手册的RTC章节。这里提一句如果PCB空间特别紧张可以使用内部LSI配合RTC校准功能通过软件定时校准来补偿误差但精度远不如LSE。所以我的建议很直接既然都为了掉电走时了该花的晶振成本不要省买精度高一点的32.768kHz晶振整年误差能控制在几十秒以内。2.3 VBAT电池电路设计VBAT电池选择上CR2032纽扣电池是绝对的主流容量大约220mAh电压范围2.0V到3.0VSTM32的VBAT工作电压范围是1.65V到3.6V整个生命周期内都能满足要求。电路上可以非常简单电池正极直接连VBAT引脚电池负极接GNDVBAT引脚对地并0.1uF电容。STM32内部电源切换机制已经处理了与VDD的隔离所以不需要额外加防倒灌二极管。但有一点要注意如果电池座离芯片比较远走线又很长建议增加一个100nF和1uF的电容并联在VBAT上防止掉电切换瞬间的电压塌陷。有些设计希望在电池供电回路里串一个二极管防止主电源异常时电流倒灌进电池。这种做法在电池电压低于3.3V时问题不大但如果用可充电锂电池来做备份电池电压可能达到3.7V到4.2V就必须加二极管或专用电源切换芯片来保护。对于常用CR2032场景直接连接即可。另外CR2032电池买回来时通常会带绝缘卡片产品出厂前要提醒组装工位把卡片拔出。我遇到过批量出货后客户反馈首上电时间不对排查半天发现是电池被隔离没有接触。这类问题看似低级但批量生产时真的会发生最好在工艺文件里写清楚。如果项目需要超长时间待机也可以考虑在VBAT上并联一颗超级电容比如0.1F或0.47F。主电源断电后超级电容能维持RTC工作几天到几周不等适合不需要长时间断电保持的场景。但要确认超级电容的漏电流是否过大否则可能缩短电池寿命。2.4 掉电切换逻辑与注意事项STM32掉电切换是硬件自动完成的原理是芯片内部比较VDD和VBAT电压VDD高于VBAT时RTC由VDD供电VDD降到低于VBAT时切换到VBAT。这个过程用户不需要干预。实际应用中有几个容易被忽略的坑。第一主电源掉电后VDD并不是瞬间降到0而是随着电源电容放电缓慢下降。如果VDD下降速度太慢可能出现VDD和VBAT都高于RTC工作最低电压的情况这时切换逻辑可能反复震荡。解决办法是让VDD上的大容量电容尽量小一些或者增加一个电压检测芯片让主控在电压跌落初期尽快保存现场并进入复位状态。第二掉电期间RTC依旧在工作但这个阶段系统其他部分已经停止运行。如果应用里有外部看门狗、电压监控或者电源管理芯片需要确保这些芯片在掉电时不会误动作把VBAT一并拉低。最好的做法是把看门狗的供电和主电源放在一起避免看门狗从电池取电。第三VBAT引脚避免与其他大电流负载共用。有人图省事把VBAT和某颗传感器的待机供电接到同一个电池结果传感器在待机时可能吸入毫安级电流直接把电池寿命拉低一个数量级。VBAT电路最好独立只给RTC相关电路用。还有一个细节如果使用可充电备份电池必须确认充电电路不会在断电后反向放电。市面上有专门的RTC备份电池管理芯片支持涓流充电和过放保护但成本较高。对大多数产品一次性纽扣电池已经足够。3. 软件设计与代码实现3.1 开发环境与工程准备软件方面我使用的是Keil MDK加STM32CubeMX固件库用HAL库。如果你习惯标准库思路完全一样只是API名不同。ST官方目前主推HAL库新项目的参考资料也更丰富建议直接用HAL库开发。用STM32CubeMX配置工程时要注意几个关键点开启RTC选择激活时钟源为LSE在RTC配置页面里设置日期时间初始值开启BKP寄存器或后备寄存器不同型号名称略有差异如果使用低功耗唤醒还要配置RTC唤醒定时器。我习惯不勾选“生成复位后重新初始化时间”的默认选项因为掉电后重新上电RTC时间仍然有效不需要再次初始化。如果CubeMX每次启动都强制把时间设回默认值那掉电保持就白做了。解决方法是使用备份寄存器做一个标志位代码逻辑里判断是否第一次启动。3.2 RTC初始化代码解析下面这段是典型的HAL库RTC初始化代码包含时钟配置和首次时间设置。static void MX_RTC_Init(void) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); __HAL_RCC_RTC_ENABLE(); RTC-ISR ~(RTC_ISR_INIT); RTC-ISR | RTC_ISR_INIT; while ((RTC-ISR RTC_ISR_INITF) 0) { // 等待初始化完成 } sTime.Hours 0x08; sTime.Minutes 0x30; sTime.Seconds 0x00; // 24小时制 sTime.TimeFormat RTC_HOURFORMAT_24; // 二进制格式 sTime.DayLightSaving RTC_DAYLIGHTSAVING_NONE; sTime.StoreOperation RTC_STOREOPERATION_RESET; HAL_RTC_SetTime(hrtc, sTime, RTC_FORMAT_BCD); sDate.WeekDay 1; sDate.Month 1; sDate.Date 1; sDate.Year 24; HAL_RTC_SetDate(hrtc, sDate, RTC_FORMAT_BCD); HAL_RTCEx_SetBKRAM(hrtc, RTC_BKP_DR0, 0x32F4); }首先使能PWR和BKP时钟然后调用HAL_PWR_EnableBkUpAccess允许访问备份域寄存器。写入RTC时间之前需要把RTC切换到初始化模式设置INIT位并等待INITF标志这一步是为了安全写入时间寄存器。时间数据默认使用BCD码格式比如0x08表示8点。如果你更习惯十进制可以把HAL_RTC_SetTime的最后一个参数改成RTC_FORMAT_BIN直接填十进制的8也行。我习惯用BCD因为很多例程都这么写后面读时间时心里有数。最后写入备份寄存器0x32F4作为初始化标志。这个值可以自定义但不能是0xFFFFFFFF否则容易被擦除后的默认值误判。实际项目中这段初始化代码不应该每次上电都无条件执行应该配合备份寄存器判断。后面在主程序里我会给出完整逻辑。3.3 读取时间与格式化输出读取RTC时间也是固定套路。先调用HAL_RTC_GetTime再调用HAL_RTC_GetDate顺序不能反。这是因为内部寄存器在读取时会锁存先读时间寄存器再读日期寄存器才能保证两个值是同一时刻的。void Get_RTC_Time(uint8_t *hour, uint8_t *min, uint8_t *sec) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; HAL_RTC_GetTime(hrtc, sTime, RTC_FORMAT_BCD); HAL_RTC_GetDate(hrtc, sDate, RTC_FORMAT_BCD); *hour sTime.Hours; *min sTime.Minutes; *sec sTime.Seconds; }返回的BCD码可以直接用于LCD或数码管显示。如果要转成十进制用一句简单计算dec (bcd 4) * 10 (bcd 0x0F)。我踩过一个坑如果只读时间不读日期下一次读时间返回的秒数可能不更新。原因是HAL库内部需要读日期寄存器来解锁时间寄存器锁存。所以哪怕你只需要时间也要先调用HAL_RTC_GetDate或者连续调用HAL_RTC_GetTime两次。这个细节在官方勘误里也有提到。3.4 掉电标志与首次启动判断主程序上电后先读取备份寄存器判断RTC时间是否已经在电池供电期间被初始化过。这能避免每次上电都把时间重置成默认值。uint8_t Check_RTC_Init(void) { uint32_t bkp HAL_RTCEx_GetBKRAM(hrtc, RTC_BKP_DR0); if (bkp ! 0x32F4) { MX_RTC_Init(); // 首次上电初始化时间并写入标志 return 1; } return 0; // 已经初始化过直接读取时间 }这个函数的工作逻辑非常直白第一次上电时备份寄存器是默认值写入标志之后无论是软件复位、掉电恢复还是看门狗复位备份寄存器里的标志都还在因此不会重新设置时间。备份寄存器的本质是备份域里的一组RAM只要有VBAT供电内容就不会丢。这也是RTC掉电保持方案中不可或缺的一部分。我建议把标志值稍微复杂一点比如0x32F4避免误判。Flash擦除后默认值是0xFFFFFFFF如果你用0xFFFFFFFF做标志上电时会误以为已经初始化过导致时间错乱。3.5 完整主程序示例下面是一个完整的示例工程主程序包含了初始化、首次判断、时间读取和串口打印。#include main.h #include rtc.h #include usart.h RTC_HandleTypeDef hrtc; uint8_t ConvertBCD2Dec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } int main(void) { uint8_t hour, minute, second; char buf[64]; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); MX_USART1_UART_Init(); if (Check_RTC_Init() 0) { // 说明时间已经保持住不需要重新设置 } while (1) { Get_RTC_Time(hour, minute, second); sprintf(buf, Time: %02d:%02d:%02d\r\n, ConvertBCD2Dec(hour), ConvertBCD2Dec(minute), ConvertBCD2Dec(second)); HAL_UART_Transmit(huart1, (uint8_t*)buf, strlen(buf), 100); HAL_Delay(1000); } }这里MX_RTC_Init函数内部要对备份寄存器进行判断如果已经初始化过就直接跳过写时间部分只保证外设时钟和RTC本身处于使能状态。我习惯把MX_RTC_Init中的时间写入逻辑全部挪到Check_RTC_Init里这样职责更清晰。实际项目里还可以加入RTC闹钟和唤醒定时器。比如需要每天定时采集数据可以配置RTC闹钟让系统在指定时间从低功耗模式醒来。这部分逻辑也是在RTC初始化完成后配置的原理类似配置闹钟比较值然后使能中断即可。如果你追求更低的功耗整个主循环都可以进入STOP模式依靠RTC闹钟或唤醒定时器周期唤醒。这种情况下VBAT电池能让RTC在系统完全掉电时继续工作而STOP模式下主电源仍存在RTC由VDD供电。两者可以结合适合电池供电的便携设备。4. 常见问题与排查技巧实录4.1 晶振不起振RTC不走时这是RTC项目里最常见的问题。现象是上电后时间不跳变或者HAL_RTC_Init卡死在等待LSE就绪的地方。排查步骤我一般这样走先用万用表测VBAT电压确认在2V以上然后用示波器或逻辑分析仪看OSC32_IN引脚波形正常能测到32.768kHz正弦波如果测不到波形优先怀疑晶振虚焊、负载电容不匹配或晶振型号不对。这里有个细节用普通示波器探头去点OSC32_IN引脚时探头本身有几pF电容容易导致原本就临界起振的电路直接停振。所以测不到波形不代表晶振一定坏了可以先换低电容探头或者用手靠近晶振区域看是否起振。还有一种情况是STM32CubeMX配置LSE时选择了偏高的驱动等级导致功耗增大反而更难起振。LSE驱动等级有Low、MediumHigh等选项一般选择默认Low即可只有晶振负载特别重时才考虑调整。如果项目对时间精度要求不高可以临时把RTC时钟源改成LSI内部时钟跳过LSE起振判断先让系统跑起来。但别忘了LSI的精度远不如LSE上线前一定要改回来。4.2 VBAT供电但时间还是丢判断标准很简单上电后读备份寄存器如果标志变成了0xFFFFFFFF说明备份域曾经断电或VBAT供电异常。可能原因有这么几个。一是电池座接触不良。CR2032座子压在电池上如果弹片氧化或者卡扣松动断电后接触电阻变大VBAT电压会掉到1V以下RTC自然停摆。解决办法是选用质量好的电池座或者直接在PCB上贴片焊接电池弹片。二是二极管压降问题。有些设计在VBAT上串整流二极管防反接但普通二极管压降0.7VCR2032满电3.0V经过二极管后还有2.3V还能用但如果电池电压已经降到2.2V经过二极管后低于1.65VRTC就挂了。一定要用肖特基管或者干脆不串。三是VBAT引脚和GND之间有微小短路。PCB洗板不干净助焊剂残留可能造成微弱漏电。量产板出现这类问题可以尝试清洗后重新测试。怀疑电池问题时最直接的办法是上电状态下测量VBAT引脚电压同时串一个万用表电流档测电池放电电流。正常备份电流应该在微安级别如果测到毫安级说明有外部漏电回路。4.3 时间误差大RTC正常运行但走得快或走得慢属于晶振频率偏差问题。32.768kHz晶振的频率初始误差一般在20ppm以内换算下来一个月大约误差52秒这是正常范围。如果误差特别大可以检查负载电容是否和晶振规格匹配。负载电容偏大频率会偏低偏小频率会偏高。调试时可以用可调电容替换试但量产不建议使用可调电容因为稳定性差。另外STM32的RTC还支持数字校准在特定温度下写入校准值可以进一步减小误差。校准原理是通过RTC_CALR寄存器配置校准脉冲。使用HAL库时可以用HAL_RTCEx_SetSmoothCalib函数。这个功能在一般项目里用得不多但如果你需要做温控设备或电能表这个功能值得研究。内部LSI的误差更大常温下可能有百分之几的偏差一天累计误差可达几十秒。所以除非你的产品对时间精度完全没要求否则不要用LSI做RTC时钟源。4.4 低功耗电流偏大与电池寿命估算如果项目需要长时间待机电池寿命是关键指标。STM32在VBAT供电状态下的典型备份电流大概是1uA到2uA不同型号有差异。CR2032标称容量约220mAh理论寿命可以这样估算寿命(小时) 220mAh / 0.0015mA ≈ 146666小时 ≈ 16.7年这是理想值。实际电池有自放电环境温度也会影响容量加上VBAT电路上其他漏电一般按5到8年设计比较稳妥。如果实测电流明显偏大可以用排除法断开电池只保留VBAT引脚到GND的电容测PCB绝缘电阻再把电容摘掉测芯片本身漏电。多数情况是外部电路漏电而不是芯片问题。我在实际项目中习惯在VBAT引脚串一个0欧电阻量产后如果需要测电流直接替换成万用表表笔位置不用割板。如果最终版本不需要测电流可以把这个电阻改成0欧不影响功能。4.5 调试工具与固件烧录的注意事项使用ST-Link或J-Link调试时调试器通常会为目标板提供3.3V电源。如果目标板已经单独接了VBAT电池调试器供电会让VDD存在此时RTC由VDD供电并不代表VBAT的真实状态。想要测试掉电保持需要在调试器不供电的情况下只靠电池让RTC工作一段时间再重新上电读取时间。在Keil里查看RTC寄存器可以进入调试模式后打开Peripherals菜单选择RTC。如果寄存器值正常递增说明RTC核心工作没问题。如果读取时间不变先检查是否所有时钟都已使能尤其是PWR时钟和BKP时钟。另外不要在RTC正常运行时强制擦除整个Flash。虽然RTC寄存器在备份域擦Flash不影响但如果你同时做了Flash模拟EEPROM存储擦除操作可能涉及全片反而把备份寄存器标志也清掉。我建议程序里对备份寄存器标志做冗余存储比如写两个备份寄存器都校验通过才算有效。5. 实操心得从原型到量产的几个建议项目走完一遍之后我有几个经验想多说几句。第一原理图阶段就把VBAT测试点留好。PCB上设计一个兼容0欧电阻和电流表的焊盘方便后续测试。别小看这个点后期批量验证时能省很多事。第二电池座的封装不要随便用。CR2032电池座有贴片式、弹簧式和卡扣式卡扣式在振动环境下更可靠。如果产品有跌落测试要求我还会在电池座周围点一点热熔胶或者加一个限位泡棉防止电池在座子里跳动。第三RTC初始化里最好加一个超时保护。比如等待LSE就绪时如果100ms内没就绪不要死等直接跳出并用LSI继续工作同时上报时钟源异常。这样即使用户忘了贴晶振设备也能跑起来只是精度差一些不会变成砖头。第四掉电检测不能只依赖RTC。我一般会用单片机的PVD可编程电压检测器做一个中断电压低于阈值时立即把关键数据写入备份寄存器或Flash然后等待真正掉电。PVD的阈值要根据主电源的下降斜率调整不能设得太高否则频繁触发中断太低又来不及保存数据。第五这个方案后续扩展空间很大。比如需要RTC闹钟定时唤醒可以在主程序里配置HAL_RTC_SetAlarm并把系统切入STOP模式。需要远程校时可以把串口接收的NTP或自定义时间协议解析后写入RTC。再配合备份寄存器保存开机次数、运行时长、错误码等数据整个设备的维护体验会好很多。我个人在反复调试中最深的一个体会是RTC功能本身不难难的是把供电、晶振、低功耗和异常处理这几个环节都考虑全面。别急着上代码先把硬件电路查一遍再写初始化最后做断电测试。只要每一步都验证到位这个方案能稳定跑很多年。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案