SilicoCode头像
关注
STM32入门教程:RTC实时时钟完整代码解析封面图

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

STM32学习专栏

⚠️ 适用平台:本文代码适用于 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 属于备份域,而备份域默认是上锁的。你要操作它,必须先做三件事:

  1. 开 PWR 时钟(电源控制,管备份域访问权限)

  2. 开 BKP 时钟(备份寄存器本身)

  3. 调用 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 省事多了。


六、踩坑记录

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 起赋值时减,读取时加,别搞混

八、验证方式(建议动手实验)

  1. 单步调试:在 MyRTC_Init() 里打断点,观察第一次和第二次复位时是否进入不同的分支。

  2. 串口打印:在 main() 里每隔 1 秒调用 MyRTC_ReadTime(),通过串口打印 MyRTC_Time 数组,观察跨天、跨月、闰年是否正确。

  3. 掉电测试:接好 VBAT 纽扣电池,主电源断开后等几分钟再上电,看时间是否连续。

  4. 时区验证:把 ±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

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--