# 引言 在工业控制、物联网设备等嵌入式应用中,RTC(实时时钟)是记录时间戳的关键外设。STM32F4 系列通常使用 32.768kHz 的 LSE 晶振作为 RTC 的低速时钟源,但 LSE 可能因温度漂移、焊接不良或电磁干扰而停振。一旦 LSE 停止,RTC 将无法计时,系统时间归零或停滞,造成严重后果。本文提出一种基于 RTC 备份寄存器的纠偏策略:在 LSE 停振时,利用备份寄存器保存关键时间信息,并在恢复后自动校正日历,实现无外部晶振的可靠运行。 # 原理剖析 ## 1. RTC 与备份寄存器架构 STM32F4 的 RTC 模块包含一个 20 位的预分频器、32 位可编程计数器(用于秒计数)以及一组 20 字节的备份寄存器(BKP Registers)。这些寄存器位于 VDD 供电域,当主电源掉电时,可由 VBAT 引脚供电,保持数据不丢失。备份寄存器的主要用途是存储系统配置或关键数据,例如 RTC 校准值、唤醒标志等。 ## 2. LSE 停振检测机制 RTC 时钟源可选 LSE、LSI 或 HSE 分频。LSE 停振时,RTC 的时钟输入丢失,但 RTC 本身不会自动切换时钟源。通过检测 RTC_ISR 寄存器的 LSERDY 位(LSE 就绪标志)可以判断 LSE 状态。当 LSERDY=0 时,表示 LSE 未就绪或停振。此时,RTC 计数器停止更新,但备份寄存器仍可读写。 ## 3. 纠偏策略设计 核心思路: - 正常运行时,定期将当前时间(如秒计数)和日期信息写入备份寄存器,作为“时间快照”。 - 检测到 LSE 停振时,立即停止依赖 RTC 的时间更新,并记录停振时刻的备份值。 - 在 LSE 恢复后,读取备份寄存器中的时间快照,结合停振持续时间(通过系统定时器或外部事件估算),重新初始化 RTC 计数器,实现日历纠偏。 # 配置步骤 ## 1. 使能 RTC 与备份寄存器时钟 首先,需要使能 PWR 和 RTC 时钟,并允许访问备份域。 ```c // 使能 PWR 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 允许访问备份域 PWR_BackupAccessCmd(ENABLE); // 使能 RTC 时钟(这里使用 LSE,但需先使能 LSE) RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待 LSE 就绪 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); // 等待 RTC 同步 ``` ## 2. 初始化 RTC 并设置时间 配置 RTC 预分频器(LSE 为 32768Hz,分频系数为 32767),并设置初始时间。 ```c RTC_InitTypeDef RTC_InitStruct; RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStruct.RTC_AsynchPrediv = 0x7F; // 异步分频 128 RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 同步分频 256,实际分频为 32768/(128*256)=1Hz RTC_Init(&RTC_InitStruct); // 设置时间:2024年1月1日 12:00:00 RTC_TimeTypeDef time = {0}; time.RTC_Hours = 12; time.RTC_Minutes = 0; time.RTC_Seconds = 0; RTC_SetTime(RTC_Format_BIN, &time); RTC_DateTypeDef date = {0}; date.RTC_Year = 24; // 2024年 date.RTC_Month = 1; date.RTC_Date = 1; date.RTC_WeekDay = 1; RTC_SetDate(RTC_Format_BIN, &date); ``` ## 3. 定期保存时间快照到备份寄存器 在应用主循环或定时器中,每隔一段时间(如 1 秒)将当前时间写入备份寄存器。 ```c void SaveTimeSnapshot(void) { RTC_TimeTypeDef time; RTC_DateTypeDef date; RTC_GetTime(RTC_Format_BIN, &time); RTC_GetDate(RTC_Format_BIN, &date); // 将时间信息打包存储(示例:使用 4 个备份寄存器) uint32_t snapshot = (time.RTC_Hours << 16) | (time.RTC_Minutes << 8) | time.RTC_Seconds; RTC_WriteBackupRegister(RTC_BKP_DR0, snapshot); snapshot = (date.RTC_Year << 16) | (date.RTC_Month << 8) | date.RTC_Date; RTC_WriteBackupRegister(RTC_BKP_DR1, snapshot); // 记录写入时的系统 tick(用于计算停振时长) RTC_WriteBackupRegister(RTC_BKP_DR2, (uint32_t)GetSystemTick()); } ``` ## 4. 检测 LSE 停振并处理 在主循环中轮询 LSE 状态,一旦发现停振,立即停止时间更新,并记录当前系统 tick。 ```c void CheckLSEStatus(void) { if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { // LSE 停振 lse_stopped = 1; stop_tick = GetSystemTick(); // 可在此关闭 RTC 中断或相关任务 } else { if (lse_stopped) { // LSE 恢复,执行纠偏 CorrectRTC(); lse_stopped = 0; } } } ``` ## 5. 纠偏函数实现 读取备份寄存器中的快照,计算停振时长(基于系统 tick 差值),然后重新设置 RTC 时间。 ```c void CorrectRTC(void) { // 读取快照 uint32_t time_snap = RTC_ReadBackupRegister(RTC_BKP_DR0); uint32_t date_snap = RTC_ReadBackupRegister(RTC_BKP_DR1); uint32_t saved_tick = RTC_ReadBackupRegister(RTC_BKP_DR2); uint32_t elapsed = GetSystemTick() - saved_tick; // 停振时长(假设 tick 为毫秒) // 解析时间 RTC_TimeTypeDef time; time.RTC_Hours = (time_snap >> 16) & 0xFF; time.RTC_Minutes = (time_snap >> 8) & 0xFF; time.RTC_Seconds = time_snap & 0xFF; RTC_DateTypeDef date; date.RTC_Year = (date_snap >> 16) & 0xFF; date.RTC_Month = (date_snap >> 8) & 0xFF; date.RTC_Date = date_snap & 0xFF; // 将停振时长加到时间上(简化处理,需考虑进位) uint32_t total_seconds = time.RTC_Hours * 3600 + time.RTC_Minutes * 60 + time.RTC_Seconds; total_seconds += elapsed / 1000; // 假设 tick 为毫秒 // 转换为时分秒 time.RTC_Hours = total_seconds / 3600; time.RTC_Minutes = (total_seconds % 3600) / 60; time.RTC_Seconds = total_seconds % 60; // 设置新时间(日期进位可忽略或另行处理) RTC_SetTime(RTC_Format_BIN, &time); RTC_SetDate(RTC_Format_BIN, &date); // 重新保存快照 SaveTimeSnapshot(); } ``` # 完整代码示例 以下是一个简化的综合示例,展示主循环中的调用逻辑。 ```c // 全局变量 volatile uint8_t lse_stopped = 0; uint32_t stop_tick = 0; // 假设系统 tick 为 1ms uint32_t GetSystemTick(void) { return HAL_GetTick(); // 使用 HAL 库 } int main(void) { HAL_Init(); // 初始化时钟、GPIO 等 // 初始化 RTC(如上) RTC_Init(); // 初始保存快照 SaveTimeSnapshot(); while (1) { CheckLSEStatus(); // 正常运行时每秒保存一次快照 if (!lse_stopped) { static uint32_t last_save = 0; if (GetSystemTick() - last_save >= 1000) { SaveTimeSnapshot(); last_save = GetSystemTick(); } } // 其他任务 } } ``` # 注意事项 - **备份寄存器数量有限**:STM32F4 有 20 个备份寄存器,每个 32 位,需合理规划存储内容。示例中使用了 3 个,可根据需要扩展。 - **时间进位处理**:纠偏时,如果停振时间较长,可能导致日期进位(如跨天、跨月),示例中未处理,实际应用需根据日期算法完善。 - **系统 tick 的可靠性**:停振期间,系统 tick 必须持续运行(如由 HSI 或 PLL 驱动),否则无法计算停振时长。若系统掉电,则备份寄存器仍可保存数据,但恢复后需人工干预。 - **LSE 恢复检测**:LSE 恢复可能需要一定时间,建议在检测到 LSERDY 置位后,延时一段时间再执行纠偏,确保时钟稳定。 - **备份域复位**:如果备份域被复位(如 VBAT 掉电),备份寄存器内容丢失,需重新初始化 RTC。 - **功耗考虑**:备份寄存器在 VBAT 供电下会消耗少量电流,设计低功耗产品时需注意。 # 总结 通过 RTC 备份寄存器保存时间快照,并在 LSE 停振后利用系统 tick 计算停振时长,可以实现无外部晶振的日历纠偏。该方法简单有效,适用于对时间精度要求不苛刻但需保证连续性的场景。开发者可根据实际需求扩展,例如增加日期进位算法、使用外部中断检测等。希望本文能帮助你在嵌入式项目中构建更健壮的 RTC 系统。