引言

在嵌入式系统中,RTC(实时时钟)是时间敏感应用的核心。STM32F4 系列默认使用 LSE(32.768kHz 低速外部晶振)作为 RTC 时钟源,其精度高、温漂小。然而,LSE 晶振对 PCB 布局、环境温度及电磁干扰极为敏感,一旦停振,RTC 将失去时钟基准,导致时间停滞或系统异常。本文提出一种基于硬件 RTC 备份域的自动恢复与时间补偿方案,确保系统在 LSE 故障时仍能维持时间连续性。

1. LSE 停振机理与备份域优势

1.1 LSE 停振原因

  • 晶振老化:长期工作导致频率偏移或起振困难。
  • 环境干扰:强电磁场或温度剧变(如 -40°C 至 +85°C)使振荡器停止。
  • 负载电容失配:PCB 寄生电容与晶振规格不符,导致负阻不足。

1.2 备份域特性

STM32F4 的备份域(Backup Domain)包含 RTC、备份寄存器(BKP)及可选的外部 32.768kHz 振荡器。其关键优势:

  • 独立供电:由 VBAT 引脚供电,主电源掉电时仍可运行。
  • 数据保持:备份寄存器(如 RTC_BKPxR)在复位或主电源断电时内容不丢失。
  • 时钟源切换:RTC 时钟源可在 LSE 和 LSI(内部低速 RC,约 32kHz)之间切换,但需谨慎处理切换时序。

2. 自动恢复方案设计

2.1 总体思路

  1. 检测停振:通过 RTC 的 LSERDY 标志位(RTC_ISR 寄存器)判断 LSE 是否就绪。
  2. 记录状态:在备份寄存器中存储 LSE 停振标志及当前时间戳。
  3. 切换时钟源:若 LSE 停振,立即将 RTC 时钟源切换至 LSI,并启用 LSI 校准。
  4. 时间补偿:记录 LSI 运行期间的累计误差,待 LSE 恢复后,根据补偿值调整 RTC 时间。

2.2 补偿机制

LSI 频率受温度影响较大(典型精度 ±5%),因此需周期性测量 LSI 频率(通过 TIM5 捕获或比较 LSE 与 LSI 计数),并计算补偿系数。本文简化处理:在 LSE 恢复后,利用备份寄存器中保存的“停振前时间”与“当前 LSI 时间”之差,一次性校准。

3. 配置步骤(基于 STM32CubeMX 与 HAL 库)

3.1 时钟配置

  • 使能 LSE(外部低速时钟)和 LSI(内部低速时钟)。
  • RTC 时钟源选择 LSE,并开启 RTC 日历功能。
  • 在 NVIC 中使能 RTC 全局中断(可选,用于周期检测)。

3.2 备份域访问使能

// 使能 PWR 和备份域访问时钟
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();

3.3 初始化 RTC 并设置时间

RTC_HandleTypeDef hrtc;

void RTC_Init(void) {
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;  // 异步分频,典型值
    hrtc.Init.SynchPrediv = 255;   // 同步分频,产生 1Hz
    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();
    }
    // 设置初始时间(示例:2025-01-01 00:00:00)
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sDate = {0};
    sTime.Hours = 0;
    sTime.Minutes = 0;
    sTime.Seconds = 0;
    sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
    sTime.StoreOperation = RTC_STOREOPERATION_RESET;
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) {
        Error_Handler();
    }
    sDate.Year = 25;  // 2025
    sDate.Month = 1;
    sDate.Date = 1;
    sDate.WeekDay = RTC_WEEKDAY_THURSDAY;
    if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK) {
        Error_Handler();
    }
}

3.4 检测 LSE 状态并切换时钟源

// 检查 LSE 是否就绪
uint8_t IsLseReady(void) {
    return (RTC->ISR & RTC_ISR_LSERDY) ? 1 : 0;
}

// 切换 RTC 时钟源至 LSI(需先停止 RTC)
void SwitchRtcClockToLsi(void) {
    // 停止 RTC
    HAL_RTC_DeInit(&hrtc);
    // 使能 LSI
    RCC->CSR |= RCC_CSR_LSION;
    while ((RCC->CSR & RCC_CSR_LSIRDY) == 0);
    // 选择 LSI 作为 RTC 时钟源
    RCC->BDCR |= RCC_BDCR_RTCSEL_1;  // 10: LSI
    // 重新初始化 RTC(保留时间寄存器?注意:重新初始化会复位时间,需先保存)
    // 此处简化:重新设置时间,实际应用中需从备份寄存器恢复
    RTC_Init();
}

3.5 备份寄存器存储与恢复

// 保存停振标志和时间戳
void SaveLseStopInfo(void) {
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xA5A5);  // 标志
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN));
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN));
}

// 恢复时间并清除标志
void RestoreTimeFromBackup(void) {
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) == 0xA5A5) {
        // 读取备份时间,并补偿 LSI 运行期间的误差(此处简化为直接恢复)
        sTime.Seconds = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
        sDate.Date = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
        HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
        HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0);  // 清除标志
    }
}

3.6 主循环中的监测与恢复

while (1) {
    if (!IsLseReady()) {
        if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0xA5A5) {
            SaveLseStopInfo();
            SwitchRtcClockToLsi();
        }
    } else {
        if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) == 0xA5A5) {
            // LSE 恢复,切换回 LSE 并补偿时间
            HAL_RTC_DeInit(&hrtc);
            RCC->BDCR |= RCC_BDCR_RTCSEL_0;  // 01: LSE
            RTC_Init();
            RestoreTimeFromBackup();
        }
    }
    HAL_Delay(1000);  // 每秒检测一次
}

4. 完整代码示例(核心部分)

#include "main.h"

RTC_HandleTypeDef hrtc;
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;

// 初始化 RTC(见上文)
void RTC_Init(void);

// 检测 LSE 就绪
uint8_t IsLseReady(void);

// 切换时钟源
void SwitchRtcClockToLsi(void);

// 备份与恢复
void SaveLseStopInfo(void);
void RestoreTimeFromBackup(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    RTC_Init();

    while (1) {
        if (!IsLseReady()) {
            if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0xA5A5) {
                SaveLseStopInfo();
                SwitchRtcClockToLsi();
            }
        } else {
            if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) == 0xA5A5) {
                // 切换回 LSE
                HAL_RTC_DeInit(&hrtc);
                RCC->BDCR &= ~RCC_BDCR_RTCSEL;  // 清除选择位
                RCC->BDCR |= RCC_BDCR_RTCSEL_0; // 选择 LSE
                RTC_Init();
                RestoreTimeFromBackup();
            }
        }
        HAL_Delay(1000);
    }
}

5. 注意事项

  • 切换时钟源前必须停止 RTC:直接切换会导致 RTC 状态机混乱,需先调用 HAL_RTC_DeInit
  • 备份寄存器数量有限:STM32F4 有 20 个 32 位备份寄存器(RTC_BKP_DR0~19),合理规划存储内容。
  • LSI 校准:若需高精度,可在 LSI 运行时利用 TIM5 捕获 LSI 与 LSE 的计数差,动态调整补偿值。
  • 电源管理:确保 VBAT 引脚连接电池或大电容,否则备份域数据在掉电时丢失。
  • 中断处理:建议使用 RTC 唤醒中断或外部中断监测 LSE 状态,避免轮询占用 CPU。
  • 测试验证:在极端温度下测试 LSE 停振与恢复场景,确保切换逻辑可靠。

结语

通过硬件 RTC 备份域与 LSI 的协同,STM32F4 在 LSE 停振时能够自动切换时钟源并保存时间信息,待 LSE 恢复后完成时间补偿,极大提升了系统时钟的鲁棒性。此方案成本低、易实现,适用于对时间连续性要求高的嵌入式产品。开发者可根据实际需求扩展 LSI 校准算法,实现更高精度的时间维持。