ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 RTC+VBAT断电不停钟完整方案与调试详解

STM32 RTC+VBAT断电不停钟完整方案与调试详解 做嵌入式这些年有一个需求我接过不下十次设备装到现场一断电时间就回到出厂值每次重新开机都要手动校时客户反馈用起来非常难受。其实STM32早就给了标准解法——RTC实时时钟配合VBAT备用电源主电源断了之后芯片内部依然靠纽扣电池维持计时重新上电直接读时间就行。这篇文章我就把整个方案从头到尾讲透包括RTC模块的工作原理、VBAT硬件电路怎么接、标准库和HAL库的代码怎么写、复位和掉电的判断逻辑怎么设计以及我在调试LSE晶振时踩过的那些坑。内容按真实项目流程来从原理图到代码再到示波器抓波形一条线走完适合正在做遥控器、记录仪、工控表计、物联网网关这些需要断电计时功能的朋友参考。1. 项目概述为什么断电之后时间不能丢1.1 真实应用场景拆解“断电不停钟”听起来像个小功能实际用起来全是细节。我最早是在做一个农业环境记录仪时接触到这个需求的设备放在大棚里用锂电池供电但电池总有耗尽或者更换的时候。如果没有RTC的备份域供电每次换电池都要重新设置时间采集到的温湿度数据因为时间戳断裂整条曲线都没法分析。后来换到工控领域很多设备需要记录故障发生时刻一旦掉电时间归零故障时间点就完全丢失了客户直接拒收。还有一类典型场景是带日历功能的智能家居设备比如定时插座、中控屏用户设定好“每天晚上7点开灯”如果断电后时间丢失定时就完全失效了。这些场景的共同点就是系统本身可以没有主电源但时间信息一定要保存住而且保存时间要尽量长。STM32的RTC模块配合外部低速晶振和VBAT引脚在纽扣电池供电的情况下能做到微安级的电流消耗完全满足这类需求。1.2 RTC VBAT这套方案的整体思路RTCReal-Time Clock的本质就是一个长期运行的计数器它靠32.768KHz的晶振产生秒脉冲内部寄存器记录秒、分、时、日期、星期等信息。关键是这个计数器所在的后备供电域Backup Domain拥有独立于主电源的供电引脚——VBAT。STM32芯片内部有一个电源切换电路当VDD主电源存在时RTC和备份寄存器由VDD供电当VDD掉电后自动切换到VBAT引脚供电整个过程无需软件干预。这种设计的巧妙之处在于RTC计时逻辑和备份寄存器的数据在掉电期间依然保持工作状态重新上电之后读到的还是断电前的时间。而且备份寄存器还能用来存一些关键标志位比如“是否发生过掉电”、“这是冷启动还是热启动”判断起来非常方便。2. RTC模块原理与VBAT硬件电路设计2.1 备份域到底是什么要理解VBAT的作用得先弄清楚STM32的电源域划分。芯片内部大致分成三个供电区域VDD核心供电域、模拟供电域VDDA、备份供电域。备份域里包含RTC计时逻辑部分、备份数据寄存器BKP Registers在F1系列上叫BKP在F4/H7等系列上归到RTC的Backup Register、以及部分唤醒逻辑。备份域的供电优先级是这样的VDD正常时优先用VDDVDD掉电后自动切换到VBAT。芯片内部有一个电源检测和切换电路不需要外部加二极管或者切换MOS管。这一点很关键我之前见过有人在外围又加了一套电源切换电路结果反而把VBAT的电压给抬高了导致电流倒灌进纽扣电池白白浪费电量。另外需要注意的是备份域复位的条件和主芯片复位的条件是分开的。系统复位、看门狗复位、甚至软件复位都不会清掉RTC当前时间和备份寄存器里的数据。只有两种情况下备份域会复位一是备份域软件复位命令比如标准库里的BKP_DeInit二是VBAT和VDD都没电了备份域彻底掉电。2.2 VBAT供电电路电池选型与连接方式在硬件连接上VBAT引脚的处理有几个要点第一VBAT引脚需要直接连接到备用电源最常见的是CR2032纽扣电池、锂亚电池或者法拉电容。CR2032容量大概在210mAh到240mAh之间按STM32备份域供电电流1.4uA到3uA来算理论上可以维持好几年的时间实际工程中也很少见到把纽扣电池用干的情况更多是担心晶振不起振或者电池座氧化导致接触不良。第二VBAT引脚上不要接任何LDO或者稳压器。很多人习惯把所有电源轨都经过稳压器但VBAT就是需要直接承受电池电压的芯片内部已经针对这个引脚的电压范围做了设计正常是1.65V到3.6V之间。如果接一个3.3V LDO等于把电池电压钳住了反而限制了电池的适用性。第三关于防倒灌的问题。如果VDD掉电后VBAT还在注意VDD引脚上的外部电路可能会通过内部二极管向其他器件倒灌电流但这些一般在芯片设计时已经考虑过。需要做的是确保VBAT引脚上不要并联大电容。有些参考设计会在VBAT到GND之间加一个0.1uF到1uF的滤波电容这也是合理的对稳定供电有好处。第四在实际产品中建议加一个电池座或者电池弹片方便工厂生产测试时插拔电池。同时还要注意VBAT走线尽量短远离高频信号线避免干扰导致RTC走时异常。如果PCB空间有限也可以用一个二极管隔离外部3.3V给VBAT和电池供电但这种方式在电池电量耗尽后会导致RTC时间丢失一般不推荐。2.3 32.768KHz晶振布局与负载电容计算RTC的走时精度很大程度上取决于LSE晶振的设计。STM32内部还有一个LSI低速内部振荡器大约32KHz精度比较差而且LSI在VDD掉电后不工作所以断电不停钟必须用LSE外部晶振这是方案选型的底线。LSE晶振常用的频率是32.768KHz也就是2的15次方。这个频率的特殊之处在于用15级分频器分频后正好得到1Hz的秒信号内部计时逻辑直接拿这个秒脉冲做累加不需要额外的补偿计算。晶振选型上最常用的是负载电容CL为6pF到12.5pF的贴片晶振。负载电容的匹配决定了振荡电路的起振裕度和频率精度计算公式是CL (C1 × C2) / (C1 C2) CS其中C1和C2是接在OSC32_IN和OSC32_OUT引脚到地的两个电容CS是引脚和走线的寄生电容一般取1pF到3pF。如果晶振规格书标称CL6pF板级寄生电容约2pF那C1和C2各取8pF比较合适。如果CL12.5pF那就各取22pF左右。实际项目中可以先按这个公式估算然后通过示波器观察波形幅度和频率偏差做微调。PCB布局上晶振尽量靠近MCU的OSC32_IN和OSC32_OUT引脚两条走线等长、短直周围铺地隔离。不要在其他层走高速信号穿越晶振下方否则会引入耦合噪声严重时直接导致不起振。另外晶振下方不要铺铜防止寄生电容增大。3. 代码实现从时钟配置到断电续走3.1 启动流程先打开备份域访问权限写代码之前先梳理一下RTC初始化的流程这个顺序在标准库和HAL库中基本一致打开PWR和BKP或者RTC对应的外设时钟。使能备份域写访问权限因为复位后备份域是写保护的。复位备份域可选通常在首次配置时做。开启LSE外部低速晶振并等待起振。选择LSE作为RTC时钟源使能RTC。配置RTC预分频器、设置时间格式。设置初始时间和日期。使能RTC中断如果需要秒中断或闹钟唤醒。以STM32F103系列的标准库为例初始化的代码是这样写的void RTC_Init(void) { // 1. 启用PWR和BKP时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 2. 允许访问备份域 PWR_BackupAccessCmd(ENABLE); // 3. 如果LSE还没使能进行首次配置 if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { // 复位备份域 BKP_DeInit(); // 开启LSE等待起振 RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET) {} // 选择LSE作为RTC时钟源 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 使能RTC时钟 RCC_RTCCLKCmd(ENABLE); // 等待RTC寄存器同步 RTC_WaitForSynchro(); // 配置RTC预分频器 RTC_WaitForLastTask(); RTC_SetPrescaler(32767); // 32768分频得到1Hz // 设置时间 RTC_WaitForLastTask(); RTC_SetCounter(UnixTimestamp); // 比如某个基准时间点 // 写备份寄存器标记 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { // 已经初始化过只需要等待同步完后直接读时间 RTC_WaitForSynchro(); } }这段代码里最关键的就是那个备份寄存器标志位0xA5A5。它的作用就是标记“RTC已经被配置过”这样系统复位之后不会重复初始化RTC。如果没有这个判断逻辑每次上电都重新配置RTC时间就会被清零断电不停钟就白做了。3.2 RTC时间设置与读取代码在实际项目中读取时间通常通过Ordinance或者直接读计数器转换成时间结构体。标准库自带RTC_GetCounter函数返回的是自1970年1月1日以来的秒数也可以直接用内部转换函数得到时间结构体。标准库的读时间代码大致是void RTC_GetTime(void) { uint32_t counter 0; // 等待RTC寄存器同步避免读到的数据还在更新 RTC_WaitForSynchro(); counter RTC_GetCounter(); // 使用C库的gmtime或自写转换函数 TimeStruct GetDateTimeFromTimestamp(counter); }注意RTC_WaitForSynchro这个函数在HAL库里对应的操作是等待RSF标志位置位。RTC内部时钟和APB总线时钟不是一个时钟域读取寄存器之前必须等它同步完成否则可能读到错误的值。这个细节很容易被忽略尤其是第一次调试RTC时很多人发现上电读取的时间乱跳大概率就是这里的问题。如果用HAL库初始化部分会优雅一些但也稍微繁琐一点void MX_RTC_Init(void) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_RTC_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 检查是否已经配置过 if (HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR0) ! 0xA5A5) { rtcHandle.Instance RTC; rtcHandle.Init.HourFormat RTC_HOURFORMAT_24; rtcHandle.Init.AsynchPrediv 0x7F; rtcHandle.Init.SynchPrediv 0xFF; rtcHandle.Init.OutPut RTC_OUTPUT_DISABLE; rtcHandle.Init.OutPutPolarity RTC_OUTPUT_POLARITY_HIGH; rtcHandle.Init.OutPutType RTC_OUTPUT_TYPE_OPENDRAIN; HAL_RTC_Init(rtcHandle); // 使能LSE __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) RESET) {} // 配置时间和日期 sTime.Hours 0x10; sTime.Minutes 0x30; sTime.Seconds 0x00; HAL_RTC_SetTime(rtcHandle, RTC_FORMAT_BCD, sTime, RTC_ALARMSUBSECOND_MASK_NONE); sDate.WeekDay RTC_WEEKDAY_MONDAY; sDate.Month RTC_MONTH_JUNE; sDate.Date 0x15; sDate.Year 0x25; HAL_RTC_SetDate(rtcHandle, RTC_FORMAT_BCD, sDate, 0); // 写入备份寄存器标志 HAL_RTCEx_BKUPWrite(rtcHandle, RTC_BKP_DR0, 0xA5A5); } }F4/F7系列RTC的预分频器拆成了异步预分频和同步预分频两个寄存器公式是fRTCCLK除以(异步分频1)再除以(同步分频1)等于1Hz。LSE频率32768Hz时一个常见的配置是异步分频127、同步分频255因为128 × 256 32768。这段配置在HAL库里已经封装好了但知道这个换算逻辑将来调试不同主频或者切换时钟源时就不会懵。3.3 掉电续走验证方案设计写完代码后最关键的验证环节就是断电测试。我常用的验证方案分三步第一步设置好当前时间记录此刻的Unix时间戳或者直接记录墙钟时间。第二步断开主电源让板子只靠纽扣电池维持VBAT供电。等待一段时间短则30秒长则数小时甚至几天然后在重新上电前先记录一下当前的真实时间。第三步重新上电通过串口打印读取到的RTC时间和真实时间对比计算出误差。这里有个细节如果断电时间特别长比如一晚上再次上电时RTC时间也应该非常接近真实时间如果有明显偏差就说明LSE晶振的频率有偏差需要校准。如果重新上电后RTC时间已经回到初始值那就要检查备份域是不是真正常供电了常见原因包括VBAT引脚没接好、纽扣电池没电、备份域被意外复位。另外我还习惯在代码里加一个掉电标志位。用一个备份寄存器存储一个魔数当系统掉电再上电时检查这个魔数是否还在如果不在了说明备份域数据丢失可以在界面上提醒用户“检测到掉电时间可能不准确”。这个功能在很多对数据完整性要求高的产品里特别有价值。4. 调试技巧与常见问题排查实录4.1 LSE晶振不起振的排查步骤RTC项目里出了问题十个里面大概有七个是LSE晶振不起振。现象就是程序卡在等待LSERDY标志位置位的死循环里或者RTC时间完全不走。我的排查顺序是这样的第一步确认硬件接线。用万用表量VBAT电压确认纽扣电池电压在2V以上。量OSC32_IN和OSC32_OUT对地的电阻排除焊接短路和虚焊。特别是贴片晶振焊接温度过高或者焊盘设计过小都很容易虚焊。第二步用示波器抓波形。示波器探头一定要打到X10档因为X1档的输入电容太大直接挂到晶振引脚上会把振荡电路拉停。有些人对这一点没概念X1档挂上去波形瞬间消失就误判晶振坏了其实是被探头搞死的。正常起振后OSC32_OUT引脚能看到接近正弦的波形幅度大概在0.5V到1V左右。第三步检查负载电容的取值。在典型电路里如果晶振的负载电容标称6pF而你用了两个30pF的电容大概率是不起振的因为等效负载电容远远偏离了晶振的设计工作点。第四步换一颗晶振再试。晶振这个东西批次差异和假货问题在市场上是真实存在的特别是一些便宜的HC-49S封装水很深。如果电路设计和布局都没问题换一个正规代理渠道的晶振通常就好了。4.2 复位后RTC时间归零的处理这个问题的常见原因其实不是硬件而是软件初始化逻辑写错了。很多人直接照搬官方例程在main函数里无条件执行RTC_Init每次复位都把RTC重新初始化一遍时间自然就归零了。解法就是我前面提到的方法用备份寄存器保存一个魔数初始化之前先读取这个魔数判断RTC是否已经被配置过。只有在魔数不对的时候才执行完整的初始化流程否则只做等待同步然后读取时间。还有一种情况是备份域被意外复位了。比如代码里调用了BKP_DeInit或者HAL_RTCEx_BKUPWrite写了一些无效数据都会导致备份域数据丢失。另外如果VDD和VBAT都没有供电备份域数据也会丢失这种属于正常现象不是bug。此外要特别留意调试器的行为。使用ST-Link或者J-Link调试时有时候复位仿真器会让整个芯片重新上电这会触发真正的掉电再上电过程如果备份域没有正常供电就会表现为“一进调试器时间就丢”。4.3 时间精度校准经验RTC走久了之后会有偏差这是晶振频率偏差导致的。普通32768Hz晶振的频率精度通常在20ppm左右折合下来一天误差大概1.7秒一个月误差50秒左右很多产品无法接受这个精度。校准手段有两个方向。方向一是硬件上选用更高精度的晶振比如带温度补偿的TCXO或者精度在5ppm以内的晶振。这个方法简单直接但成本会上涨。方向二是软件校准。STM32的RTC寄存器里有一个RTC_CALR校准寄存器部分型号有比如F4系列有RTC_CalibRegister它通过周期性地在32768个时钟周期中扣除或者增加若干脉冲微调RTC的走时频率。具体的校准思路是先用高精度时间基准比如GPS授时或者网络对时测出当前RTC在24小时内的偏差秒数然后把这个偏差转换为校准配置值写入寄存器。还有一种比较原始的校准方法是让MCU捕获一个高精度的外部1KHz或者1Hz信号作为时间基准自动修正RTC秒信号这种在电表等计量设备中见得多成本敏感型产品里非常实用。5. 工程化落地的几点经验5.1 电池寿命估算设计带RTC的产品客户一定会问“这个纽扣电池能用多久”。我们可以用STM32数据手册里的备份域电流参数来估算。F1系列在VBAT供电时的典型电流在1.4uA左右最大值不超过3.4uA。CR2032的标称容量按210mAh算如果电流按2uA算理想寿命是210mAh / 0.002mA 105000小时 ≈ 12年但这是理想值实际上电池自放电、高温环境、晶振起振瞬间的电流尖峰都会缩短寿命。工程上建议按6到8年作为设计目标。如果产品要求10年以上可以考虑更大的电池比如CR2477或者用法拉电容配合低功耗设计。这里还要注意一个细节备份域的电流消耗和LSE是否起振、温度、电压都有关系。特别是高温环境下电池自放电速率会显著增加。如果产品会放在户外暴晒环境电池座要选择耐高温的材料同时要避免电池在PCB上靠近发热源。5.2 生产与测试阶段的注意点量产阶段RTC时间初始化也是一个容易被忽视的环节。单独一片板子烧录程序后如果没有初始化RTC用户拿到手之后要自己校时体验很不好。成熟的做法是在产测环节通过工装自动写入当前时间。具体实现有两种方案一种是产测夹具通过串口发送时间设置命令固件收到命令后写RTC寄存器。这种方案灵活产线软件可控性高。另一种是在固件里通过编译宏写入编译时间即使用编译时的年份、月份、日期、小时、分钟作为初始时间。这种方案适合对时间精度要求不高的产品省掉产测环节的通信交互。但要注意编译时间并不是当前时间如果产品从编译到出厂隔了几个月出厂时显示的时间就不对了。还有一个大家常踩的坑是首次上电时RTC的初始时间选择。有人直接初始化为2000年1月1日但很多应用需要记录真实时间最好提供一个NTP或者串口校时接口或者在手机APP端由用户设置。5.3 备份寄存器的妙用STM32的备份寄存器除了存RTC初始化的标志位还有很多其他用途。例如在低功耗产品中可以保存当前的系统状态机掉电唤醒后直接恢复到掉电前的状态不用重新初始化外设。又例如可以保存设备的校准参数或者MAC地址这样即使主程序跑飞了恢复出厂设置这些关键信息也不会丢。我遇到一个比较有意思的需求是记录设备的历史掉电次数。每次上电时读一下备份寄存器里的掉电计数加1后写回这个数据对分析产品在用户家的使用情况、判断电网质量很有帮助。还有些设备需要做“首次开机引导”比如智能门锁第一次上电要进入配对模式之后开机就跳过引导。这个状态就可以存在备份寄存器里省得每次都去读外部Flash而且Flash写入次数有限备份寄存器的访问速度也更快。6. 写在最后的调试心得在我做的所有RTC项目里最让人头疼的永远是那颗不起眼的32.768KHz晶振。你永远要提前想到PCB打样回来后第一件事不是写代码而是先量一下LSE有没有正常起振。我建议你在画板子的时候就把测试点留出来把OSC32_IN和OSC32_OUT引到两个过孔或者测试焊盘上这样调试时示波器探头好挂不用去戳芯片引脚旁边那一点点露铜。还有一点要提醒的是如果你想复现这个项目建议准备一个小容量纽扣电池座、一个STM32最小系统板、一个ST-Link下载器再加一台示波器如果示波器没有逻辑分析仪也能勉强看LSE的波形。整套实验成本不高但能把RTC的原理和调试方法都完整吃透。按我上面说的步骤先让LSE起振再初始化RTC然后写备份寄存器魔数最后断电测试一步一步来基本不会跑到深沟里去。最后分享一个我习惯的小技巧在串口调试助手里周期性打印RTC的秒值变化同时把电脑的时间也显示在旁边一眼就能看出走时是否正常。这个方法虽然土但每次都能最快发现问题所在。
返回列表