STM32F4 无晶振日历自恢复:LSE 停振后利用 RTC 备份域寄存器的容错设计
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列嵌入式系统中,LSE(低速外部晶振)常为 RTC 提供精准时钟源,但晶振易受环境干扰而停振,导致日历丢失。本文深入探讨如何利用 RTC 备份域寄存器(BKP)在 LSE 失效时保存关键时间戳,并在系统重启后自动恢复日历,实现无需外部晶振的容错方案。文章涵盖原理分析、寄存器配置、完整代码示例及工程注意事项,帮助开发者构建高可靠的实时时钟系统。
# STM32F4 无晶振日历自恢复:LSE 停振后利用 RTC 备份域寄存器的容错设计
## 一、问题背景与核心原理
在 STM32F4 系列(如 STM32F407)中,RTC(实时时钟)通常依赖 LSE(32.768kHz 外部晶振)提供高精度时钟。然而,LSE 可能因焊接不良、电磁干扰或温度漂移而停振,导致 RTC 计时停止。若系统未做保护,重启后 RTC 将恢复默认值(如 2000 年 1 月 1 日),造成时间信息丢失。
**核心思路**:利用 RTC 备份域寄存器(BKP,Backup Registers)在 LSE 停振前保存当前时间戳(如 Unix 时间)。当系统检测到 LSE 故障并重启后,通过读取 BKP 中的备份值,结合内部低速时钟(LSI)或系统时钟进行粗略计时,实现日历的自动恢复。
**关键原理**:
- BKP 寄存器位于备份域,由 VBAT 供电,即使主电源断电,只要电池存在,数据不丢失。
- RTC 具有独立的校准和唤醒功能,可在 LSE 停振时切换至 LSI(约 32kHz,精度较低),但 LSI 误差较大,不适合长期计时,因此仅用于短期过渡。
- 通过软件标志位(如 BKP 寄存器中的特定值)记录 LSE 状态,重启后判断是否需要恢复。
## 二、硬件与软件准备
- 硬件:STM32F4 开发板(如 STM32F407-Discovery),备用电池连接 VBAT。
- 软件:STM32CubeIDE(或 Keil MDK),HAL 库。
- 配置:启用 RTC、BKP、PWR 时钟,并设置 RTC 时钟源为 LSE(默认),但代码中需支持切换至 LSI。
## 三、配置步骤
### 1. 使能相关时钟
在 `main.c` 中,首先使能 PWR 和 BKP 时钟,并允许访问备份域。
```c
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 允许访问备份域
```
### 2. 初始化 RTC(支持 LSE 或 LSI)
初始化 RTC 时,尝试启用 LSE,若失败则回退至 LSI,并设置标志。
```c
RTC_HandleTypeDef hrtc;
void RTC_Init_With_Fallback(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;
// 尝试启用 LSE
__HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
// 等待超时,若超时则切换 LSI
if (HAL_GetTick() > 1000) {
__HAL_RCC_LSE_CONFIG(RCC_LSE_OFF);
__HAL_RCC_LSI_ENABLE();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET);
hrtc.Init.SynchPrediv = 0x1F; // LSI 约 32kHz,调整分频
break;
}
}
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
Error_Handler();
}
}
```
### 3. 保存时间戳到备份域
在正常运行期间,定期(如每秒)将当前 Unix 时间写入 BKP 寄存器。
```c
#include
void Save_Time_To_BKP(void) {
time_t timestamp = get_rtc_timestamp(); // 从 RTC 获取 Unix 时间
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, (uint32_t)timestamp);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA5A5); // 写入标志,表示有效
}
```
### 4. 检测 LSE 状态并恢复日历
系统启动时,检查 BKP 标志,若存在有效备份,则读取时间戳并设置 RTC。
```c
void RTC_Restore_From_BKP(void) {
uint32_t backup_flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
if (backup_flag == 0xA5A5) {
uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
// 将时间戳转换为 RTC 日期时间结构
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
time_t rawtime = (time_t)timestamp;
struct tm *timeinfo = localtime(&rawtime);
sTime.Hours = timeinfo->tm_hour;
sTime.Minutes = timeinfo->tm_min;
sTime.Seconds = timeinfo->tm_sec;
sDate.Year = timeinfo->tm_year + 1900 - 2000; // 基于 2000 年
sDate.Month = timeinfo->tm_mon + 1;
sDate.Date = timeinfo->tm_mday;
sDate.WeekDay = timeinfo->tm_wday;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
// 清除标志,避免重复恢复
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x0000);
}
}
```
### 5. 主程序集成
在 `main()` 中,先初始化 RTC,然后尝试恢复,最后启动周期保存任务。
```c
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
RTC_Init_With_Fallback();
RTC_Restore_From_BKP();
while (1) {
Save_Time_To_BKP(); // 每秒调用,或使用定时器
HAL_Delay(1000);
}
}
```
## 四、完整代码示例(关键部分)
以下为整合后的核心代码,省略了错误处理和 GPIO 初始化。
```c
// rtc_fallback.c
#include "rtc_fallback.h"
RTC_HandleTypeDef hrtc;
void RTC_Init_With_Fallback(void) {
// 使能时钟和备份域访问
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
// 配置 RTC 参数
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 0x7F;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
// 尝试 LSE,超时则 LSI
__HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
uint32_t tickstart = HAL_GetTick();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
if (HAL_GetTick() - tickstart > 1000) {
__HAL_RCC_LSE_CONFIG(RCC_LSE_OFF);
__HAL_RCC_LSI_ENABLE();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET);
hrtc.Init.SynchPrediv = 0x1F; // 调整分频
break;
}
}
hrtc.Init.SynchPrediv = (hrtc.Init.SynchPrediv == 0xFF) ? 0xFF : 0x1F;
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
Error_Handler();
}
}
void RTC_Save_Time(void) {
time_t timestamp = HAL_RTC_GetUnixTime(&hrtc); // 假设有该函数,或自行转换
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, (uint32_t)timestamp);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA5A5);
}
void RTC_Restore(void) {
if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) == 0xA5A5) {
uint32_t timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
time_t rawtime = (time_t)timestamp;
struct tm *timeinfo = localtime(&rawtime);
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
sTime.Hours = timeinfo->tm_hour;
sTime.Minutes = timeinfo->tm_min;
sTime.Seconds = timeinfo->tm_sec;
sDate.Year = timeinfo->tm_year + 1900 - 2000;
sDate.Month = timeinfo->tm_mon + 1;
sDate.Date = timeinfo->tm_mday;
sDate.WeekDay = timeinfo->tm_wday;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x0000);
}
}
```
## 五、注意事项
- **备份域电源**:确保 VBAT 引脚连接电池或大电容,否则 BKP 数据在断电后丢失。
- **LSI 精度**:LSI 误差可达 ±5%,仅适合短时间过渡,长期使用需定期校准或依赖外部时间同步。
- **时间戳转换**:使用 `localtime` 需注意时区设置,嵌入式环境建议使用 UTC 存储,显示时再转换。
- **BKP 寄存器数量**:STM32F4 有 20 个 32 位备份寄存器(DR0-DR19),可存储多个数据,但注意地址映射。
- **中断处理**:若 LSE 停振,可配置 RTC 闹钟或唤醒中断,及时保存时间戳,避免数据丢失。
- **代码健壮性**:在写入 BKP 前检查备份域访问是否使能,否则可能引发硬件错误。
## 六、总结
通过利用 RTC 备份域寄存器,STM32F4 系统能够在 LSE 停振后实现日历的自恢复,无需外部晶振即可维持时间连续性。该方案成本低、实现简单,适用于对时间精度要求不苛刻但需要长期运行的嵌入式设备。开发者可根据实际需求扩展,如增加温度补偿或网络校时,进一步提升可靠性。