# STM32F4 无晶振日历自恢复:LSE 停振后利用 RTC 备份域寄存器的容错设计 ## 一、问题背景与核心原理 在 STM32F4 系列(如 STM32F407)中,RTC(实时时钟)通常依赖 LSE(32.768kHz 外部晶振)提供高精度时钟。然而,LSE 可能因焊接不良、电磁干扰或温度漂移而停振,导致 RTC 计时停止。若系统未做保护,重启后 RTC 将恢复默认值(如 2000 年 1 月 1 日),造成时间信息丢失。 **核心思路**:利用 RTC 备份域寄存器(BKP,Backup Registers)在 LSE 停振前保存当前时间戳(如 Unix 时间)。当系统检测到 LSE 故障并重启后,通过读取 BKP 中的备份值,结合内部低速时钟(LSI)或系统时钟进行粗略计时,实现日历的自动恢复。 **关键原理**: - BKP 寄存器位于备份域,由 VBAT 供电,即使主电源断电,只要电池存在,数据不丢失。 - RTC 具有独立的校准和唤醒功能,可在 LSE 停振时切换至 LSI(约 32kHz,精度较低),但 LSI 误差较大,不适合长期计时,因此仅用于短期过渡。 - 通过软件标志位(如 BKP 寄存器中的特定值)记录 LSE 状态,重启后判断是否需要恢复。 ## 二、硬件与软件准备 - 硬件:STM32F4 开发板(如 STM32F407-Discovery),备用电池连接 VBAT。 - 软件:STM32CubeIDE(或 Keil MDK),HAL 库。 - 配置:启用 RTC、BKP、PWR 时钟,并设置 RTC 时钟源为 LSE(默认),但代码中需支持切换至 LSI。 ## 三、配置步骤 ### 1. 使能相关时钟 在 `main.c` 中,首先使能 PWR 和 BKP 时钟,并允许访问备份域。 ```c __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 允许访问备份域 ``` ### 2. 初始化 RTC(支持 LSE 或 LSI) 初始化 RTC 时,尝试启用 LSE,若失败则回退至 LSI,并设置标志。 ```c RTC_HandleTypeDef hrtc; void RTC_Init_With_Fallback(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; // 尝试启用 LSE __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) { // 等待超时,若超时则切换 LSI if (HAL_GetTick() > 1000) { __HAL_RCC_LSE_CONFIG(RCC_LSE_OFF); __HAL_RCC_LSI_ENABLE(); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET); hrtc.Init.SynchPrediv = 0x1F; // LSI 约 32kHz,调整分频 break; } } if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } } ``` ### 3. 保存时间戳到备份域 在正常运行期间,定期(如每秒)将当前 Unix 时间写入 BKP 寄存器。 ```c #include void Save_Time_To_BKP(void) { time_t timestamp = get_rtc_timestamp(); // 从 RTC 获取 Unix 时间 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, (uint32_t)timestamp); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA5A5); // 写入标志,表示有效 } ``` ### 4. 检测 LSE 状态并恢复日历 系统启动时,检查 BKP 标志,若存在有效备份,则读取时间戳并设置 RTC。 ```c void RTC_Restore_From_BKP(void) { uint32_t backup_flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); if (backup_flag == 0xA5A5) { uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0); // 将时间戳转换为 RTC 日期时间结构 RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; time_t rawtime = (time_t)timestamp; struct tm *timeinfo = localtime(&rawtime); sTime.Hours = timeinfo->tm_hour; sTime.Minutes = timeinfo->tm_min; sTime.Seconds = timeinfo->tm_sec; sDate.Year = timeinfo->tm_year + 1900 - 2000; // 基于 2000 年 sDate.Month = timeinfo->tm_mon + 1; sDate.Date = timeinfo->tm_mday; sDate.WeekDay = timeinfo->tm_wday; HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 清除标志,避免重复恢复 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x0000); } } ``` ### 5. 主程序集成 在 `main()` 中,先初始化 RTC,然后尝试恢复,最后启动周期保存任务。 ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); RTC_Init_With_Fallback(); RTC_Restore_From_BKP(); while (1) { Save_Time_To_BKP(); // 每秒调用,或使用定时器 HAL_Delay(1000); } } ``` ## 四、完整代码示例(关键部分) 以下为整合后的核心代码,省略了错误处理和 GPIO 初始化。 ```c // rtc_fallback.c #include "rtc_fallback.h" RTC_HandleTypeDef hrtc; void RTC_Init_With_Fallback(void) { // 使能时钟和备份域访问 __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 配置 RTC 参数 hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; // 尝试 LSE,超时则 LSI __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); uint32_t tickstart = HAL_GetTick(); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) { if (HAL_GetTick() - tickstart > 1000) { __HAL_RCC_LSE_CONFIG(RCC_LSE_OFF); __HAL_RCC_LSI_ENABLE(); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET); hrtc.Init.SynchPrediv = 0x1F; // 调整分频 break; } } hrtc.Init.SynchPrediv = (hrtc.Init.SynchPrediv == 0xFF) ? 0xFF : 0x1F; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } } void RTC_Save_Time(void) { time_t timestamp = HAL_RTC_GetUnixTime(&hrtc); // 假设有该函数,或自行转换 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, (uint32_t)timestamp); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA5A5); } void RTC_Restore(void) { if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) == 0xA5A5) { uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0); time_t rawtime = (time_t)timestamp; struct tm *timeinfo = localtime(&rawtime); RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef sDate = {0}; sTime.Hours = timeinfo->tm_hour; sTime.Minutes = timeinfo->tm_min; sTime.Seconds = timeinfo->tm_sec; sDate.Year = timeinfo->tm_year + 1900 - 2000; sDate.Month = timeinfo->tm_mon + 1; sDate.Date = timeinfo->tm_mday; sDate.WeekDay = timeinfo->tm_wday; HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x0000); } } ``` ## 五、注意事项 - **备份域电源**:确保 VBAT 引脚连接电池或大电容,否则 BKP 数据在断电后丢失。 - **LSI 精度**:LSI 误差可达 ±5%,仅适合短时间过渡,长期使用需定期校准或依赖外部时间同步。 - **时间戳转换**:使用 `localtime` 需注意时区设置,嵌入式环境建议使用 UTC 存储,显示时再转换。 - **BKP 寄存器数量**:STM32F4 有 20 个 32 位备份寄存器(DR0-DR19),可存储多个数据,但注意地址映射。 - **中断处理**:若 LSE 停振,可配置 RTC 闹钟或唤醒中断,及时保存时间戳,避免数据丢失。 - **代码健壮性**:在写入 BKP 前检查备份域访问是否使能,否则可能引发硬件错误。 ## 六、总结 通过利用 RTC 备份域寄存器,STM32F4 系统能够在 LSE 停振后实现日历的自恢复,无需外部晶振即可维持时间连续性。该方案成本低、实现简单,适用于对时间精度要求不苛刻但需要长期运行的嵌入式设备。开发者可根据实际需求扩展,如增加温度补偿或网络校时,进一步提升可靠性。