# STM32F4 无晶振时间保持:LSE 停振后基于 RTC 备份域寄存器的可靠方案 ## 1. 问题背景与挑战 STM32F4 系列微控制器内置 RTC(实时时钟),通常依赖外部低速晶振(LSE,32.768kHz)提供精准时基。然而,LSE 晶振在恶劣环境(高湿度、强振动)或 PCB 布局不当(走线过长、寄生电容过大)时可能停振。一旦 LSE 失效,RTC 默认停止计时,系统重启后时间归零,严重影响数据记录或通信协议的时间戳功能。 传统解决方案是直接切换至内部 LSI(约 32kHz,精度 ±5%),但 LSI 频率偏差大,长时间运行会累积分钟级误差。本文提出一种结合备份域寄存器的软件补偿策略:在 LSE 停振时,自动切换至 LSI,并周期性将当前时间戳写入 RTC 备份寄存器(BKP),同时记录 LSI 的漂移系数,实现无外部晶振下的可靠时间保持。 ## 2. 原理剖析:RTC 备份域与时钟源切换 ### 2.1 备份域结构 STM32F4 的备份域包括 RTC、备份寄存器(BKP,20 个 32 位寄存器)和备份 SRAM(2KB)。该域由 VBAT 引脚供电,当主电源 VDD 掉电时,只要 VBAT 连接电池,RTC 和 BKP 数据依然保留。备份域复位独立于系统复位,可通过设置 PWR_CR 的 DBP 位解除写保护。 ### 2.2 时钟源选择 RTC 时钟源可选 HSE/128、LSE 或 LSI。LSE 精度最高(±20ppm),LSI 精度较低(±5%)。在 LSE 停振时,硬件会自动切换至 LSI(若已使能),但 RTC 的计数频率变化会导致时间跳变。因此,我们需要在软件中检测 LSE 状态,并启动补偿机制。 ### 2.3 补偿策略 - **检测**:通过 RTC_ISR 寄存器的 LSERDY 位判断 LSE 是否就绪。若为 0,则进入降级模式。 - **切换**:将 RTC 时钟源改为 LSI,并重新初始化 RTC 预分频器(异步预分频 127,同步预分频 255,得到 1Hz 计数)。 - **备份**:在切换前,将当前时间(秒数)写入 BKP 寄存器(如 BKP0R),并记录切换时刻的 LSI 计数值。 - **补偿**:在降级模式下,周期性(如每秒)读取 LSI 计数,计算实际经过时间,并修正 RTC 计数。 ## 3. 配置步骤 ### 3.1 使能备份域访问 首先,需要使能 PWR 和 BKP 时钟,并解除备份域写保护。 ```c // 使能 PWR 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 使能备份域访问 PWR_BackupAccessCmd(ENABLE); // 使能 BKP 时钟(若使用 BKP 寄存器) RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); ``` ### 3.2 配置 LSE 与 LSI 初始化 LSE 并等待就绪,同时使能 LSI 作为备用。 ```c RCC_LSEConfig(RCC_LSE_ON); // 等待 LSE 就绪,超时处理 uint32_t timeout = 0; while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout < 1000)) { timeout++; } // 使能 LSI RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); ``` ### 3.3 初始化 RTC 并选择时钟源 若 LSE 就绪,使用 LSE;否则使用 LSI。 ```c RTC_InitTypeDef RTC_InitStruct; if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); } else { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); } RCC_RTCCLKCmd(ENABLE); RTC_InitStruct.RTC_AsynchPrediv = 0x7F; // 127 RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 255 RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_Init(RTC_InitStruct); ``` ### 3.4 备份关键时间戳 在切换时钟源前,将当前 Unix 时间戳写入 BKP 寄存器。 ```c // 假设当前时间已由外部同步,存储到 BKP0R uint32_t timestamp = GetUnixTime(); // 自定义函数 BKP_WriteBackupRegister(BKP_DR1, timestamp); // 同时存储 LSI 的当前计数值(若使用 LSI) BKP_WriteBackupRegister(BKP_DR2, RTC_GetCounter()); ``` ## 4. 完整代码示例 以下代码演示了 LSE 停振检测与切换逻辑,以及基于 BKP 的补偿机制。 ```c #include "stm32f4xx.h" #include // 全局变量 volatile uint32_t last_lsi_count = 0; volatile uint32_t base_timestamp = 0; // 初始化 RTC 备份域 void RTC_BackupInit(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 使能 LSI RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); // 尝试 LSE RCC_LSEConfig(RCC_LSE_ON); uint32_t timeout = 100000; while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0)); // 选择时钟源 if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); } else { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); } RCC_RTCCLKCmd(ENABLE); RTC_InitTypeDef RTC_InitStruct; RTC_InitStruct.RTC_AsynchPrediv = 0x7F; RTC_InitStruct.RTC_SynchPrediv = 0xFF; RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_Init(RTC_InitStruct); // 读取备份时间戳(若有效) if (BKP_ReadBackupRegister(BKP_DR1) != 0xFFFFFFFF) { base_timestamp = BKP_ReadBackupRegister(BKP_DR1); last_lsi_count = BKP_ReadBackupRegister(BKP_DR2); } else { base_timestamp = 0; // 首次启动 last_lsi_count = RTC_GetCounter(); BKP_WriteBackupRegister(BKP_DR1, base_timestamp); BKP_WriteBackupRegister(BKP_DR2, last_lsi_count); } } // 获取当前时间戳(秒) uint32_t RTC_GetTimestamp(void) { uint32_t current_count = RTC_GetCounter(); uint32_t elapsed; if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) { // LSE 正常,直接返回计数 return base_timestamp + (current_count - last_lsi_count); } else { // LSI 模式,需要补偿(假设 LSI 频率为 32000Hz,实际测量后调整) // 这里简单计算:LSI 计数差值 / 32000 得到秒数,但实际需校准 elapsed = (current_count - last_lsi_count) / 32000; return base_timestamp + elapsed; } } // 每秒调用一次,更新备份寄存器 void RTC_BackupUpdate(void) { uint32_t current_count = RTC_GetCounter(); uint32_t current_ts = RTC_GetTimestamp(); // 更新备份寄存器 BKP_WriteBackupRegister(BKP_DR1, current_ts); BKP_WriteBackupRegister(BKP_DR2, current_count); } int main(void) { // 初始化系统时钟等 RTC_BackupInit(); while (1) { // 主循环 delay(1000); RTC_BackupUpdate(); printf("Timestamp: %lu\n", RTC_GetTimestamp()); } } ``` ## 5. 注意事项与优化建议 - **LSI 校准**:LSI 频率随温度漂移,建议在系统启动时测量实际 LSI 频率(通过定时器比较),并存储校准系数到 BKP 寄存器,补偿计算时使用该系数。 - **备份寄存器数量**:STM32F4 有 20 个 32 位备份寄存器,可存储多个时间戳或校准数据,但注意 VBAT 掉电后数据丢失,需定期更新。 - **切换时机**:检测到 LSE 停振后,应尽快切换,避免 RTC 计数中断。可在 RTC 闹钟中断或定时器中断中检测 LSERDY 位。 - **电源管理**:确保 VBAT 引脚连接电池或大电容,否则备份域数据在掉电时丢失。 - **代码健壮性**:在读取 BKP 寄存器前检查写保护状态,避免意外写入。 ## 6. 总结 通过合理利用 STM32F4 的备份域寄存器和时钟源切换机制,可以在 LSE 停振后维持可靠的时间保持。本文提供的方案不仅解决了时间丢失问题,还通过备份时间戳和 LSI 补偿提高了精度。实际项目中,可根据需求扩展为多级补偿或外部同步,进一步提升可靠性。