STM32 RTC实验

目录

本文将按如下内容介绍RTC及其使用步骤

  • RTC时钟简介
  • 相关寄存器
  • 相关代码
  • 几个注意点
  • 运行结果



RTC时钟简介

STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数 的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当 前的时间和日期。

RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式 唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC, 以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP) 写保护。

在这里插入图片描述
RTRTC 由两个主要部分组成, 第一部分(APB1 接口)用来和 APB1 总线相连。 此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。APB1 接口由 APB1 总 线时钟驱动,用来与 APB1 总线连接。

另一部分(RTC 核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是 RTC 的 预分频模块,它可编程产生 1 秒的 RTC 时间基准 TR_CLK。RTC 的预分频模块包含了一个 20 位的可编程分频器(RTC 预分频器)。如果在 RTC_CR 寄存器中设置了相应的允许位,则在每个 TR_CLK 周期中 RTC 产生一个中断(秒中断)。第二个模块是一个 32 位的可编程计数器,可被 初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒, 约合 136 年左右,作为一般应用,这已经是足够了的。




相关寄存器


控制寄存器 RTC_CRH

在这里插入图片描述
该寄存器比较简单,这里,我们使用的是秒中断,所以相应的位需要设置为1。


控制寄存器 RTC_CRL

在这里插入图片描述

  • 位0,秒钟标志位。进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断,然后通过软件清0
  • 位3,寄存器同步标志位。修改控制寄存器之前,必须先判断该位,是否已经同步,如果没有则等待同步
  • 位4,配置标志位。软件修改RTC_CNT / RTC_ALR / RTC_PRL的值的时候,必须先置位该位
  • 位5,RTC操作位。由硬件操作,软件只读。通过该位,可以判断上次对RTC寄存器的操作是否已经完成,如果没有完成,则需要等待完成才能开始下一次的操作

RTC预分频装载寄存器 RTC_PRLH

用来配置RTC时钟的分频数的,比如我们使用外部32.768k的晶振作为时钟的输入频率,我们要设置这两个寄存器的值为32767,以得到一秒钟的计数频率。

在这里插入图片描述


RTC预分频装载寄存器 RTC_PRLL

在这里插入图片描述

RTC计数器寄存器RTC_CNT

该寄存器由 2 个 16 位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。一般情况下,可以计算232 = 4,294,967,296‬ s,大概136年,目前情况下,是完成够使用的。


备份寄存器

因为我们要使用到备份寄存器来存储RTC的相关信息(主要用来标记时钟是否已经经过了配置)。如果配置过了,那么下一次的时候就不再配置。

备份寄存器是 42 个 16 位的寄存器(战舰开发板就是大容量的),可用来存储 84 个字节的 用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。 即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。


备份区域控制寄存器RCC_BDCR

在这里插入图片描述
这里我们选择外部低速LSE来作为我们的RTC时钟。




RTC一般配置步骤


使能电源时钟和备份区域时钟

//我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM,
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟

取消备份写保护

要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据。我们需要用到向备份区域写入一个字节,来标记时钟已经 配置过了,这样避免每次复位之后重新配置时钟。

PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问 

复位备份区域,开启外部低速振荡器

在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个 操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要 看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。
备份区域复位的函数是:

BKP_DeInit();//复位备份区域 

开启外部低速振荡器的函数是

RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器 

选择 RTC 时钟,并使能

这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI 作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟。

RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 

设置 RTC 的分频,以及配置 RTC 时钟

在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位(RTC_CRH 的 CNF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL 两个寄存器)

RTC_EnterConfigMode();/// 允许配置 
RTC_ExitConfigMode();//退出配置模式,更新配置
void RTC_SetPrescaler(uint32_t PrescalerValue); //设置 RTC 时钟分频数
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); //是设置秒中断允许,RTC 使能中断
RTC_ITConfig(RTC_IT_SEC, ENABLE);  //使能 RTC 秒中断,第一个参数是设置秒中断的类型.
void RTC_SetCounter(uint32_t CounterValue);//设置 RTC 计数值

更新配置,设置 RTC 中断分组

在设置完时钟之后,我们将配置更新同时退出配置模式

RTC_ExitConfigMode();//退出配置模式,更新配置

在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化 过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来 决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。 往备份区域写用户数据的函数是

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);	//寄存器标号和数据
BKP_WriteBackupRegister(BKP_DR1, 0X5050); 
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);	//读取寄存器的数据



相关函数


头文件定义

//时间结构体
typedef struct 
{
	vu8 hour;
	vu8 min;
	vu8 sec;			
	//公历日月年周
	vu16 w_year;
	vu8  w_month;
	vu8  w_date;
	vu8  week;
}_calendar_obj;					 
extern _calendar_obj calendar;	//日历结构体

extern u8 const mon_table[12];	//月份日期数据表
void Disp_Time(u8 x,u8 y,u8 size);//在制定位置开始显示时间
void Disp_Week(u8 x,u8 y,u8 size,u8 lang);//在指定位置显示星期
u8 RTC_Init(void);        //初始化RTC,返回0,失败;1,成功;
u8 Is_Leap_Year(u16 year);//平年,闰年判断
u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);
u8 RTC_Get(void);         //更新时间   
u8 RTC_Get_Week(u16 year,u8 month,u8 day);
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//设置时间		

RTC初始化


u8 RTC_Init(void)
{
	//检查是不是第一次配置时钟
	u8 temp=0;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKR外设时钟
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问,是用来存储一些重要的数据
	if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)		//从指定的后备寄存器中读取数据
	{	 			 
		BKP_DeInit();	//执行复位备份区域,将BKP外设寄存器复位到默认值
		RCC_LSEConfig(RCC_LSE_ON);	//设置外部低速晶振(LSE)
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)	//查看是否复位
		{
			temp++;
			delay_ms(10);
		}
		if(temp>=250)return 1;//初始化失败   
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);		//设置RTC时钟,选用LSE作为RTC的时钟 
		RCC_RTCCLKCmd(ENABLE);	//使能RTC时钟
		RTC_WaitForLastTask();	//等待上一次写操作完成
		RTC_WaitForSynchro();		//等待RTC寄存器同步,如果不同步,则对寄存器的访问是无效的。
		RTC_ITConfig(RTC_IT_SEC, ENABLE);		//使能RTC秒中断
		RTC_WaitForLastTask();	//等待上一次写操作完成
		RTC_EnterConfigMode();	//允许配置	
		RTC_SetPrescaler(32767); //设置RTC预分频的值
		RTC_WaitForLastTask();	//等待上一次写操作完成
		RTC_Set(2015,1,14,17,42,55);  //设置时间,y-m-d h-m-s
		RTC_ExitConfigMode(); //退出配置模式
		BKP_WriteBackupRegister(BKP_DR1, 0X5050);	//向指定后备寄存器中写入用户数据0x5050,下次读写需要。
	}
	else//系统继续计时
	{
		RTC_WaitForSynchro();	//等待RTC寄存器同步,如果不同步,则对寄存器的访问是无效的。
		RTC_ITConfig(RTC_IT_SEC, ENABLE);	//使能RTC秒中断
		RTC_WaitForLastTask();	//等待上一次写操作完成
	}
	RTC_NVIC_Config();//RCT中断分组设置	    				     
	RTC_Get();//更新时间
	return 0; //ok
}

RTC_Get

//得到当前时间,结果保存在calendar结构体里面
//返回值0成功,其他失败
u8 RTC_Get(void)
{
	static u16 daycnt=0;
	u32 timecount=0; 
	u32 temp=0;
	u16 temp1=0;	  
    timecount=RTC_GetCounter();	 	//得到计数器中的值(秒钟数)
 	temp=timecount/86400;   //得到天数。一天60*60*24 = 86400.
	if(daycnt!=temp)//超过一天了
	{	  
		daycnt=temp;
		temp1=1970;	//从1970年开始
		while(temp>=365)	//超过一年了
		{				 
			if(Is_Leap_Year(temp1))//是闰年
			{
				if(temp>=366)temp-=366;//闰年的秒钟数
				else {temp1++;break;}  
			}
			else temp-=365;	  //平年
			//temp是天数,-366,-365表示从0天开始。
			temp1++;  
		}   
		calendar.w_year=temp1;//当前年份
		temp1=0;
		//temp是当前的天数,比如32天,那么就需要转换成哪一个月,哪一天。
		//const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
		//temp1++ = 1.w_month = temp1+1 = 2,w_date = temp+1 = 2,应该是从1开始显示。
		while(temp>=28)//超过一个月
		{
			if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当前是闰年且为2月份
			{
				if(temp>=29)temp-=29;//闰年2月份,29天,所以减去29.
				else break; 
			}
			else 
			{
				//temp>=当前月天数,那么temp - 当前月天数,表示新的一月开始
				if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
				else break;
			}
			temp1++;  //月数+1
		}
		calendar.w_month=temp1+1;	//得到月份
		calendar.w_date=temp+1;  	//得到天数
	}
	temp=timecount%86400;     		//得到秒数 	   
	calendar.hour=temp/3600;     	//得到小时
	calendar.min=(temp%3600)/60; 	//得到分钟
	calendar.sec=(temp%3600)%60; 	//得到秒数
	calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期
	return 0;
}

RTC_Get_Week

//传入年月日,来获取星期。
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; 	 																					 
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{	
	u16 temp2;
	u8 yearH,yearL;
	yearH=year/100;	yearL=year%100; 	//yearH=20,yearL=19
	if (yearH>19)yearL+=100;			//yearL = 119
	temp2=yearL+yearL/4;				//temp2 = 119 + 29 = 148
	temp2=temp2%7; 						//temp2 = 148%7 = 1
	temp2=temp2+day+table_week[month-1];//temp2 = 1 + 2 + 5 = 8
	if (yearL%4==0&&month<3)temp2--;	
	return(temp2%7);					//8%7=1
}			
/*
	//2019-9-2,星期一	蔡勒公式
	w 星期 对7取余,0是星期天
	c 世纪数(前两位数)		20
	y 年 (后两位数)			19
	m 月 (m>=3,m<=14,也就是1,2月份要看成上一月的13,14月份来看)	9
	d 日	2
	w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1
	y = 19,[y/4]=3,[c/4]=5,2c=40,[26(m+1)/10]=26,d=2
	w = 19 + 4 + 5 - 40 + 27 = 15%7 = 1,星期一
*/ 

RTC_Set

//月份表,这里是年月日转换成秒数。
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
	//2019-9-2 10:00:00
	//计数器只认秒,所以要转换成秒。
	u16 t;
	u32 seccount=0;
	if(syear<1970||syear>2099)return 1;	   //超出范围
	for(t=1970;t<syear;t++)	//一次次判断
	{
		if(Is_Leap_Year(t))seccount+=31622400;//如果是闰年的话,加上60*60*24*366
		else seccount+=31536000;			  //否则加上60*60*24*365
	}
	smon-=1;	//月份-1计算,因为从0开始
	for(t=0;t<smon;t++)	   //计算月的时间
	{
		seccount+=(u32)mon_table[t]*86400;//60*60*24.
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年,额外+1天的秒数.   
	}
	seccount+=(u32)(sday-1)*86400;//前面一天的秒数
	seccount+=(u32)hour*3600;//当前秒数
    seccount+=(u32)min*60;	 //分钟的秒数
	seccount+=sec;//秒数

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能BKR和APB1的时钟
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问,是用来存储一些重要的数据
	RTC_SetCounter(seccount);	//改变寄存器的值

	RTC_WaitForLastTask();	//等待上一次的写操作完成	
	return 0;	    
}

RTC_IRQHandler

//RTC中断处理函数.
void RTC_IRQHandler(void)
{		 
	if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//如果是秒中断
	{							
		RTC_Get();//获取时间
 	}
	if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
	{
		RTC_ClearITPendingBit(RTC_IT_ALR);		//清除闹钟标志位	  	
	  	RTC_Get();				//获取时间
  		printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);	
  	} 				  								 
	RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);		//清除秒中断标志位
	RTC_WaitForLastTask();	  	    						 
}



几个注意点

  • 通过RTC_CRL寄存器中的第5位,RTC操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC寄存器的操作是否已经完成,如果没有,我们必须等待上一次操作结束后才能开始下一次的操作。
  • 关于预分频系数,这里我们设置RTC预分频装载寄存器的值为32767,因为我们使用外部32.768k的晶振作为时钟的输入频率。这里,给的公式是fTR_CLK = fRTCCLK/(PRL[19:0]+1) = 32.768k/(32767+1) = 1s,也就是配置计数1s,也可以使用别的寄存器,如RTC预分频器余数计数器,可以用来获得比秒钟更为准确的时钟,如0.1s,0.01s等等。
  • 关于寄存器同步问题。在没有同步的情况下修改 RTC_CRH / RTC_CRL的值是不行的。
  • 每一秒产生一次秒中断,在中断服务函数里面,有闹钟的中断和秒的中断,所以我们需要作出判断。通过函数RTC_Get(),我们来更新结构体的数值,然后显示在LCD上。
  • 关于配置模式,由控制寄存器CRL的位4可以知道,该位由软件置1以进入配置模式,从而允许向RTC_CNT(计数寄存器)、RTC_ALR(闹钟寄存器)、或RTC_PRL(预分频寄存器)写入数据。只有当此位被置1并重新由软件清0后,才会执行此操作。



运行结果

在这里插入图片描述




猜你喜欢

转载自blog.csdn.net/qq_40318498/article/details/100177352