參考資料: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 的允許配置位,設置時間或者設置鬧鐘。
寄存器方式:
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屏幕直接數字修改時間,後面再改改。
有錯誤的話歡迎指出來呀