引言

在嵌入式系统中,LSE(32.768kHz晶振)通常为RTC提供精准时钟源。然而,晶振老化、焊接问题或电磁干扰可能导致LSE停振,而系统可能仍以LSI(内部低速RC)继续运行,导致时间漂移或唤醒异常。传统故障记录依赖外部存储或中断,但会侵入主流程。本文介绍一种利用RTC备份域寄存器(BKP)的无侵入式方案:在LSE停振时,通过RTC的时钟安全系统(CSS)或轮询检测,将故障状态和上下文写入备份寄存器,系统重启后仍可读取,实现故障追溯。

原理剖析

1. RTC备份域寄存器(BKP)

STM32F4的RTC备份域包含20个32位备份寄存器(BKP0R-BKP19R),位于VDD备份供电区域。这些寄存器在系统复位、电源跌落时(若VBAT供电)仍保持内容,非常适合存储故障标志、时间戳或关键数据。访问BKP需先使能PWR和备份域时钟,并解除写保护。

2. LSE停振检测机制

  • 时钟安全系统(CSS):RTC的CSS功能可监测LSE,一旦检测到停振,自动切换时钟源并触发RTC_Alarm或RTC_WakeUp中断。但CSS仅适用于LSE作为RTC时钟源时,且需配置相关中断。
  • 轮询法:通过读取RTC_ISR的LSERDY位(或备份域控制寄存器RCC_BDCR的LSERDY),定期检查LSE状态。此方法简单,但需占用CPU时间。

本文采用轮询法,结合BKP实现无侵入记录,避免中断处理对主流程的干扰。

3. 无侵入式设计

无侵入意味着不修改现有业务代码,仅通过独立模块(如定时器或低优先级任务)周期检查LSE状态,一旦发现停振,立即将故障信息写入BKP,并置位标志。系统重启后,Bootloader或主程序可读取BKP,判断是否发生过LSE故障,并执行相应恢复策略(如切换时钟源、校准时间)。

硬件与软件准备

  • 硬件:STM32F4系列开发板(如STM32F407),外部32.768kHz晶振,VBAT接3V电池(可选)。
  • 软件:STM32CubeIDE或Keil,HAL库或标准外设库。本文以HAL库为例。

配置步骤

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

__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess(); // 解除备份域写保护

2. 配置RTC时钟源(LSE)

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) {
    // 错误处理
}

3. 初始化RTC(可选,若需时间戳)

RTC_HandleTypeDef hrtc;
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
HAL_RTC_Init(&hrtc);

4. 编写LSE检测函数

uint8_t Check_LSE_Status(void) {
    // 读取RCC_BDCR的LSERDY位
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY)) {
        return 1; // LSE正常
    } else {
        return 0; // LSE停振
    }
}

5. 故障记录函数

void Record_Fault_To_BKP(uint32_t fault_code) {
    // 写入备份寄存器0:故障代码
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, fault_code);
    // 写入备份寄存器1:当前系统时间(若RTC运行)
    RTC_TimeTypeDef sTime;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    uint32_t timestamp = (sTime.Hours << 16) | (sTime.Minutes << 8) | sTime.Seconds;
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, timestamp);
    // 写入备份寄存器2:累计故障次数(自增)
    uint32_t count = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) + 1;
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, count);
}

6. 主循环中的轮询(低优先级)

void LSE_Monitor_Task(void) {
    static uint32_t last_check = 0;
    if (HAL_GetTick() - last_check > 1000) { // 每秒检查一次
        last_check = HAL_GetTick();
        if (!Check_LSE_Status()) {
            Record_Fault_To_BKP(0xA5A5); // 自定义故障码
            // 可选:切换时钟源到LSI或执行其他恢复
        }
    }
}

7. 系统启动时读取故障记录

void Check_Fault_At_Boot(void) {
    uint32_t fault_code = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
    uint32_t count = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
    if (fault_code == 0xA5A5) {
        // 发生过失振,执行恢复策略
        printf("LSE fault detected! Count: %lu\n", count);
        // 清除故障标志(可选)
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0);
    }
}

完整代码示例

以下是一个完整的模块化示例,包含初始化、检测和记录功能。

#include "stm32f4xx_hal.h"

RTC_HandleTypeDef hrtc;

void BKP_Init(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();
}

void RTC_Init(void) {
    // 使能LSE
    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();
    }

    // 初始化RTC
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;
    hrtc.Init.SynchPrediv = 255;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    if (HAL_RTC_Init(&hrtc) != HAL_OK) {
        Error_Handler();
    }
}

uint8_t Is_LSE_Running(void) {
    return __HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY);
}

void Record_Fault(uint32_t code) {
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, code);
    RTC_TimeTypeDef sTime;
    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    uint32_t ts = (sTime.Hours << 16) | (sTime.Minutes << 8) | sTime.Seconds;
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, ts);
    uint32_t cnt = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) + 1;
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, cnt);
}

void LSE_Monitor(void) {
    static uint32_t last = 0;
    if (HAL_GetTick() - last >= 1000) {
        last = HAL_GetTick();
        if (!Is_LSE_Running()) {
            Record_Fault(0xA5A5);
            // 可在此添加切换时钟源逻辑
        }
    }
}

int main(void) {
    HAL_Init();
    BKP_Init();
    RTC_Init();
    // 启动时检查故障
    uint32_t code = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
    if (code == 0xA5A5) {
        // 处理故障
    }
    while (1) {
        LSE_Monitor();
        // 其他任务
    }
}

注意事项

  • 备份域写保护:必须调用HAL_PWR_EnableBkUpAccess(),否则写入无效。
  • VBAT供电:若系统断电,备份寄存器内容依赖VBAT引脚供电。若VBAT未接电池,断电后数据丢失,但系统复位(非断电)时仍保留。
  • 时钟源切换:检测到LSE停振后,建议将RTC时钟源切换到LSI,并重新校准时间,避免时间错误累积。
  • 轮询频率:根据应用需求调整检查间隔,过频会增加功耗,过疏可能错过瞬时故障。
  • 故障码设计:使用特定模式(如0xA5A5)作为有效标志,避免误判。
  • 多寄存器原子性:写入多个BKP寄存器时,若中途断电,可能部分写入。可先写一个“写入完成”标志寄存器,最后置位,读取时检查该标志。

总结

通过RTC备份域寄存器,我们实现了一种轻量级、无侵入的LSE故障记录方案。该方法不依赖额外硬件,不打断主流程,且能在系统重启后保留故障信息,极大方便了现场调试和远程诊断。开发者可根据实际需求扩展,例如记录更多上下文(电压、温度)或结合看门狗实现自动恢复。希望本文能为您提供有价值的参考。