開發環境:
MDK:Keil 5.30
開發闆:GD32F207I-EVAL
MCU:GD32F207IK
16.1 RTC工作原理
16.1.1 RTC簡介
GD32 的 RTC 外設,實質是一個掉電後還繼續運作的定時器。從定時器的角度來說,相對于通用定時器 TIMER 外設,它十分簡單,隻有很純粹的計時功能(當然,可以觸發中斷);但從掉電還繼續運作的角度來說,它卻是 GD32中唯一一個具有如此強大功能的外設。是以 RTC 外設的複雜之處并不在于它的定時功能,而在于它掉電還繼續運作的特性。
以上所說的掉電,是指主電源 VDD斷開的情況,為了 RTC 外設掉電繼續運作,必須給GD32晶片通過 VBAT引腳接上锂電池。當主電源 VDD有效時,由 VDD給 RTC 外設供電。當 VDD掉電後,由 VBAT給 RTC 外設供電。但無論由什麼電源供電,RTC 中的資料都儲存在屬于 RTC 的備份域中,若主電源 VDD和 VBAT都掉電,那麼備份域中儲存的所有資料将丢失。備份域除了 RTC 子產品的寄存器,還有 42 個 16 位的寄存器可以在 VDD掉電的情況下儲存使用者程式的資料,系統複位或電源複位時,這些資料也不會被複位。
從 RTC 的定時器特性來說,它是一個 32 位的計數器,隻能向上計數。它使用的時鐘源有三種,分别為高速外部時鐘的 128 分頻:HXTAL/128;低速内部時鐘IRC40K;使 HXTAL分頻時鐘或IRC40K的話,在主電源 VDD掉電的情況下,這兩個時鐘來源都會受到影響,是以沒法保證 RTC 正常工作。是以 RTC 一般使用低速外部時鐘LXTAL,頻率為實時時鐘子產品中常用的 32.768KHz,這是因為 32768 = 215,分頻容易實作,是以它被廣泛應用到 RTC 子產品。在主電源 VDD有效的情況下(待機),RTC 還可以配置鬧鐘事件使 GD32退出待機模式。
RTC子產品在相應軟體配置下,可提供時鐘月曆的功能。修改計數器的值可以重新設定系統目前的時間和日期。RTC子產品和時鐘配置系統處于後備區域,即在系統複位或從待機模式喚醒後,RTC的設定和時間維持不變。
16.1.2主要特性
可程式設計的預分頻系數:分頻系數最高為2^20
32位的可程式設計計數器,可用于較長時間段的測量。
2個分離的時鐘:用于APB1接口的PCLK1和RTC時鐘(RTC時鐘的頻率必須小于PCLK1時鐘頻率的四分之一以上)。
可以選擇以下三種RTC的時鐘源:
A) HXTAL 時鐘除以 128
B) LXTAL 振蕩電路時鐘
C) IRC40K 振蕩電路時鐘
2個獨立的複位類型:
A) APB1接口由系統複位;
B) RTC核心(預分頻器、鬧鐘、計數器和分頻隻能由後備域複位
3個專門的可屏蔽中斷:
A) 鬧鐘中斷,用來産生一個軟體可程式設計的鬧鐘中斷。
B) 秒中斷,用來産生一個可程式設計的周期性中斷信号 (最長可達1秒)。
C) 溢出中斷,訓示内部可程式設計計數器溢出并回轉為的狀态。
16.1.3 RTC架構
RTC的架構如下圖所示。
RTC 由兩個主要部分組成, 第一部分(APB1 接口)用來和 APB1 總線相連。此單元還包含一組 16 位寄存器,可通過 APB1 總線對其進行讀寫操作。 APB1 接口由 APB1 總線時鐘驅動,用來與 APB1 總線連接配接。
另一部分(RTC 核心)由一組可程式設計計數器組成,RTC核心包含兩個主要子產品。一個是RTC預分頻子產品,用來産生RTC時間基準時鐘SC_CLK。RTC預分頻子產品包含一個20位可程式設計分頻器(RTC預分頻器) ,該分頻器可以通過對RTC時鐘源分頻産生SC_CLK。如果對RTC_INTEN寄存器中的秒中斷标志位被使能, RTC會在每個SC_CLK上升沿産生一個秒中斷。 另外一個子產品是一個32 位可程式設計計數器,其數值可以被初始化為目前系統時間。如果對RTC_INTEN 寄存器的鬧鐘中斷标志位被使能, RTC會在系統時間等于鬧鐘時間(存儲于RTC_ALRMH/L 寄存器)時産生一個鬧鐘中斷。
16.2 RTC寄存器分析
16.2.1 RTC寄存器描述
RTC 總共有 2 個控制寄存器RTC_INTEN和 RTC_CTL。
RTC_INTEN寄存器用來控制中斷的,我們本章将要用到秒鐘中斷,是以在該寄存器必須設定最低位為 1,以允許秒鐘中斷。
RTC_CTL的第 0 位是秒鐘标志位,我們在進入鬧鐘中斷的時候,通過判斷這位來決定是不是發生了秒鐘中斷。然後必須通過軟體将該位清零(寫0)。第 3 位為寄存器同步标志位,我們在修改控制寄存器之前,必須先判斷該位,是否已經同步了,如果沒有則等待同步,在沒同步的情況下修改RTC_INTEN/RTC_CTL的值是不行的。第4位為配置标位,在軟體修改 RTC_CNTx/RTC_ALRMx/RTC_PSCx的值的時候,必須先軟體置位該位,以允許進入配置模式。第 5 位為 RTC 操作位,該位由硬體操作,軟體隻讀。通過該位可以判斷上次對 RTC 寄存器的操作是否完成,如果沒有,我們必須等待上一次操作結束才能開始下一次操作。
【注意】
任何标志位都将保持挂起狀态,直到适當的RTC_CTL請求位被軟體複位,表示所請求的中斷已經被接受。
在複位時禁止所有中斷,無挂起的中斷請求,可以對RTC寄存器進行寫操作。
當APB1時鐘不運作時,SCIF、ALRMIF、OVIF和RSYNF位不被更新。
SCIF、ALRMIF、OVIF和RSYNF位隻能由硬體置位,由軟體來清零。
若ALRMIF =1且ALRMIE =1,則允許産生RTC全局中斷。如果在EXTI控制器中允許産生EXTI線 17中斷,則允許産生RTC全局中斷和RTC鬧鐘中斷。
若ALRMIF =1,如果在EXTI控制器中設定了EXTI線 17的中斷模式,則允許産生RTC鬧鐘中斷;如果在EXTI控制器中設定了EXTI線 17的事件模式,則這條線上會産生一個脈沖(不會産生RTC鬧鐘中斷)。
RTC 預分頻裝載寄存器,也有 2 個寄存器組成,RTC_PSCH和RTC_PSCL。這兩個寄存器用來配置 RTC 時鐘的分頻數的,比如我們使用外部 32.768K 的晶振作為時鐘的輸入頻率,那麼我們要設定這兩個寄存器的值為 32767,以得到一秒鐘的計數頻率。RTC_PSCH的各位描述如下圖所示。
從上圖可以看出,RTC_PSCH隻有低四位有效,用來存儲PSC的 19~16 位。而PSC的前 16 位,存放在RTC_PSCL裡面,寄存器RTC_PSCL的各位描述如下圖所示。
【注】如果輸入時鐘頻率是32.768kHz(RTCCLK),這個寄存器中寫入7FFFh可獲得周期為1秒鐘的信号。
RTC 預分頻器寄存器也有 2 個寄存器組成 RTC_DIVH 和 RTC_DIVL,這兩個寄存器的作用就是用來獲得比秒鐘更為準确的時鐘,比如可以得到 0.1 秒,或者 0.01 秒等。該寄存器的值自減的,用于儲存還需要多少時鐘周期獲得一個秒信号。在一次秒鐘更新後,由硬體重新裝載。這兩個寄存器和 RTC 預分頻裝載寄存器的各位是一樣的,這裡我們就不列出來了。
接着要介紹的是 RTC 最重要的寄存器, RTC 計數器寄存器 RTC_CNT。該寄存器由 2 個 16位的寄存器組成 RTC_CNTH 和 RTC_CNTL,總共 32 位,用來記錄秒鐘值(一般情況下)。此兩個計數器也比較簡單,我們也不多說了。注意一點,在修改這個寄存器的時候要先進入配置模式。
最後我們介紹 RTC 部分的最後一個寄存器, RTC 鬧鐘寄存器,該寄存器也是由 2 個 16 為的寄存器組成 RTC_ALRH 和 RTC_ALRL。總共也是 32 位,用來标記鬧鐘産生的時間(以秒為機關),如果 RTC_CNT 的值與 RTC_ALR 的值相等,并使能了中斷的話,會産生一個鬧鐘中斷。該寄存器的修改也要進入配置模式才能進行。
因為我們使用到備份寄存器來存儲 RTC 的相關資訊(我們這裡主要用來标記時鐘是否已經經過了配置)。
16.2.2讀RTC寄存器
RTC完全獨立于RTC APB1接口。
軟體通過APB1接口通路RTC的預分頻值、計數器值和鬧鐘值。但是,相關的可讀寄存器隻在與RTC APB1時鐘進行重新同步的RTC時鐘的上升沿被更新。RTC标志也是如此的。
這意味着,如果APB1接口曾經被關閉,而讀操作又是在剛剛重新開啟APB1之後,則在第一次的内部寄存器更新之前,從APB1上讀出的RTC寄存器數值可能被破壞了(通常讀到0) 。下述幾種情況下能夠發生這種情形:
發生系統複位或電源複位
系統剛從待機模式喚醒
系統剛從停機模式喚醒
所有以上情況中,APB1接口被禁止時(複位、無時鐘或斷電)RTC核仍保持運作狀态。
是以,若在讀取RTC寄存器時,RTC 的APB1 接口曾經處于禁止狀态,則軟體首先必須等待RTC_CTL寄存器中的RSYNF位(寄存器同步标志)被硬體置’1’。
16.2.3配置RTC寄存器
必須設定RTC_CTL寄存器中的CMF位,使 RTC進入配置模式後,才能寫入 RTC_PSC、RTC_CNT、RTC_ALRM寄存器。
另外,對RTC任何寄存器的寫操作,都必須在前一次寫操作結束後進行。可以通過查詢RTC_CTL寄存器中的LWOFF狀态位,判斷RTC寄存器是否處于更新中。僅當LWOFF狀态位是’1’時,才可以寫入RTC寄存器。
配置過程:
1.查詢LWOFF位,直到LWOFF的值變為’1’
2.置CMF值為1,進入配置模式
3.對一個或多個RTC寄存器進行寫操作
4.清除CMF标志位,退出配置模式
5.查詢LWOFF,直至LWOFF位變為’1’ 以确認寫操作已經完成。
6.僅當CMF标志位被清除時,寫操作才能進行,這個過程至少需要3個RTCCLK周期。
16.3 RTC具體代碼實作
RTC 正常工作的一般配置步驟如下:
1)使能電源時鐘和備份區域時鐘。
前面已經介紹了,我們要通路 RTC 和備份區域就必須先使能電源時鐘和備份區域時鐘。
rcu_periph_clock_enable(RCU_BKPI);
rcu_periph_clock_enable(RCU_PMU);
2)取消備份區寫保護。
要向備份區域寫入資料,就要先取消備份區域寫保護(寫保護在每次硬複位之後被使能),否則是無法向備份區域寫入資料的。我們需要用到向備份區域寫入一個位元組,來标記時鐘已經配置過了,這樣避免每次複位之後重新配置時鐘。 取消備份區域寫保護的庫函數實作方法是:
pmu_backup_write_enable(); //使能 RTC 和後備寄存器通路
3)複位備份區域,開啟外部低速振蕩器。
在取消備份區域寫保護之後,我們可以先對這個區域複位,以清除前面的設定,當然這個操作不要每次都執行,因為備份區域的複位将導緻之前存在的資料丢失,是以要不要複位,要看情況而定。然後我們使能外部低速振蕩器,注意這裡一般要先判斷 RCC_BDCR 的 LSERDY位來确定低速振蕩器已經就緒了才開始下面的操作。
備份區域複位的函數是:
bkp_deinit();//複位備份區域
開啟外部低速振蕩器的函數是:
rcu_osci_on(RCU_LXTAL);// 開啟外部低速振蕩器
4)選擇 RTC 時鐘,并使能。
庫函數中,選擇 RTC 時鐘的函數是:
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL); //選擇LXTAL作為 RTC 時鐘
對于 RTC 時鐘的選擇使能 RTC 時鐘的函數是:
rcu_periph_clock_enable(RCU_RTC); //使能 RTC 時鐘
5)設定 RTC 的分頻,以及配置 RTC 時鐘。
在開啟了 RTC 時鐘之後,我們要做的就是設定 RTC 時鐘的分頻數,通過 RTC_PSCH 和RTC_PSCL 來設定,然後等待 RTC 寄存器操作完成,并同步之後,設定秒鐘中斷。然後設定RTC 的允許配置位( RTC_CTL的 CMF 位),設定時間(其實就是設定 RTC_CNTH 和 RTC_CNTL兩個寄存器)。
下面我們一一這些步驟用到的庫函數:
在進行 RTC 配置之前首先要打開允許配置位(CMF),庫函數是:
rtc_configuration_mode_enter ();/// 允許配置
在配置完成之後,千萬别忘記更新配置同時退出配置模式,函數是:
rtc_configuration_mode_exit ();//退出配置模式,更新配置
設定 RTC 時鐘分頻數, 庫函數是:
void rtc_prescaler_set(uint32_t psc)
這個函數隻有一個入口參數,就是 RTC 時鐘的分頻數,很好了解。
然後是設定秒中斷允許,RTC 使能中斷的函數是:
void rtc_interrupt_enable(uint32_t interrupt)
這個函數的第一個參數是設定秒中斷類型,這些通過宏定義定義的。 對于使能秒中斷方法是:
rtc_interrupt_enable(RTC_INT_SECOND); //使能 RTC 秒中斷
下一步便是設定時間了,設定時間實際上就是設定 RTC 的計數值,時間與計數值之間是需要換算的,當然也可先不設定。庫函數中設定 RTC 計數值的方法是:
void rtc_counter_set(uint32_t cnt)
通過這個函數直接設定 RTC 計數值。
6)更新配置,設定 RTC 中斷分組。
在設定完時鐘之後,我們将配置更新同時退出配置模式,這裡還是通過 RTC_CTL的CMF來實作。庫函數的方法是:
rtc_configuration_mode_exit ();//退出配置模式,更新配置
在退出配置模式更新配置之後我們在備份區域 BKP_DATA0中寫入 0xA5A5代表我們已經初始化過時鐘了,下次開機(或複位)的時候,先讀取 BKP_DATA0的值,然後判斷是否是 0xA5A5來決定是不是要配置。接着我們配置 RTC 的秒鐘中斷,并進行分組。
往備份區域寫使用者資料的函數是:
void bkp_data_write(bkp_data_register_enum register_number, uint16_t data)
這個函數的第一個參數就是寄存器的标号了,這個是通過宏定義定義的。 比如我們要往BKP_DATA0 寫入 0xA5A5,方法是:
bkp_data_write(BKP_DATA_0, 0xA5A5);
同時,有寫便有讀,讀取備份區域指定寄存器的使用者資料的函數是:
uint16_t bkp_data_read(bkp_data_register_enum register_number)
這個函數就很好了解了,這裡不做過多講解。
設定中斷分組的方法之前已經詳細講解過,這裡不做重複講解。
7)編寫中斷服務函數。
最後,我們要編寫中斷服務函數,在秒鐘中斷産生的時候,讀取目前的時間值。
/**
* @brief This function handles RTC exception.
* @param None
* @retval None
*/
void RTC_IRQHandler(void)
{
if(rtc_flag_get(RTC_FLAG_SECOND)!=RESET)//讀取中斷标志
{
rtc_flag_clear(RTC_FLAG_SECOND);//清楚中斷标志
tim_bz=1;//秒中斷标志
}
}
完成的配如下:
/**
* @brief RTC配置
* @param None
* @retval None
*/
void rtc_configuration(void)
{
/* enable PMU and BKPI clocks 使能電源時鐘和備份區域時鐘*/
rcu_periph_clock_enable(RCU_BKPI);
rcu_periph_clock_enable(RCU_PMU);
/* allow access to BKP domain 允許通路BKP區域*/
pmu_backup_write_enable();
//複位備份區域,開啟外部低速振蕩器
/* reset backup domain */
bkp_deinit();
/* enable LXTAL 使能外部低速晶振 32.768K */
rcu_osci_on(RCU_LXTAL);
/* wait till LXTAL is ready */
rcu_osci_stab_wait(RCU_LXTAL);
//選擇 RTC 時鐘,并使能
/* select RCU_LXTAL as RTC clock source */
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL);
/* enable RTC Clock 使能RTC時鐘 */
rcu_periph_clock_enable(RCU_RTC);
rtc_configuration_mode_enter();
/* wait for RTC registers synchronization */
rtc_register_sync_wait();
/* wait until last write operation on RTC registers has finished 等待寫RTC寄存器完成*/
rtc_lwoff_wait();
/* enable the RTC second interrupt 使能RTC秒中斷*/
rtc_interrupt_enable(RTC_INT_SECOND);
/* wait until last write operation on RTC registers has finished 等待寫RTC寄存器完成*/
rtc_lwoff_wait();
/* set RTC prescaler: set RTC period to 1s 設定預分頻*/
rtc_prescaler_set(32767);
/* wait until last write operation on RTC registers has finished 等待寫RTC寄存器完成*/
rtc_lwoff_wait();
nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
nvic_irq_enable(RTC_IRQn, 1, 0);
}
/**
* @brief RTC時鐘初始化
* @param None
* @retval None
*/
void clock_init(void)
{
if(0xA5A5 != bkp_data_read(BKP_DATA_0))
{
//第一次運作 初始化設定
rtc_configuration();//RTC初始化
/* wait until last write operation on RTC registers has finished */
rtc_lwoff_wait();
/* change the current time */
rtc_counter_set(0);
/* wait until last write operation on RTC registers has finished */
rtc_lwoff_wait();
rtc_lwoff_wait();//等待寫RTC寄存器完成
rtc_lwoff_wait();//等待寫RTC寄存器完成
bkp_data_write(BKP_DATA_0, 0xA5A5);//寫配置标志
}
else
{
/* check if the power on reset flag is set */
if(rcu_flag_get(RCU_FLAG_PORRST) != RESET)
{
printf("\r\n\n Power On Reset occurred....");
}
else if(rcu_flag_get(RCU_FLAG_SWRST) != RESET)
{
/* check if the pin reset flag is set */
printf("\r\n\n External Reset occurred....");
}
printf("\r\n No need to configure RTC....");
rtc_register_sync_wait();//等待RTC寄存器同步
rtc_interrupt_enable(RTC_INT_SECOND);//使能RTC秒中斷
rtc_lwoff_wait();//等待寫RTC寄存器完成
}
rtc_configuration_mode_exit();//退出配置模式, 更新配置
rcu_all_reset_flag_clear();//清除複位标志;
}
主函數代碼如下:
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
uint32_t TimeData=0,hh=0,mm=0,ss=0;
//usart init 115200 8-N-1
com_init(COM1);
rtc_configuration();
clock_init();
while(1)
{
if(tim_bz==1)
{
tim_bz=0;
TimeData=rtc_counter_get();
hh= TimeData/3600;
mm = (TimeData%3600)/60;
ss = TimeData%60;
printf("Time: %0.2d:%0.2d:%0.2d\r\n",hh,mm,ss);
}
}
}
16.4實驗現象
打開序列槽助手,列印資訊如下:
這裡沒有設定初始時間,時分秒是從0開始的。
公衆号[嵌入式實驗樓]
在公衆号回複關鍵詞[GD32開發實戰指南]擷取資料提取碼