STM32入门教程:RTC实时时钟完整代码解析

📅 2026/8/13 11:21:18
STM32入门教程:RTC实时时钟完整代码解析
STM32学习专栏⚠️ 适用平台本文代码适用于 STM32F103C8T6 及同系列 F1 芯片基于Keil5 STM32F10x 标准库 V3.5.0。一、RTC 到底是个啥RTC全称Real-Time Clock实时时钟。说白了它就是单片机里面一个专门用来计时的独立模块。我的理解是你可以把它想象成一块电子手表里的机芯——哪怕你手机关机了手表里的电池还在走时间照样在跑。STM32 的 RTC 也是一样的道理它挂在备份域Backup Domain下面由独立的低速晶振LSE32.768kHz供电。只要你的板子上接了纽扣电池VBAT哪怕主电源断了RTC 依然能继续计时。为什么需要 RTC像定时器TIM做延时、做 PWM 很溜但它不适合做日历。RTC 的核心优势在于低功耗 掉电保持 年月日时分秒自动进位。你要做个万年历、闹钟、数据时间戳没它真不行。二、核心知识点讲解2.1 备份域访问——PWR 和 BKP 时钟RTC 属于备份域而备份域默认是上锁的。你要操作它必须先做三件事开 PWR 时钟电源控制管备份域访问权限开 BKP 时钟备份寄存器本身调用PWR_BackupAccessCmd(ENABLE)解锁我的理解这就像是进一个保险库PWR 是保安队长BKP 是库房门你两个都要打招呼最后还要输密码BackupAccessCmd才能进去。2.2 备份寄存器标志位——判断是不是第一次配置这是江科大代码里非常经典的一个设计if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5)第一次上电时备份寄存器里全是默认值0x0000不等于0xA5A5于是进入if分支完成 RTC 的完整初始化最后往 BKP_DR1 写入 0xA5A5 做标记。下次复位不断 VBAT 电源再次执行MyRTC_Init()时读到 BKP_DR1 已经是0xA5A5了说明 RTC 之前配置好了直接跳过初始化只等同步就行。我的理解这个0xA5A5是你自己定的暗号写啥都行只要写入和判断一致。它的作用就是——防止每次复位都重新配置 RTC导致时间被重置。备份域不掉电这个暗号就一直在。2.3RTC_WaitForSynchro()和RTC_WaitForLastTask()这两个函数是 RTC 编程的铁律每次操作 RTC 寄存器前后都要等。函数等的标志位作用RTC_WaitForSynchro()RSF寄存器同步标志APB 总线时钟和 RTC 时钟不同步等它们同步了再读RTC_WaitForLastTask()RTOFF操作关闭标志上一次写 RTC 寄存器的操作真正完成了才能进行下一次写我的理解RTC 的时钟是 32.768kHzAPB 总线是 36MHz两个时钟域不一样。你写了个数据过去硬件那边还没消化完你就急着写下一个数据就乱了。所以必须等它咽下去。三、完整代码逐行解析下面是RTC.c的完整代码每一行都带注释建议配合上面的知识点一起看。3.1 头文件与全局变量#include stm32f10x.h // 包含STM32F10x标准库头文件提供所有寄存器和库函数声明 #include time.h // 包含C标准库的时间处理头文件提供mktime、localtime等函数 uint16_t MyRTC_Time[] {2026, 8, 9, 9, 56, 55}; // 定义全局时间数组依次存储年、月、日、时、分、秒 // 初始值设为2023年1月1日23:59:55方便观察跨天进位效果 void MyRTC_SetTime(void); // 函数前置声明因为MyRTC_Init里要调用它编译器需要先知道有这个函数3.2 RTC 初始化函数/** * 函 数RTC初始化 * 参 数无 * 返 回 值无 */ void MyRTC_Init(void) { /*开启时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 使能PWR时钟PWR负责电源管理和备份域访问控制 RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); // 使能BKP时钟BKP是备份寄存器的外设时钟 /*备份寄存器访问使能*/ PWR_BackupAccessCmd(ENABLE); // 调用PWR函数解除对备份域寄存器的写保护否则无法操作BKP和RTC if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) // 读取备份寄存器BKP_DR1判断是否等于自定义标志0xA5A5 // 若不相等说明是第一次上电/备份域掉电需要重新配置RTC { RCC_LSEConfig(RCC_LSE_ON); // 开启外部低速晶振LSE32.768kHz作为RTC的时钟源 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) ! SET); // 等待LSE起振稳定LSERDY标志置位后才表示晶振正常工作 // 这是个死等循环实际项目中可加入超时处理 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 配置RTCCLK时钟源为LSE即32.768kHz RCC_RTCCLKCmd(ENABLE); // 使能RTCCLKRTC模块开始接收时钟信号 RTC_WaitForSynchro(); // 等待RTC寄存器与APB总线同步同步后才能安全读取RTC寄存器 RTC_WaitForLastTask(); // 等待上一次对RTC寄存器的操作完成确保RTOFF标志置位 RTC_SetPrescaler(32768 - 1); // 设置RTC预分频器值为32767 // 32768 / (32767 1) 1Hz即每秒计数器加1 RTC_WaitForLastTask(); // 等待预分频器设置操作完成必须等 MyRTC_SetTime(); // 调用设置时间函数将全局数组MyRTC_Time的时间写入RTC计数器 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 向BKP_DR1写入标志0xA5A5标记RTC已完成首次配置 // 下次复位时读到这个值就不会再执行初始化流程 } else // BKP_DR1等于0xA5A5说明RTC之前已配置过 { RTC_WaitForSynchro(); // 只需等待同步不需要重新配置时钟源和预分频器 RTC_WaitForLastTask(); // 等待上一次操作完成 } }3.3 设置时间函数数组 → RTC/** * 函 数RTC设置时间 * 参 数无 * 返 回 值无 * 说 明调用此函数后全局数组里时间值将刷新到RTC硬件电路 */ void MyRTC_SetTime(void) { time_t time_cnt; // 定义time_t类型变量用于存储转换后的秒计数器Unix时间戳 struct tm time_date; // 定义struct tm结构体变量用于存储年月日时分秒的拆分格式 time_date.tm_year MyRTC_Time[0] - 1900; // 年份赋值tm_year从1900年开始计数所以2023要减1900得123 time_date.tm_mon MyRTC_Time[1] - 1; // 月份赋值tm_mon范围是0~11所以1月要减1得0 time_date.tm_mday MyRTC_Time[2]; // 日期赋值tm_mday范围是1~31直接赋值 time_date.tm_hour MyRTC_Time[3]; // 小时赋值0~23 time_date.tm_min MyRTC_Time[4]; // 分钟赋值0~59 time_date.tm_sec MyRTC_Time[5]; // 秒钟赋值0~59 // 注意struct tm还有tm_wday、tm_yday等字段mktime会自动计算无需手动赋值 time_cnt mktime(time_date) - 8 * 60 * 60; // 调用mktime函数将struct tm转换为time_t类型的秒计数器 // mktime默认按GMT时区计算所以北京时间要减去8小时的秒数8*60*6028800 // 最终time_cnt是GMT时间戳准备写入RTC计数器 RTC_SetCounter(time_cnt); // 将计算好的秒计数器写入RTC的32位计数器RTC_CNT RTC_WaitForLastTask(); // 等待写入操作完成这条必须有 }3.4 读取时间函数RTC → 数组/** * 函 数RTC读取时间 * 参 数无 * 返 回 值无 * 说 明调用此函数后RTC硬件电路里时间值将刷新到全局数组 */ void MyRTC_ReadTime(void) { time_t time_cnt; // 定义time_t类型变量用于接收RTC计数器的秒数值 struct tm time_date; // 定义struct tm结构体变量用于接收转换后的年月日时分秒 time_cnt RTC_GetCounter() 8 * 60 * 60; // 读取RTC_CNT寄存器的当前值GMT时间戳 // 加上8小时的秒数28800将GMT转换回北京时间 time_date *localtime(time_cnt); // 调用localtime函数将秒计数器转换为struct tm结构体 // localtime返回的是指针这里解引用赋值给time_date结构体变量 // 它会自动处理闰年、月份天数等所有进位问题 MyRTC_Time[0] time_date.tm_year 1900; // 年份还原tm_year加1900得到实际年份 MyRTC_Time[1] time_date.tm_mon 1; // 月份还原tm_mon加1得到实际月份1~12 MyRTC_Time[2] time_date.tm_mday; // 日期直接赋值 MyRTC_Time[3] time_date.tm_hour; // 小时直接赋值 MyRTC_Time[4] time_date.tm_min; // 分钟直接赋值 MyRTC_Time[5] time_date.tm_sec; // 秒钟直接赋值 }3.5现象更多工程代码STM32_Project: 基于江科大的STM32的教程编写的程序五、个人理解与心得用户自己写① 备份寄存器这个暗号设计真的是神来之笔一开始我看代码的时候还在想为啥不直接每次都重新初始化 RTC后来自己做实验才发现——如果不写这个0xA5A5标志位每次按复位键RTC 的时间都会被重置回2023-01-01 23:59:55。因为MyRTC_SetTime()在if分支里被调用了复位就等于重新上电。这个标志位的本质是利用备份域不掉电的特性在单片机内部做状态记忆。后来我举一反三用 BKP_DR2 记录系统复位次数用 BKP_DR3 记录最后一次异常代码简直打开了新世界大门。备份寄存器这 42 个 16 位寄存器存点配置参数、校准数据完全够用比外挂 EEPROM 省事多了。六、踩坑记录1tm_mon从 0 开始计数设置 1 月结果变成了 2 月这个坑我踩得实实在在。第一次测试的时候我把数组设成{2026, 1, 1, 0, 0, 0}结果读出来显示2026-02-01。排查了半小时才发现struct tm里的tm_mon范围是0~11代表 1~12 月。代码里time_date.tm_mon MyRTC_Time[1] - 1;这行如果数组传的是 1减 1 变成 0对应的就是 1 月——代码是对的是我理解错了。反过来读取的时候1还原也是同样的道理。建议大家在调试的时候先把time_date结构体的每个字段都打印出来确认偏移量没搞反再往下走。2 有些板子CNT的值它不变化这个时候就要把代码中的LSE改为LST具体代码见我的详细工程代码七、总结表格知识点核心内容注意事项时钟源LSE32.768kHz必须等LSERDY标志否则 RTC 不走预分频器32768 - 1得到 1Hz分频值 写入值 1备份域访问PWR BKP 时钟 PWR_BackupAccessCmd三步缺一不可首次配置判断BKP_DR1 写入0xA5A5做标记标记可自定义只要读写一致时间转换mktime()/localtime()C 标准库函数注意 GMT 与北京时间差 8 小时时区调整设置时-8h读取时8h搞反了时间就差 16 小时操作等待RTC_WaitForSynchro()/RTC_WaitForLastTask()每次读写 RTC 寄存器前后都要等年月日格式tm_year从 1900 起tm_mon从 0 起赋值时减读取时加别搞混八、验证方式建议动手实验单步调试在MyRTC_Init()里打断点观察第一次和第二次复位时是否进入不同的分支。串口打印在main()里每隔 1 秒调用MyRTC_ReadTime()通过串口打印MyRTC_Time数组观察跨天、跨月、闰年是否正确。掉电测试接好 VBAT 纽扣电池主电源断开后等几分钟再上电看时间是否连续。时区验证把±8*60*60去掉对比串口输出和标准时间验证时区转换的正确性。写这篇文章的时候我把江科大的视频又回顾了一遍发现 RTC 这个章节虽然代码量不大但背后的知识点很密集——备份域、时钟树、C 标准库时间函数、时区转换每一个环节都能单独展开讲。建议大家不要只是复制粘贴代码一定要自己画一遍框图理解数据是怎么从晶振 → 预分频器 → 计数器 →localtime→ 数组这条链路走的。只有搞清楚了数据流后面遇到时间不准、掉电丢失、时区错乱这些问题才能快速定位。开发环境Keil5 STM32F103C8T6最小系统板 ST-Link参考资料江科大STM32入门教程、《STM32F10xxx参考手册》前置文章STM32入门教程BKP备份寄存器 RTC实时时钟详解如果本文对你有帮助欢迎点赞 收藏 ⭐ 有任何问题欢迎在评论区交流一起学习进步