实时时钟本质是一个定时器,专门用来产生年月日时分秒,这种日期和时间信息

一、Unix时间戳

1.Unix时间戳简介

        Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒

        时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量

        世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

2.UTC/CMT

        GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。

        UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致。

3.时间戳转换

        C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换。

        我们来使用DEVC+来举例使用该函数。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

time_t time_cnt;
struct tm time_date; 
char *time_str;

int main(void) {
	//	返回当前时间 或者写 time(&time_cnt)
	time_cnt = time(NULL);				
	printf("现在时间的计数器为%d\n", time_cnt);
	
	// 秒计数器转换为伦敦时间函数
	time_cnt = 1672588795; 
	printf("%d\n", time_cnt);
	time_date = *gmtime(&time_cnt);
	printf("伦敦%d年 ", time_date.tm_year + 1900);		//	年 
	printf("%d月 ", time_date.tm_mon + 1);		//	月 
	printf("%d日 ", time_date.tm_mday);		//	日 
	printf("%d时 ", time_date.tm_hour);		//	时 
	printf("%d分 ", time_date.tm_min);		//	分 
	printf("%d秒 ", time_date.tm_sec);		//	秒 
	printf("星期%d\n", time_date.tm_wday);		//	星期 
	
	// 秒计数器转换为当地时间函数
	time_date = *localtime(&time_cnt);
	printf("当地时间%d年 ", time_date.tm_year + 1900);		//	年 
	printf("%d月 ", time_date.tm_mon + 1);		//	月 
	printf("%d日 ", time_date.tm_mday);		//	日 
	printf("%d时 ", time_date.tm_hour);		//	时 
	printf("%d分 ", time_date.tm_min);		//	分 
	printf("%d秒 ", time_date.tm_sec);		//	秒 
	printf("星期%d\n", time_date.tm_wday);		//	星期
	
	// 将当地时间转换为秒计数器时间 
	time_cnt = mktime(&time_date);
	printf("当地时间2023年1月1日23时59分55秒星期0的计数器为:%d\n", time_cnt);
	
	//	秒计数器转换为字符串
	time_str = ctime(&time_cnt); 
	printf(time_str);
	
	//	日期时间转换为字符串
	time_str = asctime(&time_date); 
	printf(time_str);
	
	char t[50];
	strftime(t, 50, "%H-%M-%S", &time_date);
	printf(t);
	
	return 0;
}

        打印结果

现在时间的计数器为1754480604
1672588795
伦敦2023年 1月 1日 15时 59分 55秒 星期0
当地时间2023年 1月 1日 23时 59分 55秒 星期0
当地时间2023年1月1日23时59分55秒星期0的计数器为:1672588795
Sun Jan 01 23:59:55 2023
Sun Jan 01 23:59:55 2023
23-59-55

        转换状态图如图所示

二、BKP

1.BKP简介

        BKP(Backup Registers)备份寄存器

        BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位

        TAMPER引脚产生的侵入事件将所有备份寄存器内容清除

        RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲

        存储RTC时钟校准寄存器

        用户数据存储容量:

                20字节(中容量和小容量)/ 84字节(大容量和互联型)

2.BKP基本结构

        橙色部分为后备区域,BKP处于后备区域,后备区域还有RTC的相关电路,STM32设备后备区域的特点是,当VDD主电源掉电时,后备区域依然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换为VDD。

        BKP中主要有数据寄存器,控制寄存器,状态寄存器,RTC时钟校准校准寄存器,数据寄存器用来存储数据,每个数据寄存器都是16位的,也就是一个数据寄存器可以存2个字节。

        BKP的功能:侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或下降沿时,清除BKP的内容,以保证安全。时钟输出可以把RTC相关的时钟从PC13位置的RTC引脚输出出去,供外部使用,其中输出校准时钟时,配合校准寄存器,可以对RTC的误差进行校准。

三、RTC

1.RTC简介

        RTC(Real Time Clock)实时时钟

        RTC是一个独立的定时器,可为系统提供时钟和日历的功能

        RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时

        32位的可编程计数器,可对应Unix时间戳的秒计数器

        20位的可编程预分频器,可适配不同频率的输入时钟

        可选择三种RTC时钟源:

                HSE时钟除以128(通常为8MHz/128)

                LSE振荡器时钟(通常为32.768KHz)

                LSI振荡器时钟(40KHz)

        整个芯片可以有4个时钟源:

                HSE——高速外部时钟信号

                HSI——高速内部时钟信号

                LSI——低速内部时钟信号

                LSE——低速外部时钟信号

        高速时钟:一般供内部程序运行和主要外设使用

        低速时钟:一般供RTC看门狗使用

2.RTC框图

        左边阴影部分是核心的分频和计数计时部分,右边是中断输出使能和NVIC部分,上面是APB1总线读写部分,下面阴影是和PWR关联的部分,意思就是RTC的闹钟可以唤醒设备,退出待机模式。图中的全部阴影部分都处于后备区域,这些电路在主电源掉电后,可以使用备用电池维持工作,这些模块在待机时都会继续维持供电,其他区域待机时不会供电。

        首先分频和计数计时部分,这一块的输入时钟是RTCCLK,RTCCLK的来源需要在RCC里进行配置,使用的是LSE振荡器时钟(通常为32.768KHz),由于计数器需要的是1Hz的频率,所以RTCCLK进来首先需要经过RTC预分频器分频,这个分频器由两个寄存器组成,上面为重装寄存器RTC_PRL——写入几就是几+1分频,下面的RTC_DIV是余数寄存器,但它还是计数器的作用,它是一个自减计数器,每来一个输入时钟,DIV值自减一次,自减到0时,DIV输出一个脉冲,产生溢出信号,同时DIV从PRL获取重装值,回到重装值继续自减,右边是计数器,可以看作是时间戳的秒计数器,然后下面有一个等宽的闹钟,这个闹钟是一个定值,当检测到闹钟值与上面的计数器值相等时,闹钟就会响,触发中断,也可以让芯片退出待机模式。

        右边就是中断部分,有三个信号可以触发中断,第一个时RTC_Second秒中断,它的来源就是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断,第二个是RTC_Overflow,溢出中断,当32位计数器计满溢出,会触发一次中断,第三个RTC_Alarm,闹钟中断。

        上面APB1总线和APB1接口就是程序读写寄存器的部分,读写寄存器可以通过APB1总线来完成,另外也可以看出 RTC是APB1总线上的设备。

3.RTC基本结构

        左边是RTCCLK时钟来源,这一块需要在RCC中配置,三个时钟选择一个当作RTCCLK,之后RTCCLK通过预分频器,对时钟进行分频,余数计数器是一个自减计数器,存储当前的计数值,重装计数器是计数目标,决定分频值,分频之后得到1HZ的秒计数信号,通向32位计数器,一秒自增一次,下面还有一个32位的闹钟值,可以设定闹钟,然后右边由三个信号可以触发中断,分别是秒信号,计数器溢出信号和闹钟信号,三个信号先通过中断输出控制进行中断使能,使能的中断才能通向NVIC然后向CPU申请中断。

4.硬件电路

5.RTC操作注意事项

            执行以下操作将使能对BKP和RTC的访问:

                   设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟

                   设置PWR_CR的DBP,使能对BKP和RTC的访问

        若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1

        必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器

        对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器。

六、RCC库函数

        void RCC_LSEConfig(uint8_t RCC_LSE);

        作用:配置LSE外部低速时钟,启动LSE使用该函数

        void RCC_LSICmd(FunctionalState NewState);

        作用:配置LSI内部低速时钟

        void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);

        作用:RTCCLK配置,选择RTCCLK的时钟源(配置数据选择器)

        void RCC_RTCCLKCmd(FunctionalState NewState);

        作用:启动RTCCLK

七、RTC库函数

        void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

        作用:配置中断输出

        void RTC_EnterConfigMode(void);

        作用:进入配置模式,设置RTC_CRL寄存器中的CNF位为1

        void RTC_ExitConfigMode(void);

        作用:退出配置模式,CNF位清零

        uint32_t  RTC_GetCounter(void);

        作用:获取CNT计数器的值

        void RTC_SetCounter(uint32_t CounterValue);

        作用:写入计数器的值

        void RTC_SetPrescaler(uint32_t PrescalerValue);

        作用:写入预分频器,写入到预分频器的PRL重装寄存器中,配置分频系数

        void RTC_SetAlarm(uint32_t AlarmValue);

        作用:写入闹钟值

        uint32_t  RTC_GetDivider(void);

        作用:读取预分频器中的DIV余数寄存器

        void RTC_WaitForLastTask(void);

        作用:等待上次操作完成,等待RTOFF状态位是1

        void RTC_WaitForSynchro(void);

        作用:等待同步,等待RSF置1

        FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);

        void RTC_ClearFlag(uint16_t RTC_FLAG);

        ITStatus RTC_GetITStatus(uint16_t RTC_IT);

        void RTC_ClearITPendingBit(uint16_t RTC_IT);

八、代码

        使用BKP或RTC都要先执行:

                ①开启PWR和BKP的时钟、

                ②使用PWR,使能BKP和RTC的访问

1.代码1——读写备份寄存器

        步骤:①初始化②写DR③读DR

        BKP的初始化步骤:

                ①开启PWR和BKP的时钟

                        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

                       RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);

                ②使用PWR,使能BKP和RTC的访问

                      PWR_BackupAccessCmd(ENABLE);

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

uint16_t ArrayWrite[2] = {0x1234, 0x5678};
uint16_t ArrayRead[2];
uint8_t KeyNum;

int main(void)
{
	OLED_Init();
	Key_Init();
	OLED_ShowString(1, 1, "W:");
	OLED_ShowString(2, 1, "R:");
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	
	PWR_BackupAccessCmd(ENABLE);
	

	while (1)
	{	
		KeyNum = Key_GetNum();
		if (KeyNum == 1)
			{
				BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]);
				BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);
				OLED_ShowHexNum(1, 3, ArrayWrite[0], 4);
				OLED_ShowHexNum(1, 8, ArrayWrite[1], 4);
				ArrayWrite[0] ++;
				ArrayWrite[1] ++;

			}
		ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1);
		ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);
			
		OLED_ShowHexNum(2, 3, ArrayRead[0] , 4);
		OLED_ShowHexNum(2, 8, ArrayRead[1] , 4);
	}	
}

        实验现象:按下按键,将数组中的数值写入备份寄存器,每按下一次,数值加1,当断开单片机供电,但是不断开VBAT,在单片机再次上电时,依然显示存入的数据。当断开VBAT,后备寄存器数据丢失。

2.代码2——实时时钟

        RTC初始化流程:

        ①开启PWR和BKP的时钟,使用PWR,使能BKP和RTC的访问

                RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

                RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);

                PWR_BackupAccessCmd(ENABLE);

        ②启动RTC时钟(使用LSE)

                RCC_LSEConfig(RCC_LSE_ON);

                while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);

        ③配置RTCCLK数据选择器,指定RTCCLK

                RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

                RCC_RTCCLKCmd(ENABLE);

        ④等待RTC_CRL寄存器中的RSF位,查询RTC_CR寄存器中的RTOFF状态位

                RTC_WaitForSynchro();

                RTC_WaitForLastTask();

        ⑤配置预分频器

                RTC_SetPrescaler(32768 - 1);

                RTC_WaitForLastTask();

        ⑥配置CNT,给RTC一个初始时间

                RTC_SetCounter(1672588795);

                RTC_WaitForLastTask();

        接下来写两个函数一个设置时间,一个读取时间

        读取时间就是读取CNT的值然后转为年月日时分秒放在一个全局数组中

void MyRTC_ReadTime(void)			//	将CNT秒数转换为数组的时间
{
	time_t time_cnt;
	struct tm time_date; 
	
	time_cnt = RTC_GetCounter() + 8 * 60 * 60;
	time_date = *localtime(&time_cnt);
	
	MyRTC_Time[0] = time_date.tm_year + 1900;
	MyRTC_Time[1] = time_date.tm_mon + 1;
	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;
}

        设置时间就是把全局数组的年月日时分秒转换为秒数,再写入到CNT

void MyRTC_SetTime(void)			//	将数组的时间转换为CNT秒数
{
	time_t time_cnt;
	struct tm time_date; 
	
	time_date.tm_year = MyRTC_Time[0] - 1900;
	time_date.tm_mon = MyRTC_Time[1] - 1;
	time_date.tm_mday = MyRTC_Time[2];
	time_date.tm_hour = MyRTC_Time[3];
	time_date.tm_min = MyRTC_Time[4];
	time_date.tm_sec = MyRTC_Time[5];
	
	time_cnt = mktime(&time_date) - 8 * 60 * 60;
	
	RTC_SetCounter(time_cnt);
	RTC_WaitForLastTask();
}

MyRTC.c模块

#include "stm32f10x.h"                  // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2025, 8, 5, 0, 43, 55};

void MyRTC_SetTime(void);

void MyRTC_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	PWR_BackupAccessCmd(ENABLE);
	
	if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
	{
		RCC_LSEConfig(RCC_LSE_ON);
		while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
	
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
		RCC_RTCCLKCmd(ENABLE);
	
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();
	
	
		RTC_SetPrescaler(32768 - 1);
		RTC_WaitForLastTask();
		
		MyRTC_SetTime();
		
		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
	}
	else
	{
		RTC_WaitForSynchro();
		RTC_WaitForLastTask();
	}
	
}

void MyRTC_SetTime(void)			//	将数组的时间转换为CNT秒数
{
	time_t time_cnt;
	struct tm time_date; 
	
	time_date.tm_year = MyRTC_Time[0] - 1900;
	time_date.tm_mon = MyRTC_Time[1] - 1;
	time_date.tm_mday = MyRTC_Time[2];
	time_date.tm_hour = MyRTC_Time[3];
	time_date.tm_min = MyRTC_Time[4];
	time_date.tm_sec = MyRTC_Time[5];
	
	time_cnt = mktime(&time_date) - 8 * 60 * 60;
	
	RTC_SetCounter(time_cnt);
	RTC_WaitForLastTask();
}

void MyRTC_ReadTime(void)			//	将CNT秒数转换为数组的时间
{
	time_t time_cnt;
	struct tm time_date; 
	
	time_cnt = RTC_GetCounter() + 8 * 60 * 60;
	time_date = *localtime(&time_cnt);
	
	MyRTC_Time[0] = time_date.tm_year + 1900;
	MyRTC_Time[1] = time_date.tm_mon + 1;
	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;
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main(void)
{
	OLED_Init();
	MyRTC_Init();
	
	OLED_ShowString(1, 1, "Data:XXXX-XX-XX");
	OLED_ShowString(2, 1, "Time:XX:XX:XX");
	OLED_ShowString(3, 1, "CNT:");
	OLED_ShowString(4, 1, "DIV:");
	
	while (1)
	{
		MyRTC_ReadTime();
		OLED_ShowNum(1, 6, MyRTC_Time[0], 4);
		OLED_ShowNum(1, 11, MyRTC_Time[1], 2);
		OLED_ShowNum(1, 14, MyRTC_Time[2], 2);
		OLED_ShowNum(2, 6, MyRTC_Time[3], 2);
		OLED_ShowNum(2, 9, MyRTC_Time[4], 2);
		OLED_ShowNum(2, 12, MyRTC_Time[5], 2);
		
		OLED_ShowNum(3, 5, RTC_GetCounter(), 10);
		OLED_ShowNum(4, 5, RTC_GetDivider(), 10);
	}	
}

        程序现象,OLED第一行显示数组中的年月日,第二行显示时分秒,第三行显示CNT的值,第四行显示分频值。分频值不断自减,当减为0,CNT的值+1,同时第二行的时间的秒也+1,依次运行。

九、总结

        STM32实时时钟(RTC)是集成在STM32微控制器中的独立计时模块,旨在提供高精度、低功耗的时间跟踪功能,即便在主电源关闭或系统进入低功耗模式时仍能持续运行。它通常由外部低速振荡器(LSE,多为32.768kHz晶体)或内部低速振荡器(LSI)提供时钟源,其中LSE因频率稳定度更高而成为主流选择,可确保时间计数的准确性。RTC的核心功能包括秒、分、时、日、星期、月、年等日历信息的实时更新与存储,支持闰年自动修正,能适应不同月份天数的变化,满足长期计时需求。此外,它还具备丰富的中断和事件功能,例如可配置闹钟中断(支持按特定时间点或周期性触发)、周期性中断(如每秒、每分、每时等固定间隔触发)以及溢出中断(当计数器达到最大值时触发),这些功能使得RTC在定时唤醒低功耗系统、触发特定任务执行、记录事件时间戳等场景中发挥关键作用。在硬件设计上,RTC通常拥有独立的电源域,可通过备用电源(如纽扣电池)供电,当主电源掉电时,备用电源能维持RTC的正常运行,避免时间信息丢失,确保系统恢复供电后时间的连续性。同时,STM32的RTC还提供了时间校准机制,允许用户通过软件微调时钟频率,补偿振荡器本身的误差或温度变化带来的漂移,进一步提升计时精度。在实际应用中,RTC广泛用于需要时间戳记录的设备(如数据记录仪、传感器节点)、定时唤醒的低功耗系统(如物联网终端)以及需要实时时钟显示的仪器(如医疗设备、工业控制器)等领域,是STM32微控制器中实现时间管理的核心模块。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐