這一節我們要講的主要內容是RTC實時時鐘,實時時鐘本質上是一個定時器,但是這個定時器是專門用來產生年月日時分秒,這種日期和時間信息的。所以學會了STM32的RTC就可以在STM32內部擁有一個獨立運行的鐘表。想要記錄或讀取日期和時間,就可以通過操作RTC來實現。
RTC這個外設比較特殊,它和備份寄存器(BKP)、電源控制(PWR)這兩章關聯性比較強,因此在RTC這一章中,就把BKP和RTC放在一起講。
我們首先就先介紹備份寄存器(BKP),其實備份寄存器(BKP)和SPI章節中學過的Flash存儲器類似,都是用來存儲數據的,只是Flash的數據是真正的掉電不丟失;而BKP的數據是需要VBAT引腳接上備用電池來維持的,只要VBAT有電池供電,即使STM32主電源斷電,BKP的值也可以維持原狀。
首先我們來看一下時間戳的知識點。Unix 時間戳(Unix Timestamp)定義為從UTC/GMT的1970年1月1日0時0分0秒開始到現在所經過的秒數,不考慮閏秒。在計算機的底層我們使用秒計數器來計時,需要給人類觀看時,就轉換為年月日時分秒這樣的格式。時間戳存儲在一個秒計數器中,秒計數器為32位/64位的整型變量,計算機為了存儲這樣一個永不進位的秒數,這個數據變量類型還是要定義大一些,這個變量類型在不同系統中定義是不一樣的。我們本節STM32中的RTC,其核心的計時部分是一個32位的可編程計數器,說明我們這款STM32,它的時間戳是32位的數據類型。世界上所有時區的秒計數器相同,不同時區通過添加偏移來得到當地時間。
GMT(Greenwich Mean Time)格林尼治標準時間是一種以地球自轉為基礎的時間計量系統。它將地球自轉一周的時間間隔等分為24小時,以此確定計時標準。
UTC(Universal Time Coordinated)協調世界時是一種以原子鐘為基礎的時間計量系統。它規定銫133原子基態的兩個超精細能級間在零磁場下躍遷輻射9,192,631,770周所持續的時間為1秒。當原子鐘計時一天的時間與地球自轉一周的時間相差超過0.9秒時,UTC會執行閏秒來保證其計時與地球自轉的協調一致
接下來我們學習時間戳中秒計數器和日期時間如何進行相互轉換,這時候我們需要用到time.h模塊,C語言的time.h模塊提供了時間獲取和時間戳轉換的相關函數,可以方便地進行秒計數器、日期時間和字符串之間的轉換,在time.h里主要有表格內的這些主要函數
時間戳轉換關系如下圖所示
接下來我們學習BKP和RTC的外設部分,我們首先學習BKP的相關知識點。BKP全稱Backup Registers,翻譯過來就是備份寄存器,BKP的用途就是可用于存儲用戶應用程序數據。其特性就是當VDD(2.0~3.6V)電源被切斷,他們仍然由VBAT(1.8~3.6V)維持供電。如果VDD斷電,VBAT也沒電,那BKP里的數據就會清零,因為BKP本質上是RAM存儲器,沒有掉電不丟失的能力。當系統在待機模式下被喚醒,或系統復位或電源復位時,他們也不會被復位;TAMPER引腳產生的侵入事件將所有備份寄存器內容清除,TEMPER是一個接到STM32外部的引腳,其位置就是VBAT旁邊的2號引腳,其與PC13、RTC共用,這個TAMPER引腳是一個安全保障設計,比如如果做一個安全系數非常高的設備,設備需要有防拆功能,BKP里也存儲了一些敏感數據,那就可以使能這個TEMPER引腳的侵入檢測功能;設計者把下面兩個RTC功能也放在了BKP中:? 1.引腳輸出RTC校準時鐘、RTC鬧鐘脈沖或者秒脈沖,RTC的引腳也是在PC13這個位置? ? ?2.存儲RTC時鐘校準寄存器。最后看一下BKP中,用戶數據的存儲容量,在中容量和小容量設備里,BKP是20個字節,在大容量和互聯型設備里,BKP是84個字節。可以看出來BKP的容量其實非常小,一般只能用來存儲少量的參數。BKP的簡介我們就介紹到這里。
下面看一下BKP的基本結構,圖中橙色部分我們可以叫做后備區域,BKP處于后備區域,但后備區域不只有BKP,還有RTC的相關電路。STM32后備區域的特性就是當VDD主電源掉電時,后備區域仍然可以由VBAT的備用電池供電,當VDD主電源上電時,后備區域供電會由VBAT切換到VDD,也就是主電源有電時,VBAT不會用到,這樣可以節省電池電量。BKP是位于后備區域的,BKP里主要有數據寄存器、控制寄存器、狀態寄存器和RTC時鐘校準寄存器。其中數據寄存器是主要部分,用來存儲數據,每個數據寄存器都是16位的,也就是一個數據寄存器可以存2個字節,對于中容量和小容量的設備,里面有DR1、DR2一直到DR10,總共10個數據寄存器,那一個寄存器兩個字節,所以容量是20個字節。對于大容量和互聯型設備,里面除了DR1到DR10還有DR11、DR12一直到DR42,總共42個數據寄存器,容量是84個字節。侵入檢測可以從PC13位置的TAMPER引腳引入一個檢測信號,當TAMPER產生上升沿或者下降沿時,清除BKP所有的內容,以保證安全,時鐘輸出可以把RTC的相關時鐘從PC13位置的RTC引腳輸出出去,供外部使用。其中,輸出校準時鐘時,再配合校準寄存器,可以對RTC的誤差進行校準。以上就是BKP外設的結構和功能。
接下來我們就繼續學習以下RTC外設,RTC英文全稱??Real Time Clock? 中文翻譯為實時時鐘,在STM32中,RTC是一個獨立的定時器,可為系統提供時鐘和日歷的功能,RTC實時時鐘,一般就指提供年月日時分秒這種日期時間信息的計時裝置;RTC和時鐘配置系統處于后備區域,系統復位時數據不清零,VDD(2.0~3.6V)斷電后可借助VBAT(1.8~3.6V)供電繼續走時;其內部設有32位的可編程計數器,可對應Unix時間戳的秒計數器。在讀取時間時,我們先得到這個計數器中的秒數,然后使用time.h模塊里的localtime函數就能立刻知道年月日時分秒的信息了,在寫入時間時,我們先填充年月日時分秒信息到struct tm結構體,然后用mktime函數得到秒數,再寫入到這個32位計數器即可。RTC外設中配有20位的可編程預分頻器,可適配不同頻率的輸入時鐘,由于32位的秒計數器顯然1秒要自增一次,所以驅動計數器的時鐘,需要是一個1Hz的信號,但是實際提供給RTC模塊的時鐘,也就是RTCCLK一般頻率都比較高。所以顯然我們需要在RTCCLK和計數器時鐘輸入之間加入一個分頻器,給RTCCLK降一降頻率,保證分頻器輸出給計數器的頻率為1Hz。那為了適配各種頻率的RTCCLK,就在其中間加入了一個20位的分頻器,可以選擇對輸入時鐘進行1-2^20這么大范圍的分頻,這樣就可以適配不同頻率的輸入時鐘,這就是這個可編程分頻器的作用;可選擇三種RTC時鐘源:1.? HSE時鐘除以128(通常為8MHz/128) 2.? ?LSE振蕩器時鐘(通常為32.768KHz) 3.? LSI振蕩器時鐘(40KHz) 這三個時鐘可以選擇其中一個介入到RTCCLK。
在時鐘樹中,高速時鐘一般供內部程序運行和主要外設使用;低速時鐘一般供RTC、看門狗這些東西使用,紅色所圈出來的地方最右側箭頭通往RTC,就是RTCCLK,RTCCLK有三個來源,第一個是OSC引腳接的HSE,外部高速晶振,這個晶振是主晶振,一般用的8MHz,8MHz進來,通過128分頻,可以產生RTCCLK信號,128分頻的原因是8MHz的主晶振太快了,如果不提前分頻,直接給RTCCLK,后續即使再通過RTC的20位分頻器也分不到1Hz這么低的頻率。中間這一路的時鐘來源是LSE,外部低速晶振,我們在OSC32這兩個引腳接上外部低速晶振,這個晶振產生的時鐘可以直接提供給RTCCLK,這個OSC32的晶振是內部RTC的專用時鐘,這個晶振的值也不是隨便選的,通常跟RTC有關的晶振,都是統一的數值,就是32.768KHz,選擇這個數值的原因是32KHz這個值附近的頻率是這個晶振工藝比較合適的頻率,另一方面是32768是一個2的次方數,2^15 =?32768 ,所以32.768KHz即32768Hz,經過一個15位分頻器的自然溢出,就能很方便地得到1Hz的頻率。自然溢出的意思就是設計一個15位的計數器,這個計數器不用設置計數目標,直接從0計到最大值,就是計到32767,計滿后自然溢出,這個溢出信號就是1Hz。所以,目前在RTC電路中,基本都是清一色的32.768KHz的晶振。最后第三路時鐘源來自LSI,內部低速RC振蕩器,LSI固定是40kHz,如果選擇LSI當作RTCCLK,后續再經過40K的分頻,就能得到1Hz的計數時鐘了。當然內部的RC振蕩器一般精準度沒有外部晶振高,所以LSI給RTCCLK可以當作一個備選方案。另外LSI還可以提供給看門狗,這個之后我們講看門狗的時候再說。
這三路時鐘中我們最常用的就是中間這一路外部32.768KHz的晶振,提供RTCCLK的時鐘。不僅因為中間這一路32.768KHz的晶振本身就是專供RTC使用的,其余的時鐘其實各自都有各自的主要任務,另外一個更重要的原因就是只有中間這一路的時鐘可以通過VBAT備用電池供電,上下兩路時鐘,在主電源斷電后,是停止運行的。所以要想實現RTC主電源掉電繼續走時的功能,必須選擇中間這一路的RTC專用時鐘。
接下來我們看一下RTC的框圖,看一下RTC外設具體是怎么設計的。先整體上劃分一下,左邊的一塊是核心的分頻和計數計時部分,右邊這一塊是中斷輸出使能和NVIC部分,最上面一塊是APB1總線讀寫部分,最下面一塊是和PWR關聯的部分,意思就是RTC的鬧鐘可以喚醒設備,推出待機模式。在圖中,有灰色填充的部分都處于后備區域,這些電路在主電源掉電后,可以使用備用電池維持工作,其他未被填充的部分就是待機時不供電,有關睡眠、停機、待機這些低功耗相關的內容,我們下節學PWR的時候再細講。
我們依次詳細看一下。首先看分頻和計數計時部分,這一塊的輸入時鐘是RTCCLK,RTCCLK的來源需要在RCC里進行配置。因為可選的三路時鐘頻率各不相同,而且都遠大于我們所需要的1Hz的秒計數頻率,所以RTCCLK進來,需要首先經過RTC預分頻器進行分頻,這個分頻器由兩個寄存器組成,上面這個是重裝載寄存器RTC_PRL,下面這個RTC_DIV,手冊里叫做余數寄存器,但實際上這一塊跟我們之前定時器時基單元里的計數器CNT和重裝值ARR是一樣的作用。分頻器其實就是一個計數器,計幾個數溢出一次,那就是幾分頻,所以對于可編程的分頻器來說,需要有兩個計數器,RTC_DIV寄存器用來不斷地計數,另一個RTC_PRL寄存器,我們寫入一個計數目標值,用來配置是幾分頻。那么PRL中就是計數目標,我們寫入6,那就是7分頻,寫入9,那就是10分頻;下面這個DIV,就是每來一個時鐘計一個數的用途了,當然這個DIV計數器是一個自減計數器,每來一個輸入時鐘,DIV的值自減一次,自減到0時,再來一個輸入時鐘,DIV輸出一個脈沖,產生溢出信號,同時DIV從PRL獲取重裝值,回到重裝值繼續自減。分頻輸出后的時鐘頻率是1Hz,提供給后續的秒計數器。然后看一下計數計時部分,32位可編程計數器RTC_CNT就是計時最核心的部分,我們可以把這個計數器看作是Unix時間戳的秒計數器,這樣借助time.h的函數就可以很方便地得到年月日時分秒了,在其下面還設計有一個鬧鐘寄存器RTC_ALR,這個ALR也是一個32位的寄存器,和上面這個CNT是等寬的,它的作用顧名思義就是設置鬧鐘,我們可以在ALR寫一個秒數,設定鬧鐘,當CNT的值跟ALR設定的鬧鐘值一樣時,這時就會產生RTC_Alarm鬧鐘信號,通往右邊的中斷系統,在中斷函數里,你可以執行相應的操作,同時這個鬧鐘還兼具一個功能,就是下面這里的鬧鐘信號可以讓STM32退出待機模式。這個功能就可以對應一些用途,比如你設計一個數據采集設備,需要在環節非常惡劣的地方工作,比如海底、高原、深井這些地方,然后要求是每天中午12點采集一次環節數據,其他時間為了節省電量,避免頻繁換電池,芯片都必須處于待機模式,這樣的話我們就可以用這個RTC自帶的鬧鐘功能。另外這個鬧鐘值是一個定值,只能響一次,所以你想實現周期性的鬧鐘,那在每次鬧鐘響之后,都需要再重新設置一下下一個鬧鐘時間。繼續往右看就是中斷部分了,在左邊這里有三個信號可以觸發中斷,第一個是RTC_Second,秒中斷,它的來源就是CNT的輸入時鐘,如果開啟這個中斷,那么程序就會每秒進一次RTC中斷;第二個是RTC_Overflow,溢出中斷,它的來源是CNT的右邊,意思就是CNT的32位計數器計滿溢出了會觸發一次中斷,所以這個中斷一般不會觸發;第三個RTC_Alarm,鬧鐘中斷,剛才說過,當計數器和鬧鐘值相等時,觸發中斷,同時,鬧鐘信號可以把設備從待機模式喚醒。中斷信號到右邊這里就是中斷標志位和中斷輸出控制,F結尾的是對應的中斷標志位,IE結尾的是中斷使能,最后三個信號通過一個或門匯聚到NVIC中斷控制器。最上面這部分APB1總線和APB1接口就是我們程序讀寫寄存器的地方了,讀寫寄存器可以通過APB1總線來完成,另外也可以看出RTC是APB1總線上的設備。最后,最下面這一塊,推出待機模式還有一個WKUP引腳,鬧鐘信號和WKUP引腳都可以喚醒設備。到這里這個RTC外設框圖就已經全部了解清楚了。
接下來看一下下面的基本結構圖,再總結一下以上內容,最左邊是RTCCLK時鐘來源,這一塊需要在RCC里配置,3個時鐘,選擇一個,當作RTCCLK,之后RTCCLK先通過預分頻器,對時鐘進行分頻,余數寄存器是一個自減寄存器,存儲當前的計數值,重裝寄存器是計數目標,決定分頻值。分頻之后得到1Hz的秒計數信號,通向32位計數器,1s自增一次,下面還有一個32位的鬧鐘值可以設定鬧鐘,右邊有三個信號可以觸發中斷,分別是秒信號、計數器溢出信號和鬧鐘信號,三個信號先通過中斷輸出控制進行中斷使能,使能的中斷才能通向NVIC,然后向CPU申請中斷。在程序中,我們配置這個數據選擇器,可以選擇時鐘來源;配置重裝寄存器,可以選擇分頻系數;配置32位計數器,可以進行日期時間的讀寫,需要鬧鐘的話,配置32位鬧鐘值即可;需要中斷的話,先允許中斷,再配置NVIC,最后寫對應的中斷函數即可,這就是RTC外設的主要內容。
最后,我們再看一些這個RTC的一些操作注意事項
1.執行以下操作將使能對BKP和RTC的訪問:設置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP時鐘;設置PWR_CR的DBP,使能對BKP和RTC的訪問。正常的外設開啟了時鐘就能使用了,但是BKP和RTC這兩個外設開啟稍微復雜些,首先要設置RCC_APB1ENR,這個實際上就是開啟APB1外設的時鐘,要同時開啟PWR和BKP的時鐘,對于RTC來說,并沒有單獨開啟時鐘的選項。然后我們還要設置PWR_CR的DBP位,來使能對BKP和RTC的訪問
2. 若在讀取RTC寄存器時,RTC的APB1接口曾經處于禁止狀態,則軟件首先必須等待RTC_CRL寄存器中的RSF位(寄存器同步標志)被硬件置1。這一步對應代碼里的一個庫函數,就是RTC等待同步,一般在剛上電的時候調用一下這個函數就行了。
3.?必須設置RTC_CRL寄存器中的CNF位,使RTC進入配置模式后,才能寫入RTC_PRL、RTC_CNT、RTC_ALR寄存器。就是RTC會有一個進入配置模式的標志位,把這一位置1,才能設置時間,其實這個操作在庫函數中,每個寫寄存器的函數都會自動加上這個操作,所以就不用再單獨調用代碼進入配置模式了。
4.?對RTC任何寄存器的寫操作,都必須在前一次寫操作結束后進行。可以通過查詢RTC_CR寄存器中的RTOFF狀態位,判斷RTC寄存器是否處于更新中。僅當RTOFF狀態位是1時,才可以寫入RTC寄存器
2和3的注意事項都是因為讀寫數據時使用的APB1總線是在PCLK1的時鐘頻率下運行的,但是RTC外設內部的工作時鐘是RTCCLK,PCLK1的頻率遠大于RTCCLK,因此任何讀寫操作都需要等待一會。
下面我們進入到代碼編寫的部分。
首先介紹一下備份寄存器BKP的庫函數,其中,BKP_DeInit函數用于恢復缺省配置在BKP的外設中是有一個用途的,就是手動清空BKP所有的數據寄存器,因為如果有備用電池的話,BKP的數據主電源掉電不清零、上電復位也不清零,它就沒清零的時候,如果我們確實想要清零,就可以使用這個函數,這樣所有BKP的數據都會變0;BKP_TamperPinLevelConfig和BKP_TamperPinCmd用于配置TAMPER侵入檢測功能,前者可以配置TAMPER引腳的有效電平,就是高電平觸發還是低電平觸發,后者就是配置是否開啟侵入檢測功能,如果需要侵入檢測的話,那就先配置TAMPER有效電平,再使能侵入檢測功能就行了;BKP_ITConfig,中斷配置,就是配置是否開啟中斷;BKP_RTCOutputConfig這是配置時鐘輸出功能,可以選擇在RTC引腳上輸出時鐘信號,輸出RTC校準時鐘、RTC鬧鐘脈沖或者秒脈沖,該配置需要通過 BKP 模塊的寄存器來實現,因此函數被歸在了BKP的庫文件之中;BKP_SetRTCCalibrationValue,用于設置RTC校準值,其實就是寫入RTC校準寄存器,校準值的設置也需要寫入 BKP 模塊的相關寄存器,因此也被歸在了BKP的庫文件之中。以上這些函數就是我們在上面說的BKP附加的小功能。之后的這幾個函數才是經常使用的:BKP_WriteBackupRegister,寫備份寄存器,其第一個參數指定要寫在哪個DR里,第二個參數填你要寫入的數據;BKP_ReadBackupRegister,讀備份寄存器。參數指定要讀哪個DR,返回值就是DR里存的值。 此外,我們還需要特別關注PWR庫函數中的PWR_BackupAccessCmd函數,即備份寄存器訪問使能,該函數中的內容就是設置PWR_CR寄存器里的DBP位,我們來調用這個函數滿足使用RTC和BKP外設時的注意事項1。
下面對于RTC實時時鐘編程,我們總結其初始化步驟如下:
1.開啟PWR時鐘和BKP時鐘,使能BKP和RTC的訪問
2.啟動RTC的時鐘,我們計劃使用LES作為系統時鐘,所以使用RCC模塊里的函數,開啟LSE的時鐘
3.配置RTCCLK這個數據選擇器,指定LSE為RTCCLK,這一步的函數也是在RCC模塊里的
4.調用注意事項中提到的等待函數,分別為等待同步和等待上一次操作完成
5.配置預分頻器,給PRL重裝寄存器一個合適的分頻值,以確保輸出給計數器的頻率是1Hz
6.配置CNT的值,給這個RTC一個初始時間
如果需要鬧鐘的話,可以配置鬧鐘值;需要中斷的話可以配置中斷部分
因為RTC比較簡單,所以庫函數并沒有使用結構體來配置,RTC也沒有RTC_Cmd這樣的函數,開啟時鐘就能自動運行了,不需要最后再啟動一下的。
在RCC庫函數中,存在著一些和RTC時鐘相關的函數。其中RCC_LSEConfig用于配置LSE外部低速時鐘,啟動LSE時鐘就調用這個函數;RCC_LSICmd函數用于配置LSI內部低速時鐘,如果出現了外部時鐘不起振的情況,也可以使用這個內部時鐘來進行實驗;RCC_RTCCLKConfig,RTCCLK配置,這個函數用來選擇RTCCLK的時鐘源,實際上就是配置簡化結構圖中的數據選擇器;RCC_RTCCLKCmd,啟動RTCCLK,在調用上一個函數選擇時鐘之后,還需要調用一下這個Cmd函數,使能一下;另外還需要用到RCC_GetFlagStatus函數獲取標志位,因為LSE時鐘不是你讓它啟動它就能立刻啟動的,調用啟動時鐘的函數之后,我們還需要等待一下標志位,等RCC的標志位LSERDY置1之后,這個時鐘才算啟動完成,工作穩定。有關RCC時鐘部分的函數就這么多。
接下來我們繼續看RTC庫函數中的函數:RTC_ITConfig用于配置中斷輸出;RTC_EnterConfigMode,進入配置模式,就是置CRL的CNF為1,進入配置模式,其對應注意事項中的第三條;RTC_ExitConfigMode,退出配置模式,就是把CNF位清零;RTC_GetCounter,獲取CNT計數器的值,顯然,讀取時鐘就靠這個函數;RTC_SetCounter,寫入CNT計數器的值,顯然,設置時間,就靠這個函數;RTC_SetPrescaler,寫入預分頻器,這個值會寫入到預分頻器的PRL重裝寄存器中,用來配置預分頻器的分頻系數;RTC_SetAlarm,寫入鬧鐘值;RTC_GetDivider,讀取預分頻器中的DIV余數寄存器,余數寄存器是一個自減寄存器,獲取余數寄存器的值,一般是為了得到更細致的時間,因為CNT計數間隔最短就是1s,如果需要更細致的時間,比如分秒、厘秒、毫秒,那就得靠這個DIV余數寄存器來實現;RTC_WaitForLastTask,等待上次操作完成,對應注意事項中的第四條,等待前一次寫操作結束;RTC_WaitForSynchro,等待同步,對應注意事項中的第二條。
在RTC顯示實時時鐘代碼中,我如何保證在系統復位后,保證時間信息仍然不被重置呢?我可以借助BKP中存儲內容在單片機供電斷開時仍然能靠電池保存的特性,在初始化代碼中加入判斷:如果BKP中任一一個數據寄存器中內容不等于約定好的數字,說明現在是單片機電源掉電、電池電源掉電之后重新啟動,因此需要進行初始化,并且在BKP對應寄存器中寫入該數據;如果其中的內容等于數字了,說明電池并未掉電,則跳過初始化。