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 时钟,并允许访问备份域。

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 允许访问备份域

2. 初始化 RTC(支持 LSE 或 LSI)

初始化 RTC 时,尝试启用 LSE,若失败则回退至 LSI,并设置标志。

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 寄存器。

#include <time.h>

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。

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,然后尝试恢复,最后启动周期保存任务。

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 初始化。

// 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 停振后实现日历的自恢复,无需外部晶振即可维持时间连续性。该方案成本低、实现简单,适用于对时间精度要求不苛刻但需要长期运行的嵌入式设备。开发者可根据实际需求扩展,如增加温度补偿或网络校时,进一步提升可靠性。