STM32F4 系列 LSE 停振后基于 RTC 备份域寄存器的无痕恢复策略

1. 问题背景与原理

在嵌入式系统中,RTC(实时时钟)通常依赖 LSE(32.768kHz 外部晶振)提供精准时基。然而,LSE 可能因晶振老化、外部干扰或 PCB 布局不当而停振。一旦 LSE 失效,RTC 将失去时钟源,时间数据无法更新,甚至可能导致系统复位后时间归零。

STM32F4 系列提供了 RTC 备份域寄存器(BKP),这是一块由 VBAT 供电的独立存储区域,即使在主电源断电或系统复位时,只要 VBAT 有电,数据就能保留。利用这一特性,我们可以在 LSE 停振前将关键状态(如时间戳、校准值、标志位)存入备份域,并在复位后读取,实现“无痕恢复”。

2. 硬件与软件准备

  • 硬件:STM32F4 开发板(如 STM32F407)、外部 32.768kHz 晶振、备用电池(连接 VBAT)。
  • 软件:STM32CubeIDE 或 Keil MDK,HAL 库或标准外设库。本文以 HAL 库为例。

3. 核心原理:备份域寄存器与 RTC 配置

3.1 备份域寄存器(BKP)

备份域包含 20 个 32 位数据寄存器(BKP0R~BKP19R),以及控制寄存器。它们由 VBAT 供电,复位后数据不丢失(除非 VBAT 掉电)。访问前需使能 PWR 和 BKP 时钟,并设置 PWR_CR 的 DBP 位(备份域写保护)。

3.2 LSE 停振检测

RTC 提供 RTC_ISR 寄存器的 LSERDY 标志 来指示 LSE 是否就绪。当 LSERDY 为 0 时,表示 LSE 停振。我们可以在主循环中周期性检查该标志,一旦发现停振,立即将当前时间戳和状态写入备份域。

3.3 恢复流程

  1. 系统上电或复位后,先检查备份域中的标志位(如 0xA5A5)是否有效。
  2. 若有效,则读取备份的时间戳,并重新配置 RTC(使用 LSI 或 HSE 作为备用时钟源),将时间写入 RTC。
  3. 清除标志位,完成恢复。

4. 配置步骤

4.1 使能备份域访问

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 设置 DBP 位

4.2 配置 RTC(正常模式,使用 LSE)

RTC_HandleTypeDef hrtc;
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
HAL_RTC_Init(&hrtc);
// 启用 LSE
__HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) != SET); // 等待就绪

4.3 检测 LSE 停振并保存状态

在主循环中,定期检查 LSERDY 标志:

if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
    // LSE 停振,保存当前时间到备份域
    uint32_t timestamp = HAL_RTC_GetTimestamp(&hrtc); // 自定义函数获取时间戳
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5); // 标志位
    // 可切换至 LSI 继续运行,或进入安全模式
}

4.4 复位后恢复

在系统初始化时,检查备份域标志:

uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
if (flag == 0xA5A5) {
    uint32_t saved_time = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    // 重新初始化 RTC,使用 LSI 作为时钟源
    __HAL_RCC_LSE_CONFIG(RCC_LSE_OFF);
    __HAL_RCC_LSI_ENABLE();
    while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) != SET);
    // 配置 RTC 使用 LSI,并设置时间
    hrtc.Init.SynchPrediv = 0x7A; // 根据 LSI 频率调整
    HAL_RTC_Init(&hrtc);
    HAL_RTC_SetTime(&hrtc, &saved_time, RTC_FORMAT_BIN);
    // 清除标志
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0);
}

5. 完整代码示例

以下是一个简化的完整示例,包含初始化、检测和恢复逻辑:

#include "stm32f4xx_hal.h"

RTC_HandleTypeDef hrtc;

void RTC_Init(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();

    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;
    hrtc.Init.SynchPrediv = 255;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    HAL_RTC_Init(&hrtc);

    // 尝试启用 LSE
    __HAL_RCC_LSE_CONFIG(RCC_LSE_ON);
    uint32_t timeout = 1000;
    while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) != SET) {
        if (--timeout == 0) break;
    }

    // 检查备份域标志,决定是否恢复
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) == 0xA5A5) {
        // 恢复时间
        RTC_TimeTypeDef sTime;
        sTime.Hours = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) >> 16) & 0xFF;
        sTime.Minutes = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) >> 8) & 0xFF;
        sTime.Seconds = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) & 0xFF;
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0); // 清除标志
    } else {
        // 正常初始化时间
        RTC_TimeTypeDef sTime = {0};
        sTime.Hours = 12;
        sTime.Minutes = 0;
        sTime.Seconds = 0;
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    }
}

void Check_LSE_And_Save(void) {
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) {
        // LSE 停振,保存当前时间
        RTC_TimeTypeDef sTime;
        HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        uint32_t packed = (sTime.Hours << 16) | (sTime.Minutes << 8) | sTime.Seconds;
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, packed);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0xA5A5);
        // 可切换至 LSI 继续运行
        __HAL_RCC_LSE_CONFIG(RCC_LSE_OFF);
        __HAL_RCC_LSI_ENABLE();
        // 重新配置 RTC 使用 LSI(略)
    }
}

int main(void) {
    HAL_Init();
    RTC_Init();
    while (1) {
        Check_LSE_And_Save();
        HAL_Delay(1000);
    }
}

6. 注意事项

  • 备份域写保护:访问 BKP 寄存器前必须使能 PWR 和 BKP 时钟,并设置 DBP 位,否则写入无效。
  • VBAT 供电:确保 VBAT 引脚连接电池或大电容,否则备份域数据在断电后丢失。
  • 时间戳格式:示例中简单打包了时分秒,实际应用可扩展为完整日期时间(使用 BKP 多个寄存器)。
  • 时钟切换:LSE 停振后,RTC 时钟源需切换为 LSI,但 LSI 精度较低,可能影响时间准确性,需在恢复后重新校准。
  • 标志位唯一性:使用 0xA5A5 作为有效标志,避免随机数据误判。
  • 中断处理:可配置 LSE 中断(RTC_LSECSS)来立即响应停振,而不是轮询。

7. 总结

通过利用 RTC 备份域寄存器,我们可以在 LSE 停振时保存关键时间信息,并在复位后无痕恢复,显著提升系统的可靠性。该方法实现简单,成本低,适用于对时间连续性要求高的嵌入式应用。开发者可根据实际需求扩展,例如保存更多状态或实现多级备份。