ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 RTC例程深度解析:备份域机制与掉电保持实践

STM32 RTC例程深度解析:备份域机制与掉电保持实践 简介STM32例程Example_RTC.7z是一份面向嵌入式开发入门者与进阶者的实时时钟功能示例工程围绕RTC的关键应用场景演示了从初始化、时钟源选择到日历配置、闹钟事件、唤醒定时器、备份寄存器读写以及低功耗模式下的状态保持与系统同步方式。资源共54个文件以C源代码、头文件、编译产生的o文件以及工程配置文件为主整体仅187KB结构紧凑适合直接对照工程阅读或迁移到实际项目中。已有249人学习下载可通过完整代码了解STM32F10x中RTC模块的寄存器操作与软件流程并借助串口输出、printf打印等手段验证计时结果。压缩包内还包含启动文件、标准外设库头文件与tim2_delay延时模块对理解工程组织、时钟树配置和低功耗唤醒设计均有参考价值。该例程适合教学演示、竞赛备赛或产品原型快速验证是一份边读边练的优质示例。1. 单独拆解STM32例程里的Example_RTC不是只读时间那么简单很多用STM32的朋友第一次拿到Example_RTC.7z时都会下意识把它当成“读个秒数、用printf打印时间”的入门demo。但实际上这个例程真正想演示的是备份域Backup Domain的工作机制在主电源切断、MCU复位、甚至进入Standby模式之后RTC仍然可以靠一颗纽扣电池或Vbat引脚继续数秒。换句话说例程里最难的不是配置寄存器而是理解“为什么掉了电时间还在”以及“怎么在启动时判断这次是真的冷启动还是掉电后恢复”。下面就把这个例程里的时钟源选择、备份域访问、串口调试和掉电恢复判断串起来讲。适合正在做低功耗表计、数据记录仪或者课程设计、毕业设计里需要RTC做定时唤醒的开发者。2. 从Example_RTC工程结构理解STM32 RTC的时钟与复位域2.1 拆开压缩包先分清哪些文件在管RTC哪些在管调试解压Example_RTC.7z后第一眼看到的结构和典型STM32标准外设库工程非常像。它包含cmsis_boot目录放启动文件和系统初始化、stm_lib标准外设库源码和头文件以及一层应用代码。下面这张表把这个例程里我关心的文件按职责分了一下方便后续对照代码。文件/目录职责和你调RTC的关系startup/*.s启动文件设置初始栈指针和中断向量表决定RTC中断是否能在正确入口被响应system_stm32f10x.c系统时钟初始化默认把SYSCLK配到72MHz影响APB1总线时钟进而影响RTC的预分频来源stm32f10x_rcc.c/h复位和时钟控制外设库开启PWR和BKP时钟操作备份域复位标志stm32f10x_rtc.c/hRTC外设库配置预分频、计数器、闹钟和中断tim2_delay.c用TIM2做毫秒级阻塞延时给RTC寄存器写入提供“写保护释放后的等待时间”printf.c重定向printf到串口输出RTC计数值和时间字符串main.c应用逻辑RTC初始化、读取、格式化和显示你可能会好奇为什么例程里要带tim2_delay.c而不是直接操作SysTick这是因为在老版本的标准外设库兼容工程里为了不干扰其他外设或者RTOS的tick很多模板工程师习惯用TIM2单独做阻塞延时。尤其写RTC寄存器有一个固定要求每次操作前要等待RTOFF置位而等待的过程往往需要几微秒到几百微秒此时有个可用的延时函数会舒服很多。stm32f10x_conf.h则集中管理外设头文件的包含关系是工程配置的一部分。2.2 RTC时钟源选型LSE、LSI与系统时钟分频的取舍STM32F10x的RTC模块比较特殊它内部并不是一个独立的日历芯片而是一组可配置的32位计数器加上二进制预分频器。计数器的时钟可以来自LSE外部低速晶振通常32.768kHz、LSI内部低速RC约40kHz或HSE/128外部高速晶振分频。这三个来源在例程里经常被写死在RCC_RTCCLKConfig调用之前初学的人往往只改了一句“我用了LSI”后面就发现掉电后时间明显漂移。选择LSE的好处是频率准32.768kHz正好是2的15次方用15位预分频器可以得到秒脉冲校准方便。LSI的优势是省一颗晶振、起振快但出厂精度只有约1%到3%的偏差且随温度变化明显。HSE/128的精度取决于外部主晶振但如果主晶振不是4MHz的整数倍分频后不会得到整数会产生累计误差。例程默认走LSE还是LSI要看代码里的RCC_RTCCLKConfig参数。实际做法是RTC只作为低功耗保持时优先用LSE如果板上空间受限只能用LSI那么必须在应用层做定期校准不能直接信任读出来的秒数。2.3 备份域复位对RTC配置的影响为什么读RTC之前要先查RCC_GetFlagStatusRTC和备份寄存器在STM32F10x里被划分到备份域靠Vbat引脚供电。这个域有一个独立的复位来源备份域复位Backup Domain Reset。当系统复位NRST引脚拉低、看门狗复位或上电复位时RTC和备份寄存器不会复位但如果是Vbat引脚掉电后再上电或者通过软件置位BDRST位整个备份域就会被重置。这个细节决定了例程里必须处理的一个分支if (RCC_GetFlagStatus(RCC_FLAG_PORRST) ! RESET) { // 真正的冷启动备份域可能还没初始化需要重新配置RTC RTC_Configuration(); } else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) ! RESET) { // 可能是外部复位RTC寄存器还保持不需要重新配置 } else { // 其他复位源比如看门狗复位RTC时间连续直接读取即可 }这段代码的核心是理解RCC_FLAG_PORRST、RCC_FLAG_PINRST不是代表当前状态而是记录“最近一次复位源”的粘性标志。用RCC_GetFlagStatus检查的是RCC_CSR寄存器中的复位标志。这里要强调每次检测完后必须调用RCC_ClearFlag()清除标志否则下次启动时读到的是上一次的残留值分支判断就会错。初学RTC时最容易犯的错误就在这里——不查备份域是否已经初始化每次上电都强制写RTC配置寄存器导致刚设置好的时间被覆盖成0。3. Example_RTC核心流程复现初始化、校准与读取时间3.1 RTC配置代码开启电源时钟和备份域访问配置预分频RTC配置的官方步骤在标准外设库的RTC_Configuration示例里已经很清晰但Example_RTC这个例程的做法更工程化它在初始化之前先判断备份域状态而不是无脑配置。下面这段代码是从标准外设库风格整理出来的最小可复现版本和例程的逻辑一致void RTC_Configuration(void) { // 1. 开启PWR和BKP时钟否则无法操作备份域寄存器 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 解除备份域写保护 // 2. 选择LSE作为RTC时钟源并启动LSE RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET) // 等待LSE起振 { // 如果这里卡死先检查外部低速晶振和负载电容 } RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); // 使能RTC时钟 // 3. 配置RTC预分频32.768kHz LSE / 32767 1 → 1Hz RTC_WaitForSynchro(); RTC_WaitForLastTask(); RTC_SetPrescaler(32767); // 预分频值必须为32767不能写成32768 RTC_WaitForLastTask(); }代码逻辑并不复杂但有两个点容易踩坑首先是等待LSE起振的while循环如果外部晶振没焊好或负载电容不对程序会一直死在这里对调试很不友好。更稳妥的做法是加一个超时计数超过比如1毫秒后报警而不是死等。其次RTC_SetPrescaler的参数是分频系数减一32.768kHz要写32767而不是32768很多新手在这里写混导致RTC走时快了1秒/s。RTC_WaitForSynchro和RTC_WaitForLastTask是标准外设库封装的两个等待函数前者等待RTC寄存器与APB1时钟同步后者等待上一次写操作完成。只要RTC时钟比APB1慢很多这两个等待几乎瞬间返回但不能省略否则在刚上电时读取时间会拿到旧值。3.2 用tim2_delay.c和printf.c把时间“看见”Example_RTC工程里有一组不太起眼但非常实用的辅助文件tim2_delay.c提供毫秒级延时printf.c重定向标准输出到USART1。它们的组合是这类例程的标配原因很简单RTC寄存器写入需要等待RTOFF而等待通常很短短到用SysTick延时可能被中断抢占用TIM2延时可以避免依赖系统tick。把printf重定向到串口则是为了在电脑上直接卡时间不需要接LCD屏。// printf.c 核心逻辑只需重写fputc即可 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }使用printf之前确保USART1已经初始化成GPIO复用推挽输出波特率、校验位和你的串口调试助手一致。我一般会先把系统时钟确认到72MHz然后用库函数USART_Init配置115200-8-N-1。tim2_delay.c的时间基准来自TIM2的定时器时钟如果用的是外部8MHz晶振那TIM2挂载在APB1上频率会被分频到36MHz延时函数的初始化参数就按36MHz填写不能想当然地用72MHz。3.3 读取与显示完整的main.c逻辑例程中main.c做的事可以用一句话概括初始化系统时钟、配置RTC、轮询读取计数器值把秒数转换成时分秒后通过串口打印。RTC读操作在标准外设库里的写法是uint32_t time_count 0; uint8_t hour, minute, second; // 读取前先同步一次避免读到APB1时钟域下的旧数据 RTC_WaitForSynchro(); time_count RTC_GetCounter(); // 返回从RTC启动开始累计的秒数 second time_count % 60; minute (time_count / 60) % 60; hour (time_count / 3600) % 24; printf(RTC Time: %02d:%02d:%02d\r\n, hour, minute, second);这里有一个隐藏的边界问题RTC_GetCounter返回的是无符号32位整数的秒数它从RTC启动时刻开始累计而不是绝对时间戳。所以例程里通常会在配置完成后用RTC_SetCounter设置一个初始值相当于校准基准。如果直接把RTC_GetCounter当Unix时间戳用会发现它远远小于真实的1970年秒数因为它只记录从配置到现在的运行时间。这个区别在写日志和文件时间戳时特别重要。设置初始时间的代码也很简单关键是计算好当前UTC秒数。比如要把RTC时间拨到2024-01-01 12:00:00的UTC时间就执行uint32_t initial_seconds 1704072000; // 2024-01-01 12:00:00 UTC RTC_SetCounter(initial_seconds); RTC_WaitForLastTask();这里还要处理时区问题如果板子处于东八区想显示北京时间12:00UTC秒数就要减去8小时即初始值写1704043200。很多工程直接在应用层加一个时区偏移量而不是修改存储值这样方便以后兼容GPS或NTP的UTC时间。4. 串口验证与掉电保持的坑参数表与实测对比4.1 串口输出时间波特率、GPIO复用和系统时钟的对应关系在Example_RTC例程里串口和RTC是一对调试搭档。很多人在不接外部晶振时只改RTC时钟源却忘了重新算波特率导致串口输出乱码。下表是我在F103C8T6上把RTC从LSE切到LSI后需要同步调整的典型参数配置项LSE32.768kHzLSI约40kHz说明RCC_RTCCLKSourceRCC_RTCCLKSource_LSERCC_RTCCLKSource_LSI切换RTC时钟源预分频值3276739999按40kHz除1Hz需要按实际LSI频率校准串口时钟APB272MHzAPB272MHz不变但建议实测输出掉电保持依靠Vbat外部晶振依靠Vbat内部RCLSI精度较差掉电后累计误差大起振等待时间一般100ms~1s几微秒LSI起振快适合调试这里的预分频值写39999是理论值因为LSI典型值的40kHz不是精确的且不同芯片之间差异很大。我通常的做法是先读RTC的校准寄存器或者用秒表对比1小时误差然后反向微调预分频数。注意这个调整必须在每次备份域复位后重新写入所以应用层要保存原始基准值。例程包里如果没有校准逻辑这一步基本就要自己补。4.2 电源断开再上电RTC时间还在吗——备份寄存器判断与校准这是很多人测试RTC例程时最想验证的一步给板子断电等几秒再上电看串口打印的时间是否连续。你很快会发现一个现象完全断电后再上电时间确实不丢但前提是Vbat引脚有电。对于只有一个USB供电的评估板Vbat通常直接连到3.3V所以断电时间一长RTC同样停走。没关系这是正常的因为Vbat没有独立供电。真正要检查的是备份域是否在复位过程中被重置。标准做法是使用备份寄存器BKP_DRn写一个“已配置”魔数比如0xA5A5每次上电后读取它if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { // 备份域数据无效需要重新初始化RTC并写入魔数 RTC_Configuration(); RTC_SetCounter(initial_seconds); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { // 备份域有效直接读取时间不需要再配置RTC RTC_WaitForSynchro(); time_count RTC_GetCounter(); }这段代码可以解决一类经典问题外部复位或看门狗复位后main函数重新执行但如果代码里无条件调用RTC_ConfigurationRTC的时间会被重置看起来就像掉电丢失。通过备份寄存器标志让RTC配置只执行一次之后每次启动都只是“读取”这是工程级例程和教学级例程的分水岭。另外RTC_Configuration里对RTC寄存器的写入都必须在PWR_BackupAccessCmd(ENABLE)之后并且每次写入之前要调RTC_WaitForLastTask否则可能出现写入失败。如果发现时间偶尔不对优先查这些等待是否在中断里被遗漏了。4.3 常见误差来源LSE起振慢、LSI温漂、分频数写错在实测过程中我遇到过几次定位很久的误差问题列在这里供排查。LSE起振慢是最常见的卡死点表现是程序停在等待LSERDY的while里。原因多半是晶振两端的负载电容不匹配或者晶振本身频率偏差大。可以试着在勘误手册推荐的电容值范围内调整或者在代码里加超时跳过。LSI温漂则表现为掉电前后走时速度不一样。因为LSI的频率会随温度变化在室温下校准好的分频值放到低温环境走时会慢放到高温环境会快。如果产品的使用温度范围宽建议还是用LSE或者定期用外部时间源校准RTC。分频数写错是纯编码问题。RTC_SetPrescaler的参数是N实际分频系数是N1。写入32768会导致实际频率变成32768/(327681)秒周期略长一天大概会慢几十秒。这种误差在连续运行几天后特别明显而且不容易被注意因为串口打印的时间看起来每秒钟都在走。5. 把Example_RTC改造成工程级时钟闹钟唤醒与备份寄存器的进阶用法5.1 用RTC闹钟中断做定时唤醒很多低功耗设备的“定时上报”功能本质上就是让MCU睡在Stop或Standby模式由RTC闹钟到点后唤醒。STM32F10x的RTC闹钟和RTC计数器共用一个32位寄存器可以通过RTC_SetAlarm设置一个绝对秒值。在Example_RTC基础上扩展只需增加以下代码void RTC_AlarmConfig(uint32_t alarm_seconds) { // 闹钟值在系统启动后按当前时间加一个固定偏移 RTC_SetAlarm(RTC_GetCounter() alarm_seconds); RTC_WaitForLastTask(); // 使能闹钟中断并挂到EXTI_Line17 EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line EXTI_Line17; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); RTC_ITConfig(RTC_IT_ALR, ENABLE); }闹钟中断服务函数里要清除RTC中断标志和EXTI线标志否则会反复进入中断。更关键的一点是如果MCU进入Standby模式RTC闹钟中断会转换为EXTI唤醒事件但前提是RTC时钟还在跑。如果之前把RTC时钟源配置成LSI在Standby下LSI是否保持需要查对应芯片的参考手册。F103的LSI在待机模式下是可以继续工作的但功耗和精度都不如LSE所以低功耗定时唤醒场景一般还是会外接32.768kHz晶振。5.2 用备份寄存器保存开机次数与数据备份寄存器BKP_DR1到BKP_DR10在F103中都是16位宽适合保存掉电不丢失的小型状态。比如记录异常复位次数、校准参数、设备运行状态。写入时要先操作PWR_BackupAccessCmd(ENABLE)这和配置RTC的步骤一致。在例程中加一个计数器的方法uint16_t boot_count BKP_ReadBackupRegister(BKP_DR2); boot_count; BKP_WriteBackupRegister(BKP_DR2, boot_count);这里省去了EEPROM的擦写流程也不占用Flash寿命但要注意备份寄存器容量很小只能存16位无符号数而且不是真正的非易失存储——Vbat掉电后同样清零。如果产品需要保存密钥或固件升级标志建议仍然用外部EEPROM或Flash而把备份寄存器当作“快速恢复标签”。配合5.1的闹钟还可以在备份寄存器里存一个“下一次唤醒的秒数”让设备在每次醒来后决定是否重新设置闹钟而不是依赖固定的定时周期。5.3 适配自己的板子更换晶振、调整预分频的实测步骤最后给一个可以直接抄的适配流程。拿到Example_RTC.7z先不要急着烧录按下面的顺序改参数确认板上LSE晶振是否焊接没有LSE就把RCC_LSEConfig相关代码改成LSI并修改预分频值为39999LSI典型值。这一步是大多数板子第一次跑起来的关键。打开system_stm32f10x.c确认系统主频宏定义是72MHz还是你实际用的频率串口波特率分频会依赖这个值。用串口助手连接USART1的TX/RX如果打印乱码优先检查波特率配置和系统时钟的倍数关系不用急着怀疑串口硬件。断电再上电测试观察打印的时间是否连续如果不连续检查备份寄存器标志是否写入成功以及RTC_Configuration是否被重复调用。对走时精度有要求的话用手机或NTP服务器对比1小时内的秒数差再按比例调整预分频值。LSI调整幅度可能到几百LSE通常只需微调个位数。这样几步走完基本上现有例程就是一个可以长期跑的设备时钟了。如果后续还要接RTOS或者低功耗框架RTC部分的读取函数建议做成独立的模块只在备份域复位时执行配置其他时候只做读计数器和换算ASCII这样能把临界区占用压到最短。本文还有配套的精品资源点击获取
返回列表