【STM32】RTC时钟学习笔记,库函数和寄存器步骤(可修改时间)

参考资料:STM32中文参考手册;正点原子STM32开发指南

RTC时钟简介

实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器。修改计数器的值可以重新设置系统当前的时间和日期。 RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。 系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
● 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
● 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。

主要特性

● 可编程的预分频系数:分频系数高为220。
● 32位的可编程计数器,可用于较长时间段的测量。
● 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟 频率的四分之一以上)。
● 可以选择以下三种RTC的时钟源:
─ HSE时钟除以128;
─ LSE振荡器时钟;
─ LSI振荡器时钟
● 2个独立的复位类型:
─ APB1接口由系统复位;
─ RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位
● 3个专门的可屏蔽中断:
─ 闹钟中断,用来产生一个软件可编程的闹钟中断。
─ 秒中断,用来产生一个可编程的周期性中断信号(长可达1秒)。
─ 溢出中断,指示内部可编程计数器溢出并回转为0的状态。

RTC描述和框图

RTC由两个主要部分组成(参见下图)。
第一部分(APB1接口)用来和APB1总线相连。此单元还包 含一组16位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线 时钟驱动,用来与APB1总线接口。
另一部分(RTC核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是RTC的预分频模块,它可编程产生长为1秒的RTC时间基准TR_CLK。RTC的预分频模块包含了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。第二个模块是一个32位的可编程计数器,可被初始 化为当前的系统时间。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时 间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断
在这里插入图片描述

在看配置步骤之前我自己是偏向于看寄存器版本的,更能理解实际的过程,但是我们常常使用库函数方式,因为进行了封装比较方便。

RTC正常工作的一般配置步骤

1.使能电源时钟和备份区域时钟
这也是很多配置过程的第一步,可以通过RCC_APB1ENR寄存器来设置。在中文参考手册中是设置寄存器RCC_APB1ENR的PWREN和BKPEN位
在这里插入图片描述
寄存器方式:RCC_APB1ENR=1<<28; RCC_APB1ENR=1<<27;
库函数方式:RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |RCC_APB1Periph_BKP, ENABLE);

2.取消备份区写保护
要向备份区写入数据先要取消备份区写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。
设置寄存器PWR_CR(电源控制寄存器)的DBP位,使能对后备寄存器和RTC的访问。
在这里插入图片描述
寄存器方式:PWR->CR|=1<<8;
库函数方式:PWR_BackupAccessCmd(ENABLE);

3.复位备份区域,开启外部低速振荡器
在取消备份区域写保护之后,可以先对这个区域复位,可以清除前面的设置,然后可以使能外部低速振荡器,这里一般要先判断RCC_BDCR(备份域控制寄存器)的LSERDY位来确定低速振荡器已经就绪。

在这里插入图片描述
寄存器方式:
RCC->BDCR|=1<<16; //备份区域软件复位
RCC->BDCR&=~(1<<16); //备份区域软件复位结束
RCC->BDCR|=1<<0;//开启外部低速振荡器
RCC->BDCR|=0X02; //外部低速LSE就绪
库函数方式:
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON);//设置外部低速晶振
RCC_GetFlagStatus(RCC_FLAG_LSERDY) = RESET; //外部低速LSE就绪
说明:在最后一步外部低速LSE就绪,一般是在if中用于判断,用==号。

4.选择RTC时钟,并使能
这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSE(32.768K 的外部晶振)作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟,为什么选这个时钟??是通过时钟树决定的,RTC时钟可以有三个来源
在这里插入图片描述
在这里插入图片描述
寄存器方式:
RCC->BDCR|=1<<8;
RCC->BDCR|=1<<15;
库函数方式:
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);

5.设置RTC的分频,以及配置RTC时钟
在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL (RTC预分频装载寄存器)来设置,但是在设置RTC时钟分频数时,要先检查RTC_CR寄存器的RTOFF位。

预分频装载寄存器用来保存RTC预分频器的周期计数值。它们受RTC_CR寄存器的RTOFF位保护,仅当RTOFF值为’1’时允许进行写操作。

然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位,设置时间或者设置闹钟。
![RTC(https://img-blog.csdnimg.cn/20200408184248329.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0NzA4NDI2,size_16,color_FFFFFF,t_70)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
寄存器方式:
while(RTC->CRL&=(1<<5));//检查RTC寄存器的RTOFF位
while(!(RTC->CRL&(1<<3))); //等待 RTC 寄存器同步
RTC->CRH|=0X01; //允许秒中断
RTC->CRH|=0X02; //允许闹钟中断
while(!(RTC->CRL&(1<<5)));//等待 RTC 寄存器操作完成
RTC->CRL|=1<<4; //允许配置
RTC->PRLH=0X0000;
RTC->PRLL=32767; //时钟周期设置 理论值:32767

库函数方式:
RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成
RTC_WaitForSynchro(); //等待 RTC 寄存器同步 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待 RTC 寄存器操作完成 RTC_EnterConfigMode(); // 允许配置
RTC_SetPrescaler(32767); //设置 RTC 预分频的值

6.更新配置,设置 RTC 中断
在设置完时钟之后,我们将配置更新,这里还是通过 RTC_CRH 的 CNF 来实现。在这之后 我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化过时钟了,下次开机(或复位) 的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来决定是不是要配置,避免重复配置。接着我们配置 RTC 的秒钟中断,并进行分组。
寄存器方式:
RTC->CRL&=~(1<<4); //配置更新
while(!(RTC->CRL&(1<<5))); //等待 RTC 寄存器操作完成
BKP->DR1=0X5050; //标记已经配置过

库函数方式:
RTC_WaitForLastTask(); //等待 RTC 寄存器操作完成
RTC_ExitConfigMode(); //退出配置模式 BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中写入用户程序数据 0x5050

在退出配置模式之前可以进行时间设置

7.编写中断服务函数

设置时间函数RTC_Set()
该函数用于设置时间,把我们输入的时间,转换为以 1970 年 1 月 1 日 0 时 0 分 0 秒当做起 始时间的秒钟信号,后续的计算都以这个时间为基准的。
在这里插入图片描述

//设置时钟
//把输入的时钟转换为秒钟 
//以 1970 年 1 月 1 日为基准 
//1970~2099 年为合法年份 
//返回值:0,成功;其他:错误代码. 
//月份数据表            
 u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表    
//平年的月份日期表 
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) 
{  
	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;//闰年的秒钟数
		else seccount+=31536000;     //平年的秒钟数  
	}  
	smon-=1;  
	for(t=0;t<smon;t++)  //把前面月份的秒钟数相加  
	{   
		seccount+=(u32)mon_table[t]*86400;   //月份秒钟数相加   
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年 2 月份增加一天的秒钟数      
	}  
	seccount+=(u32)(sday-1)*86400; //把前面日期的秒钟数相加   	 
	seccount+=(u32)hour*3600;  //小时秒钟数     
	seccount+=(u32)min*60;    //分钟秒钟数 
	seccount+=sec;   //最后的秒钟加上去  
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);  //使能 PWR 和 BKP 外设时钟    
	PWR_BackupAccessCmd(ENABLE);   //使能 RTC 和后备寄存器访问   
	RTC_SetCounter(seccount);  //设置 RTC 计数器的值  
	RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成    
	return 0;      
}

用于获取时间和日期等数据函数RTC_Get()
函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据转换为真正的时间和日期。该代码还用到了一个 calendar 的结构体。 因为 STM32 的 RTC 只有秒钟计数器,而年月日,时分秒这些需要我们自己软件计算。把计算好的值保存在 calendar 里面,方便其他程序调用。

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;	
//得到当前的时间,结果保存在 calendar 结构体里面 
//返回值:0,成功;其他:错误代码. 
u8 RTC_Get(void) 
{   
	static u16 daycnt=0;  
	u32 timecount=0;   
	u32 temp=0;  
	u16 temp1=0;      
	timecount=RTC->CNTH;     //得到计数器中的值(秒钟数)  
	timecount<<=16;  
	timecount+=RTC->CNTL;       
	temp=timecount/86400;  //得到天数(秒钟数对应的) 
	if(daycnt!=temp)    //超过一天了  
	{
		daycnt=temp;   
		temp1=1970;  //从 1970 年开始   
		while(temp>=365)   
		{         
			if(Is_Leap_Year(temp1))   //是闰年    
			{     
				if(temp>=366)temp-=366; //闰年的秒钟数     
				else break;      
			}    
			else temp-=365;   //平年     
			temp1++;     
		}      
		calendar.w_year=temp1;   //得到年份   
		temp1=0;   
		while(temp>=28)      //超过了一个月 
		{    
			if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2 月份    
			{     
				if(temp>=29)temp-=29;//闰年的秒钟数  
				else break;     
			}    
			else     
			{   
				if(temp>=mon_table[temp1])temp-=mon_table[temp1];	//平年    
				else break;    
			}    
			temp1++;     
		}   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 时钟中断 
//每秒触发一次   
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();                      
}

最后是想要用按键调整时间,后面再改成TFTLCD试试吧,main函数如下,按键写在外部中断里面:

 int main(void)
 {	 
 	u8 t=0;	
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
 	LED_Init();			     //LED端口初始化
	LCD_Init();		
	EXTIX_Init();	 
	usmart_dev.init(SystemCoreClock/1000000);	//初始化USMART	
	RTC_Init();	  			//RTC初始化
	//显示时间
	POINT_COLOR=BLUE;//设置字体为蓝色
	LCD_ShowString(60,130,200,16,16,"    -  -  ");	   
	LCD_ShowString(60,170,200,16,16,"  :  :  ");		    
	while(1)
	{	 
		if(t!=calendar.sec)
		{
			t=calendar.sec;
			LCD_ShowNum(60,130,calendar.w_year,4,16);									  
			LCD_ShowNum(100,130,calendar.w_month,2,16);									  
			LCD_ShowNum(124,130,calendar.w_date,2,16);	 
			switch(calendar.week)
			{
				case 0:
					LCD_ShowString(60,148,200,16,16,"Sunday   ");
					break;
				case 1:
					LCD_ShowString(60,148,200,16,16,"Monday   ");
					break;
				case 2:
					LCD_ShowString(60,148,200,16,16,"Tuesday  ");
					break;
				case 3:
					LCD_ShowString(60,148,200,16,16,"Wednesday");
					break;
				case 4:
					LCD_ShowString(60,148,200,16,16,"Thursday ");
					break;
				case 5:
					LCD_ShowString(60,148,200,16,16,"Friday   ");
					break;
				case 6:
					LCD_ShowString(60,148,200,16,16,"Saturday ");
					break;  
			}
			LCD_ShowNum(60,170,calendar.hour,2,16);									  
			LCD_ShowNum(84,170,calendar.min,2,16);									  
			LCD_ShowNum(108,170,calendar.sec,2,16);
			LED0=!LED0;
		}	
		delay_ms(10);								  
	}
 }
u8 cnt=0;
//外部中断0服务程序 
void EXTI0_IRQHandler(void)
{
	delay_ms(10);//消抖
	if(WK_UP==1)	 	 //WK_UP按键
	{				 
		if(cnt<4)
		{
			cnt++;
		}
		else
		{
			cnt=0;
		}
	}
	EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位  
}
 

//外部中断3服务程序
void EXTI3_IRQHandler(void)
{
	delay_ms(10);//消抖
	if(KEY1==0&&cnt==0)	 //年
	{				 
		RTC_Set(calendar.w_year+1,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
	}
	else if(KEY1==0&&cnt==1)	//月
	{
		RTC_Set(calendar.w_year,calendar.w_month+1,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
	}
	else if(KEY1==0&&cnt==2)	//日
	{
		RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date+1,calendar.hour,calendar.min,calendar.sec);
	}
	else if(KEY1==0&&cnt==3)	//时
	{
		RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour+1,calendar.min,calendar.sec);
	}
	else if(KEY1==0&&cnt==4)	//分
	{
		RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min+1,calendar.sec);
	}
	EXTI_ClearITPendingBit(EXTI_Line3);  //清除LINE3上的中断标志位  
}

void EXTI4_IRQHandler(void)
{
	delay_ms(10);//消抖
	if(KEY0==0&&cnt==0)	 //年
	{				 
		RTC_Set(calendar.w_year-1,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
	}	
	else if(KEY0==0&&cnt==1)	//月
	{
		if(calendar.w_month>0)
		{
			RTC_Set(calendar.w_year,calendar.w_month-1,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
		}
	}
	else if(KEY0==0&&cnt==2)	//日
	{
		if(calendar.w_date>0)
		{
			RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date-1,calendar.hour,calendar.min,calendar.sec);
		}
	}
	else if(KEY0==0&&cnt==3)	//时
	{
		if(calendar.hour>0)
		{
			RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour-1,calendar.min,calendar.sec);
		}
	}
	else if(KEY0==0&&cnt==4)	//分
	{
		if(calendar.min>0)
		{
			RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min-1,calendar.sec);
		}
	}
	EXTI_ClearITPendingBit(EXTI_Line4);  //清除LINE4上的中断标志位  
}

因为在减时间的时候会出现bug,目前还没看出什么毛病,所以就不允许时间跨度递减,比如从2020-1-1把月份减一变成2019-12-31可能就会出现bug,导致时钟紊乱,其他日,时,分也是。

在这里运用KEY0对时间减,KEY1对时间加,KEY_UP换位,这里用到了cnt这个标志位,初始是cnt=0,表示调整年,cnt=1,表示调整月,依此类推,但是不对秒进行调整,原因是当按键触发中断的时候要先运行中断函数对时间进行调整,导致秒中断被打断,所以秒会不准。

实验结果

在这里插入图片描述
这是毕业设计里面的一小部分,后面再把毕业设计里面的东西再总结一遍吧,其实想用TFTLCD屏幕直接数字修改时间,后面再改改。

有错误的话欢迎指出来呀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章