# STM32F4 系列 LSE 停振后的无痕复位恢复:RTC 备份寄存器的妙用 ## 引言 在嵌入式系统中,RTC(实时时钟)常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,LSE 可能因晶振老化、温度漂移或 PCB 布局问题而停振。一旦停振,RTC 时间会停止,若系统复位,默认的 RTC 配置可能丢失,导致时间错乱。本文介绍一种利用 RTC 备份寄存器(BKP)在复位后检测停振状态并恢复时间的方法,实现“无痕”复位,即用户无感知,系统时间连续。 ## 原理分析 ### LSE 停振检测机制 STM32F4 的 RTC 模块提供时钟安全系统(CSS)功能,可监测 LSE 振荡。当检测到 LSE 故障时,CSS 会触发中断(RTC_CSS_IRQn),并将 RTC 时钟源自动切换到 LSI(内部低速 RC)。但默认情况下,CSS 中断未启用,且复位后 RTC 配置可能被重置。 ### RTC 备份寄存器的作用 RTC 备份寄存器(BKP)是 20 个 32 位寄存器,位于备份域,由 VBAT 供电,在系统复位或电源掉电时保持数据。我们可以利用 BKP 存储关键标志和时间戳,实现状态持久化。 ### 无痕恢复策略 1. **检测停振**:在正常运行中,通过 CSS 中断或轮询 RTC_ISR 的 LSECSSD 标志,检测 LSE 停振。 2. **保存状态**:一旦检测到停振,立即将当前时间(从 RTC 读取)和停振标志写入 BKP。 3. **复位处理**:在系统复位后,检查 BKP 中的停振标志。若存在,则说明之前发生过停振,此时可从 BKP 恢复时间,并重新初始化 RTC,同时清除标志。若不存在,则正常初始化。 此方法的关键在于 BKP 在复位后仍保留数据,且 RTC 备份域寄存器不受系统复位影响。 ## 配置步骤 ### 1. 使能备份域访问和 BKP 时钟 首先,需要使能 PWR 和 BKP 时钟,并允许访问备份域。 ```c void BKP_Config(void) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 允许访问备份域 } ``` ### 2. 配置 RTC 和 LSE 初始化 RTC,启用 LSE,并开启 CSS 中断(可选)。 ```c void RTC_Init(void) { RTC_HandleTypeDef hrtc; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; hrtc.Init.SynchPrediv = 0xFF; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; HAL_RTC_Init(&hrtc); // 启用 LSE 和 CSS __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {} // 等待就绪 // 启用 CSS 中断(可选) HAL_RTCEx_EnableLSECSS(&hrtc); HAL_NVIC_EnableIRQ(RTC_CSS_IRQn); } ``` ### 3. 检测停振并保存状态 在 CSS 中断服务函数中,读取当前时间并写入 BKP。 ```c void RTC_CSS_IRQHandler(void) { if (__HAL_RTC_CSS_GET_FLAG(&hrtc, RTC_FLAG_LSECSSD)) { __HAL_RTC_CSS_CLEAR_FLAG(&hrtc, RTC_FLAG_LSECSSD); // 读取当前时间 RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 将时间打包为 32 位(例如:年-月-日-时-分-秒) uint32_t timestamp = ((uint32_t)sDate.Year << 24) | ((uint32_t)sDate.Month << 20) | ((uint32_t)sDate.Date << 15) | ((uint32_t)sTime.Hours << 10) | ((uint32_t)sTime.Minutes << 4) | (sTime.Seconds / 2); // 写入 BKP 寄存器(例如 BKP_DR1 存时间,BKP_DR2 存标志) HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5); // 停振标志 // 可在此切换时钟源或做其他处理 } } ``` ### 4. 复位后的恢复逻辑 在系统初始化时,检查 BKP 标志,若存在则恢复时间。 ```c void RTC_RestoreAfterReset(void) { RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2); if (flag == 0xA5A5) { // 读取保存的时间戳 uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); // 解析时间 sDate.Year = (timestamp >> 24) & 0xFF; sDate.Month = (timestamp >> 20) & 0x0F; sDate.Date = (timestamp >> 15) & 0x1F; sTime.Hours = (timestamp >> 10) & 0x1F; sTime.Minutes = (timestamp >> 4) & 0x3F; sTime.Seconds = (timestamp & 0x0F) * 2; // 重新初始化 RTC(假设 LSE 已恢复或使用 LSI) RTC_Init(); // 重新配置 RTC HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 清除标志 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0x0000); // 可选:记录恢复事件 printf("RTC restored from backup\n"); } else { // 正常初始化 RTC_Init(); } } ``` ## 完整代码示例 以下是一个整合的示例,包含初始化、中断处理和恢复逻辑。 ```c #include "stm32f4xx_hal.h" RTC_HandleTypeDef hrtc; void SystemClock_Config(void); void BKP_Config(void); void RTC_Init(void); void RTC_RestoreAfterReset(void); int main(void) { HAL_Init(); SystemClock_Config(); BKP_Config(); RTC_RestoreAfterReset(); // 检查并恢复 while (1) { // 主循环 } } void RTC_CSS_IRQHandler(void) { if (__HAL_RTC_CSS_GET_FLAG(&hrtc, RTC_FLAG_LSECSSD)) { __HAL_RTC_CSS_CLEAR_FLAG(&hrtc, RTC_FLAG_LSECSSD); RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); uint32_t timestamp = ((uint32_t)sDate.Year << 24) | ((uint32_t)sDate.Month << 20) | ((uint32_t)sDate.Date << 15) | ((uint32_t)sTime.Hours << 10) | ((uint32_t)sTime.Minutes << 4) | (sTime.Seconds / 2); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5); } } void BKP_Config(void) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); } void RTC_Init(void) { hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; hrtc.Init.SynchPrediv = 0xFF; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; HAL_RTC_Init(&hrtc); __HAL_RCC_LSE_CONFIG(RCC_LSE_ON); while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {} HAL_RTCEx_EnableLSECSS(&hrtc); HAL_NVIC_EnableIRQ(RTC_CSS_IRQn); } void RTC_RestoreAfterReset(void) { uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2); if (flag == 0xA5A5) { uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; sDate.Year = (timestamp >> 24) & 0xFF; sDate.Month = (timestamp >> 20) & 0x0F; sDate.Date = (timestamp >> 15) & 0x1F; sTime.Hours = (timestamp >> 10) & 0x1F; sTime.Minutes = (timestamp >> 4) & 0x3F; sTime.Seconds = (timestamp & 0x0F) * 2; RTC_Init(); HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0x0000); } else { RTC_Init(); } } void SystemClock_Config(void) { // 标准时钟配置,省略 } ``` ## 注意事项 - **备份域电源**:确保 VBAT 引脚连接到电池或超级电容,否则 BKP 数据在掉电后会丢失。 - **时间戳精度**:示例中秒数除以 2 是为了压缩到 4 位,实际可根据需求调整,但需保证精度。 - **CSS 中断优先级**:建议设置为高优先级,确保及时响应。 - **LSE 恢复**:若 LSE 停振后恢复,需重新初始化 RTC 并启用 CSS,否则可能再次停振。 - **多次停振**:若在恢复前再次停振,BKP 中的时间可能不是最新,可考虑增加时间戳更新频率。 - **测试**:在实际硬件上模拟 LSE 停振(如断开晶振),验证恢复逻辑。 ## 结语 通过 RTC 备份寄存器,我们可以在 LSE 停振后保存关键时间信息,并在复位后无痕恢复,大大提高了系统的可靠性。此方法不仅适用于 STM32F4,也适用于其他具有备份寄存器的 STM32 系列。希望本文能帮助你在嵌入式开发中应对类似挑战。