# 引言 在工业控制、物联网设备等嵌入式应用中,RTC 是时间管理的核心。STM32F4 系列通常使用 LSE(32.768kHz 晶振)作为 RTC 时钟源,以保证精度。然而,LSE 可能因焊接不良、环境振动或温漂而停振,导致 RTC 停止计数。若未做保护,系统重启后时间将回退至初始值。本文提出一种基于备份域(Backup Domain)的硬件方案,利用备份寄存器保存时间戳和状态标志,在 LSE 停振时自动切换至 LSI(内部 RC 振荡器),并通过软件校准实现时间恢复,确保系统时间连续不丢失。 # 备份域与 RTC 工作原理 STM32F4 的备份域由 VBAT 引脚供电(通常接电池或超级电容),包含 RTC、备份寄存器和相关控制逻辑。当主电源 VDD 掉电时,备份域仍保持供电,RTC 继续运行,备份寄存器内容不丢失。 RTC 时钟源可选 LSE、LSI 或 HSE 分频。LSE 精度高(典型 ±20ppm),但依赖外部晶振;LSI 精度低(约 ±50%),但无需外部元件。备份域寄存器(RTC_BKPxR)可用于存储时间戳、状态标志等数据,在系统复位后仍可读取。 关键点:当 LSE 停振时,RTC 的时钟源失效,RTC 计数器停止。若此时 VBAT 供电正常,备份域寄存器内容保留,但时间不再前进。因此,需要检测 LSE 状态,并快速切换至 LSI,同时利用备份寄存器保存的“最后有效时间”进行补偿。 # 无缝时间恢复方案设计 ## 总体思路 1. **LSE 状态监测**:通过 RTC 的 RTC_ISR 寄存器的 LSERDY 位或 LSE 中断检测停振。 2. **时间戳备份**:每次 RTC 秒中断时,将当前时间(Unix 时间戳)写入备份寄存器,并递增一个备份计数器。 3. **自动切换时钟源**:检测到 LSE 停振后,立即将 RTC 时钟源切换为 LSI,并记录切换时刻。 4. **时间补偿**:在 LSI 模式下,由于频率不准确,需定期校准(例如每分钟校准一次),并在恢复 LSE 后根据备份时间戳和运行时长修正时间。 5. **恢复流程**:系统复位后,读取备份寄存器中的时间戳和状态,若标志位指示 LSE 曾停振,则使用 LSI 继续计时,并尝试重新启动 LSE。 ## 硬件连接 - VBAT 引脚连接 3V 电池或 10uF 超级电容(至少维持数小时)。 - LSE 晶振(32.768kHz)并联 6pF 负载电容。 - 可选:LSE 引脚加串联电阻(如 1kΩ)以降低 EMI。 # 配置步骤(基于 HAL 库) ## 1. 使能备份域访问和 LSE ```c void BackupDomain_Init(void) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 允许访问备份域 // 使能 LSE,并等待就绪 RCC_OscInitTypeDef osc_init = {0}; osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSE; osc_init.LSEState = RCC_LSE_ON; if (HAL_RCC_OscConfig(&osc_init) != HAL_OK) { // LSE 启动失败,可能停振,后续处理 } } ``` ## 2. 配置 RTC 时钟源和日期时间 ```c void RTC_Init(void) { RTC_TimeTypeDef time = {0}; RTC_DateTypeDef date = {0}; // 选择 LSE 作为 RTC 时钟源 RCC_OscInitTypeDef osc_init = {0}; osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSE; osc_init.LSEState = RCC_LSE_ON; HAL_RCC_OscConfig(&osc_init); RCC_PeriphCLKInitTypeDef periph_clk = {0}; periph_clk.PeriphClockSelection = RCC_PERIPHCLK_RTC; periph_clk.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; HAL_RCCEx_PeriphCLKConfig(&periph_clk); // 使能 RTC 时钟 __HAL_RCC_RTC_ENABLE(); // 初始化 RTC RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; // 异步分频 hrtc.Init.SynchPrediv = 255; // 同步分频,得到 1Hz hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; HAL_RTC_Init(&hrtc); // 设置初始时间(例如 2025-01-01 00:00:00) time.Hours = 0; time.Minutes = 0; time.Seconds = 0; date.Year = 25; date.Month = 1; date.Date = 1; date.WeekDay = 4; HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN); } ``` ## 3. 备份寄存器读写与时间戳保存 ```c // 定义备份寄存器索引(0-19) #define BKP_TIMESTAMP_INDEX 0 #define BKP_STATUS_INDEX 1 void Backup_SaveTimestamp(uint32_t timestamp) { HAL_RTCEx_BKUPWrite(&hrtc, BKP_TIMESTAMP_INDEX, timestamp); } uint32_t Backup_ReadTimestamp(void) { return HAL_RTCEx_BKUPRead(&hrtc, BKP_TIMESTAMP_INDEX); } void Backup_SetStatus(uint32_t status) { HAL_RTCEx_BKUPWrite(&hrtc, BKP_STATUS_INDEX, status); } uint32_t Backup_GetStatus(void) { return HAL_RTCEx_BKUPRead(&hrtc, BKP_STATUS_INDEX); } ``` ## 4. LSE 停振检测与时钟切换 ```c void Check_LSE_And_Switch(void) { // 检查 LSE 就绪标志 if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) { // LSE 停振,记录状态 Backup_SetStatus(0xA5A5); // 自定义标志 // 切换 RTC 时钟源到 LSI RCC_OscInitTypeDef osc_init = {0}; osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSI; osc_init.LSIState = RCC_LSI_ON; HAL_RCC_OscConfig(&osc_init); RCC_PeriphCLKInitTypeDef periph_clk = {0}; periph_clk.PeriphClockSelection = RCC_PERIPHCLK_RTC; periph_clk.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; HAL_RCCEx_PeriphCLKConfig(&periph_clk); // 重新初始化 RTC 分频器(LSI 频率约 32kHz,需调整预分频) // 例如:异步 127,同步 255,得到 1Hz(假设 LSI=32.768kHz) // 若 LSI 实际频率偏差,需校准 hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; HAL_RTC_Init(&hrtc); // 注意:切换后 RTC 计数器会重置,需从备份寄存器恢复时间 uint32_t saved_ts = Backup_ReadTimestamp(); if (saved_ts != 0) { // 将时间戳转换为 RTC 时间并设置 RTC_TimeTypeDef time = {0}; RTC_DateTypeDef date = {0}; // 这里简化,实际需用时间戳转换函数 // 例如:UnixToRTC(saved_ts, &time, &date); HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN); } } } ``` ## 5. 秒中断中保存时间戳 ```c void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc) { // 获取当前时间戳(需实现 RTC 转 Unix 时间戳函数) uint32_t timestamp = GetUnixTimestamp(); Backup_SaveTimestamp(timestamp); } ``` ## 6. 系统复位后的恢复流程 ```c void System_Recovery(void) { // 读取状态标志 uint32_t status = Backup_GetStatus(); if (status == 0xA5A5) { // 之前发生过 LSE 停振,使用 LSI 模式 // 初始化 RTC 为 LSI 时钟源,并从备份寄存器恢复时间 // 同时尝试重新启动 LSE,若成功则切换回 LSE 并校准 } else { // 正常启动,使用 LSE RTC_Init(); } } ``` # 完整代码示例(简化版) ```c // 主程序示例 int main(void) { HAL_Init(); SystemClock_Config(); BackupDomain_Init(); System_Recovery(); // 使能 RTC 秒中断 HAL_RTCEx_SetSecond_IT(&hrtc); HAL_NVIC_EnableIRQ(RTC_IRQn); while (1) { // 定期检查 LSE 状态,若恢复则切换回 LSE if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == SET && Backup_GetStatus() == 0xA5A5) { // 切换回 LSE,并清除标志 Switch_To_LSE(); Backup_SetStatus(0); } HAL_Delay(1000); } } ``` # 注意事项 - **备份域电源**:VBAT 必须可靠供电,否则备份寄存器内容丢失,方案失效。建议使用低功耗电池或大电容。 - **LSI 精度校准**:LSI 频率随温度变化,需定期校准。可在秒中断中与外部参考(如 GPS 或网络)对比,调整预分频值。 - **时间戳转换**:示例中未实现 Unix 时间戳与 RTC 时间的转换,实际需编写转换函数,注意时区和闰年。 - **切换时钟源时的计数器重置**:切换 RTC 时钟源会导致 RTC 计数器清零,因此必须在切换后立即恢复时间。 - **中断处理**:秒中断中写入备份寄存器需快速,避免阻塞。备份寄存器写入有访问时间限制,建议使用影子寄存器。 - **LSE 重启**:若 LSE 因外部干扰停振,可在系统空闲时尝试重新启动 LSE,并等待就绪。多次失败后应放弃,避免频繁切换。 - **测试**:实际测试中,可通过人为断开 LSE 晶振模拟停振,验证时间恢复的准确性。 # 总结 本文提出的方案利用 STM32F4 备份域的特性,通过备份寄存器保存时间戳和状态,在 LSE 停振时自动切换至 LSI,并实现时间恢复。该方法无需外部 RTC 芯片,成本低,且能保证系统时间连续性。开发者可根据实际需求扩展,如增加温度补偿或网络校时,进一步提升精度。