RTC实时时钟——简易封装RTC库实现掉电不丢失实时时钟
一,实验准备
1,实验目的
利用RTC以及BKP的一些特性,设计一个能够实现掉电不丢失同时可以显示实时日期时间的时钟
2,RTC的基本认识
STM32 中的 RTC(Real-Time Clock)时钟是一个独立的定时器,用于提供实时时钟功能,在 STM32 微控制器中起着关键作用。
能够提供精确的时间计数,包括年、月、日、时、分、秒等信息,因为RTC位于BKP(备份寄存器区域)中,所以即使在主电源关闭或系统复位时,只要有后备电源(如电池)供电,RTC 仍能继续运行,保持时间的准确性。
RTC 有自己独立的时钟源,通常可以选择低速外部晶体振荡器(LSE,如 32.768kHz)或内部低速振荡器(LSI)作为时钟输入。这意味着即使系统时钟出现故障或停止,RTC 仍能正常工作,保证时间的连续性和准确性。
3,BKP的基本认识
STM32 的 BKP(Backup Register)即备份寄存器,是 STM32 微控制器中用于在系统复位或掉电等情况下保存关键数据的特殊功能模块。
主要功能是在系统发生复位(包括上电复位、外部复位等)或电源故障导致掉电时,保存用户定义的一些关键数据。这些数据可以是系统的配置参数、校准值、重要的状态信息等。常与实时时钟(RTC)模块紧密配合。例如,RTC 的一些配置信息和时间数据可以存储在备份寄存器中,以确保在系统复位或掉电后,RTC 能够快速恢复到之前的状态,继续准确地计时。
包含多个 16 位的寄存器,如 BKP_DR1 - BKP_DR10 等。这些寄存器可由用户自由使用,用于存储不同类型的数据。
二,工程创立
1,新建工程
点击File,选择stm32 Project,芯片依旧选择熟悉的STM32F103C8T6,工程名为RTC
2,基础配置
首先依旧是开启我们的SWD调试端口以及RCC的HSE(外部高速时钟)的晶振,唯一的区别就是这回因为RTC需要使用到低速时钟,所以我们把LSE(外部低速时钟)的晶振也开启。
之后还需要来到时钟树将主频改为72MHz ,然后将时钟树左上方的小分支中的RTC时钟来源改为LSE,频率为32.768kKHz。
3,外设参数配置
需要开启的东西很少,一个是RTC实时时钟,另一个就是负责OLED屏幕显示的I2C。
首先,来到Times,点击RTC,勾选Activate Clock Source(激活时钟源)即可,其余几个功能都不需要开启,但可以了解一下,Activate Calendar(激活日历功能 ),RTC OUT(实时时钟输出),下方的参数皆不需要更改,也可以了解一下具体含义
- Data Format:数据格式 ,这里 “BCD data format” 表示二进制编码的十进制(Binary - Coded Decimal )数据格式,即用 4 位二进制数表示一位十进制数。
- Auto Predivider Calculat.:自动预分频器计算 ,意味着系统会自动计算 RTC(实时时钟)预分频器的值,以设置合适的时钟频率。
- Asynchronous Predivider value Automatic Predivider Calculation:异步预分频器 ,自动预分频器计算 ,强调异步模式下预分频器的自动计算功能。
- Output:输出 ,这里 “Alarm pulse signal on the TAMP...” 表示在入侵检测(TAMP )引脚输出闹钟脉冲信号 ,即当设置的闹钟时间到达时,通过该引脚输出脉冲信号。
之后开启I2C1, 改为I2C模式,下方的I2C模式选择快速模式,其余参数保持默认即可
4, 生成初始化代码
来到Project Manager,选择为每个外设生成单独的.c文件和与之对应的.h文件,模块化编程有助于工程的管理和阅读,也便于移植。
之后使用Ctrl+S快捷键保存设置并等待CubeMX生成初始化代码
三,代码实现
1,代码思路
我们的目标是利用RTC来设计一个掉电不丢失且能显示年,月,日,时,分,秒的实时时钟,那么就需要最基本的一些函数,例如设置时间,读取时间,时钟初始化,当然还有一些是对RTC计数器的操作函数,所以不妨我们参考HAL库的RTC库来写一个属于自己的RTC库,将以上这些需要调用的函数都放在我们自己的库里。然后便是将时间输出出来了,这就很简单了,依旧是利用sprintf函数进行字符串格式化通过OLED屏显示即可。
2,封装RTC函数库
1,创建文件
可以先创建我们自己的RTC库,名字就可以叫My_RTC,当然.c文件和.h文件都需要,方法就是右击Src文件夹,新建文件类型选择Source File(源文件)也就是.c文件,之后右击Inc文件,新建文件类型选择Header File(头文件)。
2,函数封装
封装自己的函数之前,我们可以参考HAL库的RTC库,来到工程下方的Drivers中的STM32F1XX_HAL_Driver下方的Src中的stm32f1xx_hal_rtc.c,在这里面就有我们需要参考到的函数,我们可以在右边的大纲里快速浏览,最主要的就是以下四个:
HAL_RTC_SetTime():设置RTC当前时间
HAL_RTC_GetTime():获取RTC当前时间
RTC_WriteTimeCounter():写入时间计数器
RTC_ReadTimeCounter():读取时间计数器
这便于我们代码思路中需要使用到的函数对应上了,但当我们仔细阅读 RTC_WriteTimeCounter()函数内部时,发现它的功能实现还离不开两个函数:
RTC_EnterInitMode():进入RTC初始化模式
RTC_ExtiInitMode():退出RTC初始化模式
至此,我们所需的函数都集合完毕了,接下来便是“拿来主义”,将除了设置RTC时间和获取RTC时间之外的另外四个函数全部复制到我们自己的My_RTC.C文件中,并在.h文件里对RTC函数库进行声明。
#include "stm32f1xx_hal.h"
#include "rtc.h"
static uint32_t RTC_ReadTimeCounter(RTC_HandleTypeDef *hrtc);
static HAL_StatusTypeDef RTC_WriteTimeCounter(RTC_HandleTypeDef *hrtc, uint32_t TimeCounter);
static HAL_StatusTypeDef RTC_EnterInitMode(RTC_HandleTypeDef *hrtc);
static HAL_StatusTypeDef RTC_ExitInitMode(RTC_HandleTypeDef *hrtc);
static uint32_t RTC_ReadTimeCounter(RTC_HandleTypeDef *hrtc) //读取RTC时钟计数器
{
uint16_t high1 = 0U, high2 = 0U, low = 0U;
uint32_t timecounter = 0U;
high1 = READ_REG(hrtc->Instance->CNTH & RTC_CNTH_RTC_CNT);
low = READ_REG(hrtc->Instance->CNTL & RTC_CNTL_RTC_CNT);
high2 = READ_REG(hrtc->Instance->CNTH & RTC_CNTH_RTC_CNT);
if (high1 != high2)
{
timecounter = (((uint32_t) high2 << 16U) | READ_REG(hrtc->Instance->CNTL & RTC_CNTL_RTC_CNT));
}
else
{
timecounter = (((uint32_t) high1 << 16U) | low);
}
return timecounter;
}
static HAL_StatusTypeDef RTC_WriteTimeCounter(RTC_HandleTypeDef *hrtc, uint32_t TimeCounter) //将时间值写入RTC时间计数器中
{
HAL_StatusTypeDef status = HAL_OK;
if (RTC_EnterInitMode(hrtc) != HAL_OK)
{
status = HAL_ERROR;
}
else
{
WRITE_REG(hrtc->Instance->CNTH, (TimeCounter >> 16U));
WRITE_REG(hrtc->Instance->CNTL, (TimeCounter & RTC_CNTL_RTC_CNT));
if (RTC_ExitInitMode(hrtc) != HAL_OK)
{
status = HAL_ERROR;
}
}
return status;
}
static HAL_StatusTypeDef RTC_EnterInitMode(RTC_HandleTypeDef *hrtc) //进入实时时钟初始化模式
{
uint32_t tickstart = 0U;
tickstart = HAL_GetTick();
while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET)
{
if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE)
{
return HAL_TIMEOUT;
}
}
__HAL_RTC_WRITEPROTECTION_DISABLE(hrtc);
return HAL_OK;
}
static HAL_StatusTypeDef RTC_ExitInitMode(RTC_HandleTypeDef *hrtc) //退出实时时钟初始化模式
{
uint32_t tickstart = 0U;
__HAL_RTC_WRITEPROTECTION_ENABLE(hrtc);
tickstart = HAL_GetTick();
while ((hrtc->Instance->CRL & RTC_CRL_RTOFF) == (uint32_t)RESET)
{
if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE)
{
return HAL_TIMEOUT;
}
}
return HAL_OK;
}
到这里已经完成一大步了,之后就是对我们自己的设置RTC时间和获取RTC时间进行封装了
在这里我们需要引出C语言中的一个新的函数库:time库,因为是实时时钟嘛,所以需要使用到time库中的一些结构体变量及函数也是很正常的一件事,需要使用到的函数有:
gmtime
函数- 原型:
struct tm *gmtime(const time_t *timer)
- 功能:与
localtime
类似,但将时间戳转换为 UTC(协调世界时)时间,填充到struct tm
结构体 。 - 返回值:指向填充好 UTC 时间信息的
struct tm
结构体的指针,失败返回NULL
。
- 原型:
mktime
函数- 原型:
time_t mktime(struct tm *timeptr)
- 功能:根据本地时区,将
struct tm
结构体表示的时间转换为time_t
类型的日历时间(时间戳) 。 - 返回值:对应的时间戳,出错返回 - 1 。
- 原型:
以及一个最关键最重要的结构体:
struct tm
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
大家可以自己去time库里面自行了解一下其余函数
我们可以按照HAL库的格式来,新建一个My_RTC_SetTime(),结合time库的函数实现功能
HAL_StatusTypeDef My_RTC_SetTime(struct tm *time){ //设置时间函数
uint32_t unixTime = mktime(time); //将结构体类型的time通过函数mktime转换成unix时间戳格式
return RTC_WriteTimeCounter(&hrtc, unixTime); //将unix时间戳通过写入RTC时间计数器中,并返回
}
当然还有My_RTC_GetTime()了,这里需要注意函数的类型,以及返回值。
struct tm *My_RTC_GetTime(){ //获取时间函数
time_t unixTime=RTC_ReadTimeCounter(&hrtc); //读取写入在时间计数器中的unix时间戳
return gmtime(&unixTime); //使用gmtime函数将unix时间戳格式转换成tm结构体类型输出
}
最后就是我们的RTC初始化函数,这其中就要涉及到BKP备份寄存器的知识了,我们通过对BKP寄存器通道1赋值来判断是否将数据成功存储进备份寄存器中,防止每次初始化都将时间重置。
void My_RTC_Init(){ //RTC初始化函数
uint16_t value =HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); //读取BKP备份寄存器的值
if(value==BKPNum) return; //判断BKP备份寄存器是否正确写入,如果已经写入则直接返回,防止重复写入导致时间重置
struct tm time = //起始时间,为tm结构体类型变量
{
.tm_year=2025-1900, //时间为真实时间减去起始时间1900年
.tm_mon=4-1,
.tm_mday=12,
.tm_hour=21,
.tm_min=30,
.tm_sec=0,
};
My_RTC_SetTime(&time); //利用我们自己创建的设置时间函数将时间设置好
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, BKPNum); //写入一次BKP寄存器通道1的值,为后续判断跳过准备
}
而对于这种随机的赋值常亮的定义,为了增加代码的可读性,最好使用宏定义
#define BKPNum 0xAA //写入备份寄存器通道1的值
至此,属于我们自己RTC库便封装好了,当然别忘记将这些函数都在.h文件中声明一下了。
HAL_StatusTypeDef My_RTC_SetTime(struct tm *time);
struct tm* My_RTC_GetTime();
void My_RTC_Init();
3,函数调用
来到主函数中,将我们需要使用到的函数库都调用一下
#include "My_RTC.h"
#include "oled.h"
#include "stdio.h"
#include <string.h>
依旧是初始化OLED 屏幕并且创建几个变量
HAL_Delay(20);
OLED_Init();
char message1[50]="";
char message2[50]="";
struct tm *now;
在while循环中,写下获取当前时间的函数,并使用OLED屏幕显示,代码非常简单
OLED_NewFrame();
now = My_RTC_GetTime();
sprintf(message1,"%d-%d-%d",now->tm_year+1900,now->tm_mon+1,now->tm_mday);
sprintf(message2,"%02d:%02d:%02d",now->tm_hour,now->tm_min,now->tm_sec);
OLED_PrintString(25, 15, message1, &font16x16, OLED_COLOR_NORMAL);
OLED_PrintString(25, 30, message2, &font16x16, OLED_COLOR_NORMAL);
OLED_ShowFrame();
HAL_Delay(1000);
那么,就有兄弟问了,“博主博主,那我们刚才封装的初始化函数哪去了?”
好的兄弟,我现在就来解答
当我们点击HAL库的初始化函数MX_RTC_Init()时,我们会发现函数内部在每次初始化时都会调用一次HAL_RTC_Init函数,这个函数会导致每次按下复位键时,RTC时钟都会卡顿导致实时时钟变得不再那么实时了。
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
那么解决办法是什么呢?
还记得我们封装自己的初始化函数My_RTC_Init()时,通过对BKP备份寄存器的通道1的比较判断来跳过每次都初始化时间,那么只要我们将这个HAL_RTC_Init()函数也和初始化时间的函数放在一起,那么不就可以实现只有第一次全部执行,之后当数据写入备份寄存器之后便跳过后续初始化步骤,程序直接回到主函数继续执行。
因此我们可以把HAL_RTC_Init()直接放在我们的初始化函数中的判断BKP寄存器语句下方
void My_RTC_Init(){ //RTC初始化函数
uint16_t value =HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); //读取BKP备份寄存器的值
if(value==BKPNum) return; //判断BKP备份寄存器是否正确写入,如果已经写入则直接返回,防止重复写入导致时间重置
if (HAL_RTC_Init(&hrtc) != HAL_OK) //判断RTC初始化是否正确完成
{
Error_Handler();
}
之后就是在MX_RTC_Init里面调用My_RTC_Init()函数,并在完成后直接返回,跳过后续初始化,代码放在注释对中,可以防止被覆盖修改,当然别忘了在文件头部声明一下调用的函数库。
void MX_RTC_Init(void)
{
/* USER CODE BEGIN RTC_Init 0 */
hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
My_RTC_Init(); //通过代码注释对直接绕过后面的系统自动生成的初始化代码,来执行我们自己创建的,可以有效防止之后修改时被覆盖
return;
/* USER CODE END RTC_Init 0 */
hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
}
3,总流程图
至此,代码部分便到此结束,需要注意的地方还真不少呢,但其实大概可以分为这么几步:
- 将HAL库中我们所需要的RTC_WriteTimeCounter(),RTC_ReadTimeCounter(),
RTC_ExtiInitMode(),RTC_EnterInitMode()这四个函数全部复制到我们自己的库
-
要实现设置时间和获取时间的两个函数,可以仿照HAL的格式,结合自己的需求,来新建My_RTC_SetTime()和My_RTC_GetTime()两个函数,其中有对C语言的time库的使用和学习,但操作很简单,略微了解time库的一些常用函数和结构体用法即可
-
为了防止每次初始化时都重复设置时间导致时间重置并且HAL库自己的RTC初始化函数每次在复位时都会出现RTC停止运行的卡顿现象的问题,就可以结合BKP备份寄存器掉电不丢失的性质,除了第一次初始化时设置时间,之后全部绕过My_RTC_SetTime()设置时间函数,然后将我们的初始化函数放在HAL库的MX_RTC_Init()函数内部来代替其实现功能,这样就出现了能完美解决以上两个问题的My_RTC_Init()函数
-
最后就是对main函数的操作,这里就很清晰了,熟悉的函数声明,OLED初始化,变量名定义几个固定步骤,然后调用我们的获取时间函数My_RTC_GetTime()将时间存放在变量中,通过sprintf函数格式化字符串输出出来即可。
-
最后的最后,依旧是编译下载一气呵成!
四,硬件部分
本次的硬件非非非非非常简单,唯一需要注意的就是需要给备用电源VBAT引脚供电,来确保BKP备份寄存器的数据不丢失
五,实验现象
我们首先可以将时间设置为现在的实时时间(博主手速比较慢,先拍的图结果放图放慢了,其实是实时的,博主比较懒,不想再拍了,小伙伴们凑活看吧)
之后拔掉ST-Link的3.3V供电(备用电源VBAT引脚的不能拔!!!拔了神仙来了时间都不准了) ,可以间隔一段时间后插上,观察时间还是否准确
可以看到,当我们拔掉外部供电,仅靠VBAT的供电,依旧可以保证RTC时钟的正常走时(这回博主聪明了先截的电脑上的时间桀桀桀)
至此,RTC实时时钟实验现象完全符合预期,实验圆满完成!!!
六,实验小结
本次实验内容丰富,效果也很不错,博主非常满意哇哈哈哈哈哈哈哈哈哈。言归正传,本实验学习了RTC实时时钟的基本知识,还阅读了HAL库的RTC库,C语言的time库,其中的代码还是相当丰富的,这里便不过多赘述了,小伙伴们想看的可以自行整理,当然还有对BKP备份寄存器的认识,也知道了VBAT备用电源的妙用了,三者结合,便可做出一个属于自己的小时钟了!
学习完了这节,大家不妨想想,既然都可以正确显示时间了,那能不能做一个外接按键旋钮来实现调节校准时间,功能更加完善,实用性更强的时钟呢,这当然是可以的了,小伙伴们可以去博主恩师(b站up主keysking)的HAL库教学的27期视频(万年历系统)进行深入探索,也算是是同门师兄弟了,博主的笔记也是跟随UP主的视频一点一点总结学习出来的。
依旧是引用王阳明先生的话,知而不行,是为不知,行而不知,可以致知。本期实验接线简单,内容却很丰富,实验现象也很直观有趣,非非非常适合动手实践实践,可惜博主不卖课也没有器材开房板可以卖,所以是真心希望大家在代码学习的过程中可以结合着小实验去学习,在动手的过程中发现各种各样的问题,查漏补缺才能使自己更加无懈可击!