

⚠️ 适用平台:本文代码适用于 STM32F103C8T6 及同系列 F1 芯片,基于 Keil5 + STM32F10x 标准库 V3.5.0。
一、RTC 到底是个啥?
RTC,全称 Real-Time Clock,实时时钟。说白了,它就是单片机里面一个专门用来计时的独立模块。
我的理解是:你可以把它想象成一块电子手表里的机芯——哪怕你手机关机了,手表里的电池还在走,时间照样在跑。STM32 的 RTC 也是一样的道理,它挂在备份域(Backup Domain)下面,由独立的低速晶振(LSE,32.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.3 RTC_WaitForSynchro() 和 RTC_WaitForLastTask()
这两个函数是 RTC 编程的铁律,每次操作 RTC 寄存器前后都要等。
| 函数 | 等的标志位 | 作用 |
|---|---|---|
RTC_WaitForSynchro() | RSF(寄存器同步标志) | APB 总线时钟和 RTC 时钟不同步,等它们同步了再读 |
RTC_WaitForLastTask() | RTOFF(操作关闭标志) | 上一次写 RTC 寄存器的操作真正完成了,才能进行下一次写 |
我的理解:RTC 的时钟是 32.768kHz,APB 总线是 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); // 开启外部低速晶振LSE(32.768kHz),作为RTC的时钟源
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); // 等待LSE起振稳定,LSERDY标志置位后才表示晶振正常工作
// 这是个死等循环,实际项目中可加入超时处理
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 配置RTCCLK时钟源为LSE,即32.768kHz
RCC_RTCCLKCmd(ENABLE); // 使能RTCCLK,RTC模块开始接收时钟信号
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*60=28800)
// 最终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 省事多了。
六、踩坑记录
1
tm_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(具体代码见我的详细工程代码)
七、总结表格
| 知识点 | 核心内容 | 注意事项 |
|---|---|---|
| 时钟源 | LSE(32.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实时时钟详解
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2603_96660944/article/details/163604442




