# 引言 在嵌入式系统中,RTC(实时时钟)常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,晶振可能因环境干扰、老化或焊接问题而停振。STM32F4 系列提供了 RTC 时钟源监控和备份寄存器,但默认固件库并未完整实现故障恢复流程。本文以 STM32F407 为例,讲解如何通过 RTC 备份寄存器保存系统状态,在 LSE 停振时自动切换至 LSI(内部低速 RC),并恢复时间计数,实现无感恢复。 # 原理剖析 ## 1. LSE 停振检测机制 STM32F4 的 RTC 模块包含一个时钟源标志位 `RTC_ISR.RSF`(寄存器同步标志)和 `RTC_ISR.INITS`(初始化状态标志)。当 LSE 停振时,RTC 时钟域会失去时钟,导致寄存器无法更新。更直接的方法是使用 `RTC->ISR` 的 `LSRMF`(LSE 单调检测标志)或通过 `RTC_CR` 的 `LSCOEN` 使能 LSE 时钟输出,配合外部检测。但最可靠的方式是启用 RTC 的 `RTC_TAFCR` 寄存器中的 `TAMPFLT` 和 `TAMPxTRG` 边沿检测,或使用 `RTC_ISR` 的 `ALRAF`(闹钟标志)作为辅助。 实际工程中,常用方案是: - 使能 RTC 闹钟中断,设置一个短周期(如 1 秒)闹钟,若闹钟未触发,则判定 LSE 停振。 - 或者利用 LSE 的旁路监测功能,但 STM32F4 没有专用停振中断,需轮询。 ## 2. RTC 备份寄存器的作用 RTC 备份寄存器(BKPxR)在 VDD 供电时由 VBAT 供电,可保存用户数据。在 LSE 停振时,RTC 时间寄存器可能因时钟丢失而无法读取,但备份寄存器不受影响。因此,我们可以将当前时间戳、时钟源状态、校准值等关键信息存入备份寄存器,在恢复时读取并重建时间。 ## 3. 时钟源无缝切换策略 STM32F4 的 RTC 时钟源可选 LSE、LSI 或 HSE 分频。当检测到 LSE 停振,应: 1. 保存当前时间到备份寄存器(若可能)。 2. 关闭 RTC 时钟源选择,切换至 LSI。 3. 重新初始化 RTC 并恢复时间(从备份寄存器读取)。 4. 更新系统状态标志,通知应用层。 由于 LSI 精度较低(约 ±5%),需在恢复后定期校准或提示用户。 # 硬件与软件准备 - 硬件:STM32F407 开发板,外部 32.768kHz 晶振,备用电池连接 VBAT。 - 软件:STM32CubeIDE 或 Keil,HAL 库或标准外设库均可。本文使用 HAL 库。 # 配置步骤 ## 1. 初始化 RTC 与备份寄存器 首先,使能 PWR 和 RTC 时钟,配置 RTC 时钟源为 LSE,并使能备份域访问。 ```c void RTC_Init(void) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_RTC_ENABLE(); HAL_PWR_EnableBkUpAccess(); RCC_OscInitTypeDef oscInit = {0}; oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE; oscInit.LSEState = RCC_LSE_ON; if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) { // 若 LSE 启动失败,立即切换 LSI SwitchToLSI(); return; } RCC_PeriphCLKInitTypeDef periphClkInit = {0}; periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; HAL_RCCEx_PeriphCLKConfig(&periphClkInit); // 使能 RTC __HAL_RCC_RTC_ENABLE(); RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; HAL_RTC_Init(&hrtc); // 写入初始时间(示例) RTC_TimeTypeDef sTime = {0}; sTime.Hours = 12; sTime.Minutes = 0; sTime.Seconds = 0; HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); RTC_DateTypeDef sDate = {0}; sDate.Year = 24; sDate.Month = 1; sDate.Date = 1; HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); } ``` ## 2. 保存上下文到备份寄存器 在正常运行时,周期性将当前时间戳和状态存入备份寄存器。 ```c void SaveContextToBackup(void) { RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 将时间转换为 Unix 时间戳(简化示例,实际需考虑闰年等) uint32_t timestamp = 0; // ... 转换代码 ... HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR0, timestamp); HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0xA5A5); // 标记有效 } ``` ## 3. 检测 LSE 停振并切换时钟源 使用轮询方式检测 LSE 是否停振。例如,每秒检查 RTC 是否还在走时(通过比较秒寄存器),或使用闹钟中断。以下示例使用闹钟中断: ```c void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) { // 闹钟触发,说明 RTC 正常,更新备份寄存器 SaveContextToBackup(); // 重新设置下一次闹钟 SetAlarm(1); // 1 秒后 } void CheckLSEAndSwitch(void) { // 若闹钟未触发(通过标志位),则判定 LSE 停振 if (alarmMissed) { // 保存最后已知时间 SaveContextToBackup(); // 切换至 LSI SwitchToLSI(); } } void SwitchToLSI(void) { // 1. 关闭 RTC 时钟源 __HAL_RCC_RTC_DISABLE(); HAL_PWR_DisableBkUpAccess(); // 2. 使能 LSI 并配置为 RTC 时钟 RCC_OscInitTypeDef oscInit = {0}; oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI; oscInit.LSIState = RCC_LSI_ON; HAL_RCC_OscConfig(&oscInit); RCC_PeriphCLKInitTypeDef periphClkInit = {0}; periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; HAL_RCCEx_PeriphCLKConfig(&periphClkInit); // 3. 重新初始化 RTC __HAL_RCC_RTC_ENABLE(); RTC_HandleTypeDef hrtc = {0}; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; // 需根据 LSI 频率调整(LSI 约 32kHz) hrtc.Init.SynchPrediv = 255; HAL_RTC_Init(&hrtc); // 4. 从备份寄存器恢复时间 uint32_t timestamp = HAL_RTCEx_BKUPRead(RTC, RTC_BKP_DR0); if (timestamp != 0xFFFFFFFF) { // 将时间戳转换回 RTC 时间并设置 // ... } // 5. 设置状态标志 HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0x5A5A); // 表示已切换 } ``` ## 4. 完整代码示例(主循环) ```c int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); RTC_Init(); // 使能闹钟中断 SetAlarm(1); HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0); HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn); while (1) { // 每 100ms 检查一次闹钟是否超时 if (alarmMissed) { CheckLSEAndSwitch(); } // 其他任务 } } void SetAlarm(uint32_t seconds) { RTC_AlarmTypeDef sAlarm = {0}; sAlarm.AlarmTime.Hours = 0; sAlarm.AlarmTime.Minutes = 0; sAlarm.AlarmTime.Seconds = seconds; sAlarm.AlarmMask = RTC_ALARMMASK_DATE_WEEKDAY; HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN); } ``` # 注意事项 - **备份寄存器有效性**:在读取备份寄存器前,务必检查标记值(如 0xA5A5),避免使用未初始化的数据。 - **LSI 精度问题**:LSI 频率受温度影响大,切换后 RTC 走时误差可能较大。建议在应用层增加校准机制,或定期与外部时间源同步。 - **预分频器调整**:LSE 为 32.768kHz,LSI 约为 32kHz,但实际频率可能偏差。需根据实际 LSI 频率重新计算 AsynchPrediv 和 SynchPrediv,否则 RTC 时间不准确。 - **电源管理**:在切换时钟源时,务必先关闭 RTC 时钟,再修改时钟源,避免产生毛刺。 - **中断优先级**:闹钟中断应设为较高优先级,确保及时响应。 - **测试方法**:实际测试时,可断开 LSE 晶振引脚模拟停振,观察系统是否自动切换。 # 总结 通过 RTC 备份寄存器保存上下文,结合闹钟中断检测 LSE 停振,并在检测到故障后无缝切换至 LSI,可以显著提高 STM32F4 系统的可靠性。该方法无需外部硬件,仅依赖片内资源,适用于对时间连续性要求较高的工业设备、智能仪表等场景。开发者可根据实际需求扩展,如增加多次重试、故障日志记录等功能。