引言

在嵌入式系统中,实时时钟(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 时钟

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); // 允许访问备份域

2. 检测 LSE 状态

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 时钟源

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 并恢复时间

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

// 在应用主循环中,每秒保存一次时间戳
void Save_Time_To_BKP(void) {
    uint32_t timestamp = Get_Timestamp(); // 从 RTC 获取 Unix 时间戳
    BKP_WriteBackupRegister(BKP_DR2, timestamp);
}

完整代码示例

以下是一个简化的初始化函数,整合了上述步骤:

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 停振时仍能可靠启动并保持时间。本文提供的方案不仅解决了无外部晶振的痛点,还增强了系统的鲁棒性。开发者可根据实际需求扩展,如增加时间同步协议或动态切换时钟源。希望本文能助你在嵌入式时钟设计中游刃有余。