# 引言 在工业控制、物联网设备等嵌入式场景中,RTC(实时时钟)通常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,晶振老化、环境干扰或 PCB 布局不当可能导致 LSE 停振,此时 RTC 将退化为 LSI(约 32kHz,精度差),时间误差迅速累积。更棘手的是,若系统未检测到停振,RTC 时间会静默漂移,导致日志时间戳、定时任务错乱。本文以 STM32F4 系列为例,介绍如何利用 RTC 备份域寄存器实现停振检测、时间无痕恢复及补偿,确保系统时间连续可靠。 # 1. LSE 停振检测原理 ## 1.1 硬件机制 STM32F4 的 RTC 时钟源可选 LSE、LSI 或 HSE 分频。当 LSE 停振时,RTC 不会自动切换时钟源,但硬件会通过 **RTC_ISR 寄存器的 LSERDY 位**(LSE 就绪标志)反映状态。此外,**RTC_ISR 的 RTC_ISR[13:8] 保留位** 中,`RTC_ISR[13]`(LSRMF)可配置为 LSE 监测模式,用于检测 LSE 失效。更可靠的方式是使用 **RTC 校准寄存器(RTC_CALR)** 的 `CALP` 和 `CALW8` 位组合,配合 `RTC_ISR` 的 `CALF` 标志,实现周期性校准检测。 ## 1.2 软件检测策略 推荐采用 **双保险检测**: - **轮询 LSERDY**:周期性读取 `RTC->ISR & RTC_ISR_LSERDY`,若为 0 则判定 LSE 停振。 - **RTC 校准检测**:利用 RTC 的 488ppm 校准窗口,通过比较 RTC 计数与外部参考(如系统定时器)的偏差,间接判断 LSE 是否失效。 # 2. 备份域寄存器的作用 RTC 备份域(Backup Domain)包含 20 个 32 位备份寄存器(BKPxR),位于 VBAT 供电区域,可在主电源断电时保持数据。利用这些寄存器,我们可以在 LSE 停振前保存关键时间信息,并在恢复后用于时间补偿。 核心思路: - **保存停振时刻**:检测到 LSE 停振时,立即将当前 RTC 时间(基于 LSI 的近似值)写入备份寄存器。 - **记录停振时长**:利用备份寄存器存储一个单调递增的计数器(由 LSI 驱动),用于估算停振持续时间。 - **恢复时补偿**:当 LSE 恢复后,读取备份寄存器中的停振时刻和时长,计算出补偿值,并写入 RTC 时间寄存器。 # 3. 实现步骤 ## 3.1 硬件配置 - 确保 VBAT 引脚连接电池或超级电容,以维持备份域供电。 - 使能 PWR 和 RTC 时钟,并允许访问备份域。 ```c // 使能 PWR 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 允许访问备份域 PWR_BackupAccessCmd(ENABLE); // 使能 RTC 时钟(若未使能) RCC_APB1PeriphClockCmd(RCC_APB1Periph_RTC, ENABLE); ``` ## 3.2 初始化 RTC 与备份寄存器 ```c // 初始化 RTC,选择 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 预分频(32768 / 128 = 256 Hz) RTC_InitTypeDef RTC_InitStruct; RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStruct.RTC_AsynchPrediv = 127; RTC_InitStruct.RTC_SynchPrediv = 255; RTC_Init(&RTC_InitStruct); // 写入备份寄存器初始值(例如,0 表示无停振记录) RTC_WriteBackupRegister(RTC_BKP_DR0, 0); RTC_WriteBackupRegister(RTC_BKP_DR1, 0); ``` ## 3.3 停振检测与记录 在系统主循环或定时中断中,周期性检查 LSE 状态。 ```c void Check_LSE_Status(void) { if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { // LSE 停振! // 1. 记录当前 RTC 时间(基于 LSI 的粗略值)到备份寄存器 uint32_t current_time = RTC_GetCounter(); // 假设 RTC 计数器为秒 RTC_WriteBackupRegister(RTC_BKP_DR0, current_time); // 2. 启动一个基于 LSI 的单调计数器(例如,使用定时器或 SysTick) // 这里简化:使用一个全局变量,由 LSI 驱动的定时器中断递增 // 并在备份寄存器中存储计数器的初始值 RTC_WriteBackupRegister(RTC_BKP_DR1, 0); // 清零停振时长计数 // 3. 切换 RTC 时钟源到 LSI(可选,但建议保持 RTC 运行) // 注意:切换时钟源需谨慎,可参考 RM0090 的步骤 // 此处省略,实际应用中可配置 RTC 时钟源切换 lse_stopped = 1; // 设置标志位 } else { if (lse_stopped) { // LSE 恢复! // 1. 读取停振时刻和停振时长 uint32_t stop_time = RTC_ReadBackupRegister(RTC_BKP_DR0); uint32_t stop_duration = RTC_ReadBackupRegister(RTC_BKP_DR1); // 2. 计算补偿后的时间(假设停振期间 RTC 继续以 LSI 运行,但误差较大) // 这里需要根据实际 LSI 频率校准,假设 LSI 为 32kHz,误差约 5% uint32_t compensated_time = stop_time + (uint32_t)(stop_duration * 1.05); // 粗略补偿 // 3. 设置 RTC 时间 RTC_SetCounter(compensated_time); // 4. 清除备份寄存器记录 RTC_WriteBackupRegister(RTC_BKP_DR0, 0); RTC_WriteBackupRegister(RTC_BKP_DR1, 0); lse_stopped = 0; } } } ``` ## 3.4 时间补偿细节 实际补偿需考虑 LSI 的精度。LSI 典型频率为 32kHz,但误差可达 ±5%。为提高补偿精度,可在系统正常时测量 LSI 的实际频率(通过比较 LSE 和 LSI 的计数),并存储校准系数。 ```c // 校准 LSI:在 LSE 正常时,测量 1 秒内 LSI 的计数 void Calibrate_LSI(void) { // 启动 LSI 并等待稳定 RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); // 使用定时器测量 1 秒内 LSI 脉冲数(此处省略具体实现) // 假设测得 lsi_count_per_sec uint32_t lsi_count = 32000; // 示例值 RTC_WriteBackupRegister(RTC_BKP_DR2, lsi_count); // 保存校准值 } ``` 在补偿时,使用校准值计算实际停振时长: ```c uint32_t lsi_cal = RTC_ReadBackupRegister(RTC_BKP_DR2); if (lsi_cal != 0) { // 假设停振时长计数器以 LSI 时钟递增,则实际秒数 = counter / lsi_cal uint32_t actual_seconds = stop_duration / lsi_cal; compensated_time = stop_time + actual_seconds; } else { // 无校准值,使用默认估算 compensated_time = stop_time + (uint32_t)(stop_duration / 32000); } ``` # 4. 完整代码示例 以下是一个简化的完整示例,展示核心逻辑(省略具体外设初始化细节)。 ```c // 全局变量 volatile uint8_t lse_stopped = 0; volatile uint32_t lsi_counter = 0; // 由 LSI 驱动的定时器中断递增 // 定时器中断服务函数(假设使用 TIM2,时钟源为 LSI) void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); lsi_counter++; // 将计数器值写入备份寄存器,以便恢复时读取 RTC_WriteBackupRegister(RTC_BKP_DR1, lsi_counter); } } // 主循环中的检测函数 void LSE_Monitor(void) { if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { if (!lse_stopped) { // 首次检测到停振 lse_stopped = 1; // 保存当前 RTC 时间(基于 LSI 的粗略值) uint32_t current_time = RTC_GetCounter(); RTC_WriteBackupRegister(RTC_BKP_DR0, current_time); // 清零 LSI 计数器 lsi_counter = 0; RTC_WriteBackupRegister(RTC_BKP_DR1, 0); // 启动 LSI 定时器(此处省略配置) // 注意:RTC 时钟源切换需谨慎,建议保持 RTC 运行在 LSI 上 } } else { if (lse_stopped) { // LSE 恢复 uint32_t stop_time = RTC_ReadBackupRegister(RTC_BKP_DR0); uint32_t stop_duration = lsi_counter; // 或从备份寄存器读取 // 补偿计算(使用校准值) uint32_t lsi_cal = RTC_ReadBackupRegister(RTC_BKP_DR2); uint32_t actual_seconds; if (lsi_cal) { actual_seconds = stop_duration / lsi_cal; } else { actual_seconds = stop_duration / 32000; // 默认 32kHz } // 设置新时间 RTC_SetCounter(stop_time + actual_seconds); // 清除标志和备份寄存器 lse_stopped = 0; RTC_WriteBackupRegister(RTC_BKP_DR0, 0); RTC_WriteBackupRegister(RTC_BKP_DR1, 0); } } } int main(void) { // 初始化系统时钟、GPIO、RTC、备份寄存器等(省略) // 启动 LSI 定时器(配置 TIM2 时钟源为 LSI,并开启中断) while (1) { LSE_Monitor(); // 其他任务 } } ``` # 5. 注意事项 - **备份域访问权限**:在写入备份寄存器前,必须使能 PWR 时钟并调用 `PWR_BackupAccessCmd(ENABLE)`,否则写操作无效。 - **RTC 时钟源切换**:当 LSE 停振时,RTC 不会自动切换到 LSI,需要软件干预。切换时钟源需遵循参考手册的步骤,否则可能导致 RTC 配置丢失。建议在检测到停振后,先停止 RTC 计数,切换时钟源,再重新启动。 - **LSI 校准**:LSI 精度较差,建议在系统正常时定期校准,并将校准值存入备份寄存器。 - **备份寄存器数量**:STM32F4 有 20 个备份寄存器,可根据需求分配,但注意 VBAT 供电有限,避免频繁写入。 - **时间戳连续性**:在停振期间,RTC 基于 LSI 运行,时间误差较大,但通过补偿可保证恢复后的时间连续性,避免系统时间跳变。 - **测试验证**:实际应用中,可人为断开 LSE 模拟停振,验证恢复逻辑和补偿精度。 # 结语 通过 RTC 备份域寄存器,我们可以在 LSE 停振时保存关键时间信息,并在恢复后实现无痕补偿。该方法不仅提高了系统的鲁棒性,还避免了因时间错误导致的连锁故障。开发者可根据实际需求扩展,例如增加看门狗或报警机制,进一步提升可靠性。希望本文能为你的嵌入式项目提供有价值的参考。