# 引言 在嵌入式系统中,实时时钟(RTC)是维持时间基准的关键外设,而 STM32F4 系列通常依赖外部 32.768kHz 晶振(LSE)来驱动 RTC。然而,LSE 可能因焊接不良、振动或温度漂移而停振,导致系统时间丢失。此时,若未做特殊处理,系统启动将失败或时间无法保持。本文将介绍一种无外部晶振的可靠方案:利用 RTC 备份域寄存器(BKP)在 LSE 停振时切换至内部低速 RC(LSI),并实现时间保持与启动恢复。 # 原理剖析 ## LSE 与 RTC 的依赖关系 STM32F4 的 RTC 可由 LSE、LSI 或 HSE 分频驱动。LSE 精度高(典型 ±20ppm),但需要外部晶振;LSI 集成在芯片内部(约 32kHz,精度 ±5%),无需外部元件。当 LSE 停振时,RTC 时钟源失效,时间停止。 ## 备份域寄存器(BKP)的作用 备份域包括 RTC、备份寄存器(20 个 32 位)和备份 SRAM(4KB),由 VBAT 引脚供电。即使主电源断电,只要 VBAT 有电,备份域内容可保持。BKP 寄存器可用于存储系统状态(如时钟源标志、时间校准值),在复位后快速恢复。 ## 无外部晶振的启动策略 核心思路: - 上电时检测 LSE 是否起振(通过 LSE 就绪标志)。 - 若 LSE 正常,则使用 LSE 驱动 RTC,并在 BKP 中记录“LSE 正常”标志。 - 若 LSE 停振,则切换至 LSI,并读取 BKP 中的时间备份(若存在),否则初始化默认时间。 - 利用 BKP 的写保护机制,确保关键数据不被意外篡改。 # 配置步骤 ## 1. 使能备份域和 PWR 时钟 ```c RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 允许访问备份域 ``` ## 2. 检测 LSE 状态 ```c uint8_t LSE_Status = 0; RCC_LSEConfig(RCC_LSE_ON); // 等待 LSE 就绪,超时 500ms uint32_t timeout = 500; while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0)) { Delay(1); } if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) { LSE_Status = 1; } ``` ## 3. 根据 LSE 状态配置 RTC 时钟源 ```c if (LSE_Status) { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 记录 LSE 正常 } else { RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); BKP_WriteBackupRegister(BKP_DR1, 0x5A5A); // 记录 LSI 模式 } RCC_RTCCLKCmd(ENABLE); ``` ## 4. 初始化 RTC 并恢复时间 ```c RTC_InitTypeDef RTC_InitStruct; RTC_TimeTypeDef RTC_TimeStruct; RTC_DateTypeDef RTC_DateStruct; // 使能 RTC 时钟 RTC_WriteProtectionCmd(DISABLE); RTC_EnterInitMode(); // 配置 RTC 预分频(假设 LSI 为 32kHz,LSE 为 32.768kHz) if (LSE_Status) { RTC_InitStruct.RTC_AsynchPrediv = 0x7F; RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 1Hz } else { RTC_InitStruct.RTC_AsynchPrediv = 0x7F; RTC_InitStruct.RTC_SynchPrediv = 0x3E; // 32kHz/128/62 ≈ 1Hz } RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24; RTC_Init(&RTC_InitStruct); // 从 BKP 恢复时间(若存在) if (BKP_ReadBackupRegister(BKP_DR2) != 0xFFFFFFFF) { // 假设 BKP_DR2 存储时间戳(简化示例,实际可存结构体) uint32_t timestamp = BKP_ReadBackupRegister(BKP_DR2); // 转换并设置 RTC 时间(此处省略具体转换代码) } else { // 首次启动,设置默认时间 2024-01-01 00:00:00 RTC_TimeStruct.RTC_H12 = RTC_H12_AM; RTC_TimeStruct.RTC_Hour = 0; RTC_TimeStruct.RTC_Minute = 0; RTC_TimeStruct.RTC_Second = 0; RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct); RTC_DateStruct.RTC_Year = 24; RTC_DateStruct.RTC_Month = 1; RTC_DateStruct.RTC_Date = 1; RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct); } RTC_ExitInitMode(); RTC_WriteProtectionCmd(ENABLE); ``` ## 5. 周期性保存时间到 BKP ```c // 在应用主循环中,每秒保存一次时间戳 void Save_Time_To_BKP(void) { uint32_t timestamp = Get_Timestamp(); // 从 RTC 获取 Unix 时间戳 BKP_WriteBackupRegister(BKP_DR2, timestamp); } ``` # 完整代码示例 以下是一个简化的初始化函数,整合了上述步骤: ```c void RTC_Init_With_LSE_Fallback(void) { // 1. 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 2. 检测 LSE uint8_t lse_ok = 0; RCC_LSEConfig(RCC_LSE_ON); uint32_t timeout = 500; while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0)) { // 延时 1ms for (volatile int i = 0; i < 1000; i++); } if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) lse_ok = 1; // 3. 配置 RTC 时钟源 if (lse_ok) { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { RCC_LSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI); BKP_WriteBackupRegister(BKP_DR1, 0x5A5A); } RCC_RTCCLKCmd(ENABLE); // 4. 初始化 RTC RTC_WriteProtectionCmd(DISABLE); RTC_EnterInitMode(); RTC_InitTypeDef rtc_init; rtc_init.RTC_HourFormat = RTC_HourFormat_24; if (lse_ok) { rtc_init.RTC_AsynchPrediv = 0x7F; rtc_init.RTC_SynchPrediv = 0xFF; } else { rtc_init.RTC_AsynchPrediv = 0x7F; rtc_init.RTC_SynchPrediv = 0x3E; // 调整分频系数以接近 1Hz } RTC_Init(&rtc_init); // 5. 恢复或设置时间 uint32_t saved_ts = BKP_ReadBackupRegister(BKP_DR2); if (saved_ts != 0xFFFFFFFF) { // 将 saved_ts 转换为 RTC 时间(此处省略,可使用库函数) // 例如:RTC_SetTime 和 RTC_SetDate } else { 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 = 24; date.RTC_Month = 1; date.RTC_Date = 1; RTC_SetDate(RTC_Format_BIN, &date); } RTC_ExitInitMode(); RTC_WriteProtectionCmd(ENABLE); } ``` # 注意事项 - **备份域写保护**:在写入 BKP 前必须调用 `PWR_BackupAccessCmd(ENABLE)`,否则写操作无效。 - **LSI 精度问题**:LSI 频率误差较大(±5%),若需精确时间,建议定期校准或使用外部高精度 RTC 芯片。 - **分频系数调整**:当使用 LSI 时,需根据实际频率计算预分频值,确保 RTC 秒中断频率正确。可用定时器测量 LSI 实际频率。 - **BKP 数据有效性**:BKP 寄存器在 VBAT 掉电后会丢失,因此需在每次保存时写入校验值(如 CRC),读取时验证。 - **启动时间**:LSE 起振时间较长(可达 1s),需设置合理超时,避免阻塞过久。 - **低功耗模式**:在待机模式下,备份域仍供电,但 LSI 可能关闭,需确保 RTC 时钟源在低功耗时仍可用(可配置 LSI 在待机时保持)。 # 总结 通过巧妙利用 RTC 备份域寄存器,STM32F4 系列在 LSE 停振时仍能可靠启动并保持时间。本文提供的方案不仅解决了无外部晶振的痛点,还增强了系统的鲁棒性。开发者可根据实际需求扩展,如增加时间同步协议或动态切换时钟源。希望本文能助你在嵌入式时钟设计中游刃有余。