STM32F4 无晶振时间保持:LSE 停振后基于 RTC 备份域寄存器的可靠方案

1. 问题背景与挑战

STM32F4 系列微控制器内置 RTC(实时时钟),通常依赖外部低速晶振(LSE,32.768kHz)提供精准时基。然而,LSE 晶振在恶劣环境(高湿度、强振动)或 PCB 布局不当(走线过长、寄生电容过大)时可能停振。一旦 LSE 失效,RTC 默认停止计时,系统重启后时间归零,严重影响数据记录或通信协议的时间戳功能。

传统解决方案是直接切换至内部 LSI(约 32kHz,精度 ±5%),但 LSI 频率偏差大,长时间运行会累积分钟级误差。本文提出一种结合备份域寄存器的软件补偿策略:在 LSE 停振时,自动切换至 LSI,并周期性将当前时间戳写入 RTC 备份寄存器(BKP),同时记录 LSI 的漂移系数,实现无外部晶振下的可靠时间保持。

2. 原理剖析:RTC 备份域与时钟源切换

2.1 备份域结构

STM32F4 的备份域包括 RTC、备份寄存器(BKP,20 个 32 位寄存器)和备份 SRAM(2KB)。该域由 VBAT 引脚供电,当主电源 VDD 掉电时,只要 VBAT 连接电池,RTC 和 BKP 数据依然保留。备份域复位独立于系统复位,可通过设置 PWR_CR 的 DBP 位解除写保护。

2.2 时钟源选择

RTC 时钟源可选 HSE/128、LSE 或 LSI。LSE 精度最高(±20ppm),LSI 精度较低(±5%)。在 LSE 停振时,硬件会自动切换至 LSI(若已使能),但 RTC 的计数频率变化会导致时间跳变。因此,我们需要在软件中检测 LSE 状态,并启动补偿机制。

2.3 补偿策略

  • 检测:通过 RTC_ISR 寄存器的 LSERDY 位判断 LSE 是否就绪。若为 0,则进入降级模式。
  • 切换:将 RTC 时钟源改为 LSI,并重新初始化 RTC 预分频器(异步预分频 127,同步预分频 255,得到 1Hz 计数)。
  • 备份:在切换前,将当前时间(秒数)写入 BKP 寄存器(如 BKP0R),并记录切换时刻的 LSI 计数值。
  • 补偿:在降级模式下,周期性(如每秒)读取 LSI 计数,计算实际经过时间,并修正 RTC 计数。

3. 配置步骤

3.1 使能备份域访问

首先,需要使能 PWR 和 BKP 时钟,并解除备份域写保护。

// 使能 PWR 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 使能备份域访问
PWR_BackupAccessCmd(ENABLE);
// 使能 BKP 时钟(若使用 BKP 寄存器)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);

3.2 配置 LSE 与 LSI

初始化 LSE 并等待就绪,同时使能 LSI 作为备用。

RCC_LSEConfig(RCC_LSE_ON);
// 等待 LSE 就绪,超时处理
uint32_t timeout = 0;
while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout < 1000)) {
    timeout++;
}

// 使能 LSI
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);

3.3 初始化 RTC 并选择时钟源

若 LSE 就绪,使用 LSE;否则使用 LSI。

RTC_InitTypeDef RTC_InitStruct;

if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) {
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
} else {
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
}

RCC_RTCCLKCmd(ENABLE);
RTC_InitStruct.RTC_AsynchPrediv = 0x7F;  // 127
RTC_InitStruct.RTC_SynchPrediv = 0xFF;   // 255
RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
RTC_Init(RTC_InitStruct);

3.4 备份关键时间戳

在切换时钟源前,将当前 Unix 时间戳写入 BKP 寄存器。

// 假设当前时间已由外部同步,存储到 BKP0R
uint32_t timestamp = GetUnixTime(); // 自定义函数
BKP_WriteBackupRegister(BKP_DR1, timestamp);
// 同时存储 LSI 的当前计数值(若使用 LSI)
BKP_WriteBackupRegister(BKP_DR2, RTC_GetCounter());

4. 完整代码示例

以下代码演示了 LSE 停振检测与切换逻辑,以及基于 BKP 的补偿机制。

#include "stm32f4xx.h"
#include <stdio.h>

// 全局变量
volatile uint32_t last_lsi_count = 0;
volatile uint32_t base_timestamp = 0;

// 初始化 RTC 备份域
void RTC_BackupInit(void) {
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    PWR_BackupAccessCmd(ENABLE);
    
    // 使能 LSI
    RCC_LSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    
    // 尝试 LSE
    RCC_LSEConfig(RCC_LSE_ON);
    uint32_t timeout = 100000;
    while ((RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) && (timeout-- > 0));
    
    // 选择时钟源
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) {
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    } else {
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
    }
    
    RCC_RTCCLKCmd(ENABLE);
    
    RTC_InitTypeDef RTC_InitStruct;
    RTC_InitStruct.RTC_AsynchPrediv = 0x7F;
    RTC_InitStruct.RTC_SynchPrediv = 0xFF;
    RTC_InitStruct.RTC_HourFormat = RTC_HourFormat_24;
    RTC_Init(RTC_InitStruct);
    
    // 读取备份时间戳(若有效)
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xFFFFFFFF) {
        base_timestamp = BKP_ReadBackupRegister(BKP_DR1);
        last_lsi_count = BKP_ReadBackupRegister(BKP_DR2);
    } else {
        base_timestamp = 0; // 首次启动
        last_lsi_count = RTC_GetCounter();
        BKP_WriteBackupRegister(BKP_DR1, base_timestamp);
        BKP_WriteBackupRegister(BKP_DR2, last_lsi_count);
    }
}

// 获取当前时间戳(秒)
uint32_t RTC_GetTimestamp(void) {
    uint32_t current_count = RTC_GetCounter();
    uint32_t elapsed;
    
    if (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != RESET) {
        // LSE 正常,直接返回计数
        return base_timestamp + (current_count - last_lsi_count);
    } else {
        // LSI 模式,需要补偿(假设 LSI 频率为 32000Hz,实际测量后调整)
        // 这里简单计算:LSI 计数差值 / 32000 得到秒数,但实际需校准
        elapsed = (current_count - last_lsi_count) / 32000;
        return base_timestamp + elapsed;
    }
}

// 每秒调用一次,更新备份寄存器
void RTC_BackupUpdate(void) {
    uint32_t current_count = RTC_GetCounter();
    uint32_t current_ts = RTC_GetTimestamp();
    
    // 更新备份寄存器
    BKP_WriteBackupRegister(BKP_DR1, current_ts);
    BKP_WriteBackupRegister(BKP_DR2, current_count);
}

int main(void) {
    // 初始化系统时钟等
    RTC_BackupInit();
    
    while (1) {
        // 主循环
        delay(1000);
        RTC_BackupUpdate();
        printf("Timestamp: %lu\n", RTC_GetTimestamp());
    }
}

5. 注意事项与优化建议

  • LSI 校准:LSI 频率随温度漂移,建议在系统启动时测量实际 LSI 频率(通过定时器比较),并存储校准系数到 BKP 寄存器,补偿计算时使用该系数。
  • 备份寄存器数量:STM32F4 有 20 个 32 位备份寄存器,可存储多个时间戳或校准数据,但注意 VBAT 掉电后数据丢失,需定期更新。
  • 切换时机:检测到 LSE 停振后,应尽快切换,避免 RTC 计数中断。可在 RTC 闹钟中断或定时器中断中检测 LSERDY 位。
  • 电源管理:确保 VBAT 引脚连接电池或大电容,否则备份域数据在掉电时丢失。
  • 代码健壮性:在读取 BKP 寄存器前检查写保护状态,避免意外写入。

6. 总结

通过合理利用 STM32F4 的备份域寄存器和时钟源切换机制,可以在 LSE 停振后维持可靠的时间保持。本文提供的方案不仅解决了时间丢失问题,还通过备份时间戳和 LSI 补偿提高了精度。实际项目中,可根据需求扩展为多级补偿或外部同步,进一步提升可靠性。