STM32之RTC實時時鐘
RTC實時時鐘簡介:
STM32的RTC外設,實質是一個掉電後還繼續執行的定時器,從定時器的角度來看,相對於通用定時器TIM外設,它的功能十分簡單,只有計時功能(也可以觸發中斷).但是從掉電還能繼續執行來看,它是STM32中唯一一個具有這個功能功能的外設.(RTC外設的複雜之處不在於它的定時,而在於它掉電還可以繼續執行的特性)
所謂掉電,是指電源Vpp斷開的情況下,為了RTC外設掉電可以繼續執行,必須給STM32晶片通過VBAT引腳街上鋰電池.當主電源VDD有效時,由VDD給RTC外設供電.當VDD掉電後,由VBAT給RTC外設供電.無論由什麼電源供電,RTC中的資料始終都儲存在屬於RTC的備份域中,如果主電源和VBA都掉電,那麼備份域中儲存的所有資料都將丟失.(備份域除了RTC模組的暫存器,還有42個16位的暫存器可以在VDD掉電的情況下儲存使用者程式的數序,系統復位或電源復位時,這些資料也不會被複位).
從RTC的定時器特性來說,它是一個32位的計數器,只能向上計數.他使用的時鐘源有三種,分別為:
1,高速外部時鐘的128分頻:HSE/128;
2,低速內部時鐘LSI;
3,低速外部時鐘LSE;
使用HSE分頻時鐘或者LSI的時候,在主電源VDD掉電的情況下,這兩個時鐘來源都會受到影響,因此沒法保證RTC正常工作.所以RTC一般都時鐘低速外部時鐘LSE,頻率為實時時鐘模組中常用的32.768KHz,因為32768 = 2^15,分頻容易實現,所以被廣泛應用到RTC模組.(在主電源VDD有效的情況下(待機),RTC還可以配置鬧鐘事件使STM32退出待機模式).
RTC工作過程:
RTC架構:
圖中淺灰色的部分都是屬於備份域的,在VDD掉電時可在VBAT的驅動下繼續執行.這部分僅包括RTC的分頻器,計數器,和鬧鐘控制器.若VDD電源有效,RTC可以觸發RTC_Second(秒中斷)、RTC_Overflow(溢位事件)和RTC_Alarm(鬧鐘中斷).從結構圖可以看到到,其中的定時器溢位事件無法被配置為中斷.如果STM32原本處於待機狀態,可由鬧鐘事件或WKUP事件(外部喚醒事件,屬於EXTI模組,不屬於RTC)使它退出待機模式.鬧鐘事件是在計數器RTC_CNT的值等於鬧鐘暫存器RTC_ALR的值時觸發的.
因為RTC的暫存器是屬於備份域,所以它的所有暫存器都是16位的.它的計數RTC_CNT的32位由RTC_CNTL和RTC_CNTH兩個暫存器組成,分別儲存計數值的低16位和高16位.在配置RTC模組的時鐘時,把輸入的32768Hz的RTCCLK進行32768分頻得到實際驅動計數器的時鐘TR_CLK = RTCCLK/37768 = 1Hz,計時週期為1秒,計時器在TR_CLK的驅動下計數,即每秒計數器RTC_CNT的值加1(常用)
由於備份域的存在,使得RTC核具有了完全獨立於APB1介面的特性,也因此對RTC暫存器的訪問要遵守一定的規則.
系統復位後,禁止訪問後備暫存器和RCT,防止對後衛區域(BKP)的意外寫操作.(執行以下操作使能對後備暫存器好RTC的訪問):
1,設定RCC_APB1ENR暫存器的PWREN和BKPEN位來使能電源和後備介面時鐘.
2,設定PWR_CR暫存器的DBP位使能對後備暫存器和RTC的訪問.
設定為可訪問後,在第一次通過APB1介面訪問RTC時,必須等待APB1與RTC外設同步,確保被讀取出來的RTC暫存器值是正確的,如果在同步之後,一直沒有關閉APB1的RTC外設介面,就不需要再次同步了.
如果核心要對RTC暫存器進行任何的寫操作,在核心發出寫指令後,RTC模組在3個RTCCLK時鐘之後,才開始正式的寫RTC暫存器操作.我們知道RTCCLK的頻率比核心主頻低得多,所以必須要檢查RTC關閉操作標誌位RTOFF當這個標誌被置1時,寫操作才正式完成.
(以上操作在STM32庫裡面都有庫函式,不需要具體的查閱暫存器~~~~)
UNIX時間戳:
假如從現在起,把計數器RTC_CNT的計數值置0,然後每秒加1,RTC_CNT什麼時候會溢位? RTC_CNT是一個32位暫存器,可儲存的最大值為(2^32-1),這樣的話就是在2^32秒之後溢位,大概換算為:
Time = 2^32/365/24/60/60大約等於136年
假如某個時刻讀取到計數器的數值為X = 60*60*24*2(2天),又知道計數器是在2016年1月1日的0時0分0秒置0的,那麼根據計數器的這個相對時間數值,可以計算得到這個時刻是2016年1月3日的0時0分0秒了,而計數器會在(2016+136)年左右溢位.(如果我們穿越回到2016年1月1日,如果還在使用這個計數器提供事件的話就會出問題啦.).
定時器被置0的這個事件被稱為計時元年,相對計時元年經過的秒數稱為時間戳.
PS:
大多數作業系統都是利用時間戳和計時元年來計算當前時間的,而這個時間戳和計時元年大家都取了同一個標準——UNIX時間戳和UNIX計時元年.UNIX 計時元年被設定為格林威治時間1970年1月1日0時0分0秒,大概是為了紀念UNIX的誕生吧.而UNIX時間戳即為當前時間相對於UNIX計時元年經過的秒數.在這個計時系統中,使用的是有符號的32位整型變數來儲存UNIX時間戳的,即實際可用計數位數比我們上面例子中的少了一位,少了這一位,UNIX 計時元年也相對提前了,這個計時方法在2038年1月19日03時14分07秒將會發生溢位.這個時間離我們並不遠,UNIX時間戳被廣泛應用到各種系統中,溢位可能會導致系統發生嚴重錯誤,差不多到這個時候,記得注意這個問題呀.
例項分析:
利用RTC提供北京時間:
RTC外設這個連續計數的計數器,在相應軟體配置下,可提供時鐘日曆的功能,修改計數器的值則可以重新設定系統當前的時間和日期.而 由於它的時鐘配置系統(RCC_BDCR 暫存器)是在備份域,在系統復位或從待機模式喚醒後RTC的設定和時間維持不變,利用它,可以實現實時時鐘的功能.
main函式:
struct rtc_time systmtime;
int main(void)
{
/串列埠配置/
USART1_Config();
/配置RTC秒中斷優先順序/
NVIC_Configuration();
//RTC檢測及配置
RTC_CheckAndConfig(&systmtime);
//重新整理時間
Time_Show(&systmtime);
}
main函式流程:
1,用到了串列埠,配置好串列埠(程式碼和之前的例程一樣);
2,配置RTC秒中斷優先順序,這裡設定主優先順序為1,次優先順序為0(只用到一個RTC,中斷隨便寫都可以).(程式碼和之前的中斷例程相似,只不過中斷通道不一樣,這裡使用的中斷通道是RTC_IRQn);
3,檢視RTC外設是否在本次VDD上電前被配置過,如果沒有被配置過,則需要輸入當前時間,重新初始化RTC和配置時間;
4,配置好RTC後,根據秒中斷設定的標誌位,每隔1秒向終端更新一次;
事件管理結構體 rtc_time
struct rtc_time
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
}
這個型別的結構體有時,分,秒,日,月,年及星期7個成員.當需要給RTC的計時器重新配置時間時(更改時間戳),肯定不會詢問使用者現在距離UNIX計時元年過了多少秒,而是向用戶詢問現在的公元紀年,以及所在時區的事件.根據RTC計時器向用戶輸出時間.
這就是 rtc_time 這個結構體的作用,配置RTC時,儲存使用者輸入的時間,其它函式通過它求出UNIX時間戳,寫入RTC,RTC正常執行後,需要輸出時間時,其它函式通過RTC獲取UNIX時間戳,轉化成用友好的時間表示方式儲存在這個結構體上.
PS:
起始在C語言標準庫ANSI C中,也有類似的結構體所以 struct tm,位於標準的time.h檔案中,轉化函式是mktime()和localtime(),分別把tm結構體成員轉化成時間戳和用時間戳轉化成結構體成員.
檢查RTC RTC_CheckAndConfig()
void RTC_CheckAndConfig(struct rtc_time *tm)
{
/檢查備份暫存器BKP_DR1,內容不為0xA5A5,則需要重新配置時間並且詢問使用者調整時間/
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
printf(“\r\n\r\n RTC not yet configured….”);
/* RTC 配置 */
RTC_Configuration();
printf(“\r\n\r\n RTC configured….”);
/* 使用者輸入時間*/
Time_Adjust(tm);
/再往備份暫存器BKP_DR1寫入0xA5A5/
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
/啟動無需設定新時鐘/
else
{
/檢查是否掉電重啟/
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
{
printf(“\r\n\r\n Power On Reset occurred….”);
}
/檢查是否Reset復位/
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
{
printf(“\r\n\r\n External Reset occurred….”);
}
printf(“\r\n No need to configure RTC….”);
/等待暫存器同步/
RTC_WaitForSynchro();
/允許RTC秒中斷/
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/等待上次RTC暫存器寫操作完成/
RTC_WaitForLastTask();
}
/定義了時鐘輸出巨集,則配置校正時鐘輸出到 PC13,用於RTC時鐘頻率的校準或調整時間補償/
#ifdef RTCClockOutput_Enable
/使能PWR和BKP的時鐘/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/允許訪問BKP備份域/
PWR_BackupAccessCmd(ENABLE);
/輸出64分頻時鐘/
BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
#endif
RCC_ClearFlag();
}
if語句呼叫BKP_ReadBackupRegister()讀取RTC備份域暫存器裡面的值,判斷備份暫存器裡面的是否正確,根據後面程式碼,如果配置成功,會向備份域暫存器寫入數值0xA5A5.
(這個數值在VDD掉電後仍然會儲存,如果VBAT也掉電,那麼備份域,RTC所有暫存器將被複位,這時這個暫存器的值就不會等於0xA5A5了,RTC的計數器的值也是無效的.
簡單的說,就是寫入的這個數值用作標誌RTC是否從未被配置或配置是否已經失效,然後寫入任何數值到任何一個備份域暫存器,只要檢查的時候與寫入值匹配就行了)
RTC未被配置或者配置已經失效的情況:
1,如果RTC從未被配置或者配置已經失效(備份域暫存器寫入值等於0xA5A5)這兩種情況其中一種為真的話,則呼叫RTC_Configuration()來初始化RTC,配置RTC外設的控制引數,時鐘分頻等,並往電腦的超級終端打印出相應的除錯資訊;
2,初始化好RTC之後,呼叫函式 Time_Adjust() 讓使用者鍵入(通過超級終端輸入)時間值;
3,輸入時間值後,Time_Adjust() 函式把使用者輸入的北京時間轉化為UNIX時間戳,並把這個UNIX時間戳寫入到RTC外設的計數暫存器RTC_CNT.接著RTC外設在這個時間戳的基礎上,每秒對RTC_CNT加1,RTC時鐘就執行起來了,並且在VDD掉電還執行,以後需要知道時間就直接讀取RTC的計時值,就可以計算出時間了;
4,設定好時間後,呼叫BKP_WriteBackupRegister()把0xA5A5這個值寫入備份域暫存器,作為配置成功的標誌;
確認RTC曾經被配置過的情況:
1,呼叫RCC_GetFlagStatus檢測是上電覆位還是按鍵復位,根據不同的復位情況在超級終端中打印出不同的除錯資訊(兩種復位都不需要重新設定RTC裡面的時間值);
2,呼叫RTC_WaitForSynchro等待APB1介面與RTC外設同步,上電後第一次通過APB1介面訪問RTC時必須要等待同步;
3,同步完成後呼叫RTC_ITConfig()使能RTC外設的秒中斷(使能RTC的秒中斷是一個對RTC外設暫存器的寫操作);
4,進行寫操作以後,必須呼叫RTC_WaitForLastTask()來等待,確保寫操作完成;
在下面有一個條件編譯選項詢問是否需要output RTCCLK/64 on Tamper pin,這是RTC的時鐘輸出配置,在rtc的標頭檔案定義 RTCClockOutput_Enable這個巨集,PC13引腳會輸出RTCCLK的64分頻時鐘,主要是用於RTC時鐘頻率的校準或調整時間補償.
(如果需要用到這個時鐘訊號的話,只需要在標頭檔案定義RTCClockOutput_Enable這個巨集就行了,不要定義為0值就行了~~~~)
初始化RTC RTC_Configuration():
void RTC_Configuration(void)
{
/使能PWR和BKP時鐘/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
/對備份域進行軟體復位/
PWR_BackupAccessCmd(ENABLE);
/對備份域進行軟體復位/
BKP_DeInit();
/* 使能低速外部時鐘 LSE */
RCC_LSEConfig(RCC_LSE_ON);
/* 等待LSE起振穩定 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{}
/* 選擇LSE作為 RTC 外設的時鐘*/
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
/* 使能RTC時鐘 */
RCC_RTCCLKCmd(ENABLE);
/* 等待RTC暫存器與APB1同步*/
RTC_WaitForSynchro();
/* 等待對RTC的寫操作完成*/
RTC_WaitForLastTask();
/* 使能RTC秒中斷 */
RTC_ITConfig(RTC_IT_SEC, ENABLE);
/* 等待對RTC的寫操作完成 */
RTC_WaitForLastTask();
/* 設定RT 時鐘分頻: 使RTC定時週期為1秒 */
RTC_SetPrescaler(32767);
/* RTC 週期 = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */
/等待對RTC的寫操作完成 /
RTC_WaitForLastTask();
}
在這個初始化函式裡,沒有見到熟悉的初始化結構體,對RTC的每一個初始化引數都是使用相應的庫函式來配置的.RTC作為備份域的一份子,在訪問前首先要使能備份域、電源管理外設的時鐘,設定備份域訪問許可權,作為定時器,初始化時必須要選擇好時鐘來源,時鐘分頻.
時間調節Time_Adjust():
void Time_Adjust(struct rtc_time *tm)
{
/* 等待前面可能的 RTC 寫操作完成 */
RTC_WaitForLastTask();
/* 利用串列埠,在終端向用戶詢問當前北京時間(年月日時分秒),
寫入到 rtc_time 型結構體 */
Time_Regulate(tm);
/* 計算輸入的日期是星期幾,把rtc_time型結構體填充完整 */
GregorianDay(tm);
/* 根據輸入日期,計算出 UNIX 時間戳,修改當前 RTC 計數暫存器內容*/
RTC_SetCounter(mktimev(tm));
/* 等待 RTC 寫操作完成 */
RTC_WaitForLastTask();
}
這裡流程就是使用Time_Regulate()從終端獲取當前北京時間,然後根據使用者的輸入,呼叫函式mktimev()根據使用者輸入的年,月,日,時,.分,秒資料,計算出相應的UNIX時間戳,最後呼叫庫函式RTC_SetCounter()把這個UNIX時間戳寫入到計數器RTC_CNT,RTC就正式運行了.
獲取時間Time_Regulate():
void Time_Reglate(struct rtc_time *tm)
{
u32 Tmp_YY = 0xFF, Tmp_MM = 0xFF, Tmp_DD = 0xFF, Tmp_HH =0xFF, Tmp_MI = 0xFF, Tmp_SS = 0xFF;
printf("\r\n==========Time Settings==================");
printf("\r\n 請輸入年份(Please Set Years): 20");
while (Tmp_YY == 0xFF)
{
Tmp_YY = USART_Scanf(99);
}
printf("\n\r 年份被設定為: 20%0.2d\n\r", Tmp_YY);
tm->tm_year = Tmp_YY+2000;
Tmp_MM = 0xFF;
printf("\r\n 請輸入月份(Please Set Months): ");
while (Tmp_MM == 0xFF)
{
Tmp_MM = USART_Scanf(12);
}
printf("\n\r 月份被設定為: %d\n\r", Tmp_MM);
tm->tm_mon= Tmp_MM;
Tmp_DD = 0xFF;
printf("\r\n 請輸入日期(Please Set Dates): ");
while (Tmp_DD == 0xFF)
{
Tmp_DD = USART_Scanf(31);
}
printf("\n\r 日期被設定為: %d\n\r", Tmp_DD);
tm->tm_mday= Tmp_DD;
Tmp_HH = 0xFF;
printf("\r\n 請輸入時鐘(Please Set Hours): ");
while (Tmp_HH == 0xFF)
{
Tmp_HH = USART_Scanf(23);
}
printf("\n\r 時鐘被設定為: %d\n\r", Tmp_HH );
tm->tm_hour= Tmp_HH;
Tmp_MI = 0xFF;
printf("\r\n 請輸入分鐘(Please Set Minutes): ");
while (Tmp_MI == 0xFF)
{
Tmp_MI = USART_Scanf(59);
}
printf("\n\r 分鐘被設定為: %d\n\r", Tmp_MI);
tm->tm_min= Tmp_MI;
Tmp_SS = 0xFF;
printf("\r\n 請輸入秒鐘(Please Set Seconds): ");
while (Tmp_SS == 0xFF)
{
Tmp_SS = USART_Scanf(59);
}
printf("\n\r 秒鐘被設定為: %d\n\r", Tmp_SS);
tm->tm_sec= Tmp_SS;
}
這裡就是在裡面從終端獲取使用者輸入的時間,要留意的是,從終端輸入的ASCII碼,而不是實際數值(在USART_Scanf裡面做處理)
PS:這裡補上USART_Scanf()的程式碼,之前串列埠篇的時候好像沒有附上
static uint8_t USART_Scanf(uint32_t value)
{
uint32_t index = 0;
uint32_t tmp[2] = {0, 0};
while (index < 2)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ==RESET)
{}
tmp[index++] = (USART_ReceiveData(USART1));
/*數字0到9的ASCII碼為0x30至0x39*/
if((tmp[index - 1] < 0x30) || (tmp[index -1] > 0x39))
{
printf("\n\rPlease enter valid number between 0 and 9 -->: ")
index--;
}
}
/* 計算輸入字元的 ASCII 碼轉換為數字*/
index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10);
if (index > value)
{
printf("\n\rPlease enter valid number between 0 and %d", value);
return 0xFF;
}
return index;
}
計算UNIX時間戳mktimev():
從使用者端獲取了北京時間後,就可以用它換成 UNIX 時間戳了,但不能忽略一個重要的問題——時差.UNIX時間戳的計時元年是以標準時間(GMT 時區)為準的,而北京時間為 GMT+8,即時差為+8小時.為了保證我們寫入到RTC_CNT的是標準的UNIX時間戳(主要是為了相容),以北京時間轉化出的秒數要減去8*60*60才是標準的UNIX時間戳.
u32 mktimev(struct rtc_time *tm)
{
if (0 >= (int) (tm->tm_mon -= 2))
{
tm->tm_mon += 12;
tm->tm_year -= 1;
}
/計算出輸入的北京時間的一共的秒數/
return((( (u32)(tm->tm_year/4 - tm->tm_year/100 + tm->tm_year/400 + 367*tm->tm_mon/12 + tm->tm_mday)
+ tm->tm_year*365 - 719499)*24 + tm->tm_hour)*60 + tm->tm_min)*60 + tm->tm_sec-8*60*60;
/8*60*60把輸入的北京時間轉換為標準時間在寫入計時器中,確保計時器的資料為標準UNIX時間戳/
}
8*60*60把輸入的北京事件轉換為標準事件在寫入計時器中,確保計時器的資料為標準UNIX時間戳,如果向使用其他時區,則根據不同喲的時區修改這個值.
返回值最終被寫入到RTC_CNT計數器中RTC_SetCounter(mktimev(tm));
輸出時間到終端Time_Show():
void Time_Show(struct rtc_time *tm)
{
while (1)
{
/每個1s/
if(TimeDisplay == 1)
{
/顯示時間/
Time_Display(RTC_GetCounter(),tm);
TimeDisplay = 0;
}
}
}
TimeDisplay是RTC秒中斷標誌,RTC的秒中斷被觸發後,進入中斷服務函式,把這個變數 TimeDisplay置1.這個函式是死迴圈檢查這個標誌,變為1時,呼叫Time_Display()顯示最新時間,實現每隔1秒向終端更新一次時間,更新完後再把 TimeDisplay置0,等待下次秒中斷.
RTC秒中斷服務函式:
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
/* 清除秒中斷標誌 */
RTC_ClearITPendingBit(RTC_IT_SEC);
/* 把標誌位置 1 */
TimeDisplay = 1;
/* 等待寫操作完成 */
RTC_WaitForLastTask();
}
}
在這個函式中並沒有任何對RTC_CNT的操作,如果VDD掉電,RTC是無法觸發秒中斷的,所以想利用秒中斷的方案實現實時時鐘是不現實的,秒中斷最適合用在類似本例程的觸發顯示的時間更新場合,而不是用於計數.
顯示時間Time_Display():
void Time_Display(uint32_t TimeVar,struct rct_time *tm)
{
static uint32_t FirstDisplay = 1;
uint32_t BJ_TimeVar;
uint8_t str[15]; // 字串暫存
/* 把標準時間轉換為北京時間*/
BJ_TimeVar =TimeVar + 8*60*60;
/*利用時間戳轉換為北京時間*/
to_tm(BJ_TimeVar, tm);
if((!tm->tm_hour && !tm->tm_min && !tm->tm_sec) || (FirstDisplay))
{
GetChinaCalendar((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str);
printf("\r\n\r\n 今天農曆:%0.2d%0.2d,%0.2d,%0.2d", str[0], str[1], str[2], str[3]);
GetChinaCalendarStr((u16)tm->tm_year,(u8)tm->tm_mon,(u8)tm->tm_mday,str);
printf(" %s", str);
if(GetJieQiStr((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str))
{
printf(" %s\n\r", str);
}
FirstDisplay = 0;
}
printf("\r UNIX 時間戳 = %d ,當前時間為: %d 年(%s 年) %d 月 %d日 (星期%s) %0.2d:%0.2d:%0.2d",TimeVar,tm->tm_year, zodiac_sign[(tm->tm_year-3)%12], tm->tm_mon, tm->tm_mday,WEEK_STR[tm->tm_wday], tm->tm_hour,tm->tm_min, tm->tm_sec);
}
這裡的第一個輸入引數為UNIX時間戳,在Time_Show()呼叫的時候,利用庫函式RTC_GetCounter()讀取了RTC_CNT的當前數值,並把這個計數值作為Time_Dispaly()的輸入引數.
根據配置,RTC_CNT的計數值是標準時間GMT的UNIX時間戳,為了計算北京時間,在使用RTC_CNT計數值轉換北京時間時,要加上時差(BJ_TimeVar =TimeVar + 8*60*60;).之後,把這個變數 BJ_TimeVar作為函式 to_tm()的輸入引數,把時間戳轉換成年,月,日,時,分,秒的格式,並儲存到時間結構體中.
(to_tm()(純演算法)和GetChinaCalendar()這裡就不展開了,需要的話可以留言我會發送給你)
PS:
如果要使用普通的51晶片實現實時時鐘,需要藉助時鐘晶片,DS1302或DS12C887,在STM32裡面只要用到一個定時器就搞掂了!!!