# STM32F4 无晶振日历恢复:LSE 停振后利用 RTC 备份域寄存器的实战指南 ## 引言 在嵌入式系统中,RTC(实时时钟)是许多应用的核心组件,尤其对于需要时间戳或定时唤醒的设备。STM32F4 系列通常依赖 LSE(32.768 kHz 外部晶振)来驱动 RTC,但 LSE 可能因硬件故障、电磁干扰或焊接问题而停振。一旦 LSE 失效,RTC 将停止计时,导致日历数据丢失。本文将介绍一种利用 RTC 备份域寄存器(BKP)和 LSI(内部低速 RC 振荡器)实现日历恢复的方法,确保系统在无外部晶振时仍能维持时间基准。 ## 原理剖析 ### RTC 时钟源与备份域 STM32F4 的 RTC 支持三种时钟源:LSE、LSI(约 32 kHz,精度较低)和 HSE 分频。LSE 是首选,因为它精度高、功耗低。RTC 和备份寄存器位于备份域(VDD 供电区域),当主电源掉电时,可由 VBAT 引脚供电,保持数据不丢失。备份域寄存器(BKP)是一组 20 个 32 位寄存器,可用于存储用户数据,在系统复位或电源切换时内容保持不变。 ### LSE 停振检测 STM32F4 的 RTC 提供时钟安全系统(CSS),可监测 LSE 振荡器故障。当 LSE 停振时,CSS 会触发 RTC 闹钟或唤醒事件,并自动切换到 LSI 作为备用时钟源(需软件配置)。但 LSI 精度较差(典型 ±5%),长时间运行会导致时间漂移。因此,我们需要在 LSE 恢复后重新校准,或在停振期间利用备份寄存器保存关键时间信息。 ### 恢复策略 我们的目标是:在 LSE 停振时,RTC 继续运行(使用 LSI),同时定期将当前时间戳写入备份寄存器。当 LSE 恢复或系统重启时,软件读取备份寄存器中的时间戳,并结合 RTC 的当前计数(基于 LSI)计算出实际时间,从而实现日历恢复。由于 LSI 频率不稳定,我们还需要校准因子来修正漂移。 ## 配置步骤 ### 1. 使能备份域访问 首先,需要使能 PWR 和 RTC 时钟,并解锁备份域写保护。 ```c // 使能 PWR 和 RTC 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_RTC, ENABLE); // 解锁备份域写保护 PWR_BackupAccessCmd(ENABLE); ``` ### 2. 配置 RTC 时钟源为 LSI(备用) 如果 LSE 未启动或检测到故障,则选择 LSI 作为 RTC 时钟源。 ```c // 使能 LSI RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); // 选择 LSI 作为 RTC 时钟源 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); // 使能 RTC 时钟 RCC_RTCCLKCmd(ENABLE); ``` ### 3. 初始化 RTC 并设置时间 使用 RTC_InitTypeDef 结构体配置 RTC 参数,并设置初始时间。 ```c RTC_InitTypeDef RTC_InitStruct; RTC_TimeTypeDef RTC_TimeStruct; RTC_DateTypeDef RTC_DateStruct; // 配置 RTC 分频(LSI 约 32 kHz,分频 32 得到 1 Hz) RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStruct.RTC_AsynchPrediv = 0x7F; // 异步分频 128 RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 同步分频 256,总 32768 RTC_InitStruct.RTC_Clock = RTC_Clock_RTCCLK; RTC_Init(&RTC_InitStruct); // 设置时间(例如 2025-01-01 12:00:00) RTC_TimeStruct.RTC_H12 = RTC_H12_AM; RTC_TimeStruct.RTC_Hour = 12; RTC_TimeStruct.RTC_Minute = 0; RTC_TimeStruct.RTC_Second = 0; RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct); RTC_DateStruct.RTC_Year = 25; // 2025 RTC_DateStruct.RTC_Month = 1; RTC_DateStruct.RTC_Date = 1; RTC_DateStruct.RTC_WeekDay = 4; // 星期四 RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct); ``` ### 4. 启用 LSE 故障检测(CSS) 配置 RTC 的 CSS 功能,当 LSE 停振时触发中断。 ```c // 使能 RTC 时钟安全系统 RTC_ClearFlag(RTC_FLAG_TSOVF); // 清除标志 RTC_ITConfig(RTC_IT_ALRA, ENABLE); // 使能闹钟中断(用于 CSS) // 在中断处理函数中检测 CSS 标志 ``` ### 5. 定期保存时间戳到备份寄存器 在主循环或定时中断中,将当前时间转换为 Unix 时间戳(或 BCD 格式),写入备份寄存器。 ```c // 获取当前时间戳 uint32_t timestamp = get_unix_timestamp(); // 自定义函数 // 写入备份寄存器(例如 BKP_DR1) BKP_WriteBackupRegister(BKP_DR1, timestamp); ``` ### 6. LSE 恢复后的校准 当检测到 LSE 恢复(可通过外部中断或定期检查),重新配置 RTC 时钟源为 LSE,并计算漂移补偿。 ```c // 重新配置 LSE RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 切换 RTC 时钟源到 LSE RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 计算漂移:读取备份寄存器中的时间戳与当前 RTC 时间差 uint32_t saved_ts = BKP_ReadBackupRegister(BKP_DR1); uint32_t current_ts = get_unix_timestamp(); int32_t drift = current_ts - saved_ts; // 实际经过时间(秒) // 根据 drift 调整 RTC 时间(例如,如果 LSI 偏快,则减去差值) ``` ## 完整代码示例 以下是一个简化但完整的示例,演示如何初始化 RTC 并保存/恢复时间。 ```c #include "stm32f4xx.h" // 将时间转换为 Unix 时间戳(简化版) uint32_t get_unix_timestamp(void) { RTC_TimeTypeDef time; RTC_DateTypeDef date; RTC_GetTime(RTC_Format_BIN, &time); RTC_GetDate(RTC_Format_BIN, &date); // 使用日期库函数计算,这里省略具体实现 return 0; // 占位 } void RTC_Init(void) { // 使能 PWR 和 RTC 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_RTC, ENABLE); PWR_BackupAccessCmd(ENABLE); // 尝试启用 LSE,若失败则使用 LSI RCC_LSEConfig(RCC_LSE_ON); uint32_t timeout = 0xFFFF; while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) { if (--timeout == 0) break; } if (timeout == 0) { // LSE 启动失败,使用 LSI RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); } else { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); } RCC_RTCCLKCmd(ENABLE); // 初始化 RTC RTC_InitTypeDef RTC_InitStruct; RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_InitStruct.RTC_AsynchPrediv = 0x7F; RTC_InitStruct.RTC_SynchPrediv = 0xFF; RTC_InitStruct.RTC_Clock = RTC_Clock_RTCCLK; RTC_Init(&RTC_InitStruct); // 设置初始时间(如果备份寄存器中没有有效数据) if (BKP_ReadBackupRegister(BKP_DR1) == 0xFFFFFFFF) { RTC_TimeTypeDef time = {0}; time.RTC_H12 = RTC_H12_AM; time.RTC_Hour = 0; time.RTC_Minute = 0; time.RTC_Second = 0; RTC_SetTime(RTC_Format_BIN, &time); RTC_DateTypeDef date = {0}; date.RTC_Year = 25; date.RTC_Month = 1; date.RTC_Date = 1; date.RTC_WeekDay = 4; RTC_SetDate(RTC_Format_BIN, &date); } } int main(void) { RTC_Init(); while (1) { // 每 10 秒保存一次时间戳 static uint32_t last_save = 0; uint32_t now = get_unix_timestamp(); if (now - last_save >= 10) { BKP_WriteBackupRegister(BKP_DR1, now); last_save = now; } } } ``` ## 注意事项 - **备份寄存器数量有限**:STM32F4 有 20 个备份寄存器,每个 32 位,可用于存储时间戳和其他关键数据。避免频繁写入,因为备份寄存器写入次数有限(约 10 万次),建议定期保存(如每 10 秒或每分钟)。 - **LSI 精度问题**:LSI 的频率随温度变化,长时间运行误差较大。建议在 LSE 恢复后重新校准,或使用外部高精度时钟源(如 GPS 或网络)进行同步。 - **CSS 中断处理**:在中断服务函数中,必须清除 RTC_CSS 标志,并执行时钟源切换。注意中断优先级,避免阻塞。 - **电源管理**:备份域在 VBAT 供电下可保持数据,但 RTC 仍需要时钟源。如果 LSE 停振且 VBAT 断开,备份寄存器内容会丢失,因此需确保 VBAT 连接。 - **时间戳转换**:使用 Unix 时间戳便于计算,但需注意 2038 年问题。对于嵌入式系统,可使用 32 位无符号整数,直到 2106 年。 ## 总结 通过合理利用 RTC 备份域寄存器和 LSI 备用时钟,STM32F4 在 LSE 停振时仍能维持日历功能,并在恢复后校准时间。这种方法提高了系统的鲁棒性,尤其适用于无人值守的嵌入式设备。开发者应根据实际需求调整保存频率和校准策略,以平衡功耗和精度。