# 引言 在嵌入式系统中,RTC(实时时钟)是许多应用的核心组件,尤其对于需要时间戳或定时唤醒的设备。STM32F4 的 RTC 通常由外部 32.768 kHz 晶振(LSE)驱动,其精度高、功耗低。然而,LSE 可能因焊接不良、振动或温度变化而停振,导致 RTC 失去时钟源。此时,若未做处理,RTC 将停止计数,时间精度荡然无存。本文将介绍如何检测 LSE 停振,并利用内部 HSI 振荡器(16 MHz)作为备用时钟源,通过软件校准维持 RTC 的时间精度。 # 原理分析 ## LSE 停振检测 STM32F4 的 RTC 模块提供了 LSE 时钟安全系统(LSE CSS),通过 RTC_ISR 寄存器的 LSECSSD 位检测 LSE 故障。启用后,若 LSE 振幅异常,硬件会置位该标志并触发中断。但注意,LSE CSS 需要 LSE 处于旁路或启用状态,且仅在部分系列支持(如 F4 全系支持)。 ## HSI 作为备用时钟 当 LSE 停振时,RTC 可切换至 HSI 时钟(经分频后)。HSI 频率为 16 MHz,精度约 ±1%,远低于 LSE,但足以维持计数。关键在于校准:通过测量 HSI 与 LSE 的频率差(在 LSE 正常时),或利用外部参考信号(如 GPS 秒脉冲)进行软件校准,补偿频率偏差。 ## 校准原理 RTC 的日历计数器以 1 Hz 递增,若时钟源频率偏差,实际时间会漂移。校准思路: - 在 LSE 正常时,测量 HSI 的实际频率(通过定时器捕获或比较)。 - 当 LSE 停振后,使用 HSI 驱动 RTC,并周期性地根据校准值调整 RTC 的亚秒寄存器(RTC_SSR)或使用 RTC 的同步移位功能(RTC_SHIFTR)进行微调。 - 更简单的方法:利用 RTC 的闹钟或唤醒定时器,在固定周期内补偿累计误差。 # 配置步骤 ## 1. 启用 LSE CSS 并配置中断 ```c // 启用 LSE CSS 中断 RTC_ITConfig(RTC_IT_LSECSS, ENABLE); NVIC_EnableIRQ(RTC_IRQn); ``` 在中断处理函数中,检测 LSECSSD 标志,并切换时钟源。 ## 2. 测量 HSI 频率(校准数据获取) 在系统初始化时,若 LSE 正常,利用定时器测量 HSI 的实际频率。例如,使用 TIM5 的输入捕获模式,捕获 LSE 的 1 Hz 信号(通过 MCO 输出),计算 HSI 周期。 ```c // 伪代码:测量 HSI 频率 uint32_t measure_hsi_freq(void) { // 配置 TIM5 捕获 LSE 信号,计数 HSI 周期数 // 返回 HSI 实际频率(如 16000000 ± 偏差) } ``` ## 3. 切换 RTC 时钟源至 HSI 当检测到 LSE 停振后,需重新初始化 RTC 时钟源。注意,RTC 时钟源切换需在备份域复位后执行。 ```c void switch_rtc_clock_to_hsi(void) { // 1. 进入备份域配置 PWR_BackupAccessCmd(ENABLE); RCC_BackupResetCmd(ENABLE); // 复位备份域,但会丢失 RTC 数据 RCC_BackupResetCmd(DISABLE); // 2. 使能 HSI 并选择为 RTC 时钟 RCC_HSICmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_HSI_Div8); // HSI/8 = 2 MHz // 3. 使能 RTC RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); } ``` 注意:备份域复位会清除 RTC 寄存器,因此需在切换前保存时间信息(如通过备份寄存器)。 ## 4. 软件校准补偿 由于 HSI 精度有限,需周期性校准。利用 RTC 的同步移位功能(RTC_SHIFTR)可调整亚秒值,实现微调。 ```c void rtc_calibrate(int32_t ppm) { // 计算移位值:每个移位单位对应 1/32768 秒(约 30.5 us) // 假设 ppm 为百万分之一,则每秒误差为 ppm/1e6 秒 // 移位值 = (ppm * 32768) / 1e6 uint32_t shift = (uint32_t)((ppm * 32768) / 1000000); RTC_SynchroShiftConfig(shift, 0); // 正值为加,负值为减(通过方向位) } ``` 实际应用中,可定期(如每分钟)根据外部参考(如网络时间)计算误差,并调用校准函数。 # 完整代码示例 以下代码演示了 LSE 停振检测、时钟切换和简单校准流程。 ```c #include "stm32f4xx.h" // 全局变量保存时间 volatile uint32_t g_rtc_time = 0; // LSE CSS 中断处理 void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_LSECSS) != RESET) { RTC_ClearITPendingBit(RTC_IT_LSECSS); // 保存当前时间(从 RTC 读取) g_rtc_time = RTC_GetCounter(); // 切换至 HSI switch_rtc_clock_to_hsi(); // 重新设置时间(从备份寄存器恢复) RTC_SetCounter(g_rtc_time); // 启用校准定时器(如每 10 秒校准一次) start_calibration_timer(); } } // 初始化 RTC 并启用 LSE CSS void rtc_init(void) { // 使能 PWR 和备份域时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_BackupAccessCmd(ENABLE); RCC_BackupResetCmd(ENABLE); RCC_BackupResetCmd(DISABLE); // 启用 LSE RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 选择 LSE 作为 RTC 时钟 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); // 配置 RTC 中断 RTC_ITConfig(RTC_IT_LSECSS, ENABLE); NVIC_EnableIRQ(RTC_IRQn); // 设置初始时间(示例) RTC_SetCounter(0); } // 校准函数:根据误差 ppm 调整 RTC void rtc_calibrate(int32_t ppm) { uint32_t shift = (uint32_t)((ppm * 32768) / 1000000); // 注意:RTC_SHIFTR 的 SUBFS 字段,ADD1S 位用于方向 RTC_SynchroShiftConfig(shift, 0); } // 定时器中断(假设每 10 秒触发) void TIM_IRQHandler(void) { // 此处可根据外部参考计算误差,并调用 rtc_calibrate // 例如:误差 = (实际时间 - RTC时间) * 1e6 / 实际时间 } int main(void) { // 初始化系统时钟(HSI 作为系统时钟) SystemInit(); // 初始化 RTC rtc_init(); while (1) { // 主循环 } } ``` # 注意事项 - **备份域复位风险**:切换 RTC 时钟源时,备份域复位会清除所有备份寄存器,务必在切换前保存关键数据(如时间戳)到非易失存储或备份寄存器(但备份寄存器也会被清,所以需提前保存到 Flash 或 EEPROM)。 - **校准精度**:HSI 的精度受温度影响较大,建议在系统运行时定期校准,而非一次性校准。 - **中断优先级**:LSE CSS 中断应设置为高优先级,确保及时响应,避免时间丢失。 - **功耗考虑**:HSI 功耗高于 LSE,若系统对功耗敏感,需权衡。 - **软件补偿限制**:RTC 的同步移位功能只能调整亚秒级,若误差较大(如 HSI 偏差超过 1%),需考虑更复杂的算法,如周期性重置计数器。 # 总结 通过 LSE CSS 检测停振,并利用 HSI 作为备用时钟源,结合软件校准,可以在 LSE 故障时维持 RTC 的时间精度。虽然 HSI 精度有限,但通过定期校准,大多数应用可接受。该方案为嵌入式系统提供了可靠的时钟冗余设计,尤其适用于工业或户外环境。希望本文能帮助开发者应对类似挑战。