【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屏幕直接數字修改時間,後面再改改。

有錯誤的話歡迎指出來呀

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