尧图精选

STM32 VBAT备用电池实现RTC断电不停钟:硬件、HAL库与调试全解析

🕒 发布时间:2026/9/28 1:29:08 📁 来源:尧图网络
做项目做到一半最尴尬的往往不是代码跑不通而是你关掉电源再上电辛辛苦苦校好的墙上钟、设备日志时间、传感器打点时间全部归零当场回到 2000 年 1 月 1 日。被这种问题折磨过几次之后我把 STM32 内部 RTC 和 VBAT 备用电池的整套玩法彻底摸了一遍。这篇东西不写虚的直接围绕“如何用 VBAT 电池让 RTC 在断电之后继续跑”这条主线把硬件接线、软件初始化、掉电恢复判断、调试排坑一次讲透代码基于 HAL 库F1/F4 系列都能参考。1. 断电丢时间问题的根因RTC 的供电域设计到底在保护什么很多人在 STM32 上第一次用 RTC 都有个困惑明明初始化代码写得好好的上电也能跑时间也能读出来但只要主电源一断再上电时间就回到初始值。这不是芯片坏了也不完全是代码问题而是你还没理解 RTC 的供电架构。1.1 普通外设和 RTC 的供电差异STM32 内部大致可以分成两个供电域一个是主电源域 VDD几乎所有 GPIO、外设、Flash、RAM 都在这个域里另一个是备份域包含 RTC 核心计数器、备份寄存器BKP、以及外部低速晶振 LSE 的振荡电路。备份域比较特殊它可以在主电源 VDD 掉电之后改用 VBAT 引脚上的电池继续供电。如果你只在主电源域里跑 RTC关机等于拔掉 CPU 的“主食”RTC 自然跟着停摆。需要做的是把 RTC 放到“备份域”这个不依赖 VDD 的小房间里再给它单独接一个 VBAT 电源这样即使主电源掉电RTC 部分的电路仍有电可吃时间就不会丢。1.2 备份域和 VBAT 的关系VBAT 引脚就是专门给备份域供电的备用电源入口。正常工作时STM32 内部的电源切换电路会自动选择 VDD 供电给备份域在主电源掉电时则自动切到 VBAT 供电。整个过程不需要软件控制硬件自动完成。需要重点理解的是VBAT 不仅给 RTC 计数器供电还给整组备份寄存器供电。这就是为什么很多人掉电恢复后能靠备份寄存器里的标志来判断“是不是第一次上电”——因为只要 VBAT 有电备份寄存器里的数据就能一直保持。1.3 多数人漏掉的细节VBAT 不接电池时不能悬空如果你的项目暂时不用电池VBAT 引脚也不能空着不接。ST 官方要求是直接接到 VDD也就是 3.3V 电源上。不少开发板上预留了 VBAT 和 VDD 之间的跳线帽就是这个原因。如果悬空备份域供电处于不确定状态可能导致 RTC 工作异常甚至让内部电源切换电路发生不明行为表现在现象上就是时间偶尔跳变、复位后丢失。我第一次画板子时就把 VBAT 空着了结果并联的备用电池死活不起作用。后来查引脚手册才发现VBAT 和 VDD 之间必须有一个明确连接路径不是随便推一个电池到引脚就完事。2. 硬件侧设计要点电池选型、VBAT 引脚电路与最小系统避坑RTC 软件写得好不好暂且不说硬件这边但凡有一点草率后面调试就会花掉几倍时间。VBAT 的电路其实非常简单但“简单”不代表可以随便接几个关键点直接决定电池能不能撑住、时间能不能稳住。2.1 电池选型纽扣电池、超级电容、大电容三种方案怎么选理论上任何能从 VBAT 引脚提供 1.65V~3.6V 的电源都可以用但实际产品中主流方案就三种。纽扣电池CR1220 / CR2032是最常见的选择。容量大CR2032 典型容量在 200mAh 以上在 STM32 RTC 典型备份电流只有几微安的情况下撑两三年问题不大。缺点是体积大、需要电池座、不适合高温环境。超级电容适合短时间断电保持的场景。比如 1F 耐压 5.5V 的超级电容充满后维持 RTC 跑几小时到一天是可能的成本低、可充电、环保但不能像电池那样撑几个月。普通电解电容或陶瓷电容组只能支撑几十秒到几分钟。适合做演示、验证功能不适合实际断电保持。如果你只是想在实验室里验证“断电不停钟”最简单的方法是在 VBAT 引脚外挂一个大电容比如 1000uF再把主电源断开观察时间是否延续。注意大电容的充电电流要控制好否则首次上电瞬间可能拉低电源电压。2.2 VBAT 引脚的接线细节无论用哪种备用电源VBAT 引脚旁边都建议并联两个电容0.1uF 陶瓷电容用于滤高频干扰1uF~10uF 电容用于储能缓冲。特别是使用电池座时电池座本身有接触电阻瞬间电流需求不大但加个电容能有效防止接触抖动导致 RTC 复位。如果使用可充电电池或超级电容还需要考虑充电电路不能直接把 VBAT 接在充电输出端就完事要预留充电限流电阻或专用充电芯片避免过流损坏电池和芯片引脚。2.3 外部晶振 LSE 的电路设计用 VBAT 实现断电不停钟的前提是 RTC 的时钟源必须能在 VDD 掉电后继续振荡。内部 RC 振荡器通常在掉电时不工作或者精度不够所以正经产品都用外部 32.768kHz 晶振也就是 LSE。LSE 晶振电路不能照搬高速晶振的布局。32.768kHz 属于低频低功耗振荡对负载电容和 PCB 走线更敏感。通常晶振两端各接一个负载电容到地容值根据晶振规格书选择常见的是 6pF 到 12.5pF 负载电容对应 10pF 到 22pF 的端接电容。走线要短两个负载电容的地要就近打孔避免形成天线噪声影响起振。之前遇到过一个挺隐蔽的问题晶振外围电路完全正确但 LSE 就是偶尔不起振。后来用示波器观察发现 PCB 上晶振附近有电源走线穿过高频纹波耦合到了晶振引脚。把走线绕开、地平面铺好后问题消失。这种问题不会出现在仿真里只会在实物上恶心你。2.4 调试接口的影响使用 ST-Link 或 J-Link 调试 RTC 程序时SWD 的 SWCLK 和 SWDIO 引脚通常分别是 PA13 和 PA14大多数情况下和 VBAT、LSE 没有电路冲突。但要注意如果代码里把 PA13/PA14 配成了普通 GPIO可能导致调试器连接失败。这在调试 RTC 这种“需要反复下电验证”的场景里尤其烦因为你正要测掉电特性结果程序把调试口禁了。解决方案是在初始化阶段延迟一段时间再配置 GPIO或者调试阶段先不要配置 SWD 引脚等功能验证完再关闭。ST-Link 连不上时可以按住板子复位键在点击 Connect 的瞬间松开复位利用“芯片处于复位状态”的空窗期连接调试器再擦除 Flash。3. 软件实现思路从初始化到掉电恢复的完整链路硬件准备好之后软件才是决定成败的关键。这里给出一个比较完整的 HAL 库例程设计思路覆盖首次上电时的 RTC 初始化、正常读取时间、掉电恢复时如何保留时间。3.1 判断冷启动与掉电恢复的常用做法很多人每次上电都去执行一遍 RTC 初始化并设置时间这是最典型的错误。掉电恢复后如果重新写时间哪怕用的是 RTC-CNT 里的旧值也可能因为时序问题覆盖掉真正在走的时间。正确做法是判断备份域里的数据是否有效。思路是这样的在备份寄存器 BKP_DR1 里写一个魔数比如 0xA5A5。上电后先读这个寄存器如果值等于 0xA5A5说明备份域在上一次掉电时没有复位RTC 还在走直接跳过 RTC 初始化和时间设置如果不等于说明可能是首次上电或者备份域曾经掉电需要做完整初始化并写入当前时间。3.2 RTC 初始化代码HAL 库示例下面这段代码以 STM32F103 系列为例使用 HAL 库。工程中需要先使能电源接口时钟和备份域访问权限再操作 RTC。#include rtc.h RTC_HandleTypeDef hrtc; static uint8_t RTC_IsBackupValid(void) { return (HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1) 0xA5A5); } static void RTC_SetDefaultTime(void) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; sTime.Hours 10; sTime.Minutes 30; sTime.Seconds 0; sDate.WeekDay RTC_WEEKDAY_MONDAY; sDate.Month RTC_MONTH_JANUARY; sDate.Date 1; sDate.Year 24; HAL_RTC_SetTime(hrtc, sTime, RTC_FORMAT_BIN); HAL_RTC_SetDate(hrtc, sDate, RTC_FORMAT_BIN); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, 0xA5A5); } void MX_RTC_Init(void) { RCC_OscInitTypeDef rccOscInit {0}; RCC_PeriphCLKInitTypeDef periphClkInit {0}; __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); /* 如果备份域已经有有效数据说明 RTC 在由 VBAT 供电运行不能再重新初始化 */ if (RTC_IsBackupValid()) { hrtc.Instance RTC; return; } /* 使能 LSE 外部低速晶振 */ rccOscInit.OscillatorType RCC_OSCILLATORTYPE_LSE; rccOscInit.LSEState RCC_LSE_ON; if (HAL_RCC_OscConfig(rccOscInit) ! HAL_OK) { Error_Handler(); } periphClkInit.PeriphClockSelection RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection RCC_RTCCLKSOURCE_LSE; if (HAL_RCCEx_PeriphCLKConfig(periphClkInit) ! HAL_OK) { Error_Handler(); } __HAL_RCC_RTC_ENABLE(); hrtc.Instance RTC; hrtc.Init.HourFormat RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv 127; hrtc.Init.SynchPrediv 255; hrtc.Init.OutPut RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType RTC_OUTPUT_TYPE_OPENDRAIN; if (HAL_RTC_Init(hrtc) ! HAL_OK) { Error_Handler(); } RTC_SetDefaultTime(); }代码逻辑值得展开说几点。第一判断函数 RTC_IsBackupValid 必须在使能备份域访问之后执行。有的库版本里 HAL_RTCEx_BKUPRead 之前如果没调用 HAL_PWR_EnableBkUpAccess读回来的一直是 0。第二HAL_RTCEx_BKUPWrite 写备份寄存器的前提是 RTC 已经初始化且备份域时钟已经打开。顺序反了会出现写入失败但代码不报错的现象。第三是预分频值。F1 的 RTC 预分频是异步分频两个寄存器值分别是异步预分频和同步预分频典型配置是 127 和 255这样 32.768kHz 经过 (1271)x(2551) 分频后正好得到 1Hz。F4/H7 系列内部结构不同但原理类似配置时不要照搬 F1 的寄存器操作代码。3.3 掉电恢复后的时间读取流程掉电恢复后MCU 从头开始跑程序。由于 VBAT 有电备份域里 RTC 计数器和备份寄存器数据都还在MX_RTC_Init 里判断出备份数据有效会直接跳过重新设置时间的逻辑。接下来只需要正常读取时间即可。void RTC_GetTimeDisplay(char* buf, uint8_t len) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; HAL_RTC_GetTime(hrtc, sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(hrtc, sDate, RTC_FORMAT_BIN); snprintf(buf, len, %04d-%02d-%02d %02d:%02d:%02d, 2000 sDate.Year, sDate.Month, sDate.Date, sTime.Hours, sTime.Minutes, sTime.Seconds); }注意一个细节HAL 库要求先读时间再读日期顺序不能反过来。因为读取时间时才会将内部影子寄存器里的日期数据锁存如果你先读日期再读时间可能会导致两次读到的日期跨秒时不一致。3.4 备份寄存器不仅是魔数仓库备份寄存器除了存魔数还能存一些掉电期间必须保留的业务数据。比如一个高精度设备的校准参数、上电计数、设备序列号或者状态机状态。这样即使在主控完全断电的情况下这些数据也能由 VBAT 保留。不过备份寄存器数量有限F1 系列有 10 个 16 位寄存器F4 有 20 个 32 位寄存器用的时候要规划好别把大数组往里塞。4. 代码调试技巧LSE 不起振、时间乱跳、复位清零怎么排查这一节是全文最有价值的部分。RTC 功能本身代码量不大但坑密度极高而且很多坑不是语法错误而是硬件时序、库调用顺序、调试环境干扰造成的。我把实际开发中碰到的问题按现象分类每个都给排查链路。4.1 现象一LSE 始终不起振程序卡死在时钟配置LSE 起振需要几百毫秒到一两秒如果晶振没焊好、负载电容不对、或者晶振本身坏了HAL_RCC_OscConfig 可能一直等到超时。此时程序毫无输出调试器连上后单步会发现卡在 while 循环里。排查链路先用示波器或逻辑分析仪测晶振引脚看有没有 32.768kHz 波形。注意不要用普通 10x 探头长时间直接测探头电容可能压住振荡最好用低电容探头或干脆通过 MCO 引脚输出时钟观察。如果不方便测波形可以把 LSE 的旁路电容先减小有些晶振对负载要求更小端接电容太大反而起振困难。检查 LSE 的使能时序。如果代码里先使能了 LSE又把 RTC 使能顺序反了也会导致起振异常。按上面的示例顺序来先 PWR/BKP 时钟 → 访问备份域 → 配置 LSE → 选择 RTC 时钟源 → 使能 RTC。检查代码中是否有 RCC_BackupResetCmd(ENABLE) 之类的调用。标准库里有些人为了复位 RTC 模块会调用备份域复位如果这个调用在 LSE 起振之后被误触发就会把备份域整个清掉时间归零LSE 也重新停振。4.2 现象二主电源断电再上电后时间归零但备份寄存器里的数据还在这个现象比较奇怪像是“时间丢了但魔数没丢”。实际上是因为 RTC 计数器和备份寄存器虽然都在备份域但某些芯片中 RTC 的复位源和备份寄存器不完全一样。更多情况是你虽然判断了备份数据有效并跳过了初始化但 RTC 的预分频、计数寄存器也可能因为软件配置错误被重置。一个经典原因是 HAL_RTC_Init 在每次上电都会执行且 hrtc.Init 里配置了同步/异步预分频。这个函数内部可能根据某些标志决定是否重新初始化 RTC。如果你在判断 RTC_IsBackupValid 为真的分支里只是设置了 hrtc.Instance RTC 就返回但主流程后面的代码又把 MX_RTC_Init 重新调用了一次就会被再次配置。做法是只有在备份数据无效时才执行完整初始化并让 MX_RTC_Init 在“数据有效”分支直接 return确保不会走到 RTC 重新初始化的代码。同时检查主循环里有没有其他地方调用了 HAL_RTC_Init。4.3 现象三串口打印出来的时间每隔一段时间就跳变一次时间跳变通常有三个来源一是 RTC 计数基准不对二是串口读取时序问题三是晶振频率偏移。先说 RTC 计数基准。F1 系列 RTC 是一个 32 位计数器预分频值决定秒钟跳变节奏。如果 LSE 不是 32.768kHz而是内部低速 RCLSI计数基准就完全不对。有些人想省成本用 LSI 做 RTC断电之后 LSI 停振时间必丢而且 LSI 精度差一天可能偏几十秒。所以规划项目时就别走这条路。串口读取时序问题常见于中断和主循环同时访问 RTC 寄存器。HAL_RTC_GetTime 本身不是线程安全的如果在 RTC 闹钟中断里读时间同时主循环也在读可能导致读到半更新状态的数据。建议串口打印前关闭全局中断读完成后再打开。晶振频率偏移则需要校表。32.768kHz 晶振本身有频率误差常见在 /-20ppm 左右一天累计误差约 1.7 秒。如果产品需要精确计时得做软件校准比如用网络时间或 GPS 秒脉冲来定期校准。校准方法可以简单实现记录一天时间误差然后按比例调整每秒计数。4.4 现象四为什么一进调试器时间就乱走这一点很多新手不理解。ST-Link/J-Link 在调试状态下触发内核暂停时RTC 外设是否继续走取决于调试配置。默认情况下RTC 的时钟来自 LSELSE 振荡器是独立于内核时钟域的所以内核暂停时 RTC 依然在后台计数。等你恢复运行、再次读取时间时看到的数值已经往前走了不少这不是 bug是正常现象。如果想让 RTC 在调试暂停时也暂停可以通过调试寄存器配置但绝大多数场景不需要。把这个现象写在调试笔记里省得后续同事误解。4.5 调试工具里的实用技巧Keil 调试时可以打开 Watch 窗口直接输入RTC-CNT查看 F1 系列 RTC 计数器的实时值。如果不想看寄存器原始值可以在代码里设置断点观察sTime.Hours、sTime.Minutes等变量的变化。串口打印还是最直观的但这意味着每次调试都要接线、打开串口工具效率不高。我个人的习惯是做一个“心跳”打印函数每秒钟通过串口输出当前时间的秒数和最低 16 位计数器值。断电后重新上电如果在串口日志里看到秒数是连续递增的说明 VBAT 路径和软件判断都没问题如果看到秒数从 0 开始就缩小排查范围先量 VBAT 电压再查备份寄存器标志是否写入成功。4.6 关于 RTC 校准的进阶操作F4/H7 系列 RTC 有数字校准功能可以微调同步预分频器的计数窗口精确校准漂移。F1 系列没有这个功能只能靠外部校正。不管哪个系列一个通用做法是用 MCO 引脚输出 LSE 时钟用频率计测量实际频率再配合定时器产生秒脉冲与标准秒脉冲对比记录误差后做软件补偿。校准记录示例 标准时间 24小时RTC 显示 23:59:58慢了 2 秒 每 86400 秒慢 2 秒相当于每天误差 -23.15ppm 若同步预分频为 255可以在每次秒更新时额外补偿若干子时钟计数5. 做产品时的额外细节功耗、电池寿命和 RTC 的扩展玩法如果只做开发板功能演示直接上纽扣电池就行。但真正做产品就要从功耗、成本、可靠性三个维度重新审视 VBAT 方案。5.1 F1/F4 系列的备份域功耗STM32F103 系列在仅 VBAT 供电、LSE 运行、RTC 计数的备份域电流一般在 1.4uA 到 3.3uA 之间具体看型号和温度。F4 系列通常稍大一些但也在个位数微安。相比主控正常工作的几十毫安这个电流可以忽略不计但设计电池容量时不能只看常态电流还要算上电池自放电和温度影响。5.2 电池寿命估算以 CR2032 为例标称容量 210mAh~230mAh备份电流取 2uA理论上 210mAh / 0.002mA 105000 小时约 12 年。实际要打折因为电池自放电、高温、瞬间大电流都会缩短寿命而且电池容量在接近寿命末期时电压下降可能导致 VBAT 跌破备用域最低工作电压。工程上按 5 年左右设计比较稳妥如果产品要求 10 年不断电建议用更大容量电池或者定期更换策略。超级电容方案则要计算电容存储能量和掉电持续时间。1F 电容充到 3.3V放电到 2.0VRTC 备份域最低电压约 1.65V可供的能量大约是E 0.5 * C * (V1^2 - V2^2) 0.5 * 1 * (3.3^2 - 2.0^2) 0.5 * (10.89 - 4) 3.445 焦耳按备份功耗 P 2uA * 3.3V ≈ 6.6uW理论维持时间约 3.445 / 6.6e-6 ≈ 522000 秒约 6 天。但实际电容漏电一般不小能撑一天已经很理想。用超级电容要注意耐压5.5V 耐压比较合适3.3V 直接充没问题如果用 2.7V 耐压就不行。5.3 RTC 闹钟和唤醒功能VBAT 掉电保持不只是为了“看时间”还能配合 RTC 闹钟实现定时唤醒。系统主电源断掉以后RTC 继续走等到设定的闹钟时间到达RTC 会输出一个脉冲给外部 MCU 的唤醒引脚或者在 STM32 待机模式下由 RTC 闹钟事件唤醒 MCU。这在低功耗数据采集器里非常常见平时整机休眠每隔一小时醒来采集一次数据再继续睡。F1 系列 RTC 支持闹钟中断F4/H7 功能更强支持唤醒定时器。要注意备份域里配置闹钟后主电源掉电再上电时闹钟设置仍然存在但如果备份域复位了闹钟配置也跟着清零。5.4 防篡改和入侵检测备份域的隐藏技能STM32 的备份域里还有个外部入侵检测引脚Tamper如果发生外部事件可以自动清除备份寄存器数据。这个功能可以用来做设备防拆设备外壳一旦被打开入侵引脚电平变化RTC 时间和备份数据立即清零后续上电就知道设备被拆过。这个玩法在防盗版、计费设备里很有价值但很多人不知道备份域还有这一层存在。6. 把“掉电时间保持”做进项目的几条个人经验最后聊几句在实际产品中沉淀下来的心得不按顺序但是句句都是真金白银。第一VBAT 电路上的 0.1uF 电容千万别省。我试过一批板子为了省物料VBAT 引脚只接了电池座结果用万用表量电压正常但设备在震动环境里偶尔时间归零排查到最后是电池座接触不良瞬间断路RTC 掉电复位。加了电容后即使电池瞬间离开零点几秒电容上的电也能维持备份域不丢数据。第二用标准库还是 HAL 库不重要但初始化顺序必须固定。我见过有人把 HAL_PWR_EnableBkUpAccess 放到 MX_RTC_Init 末尾结果备份寄存器读写失败。把“时钟使能 → 备份域访问 → RTC 配置 → 设置时间 → 写标志”的顺序写成注释放在代码头部能有效防止后人改坏。第三调 RTC 一定要学会用串口辅助定位问题。关掉调试器直接串口看打印比单步执行更能复现掉电场景。先把“每次上电都自动设置时间”的代码去掉再用串口观察时间是否跳过初始值这样能快速区分是软件覆盖问题还是备份域复位问题。第四产品定型前用示波器测一测 VBAT 引脚的上电波形。如果 VBAT 电压上升斜率太慢会导致电源切换电路判断混乱。正常情况下 VBAT 应该从 0V 很快跳到电池电压如果出现阶梯状上升说明电池内阻过大或线路太长需要加粗走线或换电池。第五给 RTC 时间做显示时注意 BCD 和二进制格式的区别。HAL_RTC_SetTime 和 GetTime 函数里都带了一个 RTC_FORMAT_BIN 或 RTC_FORMAT_BCD 参数。很多从汇编或寄存器开发转过来的同事喜欢直接跟寄存器交互容易在格式上栽跟头读出来的时间像天书。第六关于“断电不停钟”的验收不要只断电一次就算完。我在最终测试时会做一个连续掉电测试在 RTC 走时过程中随机时间点切断主电源保持几秒到几分钟不等再上电读取时间反复 50 次观察是否有时间倒退或跳变。这个过程能同时验证硬件和软件逻辑的鲁棒性比单次断电更有说服力。第七如果产品对时间精度要求很高比如定位设备、计费设备不要迷信 32.768kHz 晶振的标称误差。晶振老化、温度变化、焊接应力都会改变频率。最稳妥的做法是在设备里做一个周期性校准流程用外部标准时间源校一次把误差补偿值存进备份寄存器这样即使断电再上电校准值也不会丢。回到最开头那个场景当你把设备断电再上电看到串口打印出连续跳动的时间而不是刺眼的 2000 年 1 月 1 日那种感觉确实让人安心。STM32 RTC 的代码量不大但它横跨硬件电路、时钟架构、低功耗设计、调试工具多个层面每一个环节都有细节。希望这篇实战记录能让你少走点弯路一次就把 VBAT 断电不停钟做靠谱。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →