引言

在嵌入式系统中,RTC(实时时钟)的秒中断常用于定时任务、日志时间戳或低功耗唤醒。STM32F4 系列默认推荐使用 LSE(32.768kHz 外部晶振)驱动 RTC,以获得高精度。但在实际项目中,LSE 可能因成本、空间或可靠性原因被省略,或运行中因振动、温度而停振。此时,若改用内部 LSI(约 32kHz,精度较差),秒中断会累积误差。本文将介绍一种巧妙方案:利用 RTC 备份寄存器记录补偿值,在 LSE 停振后,通过软件校准实现无外部晶振的秒中断补偿。

原理剖析

1. LSE 与 LSI 的差异

  • LSE:外部 32.768kHz 晶振,精度可达 ±20ppm,温度稳定性好,是 RTC 的理想时钟源。
  • LSI:内部 RC 振荡器,频率约 32kHz(实际范围 17~47kHz),精度差(±5% 左右),受温度影响大。

当 LSE 停振时,RTC 可切换到 LSI,但秒中断周期会偏离 1 秒。例如,若 LSI 实际为 31.5kHz,则 RTC 每秒计数 31500 次,导致实际秒周期为 31500/32768 ≈ 0.961 秒,即每秒快约 39ms。

2. RTC 备份寄存器的作用

RTC 备份寄存器(BKP Registers)是 RTC 域内的 20 个 32 位寄存器(STM32F4 系列),可在 VDD 掉电时由 VBAT 供电保持数据。它们常用来存储系统状态、校准参数等。在本方案中,我们利用备份寄存器保存两个关键值:

  • 补偿系数:每秒需要调整的计数偏移量。
  • 累计误差:用于动态修正的累积值。

3. 补偿算法设计

核心思想:在秒中断服务函数中,根据实际 LSI 频率与理想频率的偏差,动态调整 RTC 的预分频值或通过软件计数修正。由于 RTC 的异步预分频器(ASYNC_PREDIV)和同步预分频器(SYNC_PREDIV)可配置,但修改它们需要重新初始化 RTC,不适合频繁操作。因此,我们采用软件补偿:

  • 设定 RTC 使用 LSI 作为时钟源,并配置一个固定的预分频值(例如,假设 LSI 为 32kHz,设置同步预分频为 32767,得到 1Hz 中断)。
  • 在秒中断中,读取一个高精度定时器(如 TIM2)的计数值,测量实际秒周期。
  • 计算误差,并累加到备份寄存器中的累计误差变量。当累计误差超过一定阈值(如 1ms),则调整下一次中断的唤醒时间(通过修改 RTC 的唤醒定时器或利用软件延时)。

但更简洁的方法是:利用 RTC 的日历功能,在秒中断中直接修改 RTC 的秒值(通过写 RTC_TR 寄存器),但需谨慎,因为写操作可能引发冲突。本方案采用“计数补偿”法:在秒中断中,若累计误差为正(RTC 快),则跳过下一次秒中断(即软件计数减 1);若为负,则额外触发一次中断(通过设置事件标志)。

配置步骤

1. 硬件准备

  • STM32F4 开发板(如 STM32F407)
  • 无 LSE 晶振(或断开 LSE)
  • 调试器(ST-Link/J-Link)

2. 软件环境

  • STM32CubeMX 或 HAL 库
  • Keil MDK 或 IAR

3. 初始化流程

步骤 1:配置 RTC 时钟源为 LSI

在 CubeMX 中,将 RTC 时钟源选择为 LSI,并启用 RTC 日历和秒中断。

步骤 2:配置备份寄存器

使能 PWR 和 RTC 备份访问,然后写入初始补偿参数。

步骤 3:编写秒中断处理函数

在中断中实现补偿逻辑。

完整代码示例

以下基于 STM32F4 HAL 库,演示核心代码。

/* 备份寄存器地址定义 */
#define BKP_COMPENSATION   RTC->BKP0R  /* 补偿系数 */
#define BKP_ERROR_ACC      RTC->BKP1R  /* 累计误差 */

/* 初始化 RTC 为 LSI 时钟,1Hz 中断 */
void RTC_Init(void)
{
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sDate = {0};

    __HAL_RTC_PWR_CLK_ENABLE();
    __HAL_RTC_RTC_ENABLE();

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

    /* 选择 LSI 作为 RTC 时钟 */
    __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSI);
    __HAL_RCC_RTC_ENABLE();

    /* 初始化 RTC */
    RTC_HandleTypeDef hrtc;
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24HOUR;
    hrtc.Init.AsynchPrediv = 0x7F;  /* 异步预分频 127 */
    hrtc.Init.SynchPrediv = 0xFF;   /* 同步预分频 255,配合 LSI 约 32kHz,得到 1Hz */
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
    HAL_RTC_Init(&hrtc);

    /* 设置日期时间 */
    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);

    /* 使能秒中断 */
    HAL_RTCEx_SetSecond_IT(&hrtc);

    /* 初始化备份寄存器 */
    BKP_COMPENSATION = 0;  /* 补偿系数,0 表示无补偿 */
    BKP_ERROR_ACC = 0;     /* 累计误差,单位 ms */

    /* 启动定时器用于测量实际秒周期(如 TIM2) */
    TIM2_Init();
}

/* 秒中断回调 */
void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc)
{
    static uint32_t last_tick = 0;
    uint32_t current_tick = TIM2->CNT;  /* 读取定时器计数值 */
    int32_t delta = (int32_t)(current_tick - last_tick);
    last_tick = current_tick;

    /* 假设 TIM2 计数频率为 1MHz,则 delta 为实际秒周期(us) */
    int32_t error = delta - 1000000;  /* 与理想 1 秒的误差,单位 us */

    /* 累计误差,单位 ms */
    int32_t acc = (int32_t)BKP_ERROR_ACC + error / 1000;
    BKP_ERROR_ACC = acc;

    /* 根据累计误差进行补偿 */
    if (acc >= 1) {
        /* RTC 快了,跳过下一次秒中断 */
        BKP_ERROR_ACC = acc - 1;
        /* 设置标志,在下一个中断中跳过处理 */
        skip_next = 1;
    } else if (acc <= -1) {
        /* RTC 慢了,额外触发一次中断(通过软件设置事件) */
        BKP_ERROR_ACC = acc + 1;
        /* 手动调用一次处理函数 */
        RTC_CompensationHandler();
    }

    if (skip_next) {
        skip_next = 0;
        return;  /* 跳过本次处理 */
    }

    /* 正常秒中断处理 */
    RTC_CompensationHandler();
}

/* 实际任务处理函数 */
void RTC_CompensationHandler(void)
{
    /* 例如:翻转 LED,记录时间戳等 */
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

注意事项

  • 备份寄存器访问:必须先使能 PWR 时钟并调用 HAL_PWR_EnableBkUpAccess(),否则写操作无效。
  • LSI 频率校准:首次上电时,建议通过定时器测量 LSI 实际频率,并计算初始补偿系数,写入备份寄存器。
  • 中断优先级:秒中断应设置为较高优先级,避免丢失补偿时机。
  • 误差累积:补偿算法中,误差阈值设为 1ms,可根据需求调整。若系统对时间精度要求高,可改用更高分辨率的定时器(如 TIM2 的 1MHz 计数)。
  • 低功耗模式:在低功耗模式下,备份寄存器仍保持数据,但 TIM2 可能停止,需在唤醒后重新校准。
  • 备份寄存器数量:STM32F4 有 20 个备份寄存器,但部分可能被其他功能占用,请合理规划。

总结

通过 RTC 备份寄存器保存补偿参数,结合定时器测量实际秒周期,可以在 LSE 停振或缺失的情况下,利用 LSI 实现可接受的秒中断精度。该方法无需额外硬件,仅依赖软件算法,适用于对成本敏感或高可靠性的嵌入式项目。实际测试中,经过补偿后,日误差可控制在 1 秒以内(取决于 LSI 稳定性和校准频率)。希望本文能为你提供实用的解决方案。