STM32F4 系列 LSE 停振后基于硬件 RTC 备份域的无感恢复策略

一、问题背景与挑战

在嵌入式系统中,RTC(实时时钟)通常依赖外部 32.768kHz 晶振(LSE)提供精准时基。然而,在恶劣环境(如高低温、振动)或晶振老化情况下,LSE 可能停振,导致 RTC 计数停止,时间数据丢失。传统方案依赖软件检测和外部时间源(如 GPS、网络)重新同步,但存在延迟和成本问题。STM32F4 系列内置的备份域(Backup Domain)提供了一种硬件级解决方案,可在 LSE 恢复后无感恢复时间,无需外部干预。

二、硬件 RTC 备份域原理

2.1 备份域结构

STM32F4 的备份域包括:

  • RTC 计数器:独立于主电源,由 VBAT 供电,可继续运行。
  • 备份寄存器(BKP):20 个 32 位寄存器,用于保存用户数据,在 VBAT 供电下内容不丢失。
  • RTC 时钟源:可选 LSE、LSI 或 HSE 分频,其中 LSE 精度最高。

2.2 停振检测与恢复机制

当 LSE 停振时,RTC 的时钟源失效,但备份域仍由 VBAT 供电,寄存器内容保留。通过配置 RTC 的时钟源监控(如 CSS on LSE)或定期检查 RTC 是否计数,可检测停振。恢复策略的核心是:

  1. 停振前:定期将当前时间戳和校准参数写入备份寄存器。
  2. 停振期间:备份寄存器保存最后有效时间,RTC 停止但数据不丢。
  3. LSE 恢复后:重新初始化 RTC,并从备份寄存器恢复时间戳,实现无感恢复。

三、配置步骤

3.1 使能备份域访问

备份域默认受保护,需通过 PWR 和 RCC 寄存器解除写保护。

// 使能 PWR 和 RCC 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);

// 解除备份域写保护
PWR_BackupAccessCmd(ENABLE);

3.2 配置 RTC 时钟源

选择 LSE 作为 RTC 时钟源,并使能 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 同步

3.3 初始化 RTC 并设置时间

使用 RTC_InitTypeDef 配置 RTC 参数,并设置初始时间。

RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;  // 异步分频 127
RTC_InitStruct.RTC_SynchPrediv = 0xFF;   // 同步分频 255
RTC_Init(&RTC_InitStruct);

// 设置时间(示例:2025-01-01 00:00:00)
RTC_TimeTypeDef time = {0};
time.RTC_Hours = 0;
time.RTC_Minutes = 0;
time.RTC_Seconds = 0;
RTC_SetTime(RTC_Format_BIN, &time);
RTC_DateTypeDef date = {0};
date.RTC_Year = 25; // 2025 年
date.RTC_Month = 1;
date.RTC_Date = 1;
date.RTC_WeekDay = 4; // 星期四
RTC_SetDate(RTC_Format_BIN, &date);

3.4 定期保存时间到备份寄存器

在系统主循环或定时中断中,将当前时间戳(如 Unix 时间)写入备份寄存器。

void SaveTimeToBackup(void) {
    uint32_t timestamp = GetUnixTimestamp(); // 自定义函数,将 RTC 时间转为 Unix 时间戳
    BKP_WriteBackupRegister(BKP_DR1, timestamp);
    BKP_WriteBackupRegister(BKP_DR2, timestamp >> 16); // 若需更多位,可拆分
}

3.5 检测 LSE 停振并恢复

通过检查 RTC 计数是否变化或使用 LSE CSS 中断检测停振。恢复流程:

void CheckAndRestoreRTC(void) {
    // 检测 LSE 是否恢复(例如通过 RCC_GetFlagStatus)
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == SET) {
        // 重新初始化 RTC(同步骤 3.2-3.3)
        RCC_RTCCLKCmd(ENABLE);
        RTC_WaitForSynchro();
        
        // 从备份寄存器恢复时间戳
        uint32_t timestamp = BKP_ReadBackupRegister(BKP_DR1) | (BKP_ReadBackupRegister(BKP_DR2) << 16);
        SetUnixTimestamp(timestamp); // 将时间戳写回 RTC
    }
}

四、完整代码示例

以下是一个简化的完整示例,展示初始化、保存和恢复流程。

#include "stm32f4xx.h"

// 假设函数:将 RTC 时间转为 Unix 时间戳(需自行实现)
uint32_t GetUnixTimestamp(void) { return 0; }
void SetUnixTimestamp(uint32_t ts) { /* 实现 */ }

void RTC_Init(void) {
    // 使能 PWR 和备份域时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
    PWR_BackupAccessCmd(ENABLE);

    // 启用 LSE
    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);

    // 配置 RTC 时钟源
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);
    RTC_WaitForSynchro();

    // 初始化 RTC 参数
    RTC_InitTypeDef RTC_InitStruct;
    RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
    RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
    RTC_InitStruct.RTC_SynchPrediv = 0xFF;
    RTC_Init(&RTC_InitStruct);

    // 如果备份寄存器中有有效时间,则恢复;否则设置默认时间
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xFFFFFFFF) { // 假设 0xFFFFFFFF 表示无效
        uint32_t ts = BKP_ReadBackupRegister(BKP_DR1) | (BKP_ReadBackupRegister(BKP_DR2) << 16);
        SetUnixTimestamp(ts);
    } else {
        // 设置默认时间 2025-01-01 00:00:00
        RTC_TimeTypeDef time = {0};
        time.RTC_Hours = 0; time.RTC_Minutes = 0; time.RTC_Seconds = 0;
        RTC_SetTime(RTC_Format_BIN, &time);
        RTC_DateTypeDef date = {0};
        date.RTC_Year = 25; date.RTC_Month = 1; date.RTC_Date = 1; date.RTC_WeekDay = 4;
        RTC_SetDate(RTC_Format_BIN, &date);
    }
}

void SaveTimeToBackup(void) {
    uint32_t ts = GetUnixTimestamp();
    BKP_WriteBackupRegister(BKP_DR1, ts & 0xFFFF);
    BKP_WriteBackupRegister(BKP_DR2, (ts >> 16) & 0xFFFF);
}

int main(void) {
    RTC_Init();
    while (1) {
        // 定期保存时间(例如每 1 秒)
        SaveTimeToBackup();
        Delay(1000);
        // 检测 LSE 状态,若恢复则重新初始化并恢复时间
        if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == SET) {
            // 可在此处调用恢复逻辑,但注意避免重复初始化
        }
    }
}

五、注意事项

  • 备份寄存器数量有限:STM32F4 有 20 个备份寄存器,每个 32 位,可存储多个时间戳或校准数据。若需保存更多数据,可考虑压缩或使用外部 EEPROM。
  • 时间戳精度:备份寄存器保存的是最后有效时间,停振期间的时间无法补偿。若需高精度,可结合 LSI 作为备用时钟源,但精度较低。
  • 备份域电源:确保 VBAT 引脚连接电池或超级电容,否则备份域在掉电时也会丢失数据。
  • LSE 恢复检测:建议使用 RTC 的 CSS(时钟安全系统)中断,可自动检测 LSE 故障并切换时钟源,但需额外配置。
  • 初始化顺序:在恢复时间前,必须等待 RTC 同步完成(RTC_WaitForSynchro),否则写入可能失败。
  • 代码健壮性:备份寄存器初始值可能为随机值,建议使用魔术字(如 0xA5A5)验证数据有效性。

六、总结

通过利用 STM32F4 的备份域,我们实现了 LSE 停振后的无感恢复。该方案无需外部硬件,仅依赖内部备份寄存器,降低了成本,提高了系统可靠性。在实际项目中,开发者可根据需求扩展,例如增加时间校准、多备份策略等,以应对更复杂的场景。