资讯动态

STM32断电续走定时方案:内部RTC与DS3231选型及VBAT电路设计

发布时间:2026/10/29 19:24:23 来源:尧图企业网站定制
1. 断电丢时间这件事到底卡在哪儿做过定时类项目的人基本都踩过同一个坑设备跑得好好的一断电再上电时间直接回到1970年或者某个出厂默认值。像定时开关、数据记录仪、智能灌溉控制器、鱼缸灯光定时器这类项目一旦时间错乱整个逻辑就崩了——该开的时候不开该记的时间戳全乱套。这个问题的核心在于STM32芯片内部虽然自带一个RTCReal-Time Clock实时时钟外设但它是靠VBAT引脚上的备用电池或者超级电容来维持走时的。很多人画板子的时候VBAT直接悬空或者接到3.3V主电源上主电源一断RTC立刻停摆。更麻烦的是STM32的RTC计数器在断电后如果备份域也断电那连计数值都保不住重新上电就是从零开始。所以“断电后继续记时间”这件事本质上要解决两个层面的问题第一断电期间谁来给RTC供电第二断电期间RTC的计时精度和可靠性怎么保证。前者是硬件设计问题后者是芯片选型和软件配置问题。很多教程只讲怎么初始化RTC却不讲VBAT怎么接、电池怎么选、精度怎么校准结果就是实验室里跑得挺好一到现场就出问题。我自己的经验是如果你的项目对时间精度要求不高比如每天误差几分钟能接受用STM32内部RTC加一颗CR2032纽扣电池就够了。但如果要求月误差在分钟级以内或者工作温度范围比较宽那最好外挂一颗DS3231这类高精度RTC芯片。下面我会把两种方案都拆开讲清楚包括硬件接线、软件配置、精度校准和实际踩过的坑。注意VBAT引脚绝对不能悬空。即使你不用备用电池也要把VBAT接到VDD上否则RTC在断电后状态不确定可能产生额外功耗甚至影响下次上电初始化。2. 方案选型内部RTC还是外挂DS32312.1 STM32内部RTC的真实能力边界STM32内部RTC本质上是一个32位的计数器配合预分频器产生1Hz的秒信号。以STM32F103为例RTC时钟源可以选LSE外部低速晶振通常32.768kHz、LSI内部低速RC振荡器约40kHz或HSE分频。LSE的精度取决于晶振本身的精度和负载电容匹配一般普通晶振的精度在±20ppm左右换算下来每天误差大约1.7秒一个月就是50秒左右。LSI更差精度可能到±50%都有基本只能用来做看门狗或者粗略延时不能用来计时。内部RTC的优势是省事、省成本、省PCB面积。你只需要在VBAT上接一颗纽扣电池然后在软件里配置好预分频和时钟源就行。但它的劣势也很明显精度受温度影响大普通晶振在-40°C到85°C范围内的频偏可能达到±100ppm以上而且一旦主电源掉电VBAT切换电路如果设计不好可能产生漏电流电池很快就耗光。我实测过一批STM32F103C8T6的最小系统板用LSE加普通12.5pF负载电容的晶振室温下每天误差大约2秒放到冬天没暖气的房间里大概5°C左右每天误差能到5秒以上。这个精度对于定时开关灯、定时喂鱼这类应用勉强够用但对于需要记录精确时间戳的数据采集项目就不太行了。2.2 DS3231为什么成为定时项目的标配DS3231是Maxim现在被ADI收购了推出的一款低成本、高精度I2C实时时钟芯片内置温度补偿晶振TCXO精度可以达到±2ppm0°C到40°C换算下来每天误差不到0.2秒一个月误差大约5秒。它内部还集成了温度传感器可以读出环境温度精度±3°C对于很多需要温度补偿或者环境监测的项目来说算是白送的功能。DS3231的另一个好处是它自带电池切换电路主电源掉电后自动切换到纽扣电池供电而且电池漏电流极低典型值3μA左右一颗CR2032容量约220mAh理论上可以撑好几年。它还内置了闹钟功能可以产生中断唤醒STM32这对于低功耗定时唤醒的应用非常有用。不过DS3231也不是没有缺点。首先它比内部RTC多占PCB面积和成本一颗正品DS3231大概几块钱到十几块钱不等加上外围的电阻电容和电池座整体成本会增加。其次它通过I2C通信需要占用两个GPIO而且I2C总线在长距离或者干扰环境下可能出问题。另外市面上有很多翻新或者山寨的DS3231模块精度和可靠性参差不齐买的时候要小心。2.3 两种方案的对比与选择建议对比项STM32内部RTC外挂DS3231精度室温±20ppm约1.7秒/天±2ppm约0.2秒/天精度宽温差受晶振温漂影响大好内置TCXO补偿成本低只需晶振和电池中需芯片、电池、外围PCB面积小中等功耗低低额外功能无温度传感器、闹钟中断软件复杂度低直接操作寄存器中需I2C驱动适用场景精度要求不高的定时开关数据记录、精确计时选择建议很简单如果你做的是鱼缸灯定时、简单的定时插座、或者对时间精度要求不高的场合用内部RTC就够了省事省钱。如果你做的是数据采集记录仪、需要精确时间戳的物联网网关、或者工作环境温度变化大的户外设备那就上DS3231。还有一种折中方案用内部RTC计时但定期通过DS3231或者网络对时来校准这样既能省成本又能保证精度。实操心得不管用哪种方案第一次上电后都要通过串口或者按键设置一次时间。我习惯在固件里加一个“首次上电检测”逻辑——如果RTC备份域寄存器里的魔术字不对就认为时间是未初始化的强制进入时间设置模式。3. 硬件设计VBAT和电池电路怎么搞3.1 VBAT供电切换电路的正确画法STM32的VBAT引脚设计意图是接备用电源当VDD掉电时自动切换到VBAT供电。但很多开发板的参考设计里VBAT直接通过一个0欧姆电阻接到VDD或者干脆悬空这样断电后RTC就彻底没电了。正确的做法是在VBAT引脚上接一颗纽扣电池CR2032或CR1220同时用两个肖特基二极管组成一个简单的“或”电路VDD通过一个二极管接到VBAT电池通过另一个二极管也接到VBAT。这样当VDD存在时由于VDD电压通常高于电池电压电池不供电当VDD掉电时电池通过二极管给VBAT供电。二极管的压降要选低的比如BAT54系列压降只有0.3V左右对RTC供电影响不大。但更推荐的做法是使用专门的电源切换芯片比如TPS2113或者MAX40200它们可以自动在主电源和备用电池之间切换而且压降更小、漏电流更低。不过对于大多数DIY项目来说两个肖特基二极管加一颗纽扣电池已经足够可靠了。还有一个细节VBAT引脚旁边要放一个0.1μF的陶瓷电容做去耦这个电容要尽量靠近引脚。另外如果使用外部晶振LSE晶振和负载电容要尽量靠近OSC32_IN和OSC32_OUT引脚走线要短且对称下面不要走其他信号线否则容易引入干扰导致RTC走时不准甚至停振。3.2 纽扣电池选型和寿命估算CR2032是最常见的选择标称电压3V容量约220mAh。STM32内部RTC在VBAT模式下的典型功耗大约是1μA到2μA取决于温度和配置DS3231的电池电流典型值是3μA。按2μA算220mAh除以2μA等于110000小时大约12.5年。但实际上电池有自放电而且高温下漏电流会增加所以实际寿命可能只有5到8年。CR1220容量小一些大约40mAh寿命大概2到3年。如果你的设备预期寿命超过5年建议用CR2032或者更大的电池。另外要注意电池座的接触可靠性有些便宜的电池座用久了会氧化导致接触不良RTC就会在断电后停走。我遇到过好几次现场设备时间错乱最后查出来是电池座接触问题。注意焊接电池座的时候要小心不要让焊锡或者助焊剂污染电池座内部。另外电池不要提前装上去等板子清洗干净、测试完毕再装避免焊接热量影响电池寿命。3.3 外部晶振的选型和布局要点如果使用STM32内部RTCLSE晶振的选型很关键。要选32.768kHz的圆柱形或者贴片晶振负载电容根据晶振规格书来配通常是6pF或者12.5pF。STM32的LSE驱动能力有限晶振的等效串联电阻ESR不能太大一般要求小于70kΩ。如果晶振起振困难可以尝试减小负载电容或者换用ESR更低的晶振。PCB布局上晶振要尽量靠近芯片的OSC32_IN和OSC32_OUT引脚走线长度最好控制在10mm以内并且要包地处理。晶振下面不要走任何其他信号线尤其是高频信号。负载电容的地要接到干净的模拟地或者就近打孔到地平面。我踩过的一个坑有一次画板子的时候把LSE晶振放在板子边缘旁边走了一根PWM信号线结果RTC走时每天快十几秒后来把信号线移走就好了。所以布局这件事真的不能马虎。4. 软件配置从初始化到读写时间4.1 STM32内部RTC的初始化流程以STM32F1系列标准库为例内部RTC的初始化大致分这几步使能PWR和BKP时钟使能备份域访问复位备份域使能LSE并等待就绪选择RTC时钟源为LSE使能RTC时钟配置预分频器比如32767然后设置初始时间。这里有个关键点备份域复位后RTC配置才会生效。如果你在调试的时候发现RTC不走先检查是不是忘了调用BKP_DeInit()或者没有使能PWR_BackupAccessCmd(ENABLE)。另外RTC的寄存器是写保护的每次写之前要调用RTC_WaitForLastTask()等待上一次操作完成然后RTC_EnterConfigMode()进入配置模式写完再RTC_ExitConfigMode()退出。下面是一段我常用的初始化代码框架void RTC_Init_Config(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { BKP_DeInit(); RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_WaitForLastTask(); RTC_SetPrescaler(32767); RTC_WaitForLastTask(); RTC_SetCounter(0); RTC_WaitForLastTask(); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { RTC_WaitForSynchro(); RTC_WaitForLastTask(); } }这段代码里用BKP_DR1寄存器存了一个魔术字0xA5A5用来判断RTC是否已经初始化过。如果已经初始化过就不重新配置直接等待同步即可。这样可以避免每次上电都重置时间。4.2 DS3231的I2C驱动和时间读写DS3231通过I2C接口通信从机地址是0x687位地址。它的寄存器地址0x00到0x06分别存储秒、分、时、星期、日、月、年都是BCD格式。读写的时候要注意BCD和二进制之间的转换。初始化的时候先检查状态寄存器0x0F的OSF位Oscillator Stop Flag如果OSF为1说明晶振曾经停振过时间可能不准确需要重新设置。设置时间的时候先写寄存器0x00到0x06然后清除OSF位。下面是一个简单的读写函数示例#define DS3231_ADDR 0x68 uint8_t BCD2DEC(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); } void DS3231_SetTime(uint8_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec) { I2C_Start(); I2C_SendByte(DS3231_ADDR 1); I2C_WaitAck(); I2C_SendByte(0x00); I2C_WaitAck(); I2C_SendByte(DEC2BCD(sec)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(min)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(hour)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(1)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(day)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(month)); I2C_WaitAck(); I2C_SendByte(DEC2BCD(year)); I2C_WaitAck(); I2C_Stop(); // 清除OSF标志 I2C_Start(); I2C_SendByte(DS3231_ADDR 1); I2C_WaitAck(); I2C_SendByte(0x0F); I2C_WaitAck(); I2C_SendByte(0x00); I2C_WaitAck(); I2C_Stop(); }读时间的流程类似先写寄存器地址0x00然后重启I2C读7个字节再转换成十进制。注意DS3231的小时寄存器bit6是12/24小时模式选择位默认是24小时模式读出来的时候要屏蔽掉。4.3 时间格式转换和时区处理从RTC读出来的时间是UTC或者本地时间取决于你设置的时候怎么定的。我一般习惯在RTC里存UTC时间然后在应用层根据时区偏移转换成当地时间。这样做的好处是如果设备部署在不同时区只需要改一个偏移量就行不用重新设置RTC。时间格式转换主要涉及BCD和二进制之间的转换以及年月日到Unix时间戳的转换。Unix时间戳是从1970年1月1日00:00:00 UTC开始的秒数很多物联网平台和数据库都用这个格式。转换的时候要注意闰年和平年的天数差异以及每个月的天数不同。下面是一个简单的日期到Unix时间戳的转换函数uint32_t DateToUnix(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec) { uint32_t days 0; for (uint16_t y 1970; y year; y) { days (y % 4 0 (y % 100 ! 0 || y % 400 0)) ? 366 : 365; } uint8_t month_days[] {31,28,31,30,31,30,31,31,30,31,30,31}; if (year % 4 0 (year % 100 ! 0 || year % 400 0)) { month_days[1] 29; } for (uint8_t m 0; m month - 1; m) { days month_days[m]; } days day - 1; return days * 86400 hour * 3600 min * 60 sec; }这个函数虽然效率不高但对于每秒调用一次都不到的应用来说完全够用。如果对性能有要求可以用查表法或者更高效的算法。5. 精度校准和温度补偿实战5.1 内部RTC的精度校准方法STM32的RTC提供了一个数字校准寄存器BKP_RTCCR或者RTC_CALIBR可以通过调整预分频器或者校准值来微调走时速度。以STM32F1为例RTC有一个校准寄存器可以调整每2^20个时钟周期插入或删除一个时钟脉冲从而微调频率。具体操作是先让RTC跑一段时间比如24小时然后对比标准时间计算出每天误差多少秒。如果每天快X秒就需要减慢频率如果每天慢X秒就需要加快频率。校准值的计算公式是校准值 (误差秒数 / 86400) * 2^20。比如每天快2秒校准值 (2/86400) * 1048576 ≈ 24.3取整为24。不过这个校准是粗调而且受温度影响校准一次只能在特定温度下有效。如果工作温度变化大还是建议用DS3231。5.2 DS3231的温度补偿和老化校准DS3231内部已经做了温度补偿不需要用户手动干预。但它有一个老化偏移寄存器0x10可以用来微调晶振频率补偿长期老化。这个寄存器出厂时已经校准过一般不需要动。如果你发现DS3231走时偏快或者偏慢可以微调这个寄存器的值范围是-128到127每个单位大约对应0.1ppm的频率变化。调整方法是先让DS3231跑一个月对比标准时间计算月误差。如果每月快10秒换算成ppm大约是10 / (30*86400) * 1e6 ≈ 3.86ppm。然后调整老化寄存器每个单位0.1ppm需要调整大约39个单位。不过这个操作比较精细而且需要长期观察一般项目不需要做到这个程度。5.3 实际项目中的校准记录和对比我在一个数据采集项目里同时用了内部RTC和DS3231做对比测试环境温度控制在25°C左右跑了30天。结果如下天数内部RTC误差秒DS3231误差秒11.80.2712.51.41425.12.83053.66.1可以看到内部RTC一个月快了将近54秒而DS3231只快了6秒左右。这个差距在需要精确时间戳的应用里是致命的。所以如果你的项目对时间精度有要求DS3231多花的那几块钱绝对值得。实操心得校准的时候不要只看一天的数据至少观察一周以上。因为晶振的频率会受温度、电压、负载电容等多种因素影响短期波动可能掩盖真实趋势。另外校准后要记录校准日期和校准值方便以后追溯。6. 常见问题排查与避坑指南6.1 RTC不走时或者走时不准的排查思路遇到RTC不走时先按以下顺序排查检查VBAT电压用万用表量VBAT引脚对地电压正常应该在2.5V到3.3V之间。如果低于2V可能是电池没电或者切换电路有问题。检查LSE晶振是否起振用示波器看OSC32_OUT引脚应该有32.768kHz的正弦波。如果没有波形检查晶振、负载电容和焊接。检查备份域是否被复位如果每次上电RTC都归零可能是备份域复位电路有问题或者软件里误调用了BKP_DeInit()。检查预分频器配置预分频器值不对会导致走时速度错误。LSE是32768Hz预分频器应该设为32767这样计数频率是1Hz。检查I2C通信如果用DS3231用逻辑分析仪抓I2C波形看地址和读写时序是否正确。6.2 I2C通信失败的典型原因DS3231的I2C通信失败通常有这几个原因上拉电阻缺失或者阻值不对一般用4.7kΩ到10kΩ、从机地址搞错0x68是7位地址HAL库里要左移一位变成0xD0、电源电压不匹配DS3231工作电压3.3V如果STM32是5V系统需要电平转换、总线电容过大导致波形上升沿变缓。我遇到过最诡异的一次是DS3231模块上的上拉电阻焊成了100kΩ结果I2C波形上升沿特别慢低速通信勉强能用一提高速度就失败。后来换成4.7kΩ就正常了。所以买模块的时候最好检查一下上面的电阻值。6.3 电池寿命短和漏电流问题如果发现纽扣电池很快就没电了重点检查这几个地方VBAT引脚是否有漏电流可以用微安表串在电池回路里测、电池座是否有污染或者受潮、RTC是否配置了闹钟中断导致频繁唤醒、PCB上是否有其他电路通过寄生路径消耗电池电量。还有一个容易被忽略的点如果使用DS3231它的INT/SQW引脚如果配置为方波输出会持续输出信号增加功耗。不用的时候最好把这个引脚配置为中断模式或者关闭输出。6.4 常见问题速查表现象可能原因排查方法解决方案断电后时间归零VBAT无供电测VBAT电压接电池或检查切换电路走时偏快/偏慢晶振精度差对比标准时间校准或换DS3231I2C读不到数据上拉电阻问题看波形上升沿换4.7kΩ上拉电池很快耗尽漏电流大测电池回路电流查漏电路径时间偶尔跳变电源干扰看电源纹波加去耦电容首次上电时间不对未初始化检查魔术字加初始化逻辑7. 低功耗场景下的RTC应用技巧7.1 用RTC闹钟唤醒STM32在很多电池供电的定时项目里STM32大部分时间处于待机或者停止模式靠RTC闹钟定期唤醒。STM32的RTC闹钟可以配置为在某个特定时间或者每隔一定周期产生中断把MCU从低功耗模式唤醒。以STM32F1为例配置RTC闹钟的步骤是先RTC_WaitForLastTask()然后RTC_SetAlarm()设置闹钟值再RTC_ITConfig(RTC_IT_ALR, ENABLE)使能闹钟中断最后配置NVIC。进入待机模式用PWR_EnterSTANDBYMode()进入停止模式用PWR_EnterSTOPMode()。待机模式下功耗最低大概2μA左右但唤醒后相当于复位程序从头开始跑。停止模式下功耗稍高大概20μA但唤醒后继续执行。选择哪种模式取决于你的应用需求。7.2 低功耗模式下的时间保持策略在低功耗模式下RTC继续由VBAT或者主电源供电取决于模式。如果主电源还在RTC由VDD供电如果主电源掉了切换到VBAT。所以即使MCU在待机模式RTC也能正常走时。但要注意进入低功耗模式前要确保RTC配置正确尤其是闹钟中断要清除干净否则可能一进低功耗就被立刻唤醒。另外唤醒后的时钟配置要重新初始化因为低功耗模式下HSE可能被关闭。7.3 实际项目中的功耗实测数据我在一个智能灌溉控制器项目里实测过不同模式下的功耗运行模式72MHz大约35mA停止模式RTC运行大约18μA待机模式RTC运行大约2.5μA。用一颗2000mAh的锂电池待机模式下理论可以撑2000/0.0025800000小时大约91年当然实际受电池自放电影响会短很多。但至少说明RTC低功耗唤醒方案在电池供电项目里是完全可行的。实操心得进入低功耗模式前把不用的GPIO配置为模拟输入或者下拉避免悬空引脚产生漏电流。另外如果板子上有LED或者上拉电阻也要考虑它们的功耗。我见过一个项目待机电流死活降不下来最后发现是电源指示灯LED没关白白消耗了几百微安。8. 项目实战做一个断电续走的定时开关8.1 需求分析和方案确定假设我们要做一个鱼缸灯光定时器要求每天固定时间开灯和关灯断电后时间不丢失重新上电后继续按原定时间执行。精度要求不高每天误差几分钟以内都能接受。方案选择用STM32F103C8T6内部RTC加CR2032纽扣电池LSE晶振计时。灯光控制用继电器或者MOS管驱动LED灯带。时间设置通过两个按键加一个OLED显示屏或者通过串口命令设置。这个方案成本低、电路简单适合DIY。如果要做成产品建议换成DS3231提高精度并且加上外壳和电源适配器。8.2 硬件接线和关键元器件选型核心元器件清单STM32F103C8T6最小系统板、32.768kHz圆柱晶振、12.5pF负载电容两个、CR2032电池座、BAT54S肖特基二极管两个、0.1μF电容若干、继电器模块或者MOS管模块、OLED显示屏SSD1306、按键两个。接线要点晶振接OSC32_IN和OSC32_OUT负载电容接地VBAT接电池正极电池负极接地VDD通过二极管接VBAT电池通过另一个二极管接VBATOLED接I2C1PB6/PB7按键接PA0和PA1配置为上拉输入继电器控制引脚接PA2。8.3 软件流程和关键代码片段软件主流程上电后初始化系统时钟、GPIO、I2C、OLED、RTC检查备份域魔术字如果未初始化则进入时间设置模式读取当前时间判断是否在开灯时间段内控制继电器每秒更新一次OLED显示检测按键如果按下则进入时间设置模式。关键代码片段——判断是否在开灯时间段uint8_t IsLightOnTime(uint8_t hour, uint8_t min) { uint16_t now hour * 60 min; uint16_t on_time 8 * 60; // 早上8点开 uint16_t off_time 20 * 60; // 晚上8点关 if (on_time off_time) { return (now on_time now off_time); } else { return (now on_time || now off_time); } }这个函数处理了跨天的情况比如晚上10点开、早上6点关on_time大于off_time逻辑依然正确。8.4 实测效果和长期运行记录我做的这个鱼缸灯定时器已经连续跑了半年多中间经历过几次停电每次来电后时间都正常灯光按原定时间开关。用手机秒表对比一个月误差大约40秒左右对于鱼缸灯来说完全够用。电池电压从3.2V降到3.0V估计还能用一两年。唯一遇到的问题是夏天室温高的时候35°C以上误差会稍微大一点每天大概快3秒。但鱼缸灯不需要精确到秒所以没管它。如果要做精确计时还是得上DS3231。这个项目后续还可以扩展加一个光敏电阻根据环境光自动调整开关时间或者加一个ESP8266模块联网对时这样连电池都不用换每次联网自动校准时间。不过那是另一个话题了先把基础的时间保持做扎实再说。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑