引言

在嵌入式系统中,RTC(实时时钟)常依赖 LSE(32.768kHz 晶振)提供精准时基。然而,晶振可能因环境干扰、老化或焊接问题而停振。STM32F4 系列提供了 RTC 时钟源监控和备份寄存器,但默认固件库并未完整实现故障恢复流程。本文以 STM32F407 为例,讲解如何通过 RTC 备份寄存器保存系统状态,在 LSE 停振时自动切换至 LSI(内部低速 RC),并恢复时间计数,实现无感恢复。

原理剖析

1. LSE 停振检测机制

STM32F4 的 RTC 模块包含一个时钟源标志位 RTC_ISR.RSF(寄存器同步标志)和 RTC_ISR.INITS(初始化状态标志)。当 LSE 停振时,RTC 时钟域会失去时钟,导致寄存器无法更新。更直接的方法是使用 RTC->ISRLSRMF(LSE 单调检测标志)或通过 RTC_CRLSCOEN 使能 LSE 时钟输出,配合外部检测。但最可靠的方式是启用 RTC 的 RTC_TAFCR 寄存器中的 TAMPFLTTAMPxTRG 边沿检测,或使用 RTC_ISRALRAF(闹钟标志)作为辅助。

实际工程中,常用方案是:

  • 使能 RTC 闹钟中断,设置一个短周期(如 1 秒)闹钟,若闹钟未触发,则判定 LSE 停振。
  • 或者利用 LSE 的旁路监测功能,但 STM32F4 没有专用停振中断,需轮询。

2. RTC 备份寄存器的作用

RTC 备份寄存器(BKPxR)在 VDD 供电时由 VBAT 供电,可保存用户数据。在 LSE 停振时,RTC 时间寄存器可能因时钟丢失而无法读取,但备份寄存器不受影响。因此,我们可以将当前时间戳、时钟源状态、校准值等关键信息存入备份寄存器,在恢复时读取并重建时间。

3. 时钟源无缝切换策略

STM32F4 的 RTC 时钟源可选 LSE、LSI 或 HSE 分频。当检测到 LSE 停振,应:

  1. 保存当前时间到备份寄存器(若可能)。
  2. 关闭 RTC 时钟源选择,切换至 LSI。
  3. 重新初始化 RTC 并恢复时间(从备份寄存器读取)。
  4. 更新系统状态标志,通知应用层。

由于 LSI 精度较低(约 ±5%),需在恢复后定期校准或提示用户。

硬件与软件准备

  • 硬件:STM32F407 开发板,外部 32.768kHz 晶振,备用电池连接 VBAT。
  • 软件:STM32CubeIDE 或 Keil,HAL 库或标准外设库均可。本文使用 HAL 库。

配置步骤

1. 初始化 RTC 与备份寄存器

首先,使能 PWR 和 RTC 时钟,配置 RTC 时钟源为 LSE,并使能备份域访问。

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

    RCC_OscInitTypeDef oscInit = {0};
    oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    oscInit.LSEState = RCC_LSE_ON;
    if (HAL_RCC_OscConfig(&oscInit) != HAL_OK) {
        // 若 LSE 启动失败,立即切换 LSI
        SwitchToLSI();
        return;
    }

    RCC_PeriphCLKInitTypeDef periphClkInit = {0};
    periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
    HAL_RCCEx_PeriphCLKConfig(&periphClkInit);

    // 使能 RTC
    __HAL_RCC_RTC_ENABLE();

    RTC_HandleTypeDef hrtc = {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;
    HAL_RTC_Init(&hrtc);

    // 写入初始时间(示例)
    RTC_TimeTypeDef sTime = {0};
    sTime.Hours = 12;
    sTime.Minutes = 0;
    sTime.Seconds = 0;
    HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    RTC_DateTypeDef sDate = {0};
    sDate.Year = 24;
    sDate.Month = 1;
    sDate.Date = 1;
    HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
}

2. 保存上下文到备份寄存器

在正常运行时,周期性将当前时间戳和状态存入备份寄存器。

void SaveContextToBackup(void) {
    RTC_TimeTypeDef sTime;
    RTC_DateTypeDef sDate;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    
    // 将时间转换为 Unix 时间戳(简化示例,实际需考虑闰年等)
    uint32_t timestamp = 0;
    // ... 转换代码 ...
    
    HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR0, timestamp);
    HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0xA5A5); // 标记有效
}

3. 检测 LSE 停振并切换时钟源

使用轮询方式检测 LSE 是否停振。例如,每秒检查 RTC 是否还在走时(通过比较秒寄存器),或使用闹钟中断。以下示例使用闹钟中断:

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) {
    // 闹钟触发,说明 RTC 正常,更新备份寄存器
    SaveContextToBackup();
    // 重新设置下一次闹钟
    SetAlarm(1); // 1 秒后
}

void CheckLSEAndSwitch(void) {
    // 若闹钟未触发(通过标志位),则判定 LSE 停振
    if (alarmMissed) {
        // 保存最后已知时间
        SaveContextToBackup();
        // 切换至 LSI
        SwitchToLSI();
    }
}

void SwitchToLSI(void) {
    // 1. 关闭 RTC 时钟源
    __HAL_RCC_RTC_DISABLE();
    HAL_PWR_DisableBkUpAccess();
    
    // 2. 使能 LSI 并配置为 RTC 时钟
    RCC_OscInitTypeDef oscInit = {0};
    oscInit.OscillatorType = RCC_OSCILLATORTYPE_LSI;
    oscInit.LSIState = RCC_LSI_ON;
    HAL_RCC_OscConfig(&oscInit);
    
    RCC_PeriphCLKInitTypeDef periphClkInit = {0};
    periphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    periphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
    HAL_RCCEx_PeriphCLKConfig(&periphClkInit);
    
    // 3. 重新初始化 RTC
    __HAL_RCC_RTC_ENABLE();
    RTC_HandleTypeDef hrtc = {0};
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127; // 需根据 LSI 频率调整(LSI 约 32kHz)
    hrtc.Init.SynchPrediv = 255;
    HAL_RTC_Init(&hrtc);
    
    // 4. 从备份寄存器恢复时间
    uint32_t timestamp = HAL_RTCEx_BKUPRead(RTC, RTC_BKP_DR0);
    if (timestamp != 0xFFFFFFFF) {
        // 将时间戳转换回 RTC 时间并设置
        // ...
    }
    
    // 5. 设置状态标志
    HAL_RTCEx_BKUPWrite(RTC, RTC_BKP_DR1, 0x5A5A); // 表示已切换
}

4. 完整代码示例(主循环)

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    
    RTC_Init();
    
    // 使能闹钟中断
    SetAlarm(1);
    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
    
    while (1) {
        // 每 100ms 检查一次闹钟是否超时
        if (alarmMissed) {
            CheckLSEAndSwitch();
        }
        // 其他任务
    }
}

void SetAlarm(uint32_t seconds) {
    RTC_AlarmTypeDef sAlarm = {0};
    sAlarm.AlarmTime.Hours = 0;
    sAlarm.AlarmTime.Minutes = 0;
    sAlarm.AlarmTime.Seconds = seconds;
    sAlarm.AlarmMask = RTC_ALARMMASK_DATE_WEEKDAY;
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}

注意事项

  • 备份寄存器有效性:在读取备份寄存器前,务必检查标记值(如 0xA5A5),避免使用未初始化的数据。
  • LSI 精度问题:LSI 频率受温度影响大,切换后 RTC 走时误差可能较大。建议在应用层增加校准机制,或定期与外部时间源同步。
  • 预分频器调整:LSE 为 32.768kHz,LSI 约为 32kHz,但实际频率可能偏差。需根据实际 LSI 频率重新计算 AsynchPrediv 和 SynchPrediv,否则 RTC 时间不准确。
  • 电源管理:在切换时钟源时,务必先关闭 RTC 时钟,再修改时钟源,避免产生毛刺。
  • 中断优先级:闹钟中断应设为较高优先级,确保及时响应。
  • 测试方法:实际测试时,可断开 LSE 晶振引脚模拟停振,观察系统是否自动切换。

总结

通过 RTC 备份寄存器保存上下文,结合闹钟中断检测 LSE 停振,并在检测到故障后无缝切换至 LSI,可以显著提高 STM32F4 系统的可靠性。该方法无需外部硬件,仅依赖片内资源,适用于对时间连续性要求较高的工业设备、智能仪表等场景。开发者可根据实际需求扩展,如增加多次重试、故障日志记录等功能。