STM32入门教程(RTC实时时钟&BKP备份寄存器篇)

参考教程:[12-1] Unix时间戳_哔哩哔哩_bilibili

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

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

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

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

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

4、C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间(是个结构体,其中的参数为年月日时分秒以及星期几等)和字符串之间的转换。

函数

作用

time_t time(time_t*);

获取系统时钟

struct tm* gmtime(const time_t*);

秒计数器转换为日期时间(格林尼治时间)

struct tm* localtime(const time_t*);

秒计数器转换为日期时间(当地时间)

time_t mktime(struct tm*);

日期时间转换为秒计数器(当地时间)

char* ctime(const time_t*);

秒计数器转换为字符串(默认格式)

char* asctime(const struct tm*);

日期时间转换为字符串(默认格式)

size_t strftime(char*, size_t, const char*, const struct tm*);

日期时间转换为字符串(自定义格式)

5、BKP(Backup Registers)备份寄存器:

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

(2)TAMPER引脚产生的侵入事件会将所有备份寄存器的内容清除,同时还会申请中断,在中断函数中可以继续清除其它存储器的数据以及锁死设备,可用于预防数据被窃取等恶性事件。

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

(4)存储RTC时钟校准寄存器。

(5)用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型)。

6、BKP基本结构:

7、RTC(Real Time Clock)实时时钟:

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

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

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

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

(5)可选择三种RTC时钟源:

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

②LSE振荡器时钟(通常为32.768KHz,一般都选用该时钟作为RTCCLK)

③LSI振荡器时钟(40KHz)

8、RTC框图:

(1)后备区域的电路在主电源掉电后可以使用备用电池维持工作。

(2)RTCCLK是经过选择器输入RTC的时钟,一般选择LSE振荡器时钟,进入RTC的时钟首先由预分频器进行分频

(3)RTC的预分频器由两个寄存器组成,RTC_PRL是重装载寄存器(决定分频系数),RTC_DIV是余数寄存器(本质上是一个自减计数器),DIV负则对RTCCLK的时钟脉冲进行计数,当自减为0时会输出一个时钟脉冲到TR_CLK上,同时PRL将重装载值写进DIV中,以此往复达到分频的效果。

(4)RTC_CNT就是秒计数器,TR_CLK每有一个时钟脉冲,计数器的值+1,理论上TE_CLK应该配置为每秒产生一个时钟脉冲

(5)RTC_ALR是闹钟寄存器,它与RTC_CNT等长,用于设置闹钟,用户可以在ALR中写一个时间戳,当ALR与CNT的值相等时会产生RTC_Alarm信号通往右侧的中断系统,同时还可以使STM32退出待机模式。(RTC_ALR的值不会自己改变,需要用户设定)

(6)RTC_Second是秒中断信号,每有一个时钟脉冲就会产生一个秒中断信号;RTC_Overflow是溢出中断信号,不过对于RTC_CNT而言,它存储的是32位的无符号数,到2016年才会发生计数溢出。(SECF、OWF、ALRF是中断标志位,SECIE、OWIE、ALRIE是中断使能位)

(7)读写RTC中的寄存器需要通过APB1总线(RTC是APB1总线上的设备)。

9、硬件电路:

10、BKP和RTC操作的注意事项:

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

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

设置PWR_CR的DBP,使能对BKP和RTC的访问。(使用PWR_BackupAccessCmd函数)

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

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

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

11、读写备份寄存器:

(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。

(2)在stm32f10x_bkp.h文件中有BKP相关的函数:

[1]BKP_DeInit函数:恢复缺省配置,可以用于清空BKP的所有数据。

[2]BKP_TamperPinLevelConfig函数:配置TAMPER是高电平触发还是低电平触发(侵入检测功能)。

[3]BKP_TamperPinCmd函数:选择是否开启侵入检测功能。

[4]BKP_ITConfig函数:选择是否开启中断。

[5]BKP_RTCOutputConfig函数:配置时钟输出功能,选择输出的时钟源。

[6]BKP_SetRTCCalibrationValue函数:设置RTC校准值。

[7]BKP_WriteBackupRegister函数:写备份寄存器。

[8]BKP_ReadBackupRegister函数:读备份寄存器。

[9]BKP_GetFlagStatus函数:获取标志位。

[10]BKP_ClearFlag函数:清除标志位。

[11]BKP_GetITStatus函数:在中断函数中获取标志位。

[12]BKP_ClearITPendingBit函数:在中断函数中清除标志位。

(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下BKP中的数据是否会被重置)。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "Key.h"

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

uint8_t KeyNum;

int main()
{
	OLED_Init();
	Key_Init();
	
	OLED_ShowString(1, 1, "W:");
	OLED_ShowString(2, 1, "R:");
	
	//使能PWR和BKP时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	
	//使能对BKP和RTC的访问
	PWR_BackupAccessCmd(ENABLE);
	
	while(1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)   //按下按键1,ArrayWrite中的数据自增并写入BKP
		{
			ArrayWrite[0]++;
			ArrayWrite[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);
		}
		//读取BKP中的数据
		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);
	}
}

12、实时时钟:

(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。

(2)在stm32f10x_rcc.h文件中有几个本例相关的函数。

[1]RCC_LSEConfig函数:配置LES外部低速时钟。

[2]RCC_LSICmd函数:配置LSI内部低速时钟。

[3]RCC_RTCCLKConfig函数:配置RTCCLK的数据选择器,选择RTCCLK的时钟源。

[4]RCC_RTCCLKCmd函数:允许RTCCLK选择的时钟进入RTC。

[5]RCC_GetFlagStatus函数:获取标志位。

[6]RCC_ClearFlag函数:清除标志位。

(3)在stm32f10x_rtc.h文件中有RTC模块相关的函数。

[1]RTC_ITConfig函数:配置中断输出。

[2]RTC_EnterConfigMode函数:RTC进入配置模式(置CRL寄存器的CNF位为1)。

[3]RTC_ExitConfigMode函数:RTC退出配置模式(置CRL寄存器的CNF位01)。

[4]RTC_GetCounter函数:读CNT计数器(获取时间)。

[5]RTC_SetCounter函数:写CNT计数器(设置时间)。

[6]RTC_SetPrescaler函数:写预分频器的PRL重装寄存器,也就是设置分频系数。

[7]RTC_SetAlarm函数:写闹钟寄存器。

[8]RTC_GetDivider函数:读预分频器的DIV余数寄存器。

[9]RTC_WaitForLastTask函数:等待上次操作完成(等待RTOFF=1)。

[10]RTC_WaitForSynchro函数:等待同步(等待REF=1)。

[11]RTC_GetFlagStatus函数:获取标志位。

[12]RTC_ClearFlag函数:清除标志位。

[13]RTC_GetITStatus函数:在中断函数中获取标志位。

[14]RTC_ClearITPendingBit函数:在中断函数中清除标志位。

(3)在项目的System组中添加MyRTC.h文件和MyRTC.c文件用于封装RTC模块的代码。

①MyRTC.h文件:

#ifndef __MyRTC_H
#define __MyRTC_H

extern uint16_t MyRTC_Time[];

void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);

#endif

②MyRTC.c文件:

#include "stm32f10x.h"                  // Device header
#include <time.h>

uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55};  //2023年1月1日23:59:55

void MyRTC_SetTime(void);

void MyRTC_Init(void)
{
	//使能PWR和BKP时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
	
	//使能对BKP和RTC的访问
	PWR_BackupAccessCmd(ENABLE);
	
	if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)  //RTC只需初始化一遍即可(关机后再开机,计时不会被重置)
	{
		//开启LSE的时钟
		RCC_LSEConfig(RCC_LSE_ON);
		
		//等待LES时钟开启
		while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
		
		//配置RTCCLK的数据选择器,指定LSE为时钟源
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
		
		//允许RTCCLK选择的时钟进入RTC
		RCC_RTCCLKCmd(ENABLE);
		
		//等待同步,等待前一次写操作结束
		RTC_WaitForLastTask();
		RTC_WaitForSynchro();
		
		//配置预分频器,输出1Hz的时钟(RTC_SetPrescaler函数中有使RTC进入配置模式的过程)
		RTC_SetPrescaler(32768 - 1);  //32.768KHz / 32768 = 1Hz
		RTC_WaitForLastTask();  //等待前一次写操作结束
		
		//给RTC一个初始时间
		MyRTC_SetTime();
		
		//第一次初始化RTC时给BKP写值,如果程序复位,初始化函数会再执行一遍
		//而程序复位时BKP不会被重置,可以依据BKP中的值是否为A5A5判断是否需要初始化RTC
		BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
	}
	else
	{
		RTC_WaitForLastTask();
		RTC_WaitForSynchro();
	}
}

void MyRTC_SetTime(void)
{
	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;  
	//日期时间结构体转换为伦敦时间的时间戳,再调整为北京时间的时间戳
	//(这步只是为了CNT使用得到北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
	
	RTC_SetCounter(time_cnt);  //写CNT(设置时间)
	RTC_WaitForLastTask();  //等待前一次写操作结束
}

void MyRTC_ReadTime(void)
{
	time_t time_cnt;       //秒计数器数据类型
	struct tm time_date;   //日期时间数据类型
	
	time_cnt = RTC_GetCounter() + 8 * 60 * 60;
	//当前时间戳是北京时间的时间戳,进行转换时需要换回伦敦时间的时间戳
	//(这步只是为了CNT使用北京时间的时间戳,即使使用伦敦的时间戳也不影响最终结果)
	
	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;
}

(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中并进行调试(主要是开发板掉电后VBAT引脚通电与不通电的区别,观察两种情况下RTC的计时是否被重置)。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "MyRTC.h"

int main()
{
	OLED_Init();
	MyRTC_Init();
	
	OLED_ShowString(1, 1, "Date: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,6,RTC_GetCounter(),10);
		OLED_ShowNum(4,6,RTC_GetDivider(),10);
	}
}

猜你喜欢

转载自blog.csdn.net/Zevalin/article/details/134788248