STM32 RTC(hal库)
RTC实时时钟:
RTC的结构非常像一个简化版的定时器,核心当然是一个计数器,与定时器的16位计数器只能从0计数到65535不同,RTC的计数器是32位的,可以从0技术到4294967295,RTC的计数器前还有个RTC预分频器,可以将时钟源的时钟信号分频为1Hz的信号,我们往往会采用unix时间戳的形式在RTC计数器中记录时间,unix时间戳是一种在计算机领域通用的时间表示方式,其记录的是从1970年1月1日0时0分0秒到当前时刻的秒数,一些使用有符号32位整数记录unix时间戳的系统,其仅能计数到2038年,随后就会溢出,不过我们的RTC计数器无需符号位,可以从1970年记录到2106年。
我们通过将3.3V接入VDD引脚为其供电,以执行我们的代码程序,而VDD掉电后,程序便无法继续执行。那RTC时钟如何保持掉电后继续走时呢?STM32还为我们提供了VBAT引脚,即使VDD已经掉电,只要我们继续为VBAT引脚供电,STM32就可以维持芯片上一块叫做后备区域的地方继续运行,后备区域内的功能比较简单,耗电很少,因而我们就可以在PCB电路板上板载一颗纽扣电池为其供电,而RTC就在这后备区域中。
接入到RTC的时钟源有三种:LSE(低速外部时钟)、LSI(低速内部时钟)、HSE(高速外部时钟)的128分频。但只有LSE可以在VDD掉电后继续提供时钟信号,因而通常选择低速外部时钟LSE作为RTC时钟信号。其频率为32.768KHz,后经分频器分频为1Hz。另外STM32的RTC上还有简单的闹钟功能,往闹钟寄存器上设置一个时间戳,计数器与闹钟寄存器数值相等时,触发对应的"闹钟中断"。
除了RTC时钟,后备区域内另一个主要功能叫做备份寄存器,在我们使用的STM32F103C8T6芯片中,有10个16位的备份寄存器可以用于存储数据,这些寄存器在VDD掉电后依旧靠VBAT维持,因而可以掉电不丢失,并且配有入侵检测功能,产品可以设计使得外壳被开启后给予入侵检测引脚信号,即使是设备外部供电已经断掉,备份寄存器中的数据也能被清除,适合防止重要数据。此外由于均是VBAT供电,VBAT掉电后RTC与备份寄存器中的数据均会丢失,因而备份寄存器也可以用来检测RTC时钟是否被设置过,或者时间是否因为VBAT掉电而丢失。
为了兼容F407、H743等具有独立日期寄存器的芯片,HAL库的RTC时钟部分将日期与时间分开处理,导致未将日期数据记录在RTC计数器中,从而使得日期数据会在VDD掉电后丢失,另外默认生成的代码每次都会初始化RTC,初始化过程中会使得RTC停止运行一小会,导致每次程序重启时RTC时间会变慢一点,因而本小节我们会写一个自己的RTC时钟库,方便日后使用。
下面我们开始CubeMX配置:
先到RCC里设置高速外部时钟源为晶振,为了设置RTC时钟源为LSE(低速外部时钟),所以我们LED也选择晶振为时钟源
然后点开Timers,只需要勾选Activate Clock Source就可以将其开启:
如果勾选Activate Calendar,那么在下方的详细设置还可以直接设置一个时间(年月日时分秒),不过我们要自己写库,因此不需要,RTC OUT可以将RTC的信号输出到GPIO口上,Tamper是刚刚提到的入侵检测功能的开关,看到下面的详细设置,第一个是数据格式,我们自己写库,因此无需理会,Auto Predivider Calculation自动计算预分频器要开启,如果关闭了在下方的Asynchronous Predivider value需要我们自己计算填写预分频器的值;Output是与上面的RTC OUT功能配合的,用于选择将哪种信号输出到GPIO口,无需理会。
MyRTC库:
接下来我们开始创建自己的RTC库,方便我们以后使用,不过我们是根据hal库的基础上进行改动。
首先我们先来到stm32f1xx_hal_rtc.c文件,在704行找到HAL_RTC_SetTime函数,在这个函数的下面也就是914行可以看到还有一个设置日期的函数,所以这个函数不涉及年月日数据,其代码也可以印证这一点

这些代码就是将时分秒数据单位均换算为秒并相加,随后在内部调用了RTC_WriteTimeCounter函数,这就是用来设置RTC的计数器的函数,我们点进去看看
进入函数首先可以看到调用的是RTC_EnterInitMove(进入初始模式函数),下面两个是写计数器函数,虽然我们说RTC的计数器是32位,但其实这是由两个16位寄存器组成的,所以这里分两次写入计数器的高低两个寄存器
随后又是调用了RTC_ExitInitMode(退出初始模式函数),为什么要进入和退出初始模式呢?下面的参考手册给出答案:
图中说明必须要设置RTC_CRL寄存器中的CNF,才可以对计数器进行写操作,而且在进行写操作之前还必须查询RTC——CR寄存器中的RTOFF状态位是否置1,只有置1才能进行写操作。
所以进入初始化模式函数对应1,2,写计数器对应3,退出初始化模式对应3,4,你可以点进去这些函数内部,就可以看到对应的代码设置
最后总结操作流程如下:
因此**,我们自己的RTC库要写设置时间函数的话,首先是将年月日时分秒信息转化为unix时间戳,然后调用RTC_WriteTimeCount函数设置计数器为此时间戳**,可惜这个函数被static关键字声明了,因此在我们的.c/.h文件里无法调用,后面我们直接复制过来就行了。
创建自己RTC库函函数:
先将进入与退出初始模式的函数复制到myrtc.c中,然后是RTC_WriteTimeCounter,写计数器的函数,顺便将其上面的读计数器的函数RTC_ReadTimeCounter也复制过来,在myrtc.h里引用stm32f1xx_hal.h和rtc.h解决报错,现在我们来写自己的时间设置函数,仿照HAL库的写法:
我们的返回值也为HAL_StatusTypeDef,第一个参数由于我们在myrtc.h中包含了rtc.h,这里边就声明了这个指针,因此我们直接使用就行,不需要传入指针句柄;后面两个参数是HAL库自己定义的时间结构体以及时间格式,我们使用C语言提供的用于表示日期和时间的结构体tm比较好,在myrtc.h中包含time.h,tm结构体内有这些成员变量:
我们要做的事情很简单,将传进来的tm类型的参数,转换为32位的unix时间戳,然后将其设置到RTC计数器就好了,即:RTC_WriteTimeCount函数,关于将tm类型参数转换为unix时间戳的方式,time.h已经为我们准备函数,调用mktime函数即可完成转换,传入参数也是struct tm的指针类型,然后返回写计数器的函数就好了:
HAL_StatusTypeDef My_RTC_SetTime(struct tm*time)
{
uint32_t UnixTime=mktime(time);
RTC_WriteTimeCounter(&hrtc,UnixTime);
}
接下来写读取RTC时间的函数,返回值为tm的指针,没有入参;思路很简单,读取RTC计数器中的时间戳,转换为tm类型即可;读取RTC的函数我们已经复制过来了,RTC_ReadTimeCounter,接收变量的类型需要注意一下,需要使用time.h提供的time_t(64为位),而不是uint32_t,然后就可以使用time.h的时间戳转换为结构体tm的函数localtime进行转换,其接收time_t类型的指针:
struct tm* My_RTC_ReadTime(void)
{
uint32_t UnixTime=RTC_ReadTimeCounter(&hrtc);
return localtime(&UnixTime);
}
接下来到main.c开始写代码:
OLED_Init();
/* USER CODE END 2 */
struct tm* Now; //定义一个结构体指针用来接收RTC_ReadTime返回的地址
struct tm Time=
{
.tm_year=2026-1900, //这个成员变量储存的是现在到1900年份的差值
.tm_mon=8-1, //取值0~11月份,所以要减1
.tm_mday=5,
.tm_hour=17,
.tm_min=16,
.tm_sec=55,
};
My_RTC_SetTime(&Time);
char Mes[50]="";
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Now=My_RTC_ReadTime();
sprintf(Mes,"%d-%02d-%02d",Now->tm_year+1900,Now->tm_mon+1,Now->tm_mday);
OLED_ShowString(1,1,Mes);
sprintf(Mes," %02d:%02d:%02d",Now->tm_hour,Now->tm_min,Now->tm_sec);
OLED_ShowString(2,1,Mes);
HAL_Delay(200);
}
struct tm Time的代码逻辑是定义一个 tm 结构体变量,给成员赋值,把结构体地址传给 KK_RTC_SetTime ;函数拿到这个地址,读取各个成员,再转换成时间戳(或者直接写RTC寄存器),配置RTC硬件时间。
now拿到时间戳转换完成后的tm日历结构体的地址,通过这个地址访问年月日时分秒成员;因为这块内存是共享的,会被下一次读取覆盖,所以最好拷贝到自己定义的变量。
不过每次复位都会重新从1月1号23点59分55秒开始计时,这就需要用到我们之前说的备份寄存器,实现掉电不丢失(VDD掉电后由VBAT引脚供电)。思路如下:我们可以在初始化设定时间时,同时往一个备份寄存器中写一个数据,每次启动代码时,检查此备份寄存器中是否有我们写入的数据,如果是,说明已经设定过时间,就跳过此步骤;否则便设置时间并写此备份寄存器:
My_RTC.c代码如下:
void My_RTC_Init(void)
{
uint32_t MyRTC_Flag;
//读取备份寄存器函数:第一个参数是操作句柄指针,第二个参数是要读哪个备份寄存器
MyRTC_Flag=HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1);
if(MyRTC_Flag==RTC_BKP_Flag){return;} //如果读取到备份寄存器值等于RTC_BKP_Flag就跳出函数
struct tm Time=
{
.tm_year=2026-1900, //这个成员变量储存的是现在到1900年份的差值
.tm_mon=8-1, //取值0~11月份,所以要减1
.tm_mday=5,
.tm_hour=17,
.tm_min=16,
.tm_sec=55,
};
My_RTC_SetTime(&Time);
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,RTC_BKP_Flag);
}
main.c代码如下:
struct tm* Now; //定义一个结构体指针用来接收RTC_ReadTime返回的地址
char Mes[50]="";
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Now=My_RTC_ReadTime();
sprintf(Mes,"%d-%02d-%02d",Now->tm_year+1900,Now->tm_mon+1,Now->tm_mday);
OLED_ShowString(1,1,Mes);
sprintf(Mes," %02d:%02d:%02d",Now->tm_hour,Now->tm_min,Now->tm_sec);
OLED_ShowString(2,1,Mes);
HAL_Delay(200);
}
不过这里还有一个bug,如果你不停按复位键,发现它会少读一秒即RTC停止运行,一直卡在同一秒,这是因为在CubeMX生成的MX_RTC_Init()函数调用了HAL_RTC_Init()函数,其在初始化函数中会使RTC停止运行一会
解决思路也很简单:只要我们初始化过RTC后,就可以不用一直初始化了,所以我们可以将HAL_RTC_Init()函数放到My_RTC_Init()函数里面,然后在rtc.c里面声明一下My_RTC,调用一下My_RTC_Init(),这样初始化后直接跳过这函数就就行了
代码如下:
My_RTC:
void My_RTC_Init(void)
{
uint32_t MyRTC_Flag;
//读取备份寄存器函数:第一个参数是操作句柄指针,第二个参数是要读哪个备份寄存器
MyRTC_Flag=HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1);
if(MyRTC_Flag==RTC_BKP_Flag){return;} //如果读取到备份寄存器值等于RTC_BKP_Flag就跳出函数
if (HAL_RTC_Init(&hrtc) != HAL_OK) //rtc初始化
{
Error_Handler();
}
struct tm Time=
{
.tm_year=2026-1900, //这个成员变量储存的是现在到1900年份的差值
.tm_mon=8-1, //取值0~11月份,所以要减1
.tm_mday=5,
.tm_hour=17,
.tm_min=16,
.tm_sec=55,
};
My_RTC_SetTime(&Time);
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,RTC_BKP_Flag);
}
rtc.c:
#include "rtc.h"
/* USER CODE BEGIN 0 */
#include "MyRTC.h"
/* USER CODE END 0 */
RTC_HandleTypeDef hrtc;
/* RTC init function */
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;
main.c
struct tm* Now; //定义一个结构体指针用来接收RTC_ReadTime返回的地址
char Mes[50]="";
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Now=My_RTC_ReadTime();
sprintf(Mes,"%d-%02d-%02d",Now->tm_year+1900,Now->tm_mon+1,Now->tm_mday);
OLED_ShowString(1,1,Mes);
sprintf(Mes," %02d:%02d:%02d",Now->tm_hour,Now->tm_min,Now->tm_sec);
OLED_ShowString(2,1,Mes);
HAL_Delay(200);
}
更多推荐


所有评论(0)