# 引言 在嵌入式系统中,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. 使能备份域时钟和访问 ```c __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 解除备份域写保护 ``` ## 2. 配置RTC时钟源(LSE) ```c 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(可选,若需时间戳) ```c 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检测函数 ```c 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. 故障记录函数 ```c 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. 主循环中的轮询(低优先级) ```c 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. 系统启动时读取故障记录 ```c 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); } } ``` # 完整代码示例 以下是一个完整的模块化示例,包含初始化、检测和记录功能。 ```c #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故障记录方案。该方法不依赖额外硬件,不打断主流程,且能在系统重启后保留故障信息,极大方便了现场调试和远程诊断。开发者可根据实际需求扩展,例如记录更多上下文(电压、温度)或结合看门狗实现自动恢复。希望本文能为您提供有价值的参考。