STM32F4 无外部晶振的可靠启动与时间保持:LSE 停振后 RTC 备份域寄存器的妙用
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列嵌入式开发中,外部低速晶振(LSE)常因成本、空间或环境因素而失效,导致 RTC 无法维持时间。本文深入探讨 LSE 停振后的应对策略,重点讲解如何利用 RTC 备份域寄存器(BKP)实现无外部晶振的可靠启动与时间保持。通过原理分析、寄存器配置、完整代码示例及注意事项,帮助开发者构建高鲁棒性的嵌入式时钟系统,适用于工业控制、物联网设备等场景。
# 引言
在嵌入式系统中,实时时钟(RTC)是维持时间基准的关键外设,而 STM32F4 系列通常依赖外部 32.768kHz 晶振(LSE)来驱动 RTC。然而,LSE 可能因焊接不良、振动或温度漂移而停振,导致系统时间丢失。此时,若未做特殊处理,系统启动将失败或时间无法保持。本文将介绍一种无外部晶振的可靠方案:利用 RTC 备份域寄存器(BKP)在 LSE 停振时切换至内部低速 RC(LSI),并实现时间保持与启动恢复。
# 原理剖析
## LSE 与 RTC 的依赖关系
STM32F4 的 RTC 可由 LSE、LSI 或 HSE 分频驱动。LSE 精度高(典型 ±20ppm),但需要外部晶振;LSI 集成在芯片内部(约 32kHz,精度 ±5%),无需外部元件。当 LSE 停振时,RTC 时钟源失效,时间停止。
## 备份域寄存器(BKP)的作用
备份域包括 RTC、备份寄存器(20 个 32 位)和备份 SRAM(4KB),由 VBAT 引脚供电。即使主电源断电,只要 VBAT 有电,备份域内容可保持。BKP 寄存器可用于存储系统状态(如时钟源标志、时间校准值),在复位后快速恢复。
## 无外部晶振的启动策略
核心思路:
- 上电时检测 LSE 是否起振(通过 LSE 就绪标志)。
- 若 LSE 正常,则使用 LSE 驱动 RTC,并在 BKP 中记录“LSE 正常”标志。
- 若 LSE 停振,则切换至 LSI,并读取 BKP 中的时间备份(若存在),否则初始化默认时间。
- 利用 BKP 的写保护机制,确保关键数据不被意外篡改。
# 配置步骤
## 1. 使能备份域和 PWR 时钟
```c
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); // 允许访问备份域
```
## 2. 检测 LSE 状态
```c
uint8_t LSE_Status = 0;
RCC_LSEConfig(RCC_LSE_ON);
// 等待 LSE 就绪,超时 500ms
uint32_t timeout = 500;
while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0)) {
Delay(1);
}
if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) {
LSE_Status = 1;
}
```
## 3. 根据 LSE 状态配置 RTC 时钟源
```c
if (LSE_Status) {
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 记录 LSE 正常
} else {
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
BKP_WriteBackupRegister(BKP_DR1, 0x5A5A); // 记录 LSI 模式
}
RCC_RTCCLKCmd(ENABLE);
```
## 4. 初始化 RTC 并恢复时间
```c
RTC_InitTypeDef RTC_InitStruct;
RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;
// 使能 RTC 时钟
RTC_WriteProtectionCmd(DISABLE);
RTC_EnterInitMode();
// 配置 RTC 预分频(假设 LSI 为 32kHz,LSE 为 32.768kHz)
if (LSE_Status) {
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
RTC_InitStruct.RTC_SynchPrediv = 0xFF; // 1Hz
} else {
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
RTC_InitStruct.RTC_SynchPrediv = 0x3E; // 32kHz/128/62 ≈ 1Hz
}
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_Init(&RTC_InitStruct);
// 从 BKP 恢复时间(若存在)
if (BKP_ReadBackupRegister(BKP_DR2) != 0xFFFFFFFF) {
// 假设 BKP_DR2 存储时间戳(简化示例,实际可存结构体)
uint32_t timestamp = BKP_ReadBackupRegister(BKP_DR2);
// 转换并设置 RTC 时间(此处省略具体转换代码)
} else {
// 首次启动,设置默认时间 2024-01-01 00:00:00
RTC_TimeStruct.RTC_H12 = RTC_H12_AM;
RTC_TimeStruct.RTC_Hour = 0;
RTC_TimeStruct.RTC_Minute = 0;
RTC_TimeStruct.RTC_Second = 0;
RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct);
RTC_DateStruct.RTC_Year = 24;
RTC_DateStruct.RTC_Month = 1;
RTC_DateStruct.RTC_Date = 1;
RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct);
}
RTC_ExitInitMode();
RTC_WriteProtectionCmd(ENABLE);
```
## 5. 周期性保存时间到 BKP
```c
// 在应用主循环中,每秒保存一次时间戳
void Save_Time_To_BKP(void) {
uint32_t timestamp = Get_Timestamp(); // 从 RTC 获取 Unix 时间戳
BKP_WriteBackupRegister(BKP_DR2, timestamp);
}
```
# 完整代码示例
以下是一个简化的初始化函数,整合了上述步骤:
```c
void RTC_Init_With_LSE_Fallback(void) {
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 2. 检测 LSE
uint8_t lse_ok = 0;
RCC_LSEConfig(RCC_LSE_ON);
uint32_t timeout = 500;
while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0)) {
// 延时 1ms
for (volatile int i = 0; i < 1000; i++);
}
if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) lse_ok = 1;
// 3. 配置 RTC 时钟源
if (lse_ok) {
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
} else {
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
BKP_WriteBackupRegister(BKP_DR1, 0x5A5A);
}
RCC_RTCCLKCmd(ENABLE);
// 4. 初始化 RTC
RTC_WriteProtectionCmd(DISABLE);
RTC_EnterInitMode();
RTC_InitTypeDef rtc_init;
rtc_init.RTC_HourFormat = RTC_HourFormat_24;
if (lse_ok) {
rtc_init.RTC_AsynchPrediv = 0x7F;
rtc_init.RTC_SynchPrediv = 0xFF;
} else {
rtc_init.RTC_AsynchPrediv = 0x7F;
rtc_init.RTC_SynchPrediv = 0x3E; // 调整分频系数以接近 1Hz
}
RTC_Init(&rtc_init);
// 5. 恢复或设置时间
uint32_t saved_ts = BKP_ReadBackupRegister(BKP_DR2);
if (saved_ts != 0xFFFFFFFF) {
// 将 saved_ts 转换为 RTC 时间(此处省略,可使用库函数)
// 例如:RTC_SetTime 和 RTC_SetDate
} else {
RTC_TimeTypeDef time = {0};
time.RTC_H12 = RTC_H12_AM;
time.RTC_Hour = 0;
time.RTC_Minute = 0;
time.RTC_Second = 0;
RTC_SetTime(RTC_Format_BIN, &time);
RTC_DateTypeDef date = {0};
date.RTC_Year = 24;
date.RTC_Month = 1;
date.RTC_Date = 1;
RTC_SetDate(RTC_Format_BIN, &date);
}
RTC_ExitInitMode();
RTC_WriteProtectionCmd(ENABLE);
}
```
# 注意事项
- **备份域写保护**:在写入 BKP 前必须调用 `PWR_BackupAccessCmd(ENABLE)`,否则写操作无效。
- **LSI 精度问题**:LSI 频率误差较大(±5%),若需精确时间,建议定期校准或使用外部高精度 RTC 芯片。
- **分频系数调整**:当使用 LSI 时,需根据实际频率计算预分频值,确保 RTC 秒中断频率正确。可用定时器测量 LSI 实际频率。
- **BKP 数据有效性**:BKP 寄存器在 VBAT 掉电后会丢失,因此需在每次保存时写入校验值(如 CRC),读取时验证。
- **启动时间**:LSE 起振时间较长(可达 1s),需设置合理超时,避免阻塞过久。
- **低功耗模式**:在待机模式下,备份域仍供电,但 LSI 可能关闭,需确保 RTC 时钟源在低功耗时仍可用(可配置 LSI 在待机时保持)。
# 总结
通过巧妙利用 RTC 备份域寄存器,STM32F4 系列在 LSE 停振时仍能可靠启动并保持时间。本文提供的方案不仅解决了无外部晶振的痛点,还增强了系统的鲁棒性。开发者可根据实际需求扩展,如增加时间同步协议或动态切换时钟源。希望本文能助你在嵌入式时钟设计中游刃有余。