# 引言 在嵌入式系统中,RTC(实时时钟)是时间敏感应用的核心。STM32F4 系列默认使用 LSE(32.768kHz 外部晶振)作为 RTC 时钟源,其精度高、功耗低。但 LSE 晶振易受环境干扰或 PCB 设计不良影响而停振,一旦停振,RTC 将停止计时,时间数据丢失。本文提供一种基于硬件备份域的解决方案,在 LSE 失效时无缝切换至 LSI(内部 RC 振荡器),并利用备份寄存器保存时间与校准参数,确保系统重启或 LSE 恢复后时间依然准确。 # 1. 备份域与 RTC 时钟架构 STM32F4 的备份域(Backup Domain)由 VBAT 引脚供电,包含 RTC、备份寄存器(20 个 32 位)和备份 SRAM(可选)。当主电源 VDD 掉电时,只要 VBAT 有电,备份域数据不丢失。RTC 的时钟源可选 LSE、LSI 或 HSE(分频),通过 RTCSEL[1:0] 位配置。 关键点: - RTC 时钟源切换必须在备份域复位后、RTC 使能前完成,否则无效。 - 备份域寄存器(RCC_BDCR)受写保护,需先开启 PWR 时钟并取消备份域写保护。 - LSE 停振检测可通过 RTC 的 LSEDRV 或外部中断,但更实用的是在 RTC 中断中检查 RTC_ISR 的 LSERDY 标志。 # 2. 硬件设计要点 - VBAT 引脚接 3V 纽扣电池或超级电容,确保主电源断电时备份域持续供电。 - LSE 晶振负载电容需匹配,走线尽量短且远离高频信号。 - 建议在 LSE 输出引脚并联 1MΩ 反馈电阻,提高起振可靠性。 # 3. 软件配置步骤 ## 3.1 使能 PWR 时钟并解锁备份域 ```c void BackupDomain_Unlock(void) { __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 取消备份域写保护 } ``` ## 3.2 配置 RTC 时钟源为 LSE,并启用自动切换 STM32F4 的 RTC 不支持硬件自动切换时钟源,但我们可以通过软件检测 LSE 状态,在 LSE 停振时切换至 LSI。切换步骤: 1. 复位备份域(RCC_BDCR 的 BDRST 位),确保 RTC 配置干净。 2. 选择时钟源:先尝试 LSE,若未就绪则用 LSI。 3. 使能 RTC 并配置预分频。 ```c void RTC_ClockConfig(void) { RCC_OscInitTypeDef oscInit = {0}; RCC_PeriphCLKInitTypeDef periphClkInit = {0}; // 尝试启用 LSE oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE; oscInit.LSEState = RCC_LSE_ON; if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) { // LSE 启动失败,回退到 LSI oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI; oscInit.LSIState = RCC_LSI_ON; HAL_RCC_OscConfig(&oscInit); periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; } else { periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; } HAL_RCCEx_PeriphCLKConfig(&periphClkInit); // 使能 RTC 时钟 __HAL_RCC_RTC_ENABLE(); } ``` ## 3.3 初始化 RTC 并设置时间 ```c RTC_HandleTypeDef hrtc; void RTC_Init(void) { hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; // 128 分频 hrtc.Init.SynchPrediv = 0xFF; // 256 分频,得到 1Hz 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(); } // 设置初始时间(例如 2025-01-01 00:00:00) RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef sDate = {0}; sTime.Hours = 0; sTime.Minutes = 0; sTime.Seconds = 0; sTime.TimeFormat = RTC_HOURFORMAT24; sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; sTime.StoreOperation = RTC_STOREOPERATION_RESET; HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); sDate.Year = 25; // 2025 sDate.Month = 1; sDate.Date = 1; sDate.WeekDay = 4; // 星期四 HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); } ``` ## 3.4 检测 LSE 停振并切换时钟源 在运行中,通过轮询或中断检测 LSE 状态。若发现 LSE 停振,则执行切换流程: ```c void Check_LSE_And_Switch(void) { // 读取 RCC_BDCR 的 LSERDY 位 if (!(RCC->BDCR & RCC_BDCR_LSERDY)) { // LSE 停振,切换至 LSI RCC->BDCR &= ~RCC_BDCR_RTCEN; // 关闭 RTC 时钟 while (RCC->BDCR & RCC_BDCR_RTCEN); // 重新配置时钟源为 LSI RCC_OscInitTypeDef oscInit = {0}; oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI; oscInit.LSIState = RCC_LSI_ON; HAL_RCC_OscConfig(&oscInit); RCC_PeriphCLKInitTypeDef periphClkInit = {0}; periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; HAL_RCCEx_PeriphCLKConfig(&periphClkInit); __HAL_RCC_RTC_ENABLE(); // 注意:切换后 RTC 会复位,需重新初始化时间和日期(可从备份寄存器恢复) RTC_Init(); // 重新初始化,但时间会丢失,需从备份寄存器恢复 } } ``` ## 3.5 利用备份寄存器保存时间与校准参数 为避免切换时时间丢失,在每次 RTC 中断(秒中断)中,将当前时间写入备份寄存器。切换后从备份寄存器恢复。 ```c // 保存时间到备份寄存器(假设 BKP_REG_INDEX 为 0) void Save_Time_To_Backup(RTC_TimeTypeDef *time, RTC_DateTypeDef *date) { uint32_t packed = (time->Hours << 16) | (time->Minutes << 8) | time->Seconds; HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, packed); uint32_t date_packed = (date->Year << 16) | (date->Month << 8) | date->Date; HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, date_packed); } // 从备份寄存器恢复 void Restore_Time_From_Backup(RTC_TimeTypeDef *time, RTC_DateTypeDef *date) { uint32_t packed = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0); time->Hours = (packed >> 16) & 0xFF; time->Minutes = (packed >> 8) & 0xFF; time->Seconds = packed & 0xFF; uint32_t date_packed = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); date->Year = (date_packed >> 16) & 0xFF; date->Month = (date_packed >> 8) & 0xFF; date->Date = date_packed & 0xFF; } ``` 在 RTC 秒中断回调中调用保存函数,切换后调用恢复函数。 # 4. 完整代码示例 以下为集成以上步骤的简化示例(基于 HAL 库): ```c // main.c 片段 int main(void) { HAL_Init(); SystemClock_Config(); BackupDomain_Unlock(); RTC_ClockConfig(); RTC_Init(); // 使能秒中断 HAL_RTCEx_SetSecond_IT(&hrtc); HAL_NVIC_EnableIRQ(RTC_IRQn); while (1) { Check_LSE_And_Switch(); // 周期检测 LSE 状态 HAL_Delay(1000); } } void RTC_IRQHandler(void) { HAL_RTCEx_RTCIRQHandler(&hrtc); } void HAL_RTCEx_RTCIRQHandler(RTC_HandleTypeDef *hrtc) { if (__HAL_RTC_SECOND_GET_IT(hrtc, RTC_IT_SEC) != RESET) { __HAL_RTC_SECOND_CLEAR_IT(hrtc, RTC_IT_SEC); // 读取当前时间并保存到备份寄存器 RTC_TimeTypeDef time; RTC_DateTypeDef date; HAL_RTC_GetTime(hrtc, &time, RTC_FORMAT_BIN); HAL_RTC_GetDate(hrtc, &date, RTC_FORMAT_BIN); Save_Time_To_Backup(&time, &date); } } ``` # 5. 注意事项 - **备份域复位**:切换时钟源前必须复位备份域,否则 RTC 配置无效,但复位会清空备份寄存器,因此需先保存数据。 - **LSI 精度**:LSI 精度约 ±5%,若对时间精度要求高,建议在 LSE 恢复后重新校准,或使用外部高精度 RTC 芯片。 - **中断优先级**:RTC 秒中断优先级应设置合理,避免长时间阻塞导致时间跳变。 - **电源管理**:确保 VBAT 供电稳定,否则备份域数据仍会丢失。 - **调试技巧**:在切换时钟源后,可通过串口打印当前时钟源状态,便于验证。 # 结语 通过以上配置,STM32F4 在 LSE 停振时能快速切换至 LSI,并利用备份寄存器保存时间,实现时间不丢失。该方法适用于对成本敏感但需高可靠性的嵌入式系统。实际项目中,建议结合看门狗和错误日志,进一步提升系统鲁棒性。