STM32F4 系列 LSE 停振后 RTC 无感恢复与时间校准实战指南

一、问题背景与挑战

在嵌入式系统中,RTC(实时时钟)通常依赖外部 32.768kHz 晶振(LSE)提供精准计时。然而,LSE 晶振易受温度、振动、PCB 布局等因素影响,导致停振。一旦 LSE 停振,RTC 将失去时钟源,时间停止更新,甚至可能因电源波动导致时间寄存器被清零。传统恢复方案往往需要系统重启或人工干预,严重影响设备可靠性。

STM32F4 系列提供了强大的备份域(Backup Domain),包含 20 个 32 位备份寄存器(BKPxR),可在 VDD 断电时由 VBAT 供电保持数据。利用这些寄存器,我们可以实现 LSE 停振后的无感恢复:在停振前保存关键时间信息,恢复后自动校准,无需用户感知。

二、核心原理

1. RTC 时钟源与备份域

STM32F4 的 RTC 可选三种时钟源:LSE、LSI(内部低速时钟)和 HSE 分频。默认使用 LSE 以获得高精度。备份域包括 RTC 寄存器、备份寄存器和备份 SRAM,由 VBAT 引脚供电,当主电源 VDD 掉电时,备份域数据依然保留。

2. LSE 停振检测

RTC 提供了时钟安全系统(CSS)功能,可监测 LSE 振荡器状态。当检测到 LSE 停振时,CSS 会产生中断,通知软件。同时,RTC 的 RTC_ISR 寄存器中的 LSERDY 位可反映 LSE 就绪状态。我们可以在中断中记录停振时刻,并切换到 LSI 作为临时时钟源,维持 RTC 计数继续运行(但精度降低)。

3. 备份寄存器的作用

备份寄存器(BKPxR)在备份域中,不受系统复位影响,只要 VBAT 供电即可保持。我们可以利用它们存储:

  • 停振前最后一次有效的时间戳(Unix 时间戳或自定义格式)
  • 停振发生时的系统运行时间(如 tick 计数)
  • 校准参数(如 LSE 频率偏差补偿值)
  • 状态标志位(如是否发生过停振)

当 LSE 恢复后,RTC 继续以 LSE 计时,但时间可能已经偏差。此时,我们读取备份寄存器中的时间戳和停振持续时间(通过 LSI 计时),计算出正确时间并写入 RTC。

三、硬件与软件准备

  • 硬件:STM32F407 开发板(或其他 F4 系列),外部 32.768kHz 晶振,VBAT 接 3V 电池。
  • 软件:STM32CubeIDE(或 Keil),HAL 库。

四、配置步骤

1. 使能备份域访问和 RTC 时钟

首先,使能 PWR 和 RTC 时钟,并允许访问备份域。

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_RTC_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 使能备份域访问

2. 配置 RTC 时钟源为 LSE,并启用 CSS

RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); }

RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { Error_Handler(); }

// 使能 CSS 中断(LSE 停振检测)
HAL_RCCEx_EnableLSECSS();

3. 初始化 RTC 并设置时间

RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};

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;
if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); }

// 设置初始时间(例如 2024-01-01 00:00:00)
sTime.Hours = 0;
sTime.Minutes = 0;
sTime.Seconds = 0;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);

sDate.Year = 24;
sDate.Month = 1;
sDate.Date = 1;
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

4. 保存初始时间戳到备份寄存器

在系统启动或时间校准时,将当前 Unix 时间戳写入备份寄存器。

uint32_t timestamp = get_unix_timestamp(); // 自定义函数,从 RTC 获取
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0); // 状态标志,0表示正常

5. 实现 CSS 中断回调

当 LSE 停振时,HAL 库会调用 HAL_RCCEx_LSECSS_Callback。在此回调中,我们记录停振时间,并切换到 LSI 作为临时时钟源。

void HAL_RCCEx_LSECSS_Callback(void)
{
    // 记录停振时的 RTC 时间(基于 LSI 继续计数)
    RTC_TimeTypeDef sTime;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    uint32_t stop_timestamp = get_unix_timestamp_from_rtc();
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, stop_timestamp); // 保存停振时间戳
    
    // 设置状态标志
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0x5A5A); // 0x5A5A 表示停振发生
    
    // 切换到 LSI(临时)
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
    HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
    
    // 重新初始化 RTC 以使用新时钟源(注意:RTC 会复位,但备份寄存器保留)
    HAL_RTC_DeInit(&hrtc);
    hrtc.Init.AsynchPrediv = 127;
    hrtc.Init.SynchPrediv = 255;
    HAL_RTC_Init(&hrtc);
    
    // 恢复时间(从备份寄存器读取停振前时间)
    uint32_t last_timestamp = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    set_rtc_from_timestamp(last_timestamp); // 自定义函数
}

6. 检测 LSE 恢复并校准时间

在 LSE 恢复后(通过轮询 LSERDY 位或外部事件),我们需要计算停振持续时间,并校准 RTC。

void check_lse_recovery(void)
{
    // 检查 LSE 是否就绪
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) != RESET)
    {
        // 读取状态标志
        uint32_t status = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
        if (status == 0x5A5A) // 曾发生停振
        {
            // 获取停振时间戳(备份寄存器3)
            uint32_t stop_ts = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3);
            // 获取当前 LSI 计数时间(假设我们有一个基于 LSI 的 tick 计数器)
            uint32_t current_lsi_tick = get_lsi_tick(); // 自定义,例如 SysTick
            uint32_t elapsed = current_lsi_tick - stop_lsi_tick; // 需要保存停振时的 LSI tick
            // 计算当前正确时间戳
            uint32_t correct_ts = stop_ts + elapsed;
            
            // 将 RTC 切换回 LSE
            RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
            PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
            PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
            HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
            HAL_RTC_DeInit(&hrtc);
            HAL_RTC_Init(&hrtc);
            
            // 写入校准后的时间
            set_rtc_from_timestamp(correct_ts);
            
            // 清除状态标志
            HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0);
        }
    }
}

7. 辅助函数:时间戳与 RTC 互转

uint32_t get_unix_timestamp_from_rtc(void)
{
    RTC_TimeTypeDef sTime;
    RTC_DateTypeDef sDate;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    // 使用标准库函数转换为 Unix 时间戳(需实现)
    return rtc_to_timestamp(sDate.Year, sDate.Month, sDate.Date, sTime.Hours, sTime.Minutes, sTime.Seconds);
}

void set_rtc_from_timestamp(uint32_t ts)
{
    // 将时间戳转换为日期时间结构体
    // 然后调用 HAL_RTC_SetTime 和 HAL_RTC_SetDate
}

五、完整代码示例(简化)

// main.c 片段
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 初始化 RTC 和备份寄存器(如前所述)
    RTC_Init();
    
    // 保存初始时间戳
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, get_unix_timestamp_from_rtc());
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, 0);
    
    while (1)
    {
        // 周期性检查 LSE 恢复
        check_lse_recovery();
        HAL_Delay(1000);
    }
}

void HAL_RCCEx_LSECSS_Callback(void)
{
    // 停振处理:保存时间戳,切换 LSI,设置标志
    // 注意:此回调在中断上下文中,应尽量简短
}

六、注意事项

  • 备份寄存器数量有限:STM32F4 有 20 个备份寄存器,合理分配用途,避免浪费。
  • LSI 精度问题:LSI 频率不准确(典型 32kHz,误差可达 ±50%),长时间停振会导致时间偏差较大。建议在停振期间使用外部低频时钟(如 32768Hz 的 RC 振荡器)或定期校准 LSI。
  • 电源管理:确保 VBAT 引脚连接电池或大电容,否则备份域数据会丢失。
  • CSS 中断优先级:CSS 中断应设置为高优先级,确保及时响应。
  • 时间戳转换:实现时间戳与日期时间的转换时,注意闰年、时区等问题,可使用标准库如 time.h 或自行实现。
  • 测试验证:在实际硬件上模拟 LSE 停振(如断开晶振),验证恢复逻辑是否可靠。

七、总结

本文详细介绍了 STM32F4 系列在 LSE 停振后,利用 RTC 备份寄存器实现无感恢复与时间校准的方法。通过备份寄存器保存关键时间信息,结合 CSS 中断和 LSI 临时时钟,系统能够在 LSE 恢复后自动校准时间,无需人工干预。该方案适用于对时间可靠性要求高的嵌入式应用,如智能电表、工业控制器等。开发者可根据实际需求扩展,例如增加多次停振记录、远程时间同步等功能。