STM32F4 系列 LSE 停振后基于硬件 RTC 备份域的无缝时间恢复策略
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,RTC(实时时钟)依赖 LSE(低速外部晶振)维持时间基准,但 LSE 可能因环境干扰或晶振失效而停振,导致时间丢失。本文深入剖析 STM32F4 系列备份域的工作原理,提出一种利用备份域寄存器和 VBAT 供电的硬件方案,在 LSE 停振后自动切换至 LSI 并恢复时间戳,实现无缝衔接。文章涵盖原理、配置步骤、完整代码及注意事项,帮助开发者构建高可靠性的时间系统。
# 引言
在工业控制、物联网设备等嵌入式应用中,RTC 是时间管理的核心。STM32F4 系列通常使用 LSE(32.768kHz 晶振)作为 RTC 时钟源,以保证精度。然而,LSE 可能因焊接不良、环境振动或温漂而停振,导致 RTC 停止计数。若未做保护,系统重启后时间将回退至初始值。本文提出一种基于备份域(Backup Domain)的硬件方案,利用备份寄存器保存时间戳和状态标志,在 LSE 停振时自动切换至 LSI(内部 RC 振荡器),并通过软件校准实现时间恢复,确保系统时间连续不丢失。
# 备份域与 RTC 工作原理
STM32F4 的备份域由 VBAT 引脚供电(通常接电池或超级电容),包含 RTC、备份寄存器和相关控制逻辑。当主电源 VDD 掉电时,备份域仍保持供电,RTC 继续运行,备份寄存器内容不丢失。
RTC 时钟源可选 LSE、LSI 或 HSE 分频。LSE 精度高(典型 ±20ppm),但依赖外部晶振;LSI 精度低(约 ±50%),但无需外部元件。备份域寄存器(RTC_BKPxR)可用于存储时间戳、状态标志等数据,在系统复位后仍可读取。
关键点:当 LSE 停振时,RTC 的时钟源失效,RTC 计数器停止。若此时 VBAT 供电正常,备份域寄存器内容保留,但时间不再前进。因此,需要检测 LSE 状态,并快速切换至 LSI,同时利用备份寄存器保存的“最后有效时间”进行补偿。
# 无缝时间恢复方案设计
## 总体思路
1. **LSE 状态监测**:通过 RTC 的 RTC_ISR 寄存器的 LSERDY 位或 LSE 中断检测停振。
2. **时间戳备份**:每次 RTC 秒中断时,将当前时间(Unix 时间戳)写入备份寄存器,并递增一个备份计数器。
3. **自动切换时钟源**:检测到 LSE 停振后,立即将 RTC 时钟源切换为 LSI,并记录切换时刻。
4. **时间补偿**:在 LSI 模式下,由于频率不准确,需定期校准(例如每分钟校准一次),并在恢复 LSE 后根据备份时间戳和运行时长修正时间。
5. **恢复流程**:系统复位后,读取备份寄存器中的时间戳和状态,若标志位指示 LSE 曾停振,则使用 LSI 继续计时,并尝试重新启动 LSE。
## 硬件连接
- VBAT 引脚连接 3V 电池或 10uF 超级电容(至少维持数小时)。
- LSE 晶振(32.768kHz)并联 6pF 负载电容。
- 可选:LSE 引脚加串联电阻(如 1kΩ)以降低 EMI。
# 配置步骤(基于 HAL 库)
## 1. 使能备份域访问和 LSE
```c
void BackupDomain_Init(void) {
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 允许访问备份域
// 使能 LSE,并等待就绪
RCC_OscInitTypeDef osc_init = {0};
osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSE;
osc_init.LSEState = RCC_LSE_ON;
if (HAL_RCC_OscConfig(&osc_init) != HAL_OK) {
// LSE 启动失败,可能停振,后续处理
}
}
```
## 2. 配置 RTC 时钟源和日期时间
```c
void RTC_Init(void) {
RTC_TimeTypeDef time = {0};
RTC_DateTypeDef date = {0};
// 选择 LSE 作为 RTC 时钟源
RCC_OscInitTypeDef osc_init = {0};
osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSE;
osc_init.LSEState = RCC_LSE_ON;
HAL_RCC_OscConfig(&osc_init);
RCC_PeriphCLKInitTypeDef periph_clk = {0};
periph_clk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periph_clk.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
HAL_RCCEx_PeriphCLKConfig(&periph_clk);
// 使能 RTC 时钟
__HAL_RCC_RTC_ENABLE();
// 初始化 RTC
RTC_HandleTypeDef hrtc = {0};
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127; // 异步分频
hrtc.Init.SynchPrediv = 255; // 同步分频,得到 1Hz
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
HAL_RTC_Init(&hrtc);
// 设置初始时间(例如 2025-01-01 00:00:00)
time.Hours = 0; time.Minutes = 0; time.Seconds = 0;
date.Year = 25; date.Month = 1; date.Date = 1; date.WeekDay = 4;
HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN);
}
```
## 3. 备份寄存器读写与时间戳保存
```c
// 定义备份寄存器索引(0-19)
#define BKP_TIMESTAMP_INDEX 0
#define BKP_STATUS_INDEX 1
void Backup_SaveTimestamp(uint32_t timestamp) {
HAL_RTCEx_BKUPWrite(&hrtc, BKP_TIMESTAMP_INDEX, timestamp);
}
uint32_t Backup_ReadTimestamp(void) {
return HAL_RTCEx_BKUPRead(&hrtc, BKP_TIMESTAMP_INDEX);
}
void Backup_SetStatus(uint32_t status) {
HAL_RTCEx_BKUPWrite(&hrtc, BKP_STATUS_INDEX, status);
}
uint32_t Backup_GetStatus(void) {
return HAL_RTCEx_BKUPRead(&hrtc, BKP_STATUS_INDEX);
}
```
## 4. LSE 停振检测与时钟切换
```c
void Check_LSE_And_Switch(void) {
// 检查 LSE 就绪标志
if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
// LSE 停振,记录状态
Backup_SetStatus(0xA5A5); // 自定义标志
// 切换 RTC 时钟源到 LSI
RCC_OscInitTypeDef osc_init = {0};
osc_init.OscillatorType = RCC_OSCILLATORTYPE_LSI;
osc_init.LSIState = RCC_LSI_ON;
HAL_RCC_OscConfig(&osc_init);
RCC_PeriphCLKInitTypeDef periph_clk = {0};
periph_clk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periph_clk.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
HAL_RCCEx_PeriphCLKConfig(&periph_clk);
// 重新初始化 RTC 分频器(LSI 频率约 32kHz,需调整预分频)
// 例如:异步 127,同步 255,得到 1Hz(假设 LSI=32.768kHz)
// 若 LSI 实际频率偏差,需校准
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
HAL_RTC_Init(&hrtc);
// 注意:切换后 RTC 计数器会重置,需从备份寄存器恢复时间
uint32_t saved_ts = Backup_ReadTimestamp();
if (saved_ts != 0) {
// 将时间戳转换为 RTC 时间并设置
RTC_TimeTypeDef time = {0};
RTC_DateTypeDef date = {0};
// 这里简化,实际需用时间戳转换函数
// 例如:UnixToRTC(saved_ts, &time, &date);
HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN);
}
}
}
```
## 5. 秒中断中保存时间戳
```c
void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc) {
// 获取当前时间戳(需实现 RTC 转 Unix 时间戳函数)
uint32_t timestamp = GetUnixTimestamp();
Backup_SaveTimestamp(timestamp);
}
```
## 6. 系统复位后的恢复流程
```c
void System_Recovery(void) {
// 读取状态标志
uint32_t status = Backup_GetStatus();
if (status == 0xA5A5) {
// 之前发生过 LSE 停振,使用 LSI 模式
// 初始化 RTC 为 LSI 时钟源,并从备份寄存器恢复时间
// 同时尝试重新启动 LSE,若成功则切换回 LSE 并校准
} else {
// 正常启动,使用 LSE
RTC_Init();
}
}
```
# 完整代码示例(简化版)
```c
// 主程序示例
int main(void) {
HAL_Init();
SystemClock_Config();
BackupDomain_Init();
System_Recovery();
// 使能 RTC 秒中断
HAL_RTCEx_SetSecond_IT(&hrtc);
HAL_NVIC_EnableIRQ(RTC_IRQn);
while (1) {
// 定期检查 LSE 状态,若恢复则切换回 LSE
if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == SET && Backup_GetStatus() == 0xA5A5) {
// 切换回 LSE,并清除标志
Switch_To_LSE();
Backup_SetStatus(0);
}
HAL_Delay(1000);
}
}
```
# 注意事项
- **备份域电源**:VBAT 必须可靠供电,否则备份寄存器内容丢失,方案失效。建议使用低功耗电池或大电容。
- **LSI 精度校准**:LSI 频率随温度变化,需定期校准。可在秒中断中与外部参考(如 GPS 或网络)对比,调整预分频值。
- **时间戳转换**:示例中未实现 Unix 时间戳与 RTC 时间的转换,实际需编写转换函数,注意时区和闰年。
- **切换时钟源时的计数器重置**:切换 RTC 时钟源会导致 RTC 计数器清零,因此必须在切换后立即恢复时间。
- **中断处理**:秒中断中写入备份寄存器需快速,避免阻塞。备份寄存器写入有访问时间限制,建议使用影子寄存器。
- **LSE 重启**:若 LSE 因外部干扰停振,可在系统空闲时尝试重新启动 LSE,并等待就绪。多次失败后应放弃,避免频繁切换。
- **测试**:实际测试中,可通过人为断开 LSE 晶振模拟停振,验证时间恢复的准确性。
# 总结
本文提出的方案利用 STM32F4 备份域的特性,通过备份寄存器保存时间戳和状态,在 LSE 停振时自动切换至 LSI,并实现时间恢复。该方法无需外部 RTC 芯片,成本低,且能保证系统时间连续性。开发者可根据实际需求扩展,如增加温度补偿或网络校时,进一步提升精度。