STM32F4 实战:LSE 停振后,硬件 RTC 备份域如何守住时间不丢失
👁 2 阅读 · 2026-08-27 · 嵌入式
在工业与物联网场景中,STM32F4 的 LSE 晶振常因温漂、干扰或焊接问题停振,导致 RTC 时间错乱。本文深入剖析备份域供电与 RTC 时钟源切换机制,手把手教你配置硬件 RTC 在 LSE 失效时自动切换至 LSI,并利用备份寄存器保存校准参数,确保时间连续不丢失。附完整工程代码与调试要点,助你打造高可靠时钟系统。
# 引言
在嵌入式系统中,RTC(实时时钟)是时间敏感应用的核心。STM32F4 系列默认使用 LSE(32.768kHz 外部晶振)作为 RTC 时钟源,其精度高、功耗低。但 LSE 晶振易受环境干扰或 PCB 设计不良影响而停振,一旦停振,RTC 将停止计时,时间数据丢失。本文提供一种基于硬件备份域的解决方案,在 LSE 失效时无缝切换至 LSI(内部 RC 振荡器),并利用备份寄存器保存时间与校准参数,确保系统重启或 LSE 恢复后时间依然准确。
# 1. 备份域与 RTC 时钟架构
STM32F4 的备份域(Backup Domain)由 VBAT 引脚供电,包含 RTC、备份寄存器(20 个 32 位)和备份 SRAM(可选)。当主电源 VDD 掉电时,只要 VBAT 有电,备份域数据不丢失。RTC 的时钟源可选 LSE、LSI 或 HSE(分频),通过 RTCSEL[1:0] 位配置。
关键点:
- RTC 时钟源切换必须在备份域复位后、RTC 使能前完成,否则无效。
- 备份域寄存器(RCC_BDCR)受写保护,需先开启 PWR 时钟并取消备份域写保护。
- LSE 停振检测可通过 RTC 的 LSEDRV 或外部中断,但更实用的是在 RTC 中断中检查 RTC_ISR 的 LSERDY 标志。
# 2. 硬件设计要点
- VBAT 引脚接 3V 纽扣电池或超级电容,确保主电源断电时备份域持续供电。
- LSE 晶振负载电容需匹配,走线尽量短且远离高频信号。
- 建议在 LSE 输出引脚并联 1MΩ 反馈电阻,提高起振可靠性。
# 3. 软件配置步骤
## 3.1 使能 PWR 时钟并解锁备份域
```c
void BackupDomain_Unlock(void) {
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 取消备份域写保护
}
```
## 3.2 配置 RTC 时钟源为 LSE,并启用自动切换
STM32F4 的 RTC 不支持硬件自动切换时钟源,但我们可以通过软件检测 LSE 状态,在 LSE 停振时切换至 LSI。切换步骤:
1. 复位备份域(RCC_BDCR 的 BDRST 位),确保 RTC 配置干净。
2. 选择时钟源:先尝试 LSE,若未就绪则用 LSI。
3. 使能 RTC 并配置预分频。
```c
void RTC_ClockConfig(void) {
RCC_OscInitTypeDef oscInit = {0};
RCC_PeriphCLKInitTypeDef periphClkInit = {0};
// 尝试启用 LSE
oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE;
oscInit.LSEState = RCC_LSE_ON;
if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) {
// LSE 启动失败,回退到 LSI
oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI;
oscInit.LSIState = RCC_LSI_ON;
HAL_RCC_OscConfig(&oscInit);
periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
} else {
periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
}
HAL_RCCEx_PeriphCLKConfig(&periphClkInit);
// 使能 RTC 时钟
__HAL_RCC_RTC_ENABLE();
}
```
## 3.3 初始化 RTC 并设置时间
```c
RTC_HandleTypeDef hrtc;
void RTC_Init(void) {
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 0x7F; // 128 分频
hrtc.Init.SynchPrediv = 0xFF; // 256 分频,得到 1Hz
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
Error_Handler();
}
// 设置初始时间(例如 2025-01-01 00:00:00)
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
sTime.Hours = 0;
sTime.Minutes = 0;
sTime.Seconds = 0;
sTime.TimeFormat = RTC_HOURFORMAT24;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
sDate.Year = 25; // 2025
sDate.Month = 1;
sDate.Date = 1;
sDate.WeekDay = 4; // 星期四
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
}
```
## 3.4 检测 LSE 停振并切换时钟源
在运行中,通过轮询或中断检测 LSE 状态。若发现 LSE 停振,则执行切换流程:
```c
void Check_LSE_And_Switch(void) {
// 读取 RCC_BDCR 的 LSERDY 位
if (!(RCC->BDCR & RCC_BDCR_LSERDY)) {
// LSE 停振,切换至 LSI
RCC->BDCR &= ~RCC_BDCR_RTCEN; // 关闭 RTC 时钟
while (RCC->BDCR & RCC_BDCR_RTCEN);
// 重新配置时钟源为 LSI
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);
__HAL_RCC_RTC_ENABLE();
// 注意:切换后 RTC 会复位,需重新初始化时间和日期(可从备份寄存器恢复)
RTC_Init(); // 重新初始化,但时间会丢失,需从备份寄存器恢复
}
}
```
## 3.5 利用备份寄存器保存时间与校准参数
为避免切换时时间丢失,在每次 RTC 中断(秒中断)中,将当前时间写入备份寄存器。切换后从备份寄存器恢复。
```c
// 保存时间到备份寄存器(假设 BKP_REG_INDEX 为 0)
void Save_Time_To_Backup(RTC_TimeTypeDef *time, RTC_DateTypeDef *date) {
uint32_t packed = (time->Hours << 16) | (time->Minutes << 8) | time->Seconds;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, packed);
uint32_t date_packed = (date->Year << 16) | (date->Month << 8) | date->Date;
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, date_packed);
}
// 从备份寄存器恢复
void Restore_Time_From_Backup(RTC_TimeTypeDef *time, RTC_DateTypeDef *date) {
uint32_t packed = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
time->Hours = (packed >> 16) & 0xFF;
time->Minutes = (packed >> 8) & 0xFF;
time->Seconds = packed & 0xFF;
uint32_t date_packed = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
date->Year = (date_packed >> 16) & 0xFF;
date->Month = (date_packed >> 8) & 0xFF;
date->Date = date_packed & 0xFF;
}
```
在 RTC 秒中断回调中调用保存函数,切换后调用恢复函数。
# 4. 完整代码示例
以下为集成以上步骤的简化示例(基于 HAL 库):
```c
// main.c 片段
int main(void) {
HAL_Init();
SystemClock_Config();
BackupDomain_Unlock();
RTC_ClockConfig();
RTC_Init();
// 使能秒中断
HAL_RTCEx_SetSecond_IT(&hrtc);
HAL_NVIC_EnableIRQ(RTC_IRQn);
while (1) {
Check_LSE_And_Switch(); // 周期检测 LSE 状态
HAL_Delay(1000);
}
}
void RTC_IRQHandler(void) {
HAL_RTCEx_RTCIRQHandler(&hrtc);
}
void HAL_RTCEx_RTCIRQHandler(RTC_HandleTypeDef *hrtc) {
if (__HAL_RTC_SECOND_GET_IT(hrtc, RTC_IT_SEC) != RESET) {
__HAL_RTC_SECOND_CLEAR_IT(hrtc, RTC_IT_SEC);
// 读取当前时间并保存到备份寄存器
RTC_TimeTypeDef time;
RTC_DateTypeDef date;
HAL_RTC_GetTime(hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(hrtc, &date, RTC_FORMAT_BIN);
Save_Time_To_Backup(&time, &date);
}
}
```
# 5. 注意事项
- **备份域复位**:切换时钟源前必须复位备份域,否则 RTC 配置无效,但复位会清空备份寄存器,因此需先保存数据。
- **LSI 精度**:LSI 精度约 ±5%,若对时间精度要求高,建议在 LSE 恢复后重新校准,或使用外部高精度 RTC 芯片。
- **中断优先级**:RTC 秒中断优先级应设置合理,避免长时间阻塞导致时间跳变。
- **电源管理**:确保 VBAT 供电稳定,否则备份域数据仍会丢失。
- **调试技巧**:在切换时钟源后,可通过串口打印当前时钟源状态,便于验证。
# 结语
通过以上配置,STM32F4 在 LSE 停振时能快速切换至 LSI,并利用备份寄存器保存时间,实现时间不丢失。该方法适用于对成本敏感但需高可靠性的嵌入式系统。实际项目中,建议结合看门狗和错误日志,进一步提升系统鲁棒性。