STM32F4 无外部晶振日历纠偏:LSE 停振后 RTC 备份寄存器的实战应用
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,RTC 依赖 LSE 晶振维持时间基准,但 LSE 可能因环境或硬件问题停振,导致日历失效。本文深入剖析 STM32F4 的 RTC 备份寄存器机制,提供一套无需外部晶振的日历纠偏方案,涵盖原理、配置步骤、完整代码与注意事项,帮助开发者构建高可靠性的时间系统。
# 引言
在工业控制、物联网设备等嵌入式应用中,RTC(实时时钟)是记录时间戳的关键外设。STM32F4 系列通常使用 32.768kHz 的 LSE 晶振作为 RTC 的低速时钟源,但 LSE 可能因温度漂移、焊接不良或电磁干扰而停振。一旦 LSE 停止,RTC 将无法计时,系统时间归零或停滞,造成严重后果。本文提出一种基于 RTC 备份寄存器的纠偏策略:在 LSE 停振时,利用备份寄存器保存关键时间信息,并在恢复后自动校正日历,实现无外部晶振的可靠运行。
# 原理剖析
## 1. RTC 与备份寄存器架构
STM32F4 的 RTC 模块包含一个 20 位的预分频器、32 位可编程计数器(用于秒计数)以及一组 20 字节的备份寄存器(BKP Registers)。这些寄存器位于 VDD 供电域,当主电源掉电时,可由 VBAT 引脚供电,保持数据不丢失。备份寄存器的主要用途是存储系统配置或关键数据,例如 RTC 校准值、唤醒标志等。
## 2. LSE 停振检测机制
RTC 时钟源可选 LSE、LSI 或 HSE 分频。LSE 停振时,RTC 的时钟输入丢失,但 RTC 本身不会自动切换时钟源。通过检测 RTC_ISR 寄存器的 LSERDY 位(LSE 就绪标志)可以判断 LSE 状态。当 LSERDY=0 时,表示 LSE 未就绪或停振。此时,RTC 计数器停止更新,但备份寄存器仍可读写。
## 3. 纠偏策略设计
核心思路:
- 正常运行时,定期将当前时间(如秒计数)和日期信息写入备份寄存器,作为“时间快照”。
- 检测到 LSE 停振时,立即停止依赖 RTC 的时间更新,并记录停振时刻的备份值。
- 在 LSE 恢复后,读取备份寄存器中的时间快照,结合停振持续时间(通过系统定时器或外部事件估算),重新初始化 RTC 计数器,实现日历纠偏。
# 配置步骤
## 1. 使能 RTC 与备份寄存器时钟
首先,需要使能 PWR 和 RTC 时钟,并允许访问备份域。
```c
// 使能 PWR 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 允许访问备份域
PWR_BackupAccessCmd(ENABLE);
// 使能 RTC 时钟(这里使用 LSE,但需先使能 LSE)
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待 LSE 就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro(); // 等待 RTC 同步
```
## 2. 初始化 RTC 并设置时间
配置 RTC 预分频器(LSE 为 32768Hz,分频系数为 32767),并设置初始时间。
```c
RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F; // 异步分频 128
RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 同步分频 256,实际分频为 32768/(128*256)=1Hz
RTC_Init(&RTC_InitStruct);
// 设置时间:2024年1月1日 12:00:00
RTC_TimeTypeDef time = {0};
time.RTC_Hours = 12;
time.RTC_Minutes = 0;
time.RTC_Seconds = 0;
RTC_SetTime(RTC_Format_BIN, &time);
RTC_DateTypeDef date = {0};
date.RTC_Year = 24; // 2024年
date.RTC_Month = 1;
date.RTC_Date = 1;
date.RTC_WeekDay = 1;
RTC_SetDate(RTC_Format_BIN, &date);
```
## 3. 定期保存时间快照到备份寄存器
在应用主循环或定时器中,每隔一段时间(如 1 秒)将当前时间写入备份寄存器。
```c
void SaveTimeSnapshot(void) {
RTC_TimeTypeDef time;
RTC_DateTypeDef date;
RTC_GetTime(RTC_Format_BIN, &time);
RTC_GetDate(RTC_Format_BIN, &date);
// 将时间信息打包存储(示例:使用 4 个备份寄存器)
uint32_t snapshot = (time.RTC_Hours << 16) | (time.RTC_Minutes << 8) | time.RTC_Seconds;
RTC_WriteBackupRegister(RTC_BKP_DR0, snapshot);
snapshot = (date.RTC_Year << 16) | (date.RTC_Month << 8) | date.RTC_Date;
RTC_WriteBackupRegister(RTC_BKP_DR1, snapshot);
// 记录写入时的系统 tick(用于计算停振时长)
RTC_WriteBackupRegister(RTC_BKP_DR2, (uint32_t)GetSystemTick());
}
```
## 4. 检测 LSE 停振并处理
在主循环中轮询 LSE 状态,一旦发现停振,立即停止时间更新,并记录当前系统 tick。
```c
void CheckLSEStatus(void) {
if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) {
// LSE 停振
lse_stopped = 1;
stop_tick = GetSystemTick();
// 可在此关闭 RTC 中断或相关任务
} else {
if (lse_stopped) {
// LSE 恢复,执行纠偏
CorrectRTC();
lse_stopped = 0;
}
}
}
```
## 5. 纠偏函数实现
读取备份寄存器中的快照,计算停振时长(基于系统 tick 差值),然后重新设置 RTC 时间。
```c
void CorrectRTC(void) {
// 读取快照
uint32_t time_snap = RTC_ReadBackupRegister(RTC_BKP_DR0);
uint32_t date_snap = RTC_ReadBackupRegister(RTC_BKP_DR1);
uint32_t saved_tick = RTC_ReadBackupRegister(RTC_BKP_DR2);
uint32_t elapsed = GetSystemTick() - saved_tick; // 停振时长(假设 tick 为毫秒)
// 解析时间
RTC_TimeTypeDef time;
time.RTC_Hours = (time_snap >> 16) & 0xFF;
time.RTC_Minutes = (time_snap >> 8) & 0xFF;
time.RTC_Seconds = time_snap & 0xFF;
RTC_DateTypeDef date;
date.RTC_Year = (date_snap >> 16) & 0xFF;
date.RTC_Month = (date_snap >> 8) & 0xFF;
date.RTC_Date = date_snap & 0xFF;
// 将停振时长加到时间上(简化处理,需考虑进位)
uint32_t total_seconds = time.RTC_Hours * 3600 + time.RTC_Minutes * 60 + time.RTC_Seconds;
total_seconds += elapsed / 1000; // 假设 tick 为毫秒
// 转换为时分秒
time.RTC_Hours = total_seconds / 3600;
time.RTC_Minutes = (total_seconds % 3600) / 60;
time.RTC_Seconds = total_seconds % 60;
// 设置新时间(日期进位可忽略或另行处理)
RTC_SetTime(RTC_Format_BIN, &time);
RTC_SetDate(RTC_Format_BIN, &date);
// 重新保存快照
SaveTimeSnapshot();
}
```
# 完整代码示例
以下是一个简化的综合示例,展示主循环中的调用逻辑。
```c
// 全局变量
volatile uint8_t lse_stopped = 0;
uint32_t stop_tick = 0;
// 假设系统 tick 为 1ms
uint32_t GetSystemTick(void) {
return HAL_GetTick(); // 使用 HAL 库
}
int main(void) {
HAL_Init();
// 初始化时钟、GPIO 等
// 初始化 RTC(如上)
RTC_Init();
// 初始保存快照
SaveTimeSnapshot();
while (1) {
CheckLSEStatus();
// 正常运行时每秒保存一次快照
if (!lse_stopped) {
static uint32_t last_save = 0;
if (GetSystemTick() - last_save >= 1000) {
SaveTimeSnapshot();
last_save = GetSystemTick();
}
}
// 其他任务
}
}
```
# 注意事项
- **备份寄存器数量有限**:STM32F4 有 20 个备份寄存器,每个 32 位,需合理规划存储内容。示例中使用了 3 个,可根据需要扩展。
- **时间进位处理**:纠偏时,如果停振时间较长,可能导致日期进位(如跨天、跨月),示例中未处理,实际应用需根据日期算法完善。
- **系统 tick 的可靠性**:停振期间,系统 tick 必须持续运行(如由 HSI 或 PLL 驱动),否则无法计算停振时长。若系统掉电,则备份寄存器仍可保存数据,但恢复后需人工干预。
- **LSE 恢复检测**:LSE 恢复可能需要一定时间,建议在检测到 LSERDY 置位后,延时一段时间再执行纠偏,确保时钟稳定。
- **备份域复位**:如果备份域被复位(如 VBAT 掉电),备份寄存器内容丢失,需重新初始化 RTC。
- **功耗考虑**:备份寄存器在 VBAT 供电下会消耗少量电流,设计低功耗产品时需注意。
# 总结
通过 RTC 备份寄存器保存时间快照,并在 LSE 停振后利用系统 tick 计算停振时长,可以实现无外部晶振的日历纠偏。该方法简单有效,适用于对时间精度要求不苛刻但需保证连续性的场景。开发者可根据实际需求扩展,例如增加日期进位算法、使用外部中断检测等。希望本文能帮助你在嵌入式项目中构建更健壮的 RTC 系统。