萬年歷實驗
本小節(jié)講解的是如何利用RTC外設(shè)實現(xiàn)萬年歷功能,本實驗工程與RTC底層驅(qū)動相關(guān)的文件為bsp_rtc.c/h,在底層驅(qū)動之上我們添加了bsp_calendar.c/h和bsp_date.c/h文件,用于萬年歷的計算。
程序設(shè)計要點
(1)初始化RTC外設(shè);
(2)設(shè)置時間以及添加配置標(biāo)志;
(3)獲取當(dāng)前時間;
代碼分析
1、RTC實驗配置相關(guān)宏定義
在這個RTC實驗中的bsp_rtc.h文件中添加了一些宏定義用于切換工程的配置。本實驗?zāi)J(rèn)使用LSI內(nèi)部時鐘,使用內(nèi)部時鐘時,即使安裝了鈕扣電池,主電源掉電后時間是不會繼續(xù)走的,只會保留上次斷電的時間。若要持續(xù)運行,需要切換成使用LSE外部時鐘。
#define RTC_CLOCK_SOURCE_LSI //使用LS內(nèi)部時鐘
#define RTC_BKP_DRX BKP_DR1
#define RTC_BKP_DATA 0xA5A5//寫入到備份寄存器的數(shù)據(jù)宏定義
#define TIME_ZOOM (8*60*60)//北京時間的時區(qū)秒數(shù)差
RTC_BKP_DRX和RTC_BKP_DATA:這兩個宏用于在備份域寄存器設(shè)置RTC已配置標(biāo)志,本實驗中初始化RTC后,向備份域寄存器寫入一個數(shù)字,若下次芯片上電檢測到該標(biāo)志,說明RTC之前已經(jīng)配置好時間,所以不應(yīng)該再設(shè)置RTC,而如果備份域電源也掉電,備份域內(nèi)記錄的該標(biāo)志也會丟失,所以芯片上電后需要重新設(shè)置時間。這兩個宏的值中,BKP_DR1是備份域的其中一個寄存器,而0xA5A5則是隨意選擇的數(shù)字,只要寫入和檢測一致即可。
TIME_ZOOM:這個宏用于設(shè)置時區(qū)的秒數(shù)偏移,例如北京時間為(GMT+8)時區(qū),即相對于格林威治時間(GMT)早8個小時,此處使用的宏值即為8個小時的秒數(shù)(8*60*60),若使用其它時區(qū),修改該宏即可。
2、初始化RTC
在本工程中,我們編寫了RTC_Configuration函數(shù)對RTC進(jìn)行初始化。這個初始化的流程如下:使用RCC_APB1PeriphClockCmd使能PWR和BKP區(qū)域(即備份域)的時鐘系統(tǒng),使用PWR_BackupAccessCmd設(shè)置允許對BKP區(qū)域的訪問,使能LSI時鐘,選擇LSI作為RTC的時鐘源并使能RTC時鐘,利用庫函數(shù)RTC_WaitForSynchro對備份域和APB進(jìn)行同步,用RTC_ITConfig使能秒中斷,使用RTC_SetPrescaler分頻配置把RTC時鐘頻率設(shè)置為1Hz,那么RTC每個時鐘周期都會產(chǎn)生一次中斷。經(jīng)過這樣的配置后,RTC每秒產(chǎn)生一次中斷事件,實驗中在中斷設(shè)置標(biāo)志位以便更新時間。
/*
* 函數(shù)名:RTC_Configuration
* 描述 :配置RTC
* 輸入 :無
* 輸出 :無
*/
void RTC_Configuration(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//使能PWR和Backup時鐘
PWR_BackupAccessCmd(ENABLE);//允許訪問 Backup 區(qū)域
BKP_DeInit();//復(fù)位 Backup 區(qū)域
RCC_LSICmd(ENABLE);//使能 LSI
while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); //等待 LSI 準(zhǔn)備好
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);//選擇 LSI 作為 RTC 時鐘源
RCC_RTCCLKCmd(ENABLE);//使能 RTC 時鐘
RTC_WaitForSynchro();//因為RTC時鐘是低速的,內(nèi)環(huán)時鐘是高速的,所以要等待 RTC 寄存器同步
RTC_WaitForLastTask();//確保上一次 RTC 的操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能 RTC 秒中斷
RTC_WaitForLastTask();//確保上一次 RTC 的操作完成
/* 設(shè)置 RTC 分頻: 使 RTC 周期為1s ,LSI約為40KHz */
RTC_SetPrescaler(40000-1); //RTC period = RTCCLK/RTC_PR = (40 KHz)/(40000-1+1) = 1HZ
RTC_WaitForLastTask();//確保上一次 RTC 的操作完成
}
3、時間管理結(jié)構(gòu)體
RTC初始化完成后可以直接往它的計數(shù)器寫入時間戳,但是時間戳對用戶不友好,不方便配置和顯示時間,在本工程中,使用bsp_date.h文件的rtc_time結(jié)構(gòu)體來管理時間。這個類型的結(jié)構(gòu)體具有時、分、秒、日、月、年及星期這7個成員。當(dāng)需要給RTC的計時器重新配置或顯示時間時,使用這種容易接受的時間表示方式。
struct rtc_time {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
};
在配置RTC時,使用這種類型的變量保存用戶輸入的時間,然后利用函數(shù)由該時間求出對應(yīng)的UNIX時間戳,寫入RTC的計數(shù)器;RTC正常運行后,需要輸出時間時,利用函數(shù)通過RTC的計數(shù)器獲取UNIX時間戳,轉(zhuǎn)化成這種友好的時間表示方式保存到變量輸出。
4、時間格式轉(zhuǎn)換
在本實驗中,tm格式轉(zhuǎn)時間戳使用mktimev函數(shù),時間戳轉(zhuǎn)tm格式使用to_tm函數(shù),這兩個函數(shù)都定義在bsp_date.c文件中。關(guān)于日期計算的細(xì)節(jié)此處不作詳細(xì)分析,其原理是以1970年1月1日0時0分0秒為計時基點,對日期和以秒數(shù)表示時間戳進(jìn)行互相轉(zhuǎn)化,轉(zhuǎn)化重點在于閏年的計算。這兩個函數(shù)都是以格林威治時間(GMT)時區(qū)來計算的,在調(diào)用這些函數(shù)時我們會對輸入參數(shù)加入時區(qū)偏移的運算,進(jìn)行調(diào)整。
/* Converts Gregorian date to seconds since 1970-01-01 0000.
* Assumes input in normal date format, i.e. 1980-12-31 2359
* => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
*/
u32 mktimev(struct rtc_time *tm)
{
if (0 >= (int) (tm->tm_mon -= 2)) { /* 1..12 -> 11,12,1..10 */
tm->tm_mon += 12; /* Puts Feb last since it has leap day */
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 /* now have hours */
)*60 + tm->tm_min /* now have minutes */
)*60 + tm->tm_sec; /* finally seconds */
}
void to_tm(u32 tim, struct rtc_time * tm)
{
register u32 i;
register long hms, day;
day = tim / SECDAY; //有多少天
hms = tim % SECDAY; //今天的時間,單位s
tm->tm_hour = hms / 3600;//時
tm->tm_min = (hms % 3600) / 60;//分
tm->tm_sec = (hms % 3600) % 60;//秒
/*算出當(dāng)前年份,起始的計數(shù)年份為1970年*/
for (i = STARTOFTIME; day >= days_in_year(i); i++) {
day -= days_in_year(i);
}
tm->tm_year = i;
/*計算當(dāng)前的月份*/
if (leapyear(tm->tm_year)) {
days_in_month(FEBRUARY) = 29;
}
for (i = 1; day >= days_in_month(i); i++) {
day -= days_in_month(i);
}
days_in_month(FEBRUARY) = 28;
tm->tm_mon = i;
tm->tm_mday = day + 1;//計算當(dāng)前日期
GregorianDay(tm); //計算當(dāng)前是星期幾
}
5、配置時間
有了以上的準(zhǔn)備,接下來學(xué)習(xí)一下Time_Adjust函數(shù)。Time_Adjust函數(shù)用于配置時間,它先調(diào)用前面的RTC_Configuration初始化RTC,接著調(diào)用庫函數(shù)RTC_SetCounter向RTC計數(shù)器寫入要設(shè)置時間的時間戳值,而時間戳的值則使用mktimev函數(shù)通過輸入?yún)?shù)tm來計算,計算后還與宏TIME_ZOOM運算,計算時區(qū)偏移值。此處的輸入?yún)?shù)tm是北京時間,所以“mktimev(tm)- TIME_ZOOM”計算后寫入到RTC計數(shù)器的是格林威治時區(qū)的標(biāo)準(zhǔn)UNIX時間戳。
/*
* 函數(shù)名:Time_Adjust
* 描述 :時間調(diào)節(jié)
* 輸入 :用于讀取RTC時間的結(jié)構(gòu)體指針(北京時間)
* 輸出 :無
*/
void Time_Adjust(struct rtc_time *tm)
{
RTC_Configuration();//RTC 配置
RTC_WaitForLastTask(); //等待確保上一次操作完成
GregorianDay(tm); //計算星期
RTC_SetCounter(mktimev(tm)-TIME_ZOOM); //由日期計算時間戳并寫入到RTC計數(shù)寄存器
RTC_WaitForLastTask(); //等待確保上一次操作完成
}
6、檢查并配置RTC
上面的Time_Adjust函數(shù)直接把參數(shù)寫入到RTC中修改配置,但在芯片每次上電時,并不希望每次都修改系統(tǒng)時間,所以我們增加了RTC_CheckAndConfig函數(shù)用于檢查是否需要向RTC寫入新的配置。
/*
* 函數(shù)名:RTC_CheckAndConfig
* 描述 :檢查并配置RTC
* 輸入 :用于讀取RTC時間的結(jié)構(gòu)體指針
* 輸出 :無
*/
void RTC_CheckAndConfig(struct rtc_time *tm)
{
/*在啟動時檢查備份寄存器BKP_DR1,如果內(nèi)容不是0xA5A5,則需重新配置時間并詢問用戶調(diào)整時間*/
if (BKP_ReadBackupRegister(RTC_BKP_DRX) != RTC_BKP_DATA)
{
Time_Adjust(tm);//使用tm的時間配置RTC寄存器
BKP_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);//向BKP_DR1寄存器寫入標(biāo)志
}
else
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//使能時鐘
PWR_BackupAccessCmd(ENABLE);//允許訪問 Backup 區(qū)域
#ifdef RTC_CLOCK_SOURCE_LSI// LSE啟動無需設(shè)置新時鐘
RCC_LSICmd(ENABLE);//使能 LSI
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET); //等待 LSI 準(zhǔn)備好
#endif
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)//檢查是否掉電重啟
{
printf(" Power On Reset occurred....");
}
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)//檢查是否Reset復(fù)位
{
printf(" External Reset occurred....");
}
printf(" No need to configure RTC....");
RTC_WaitForSynchro();//等待寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE);//允許RTC秒中斷
RTC_WaitForLastTask();//等待上次RTC寄存器寫操作完成
}
RCC_ClearFlag();//清除復(fù)位標(biāo)志 flags
}
在本函數(shù)中,會檢測備份域寄存器RTC_BKP_DRX內(nèi)的值是否等于RTC_BKP_DATA而分成兩個分支。若不等,說明之前沒有配置RTC所以直接調(diào)用Time_Adjust函數(shù)初始化RTC并寫入時間戳進(jìn)行計時,配置完成后向備份域寄存器RTC_BKP_DRX寫入值RTC_BKP_DATA作為標(biāo)志,這樣該標(biāo)志就可以指示RTC的配置情況了,因為備份域不掉電時,RTC和該寄存器的值都會保存完好,而如果備份域掉電,那么RTC配置和該標(biāo)志都會一同丟失。
若本函數(shù)的標(biāo)志判斷相等,進(jìn)入else分支,不再調(diào)用Time_Adjust函數(shù)初始化RTC,而只是使用RTC_WaitForSynchro和RTC_ITConfig同步RTC域和APB以及使能中斷,以便獲取時間。如果使用的是LSI時鐘,還需要使能LSI時鐘,RTC才會正常運行,這是因為當(dāng)主電源掉電和備份域的情況下LSI會關(guān)閉,而LSE則會正常運行,驅(qū)動RTC計時。
7、轉(zhuǎn)換并輸出時間
RTC正常運行后,可以使用Time_Display函數(shù)轉(zhuǎn)換時間格式并輸出到串口。
/*
* 函數(shù)名:Time_Display
* 描述 :顯示當(dāng)前時間值
* 輸入 :-TimeVar RTC計數(shù)值,單位為 s
* 輸出 :無
*/
void Time_Display(uint32_t TimeVar,struct rtc_time *tm)
{
static uint32_t FirstDisplay = 1;
uint32_t BJ_TimeVar;
uint8_t str[200]; //字符串暫存
BJ_TimeVar =TimeVar + TIME_ZOOM; //把標(biāo)準(zhǔn)時間轉(zhuǎn)換為北京時間
to_tm(BJ_TimeVar, tm);//把定時器的值轉(zhuǎn)換為北京時間
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(" 今天新歷:%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(" 今天農(nóng)歷:%s ", str);
if(GetJieQiStr((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str))
{
printf(" 今天農(nóng)歷:%s ", str);
}
FirstDisplay = 0;
}
/* 輸出時間戳,公歷時間 */
printf(" UNIX時間戳 = %d 當(dāng)前時間為: %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);}
本函數(shù)的核心部分已加粗顯示,主要是使用to_tm把時間戳轉(zhuǎn)換成日常生活中使用的時間格式,to_tm以BJ_TimeVar作為輸入?yún)?shù),而BJ_TimeVar對時間戳變量Time_Var進(jìn)行了時區(qū)偏移,也就是說調(diào)用Time_Display函數(shù)時,以RTC計數(shù)器的值作為TimeVar作為輸入?yún)?shù)即可,最終會輸出北京時間。利用to_tm轉(zhuǎn)換格式后,調(diào)用bsp_calendar.c文件中的日歷計算函數(shù),求出星期、農(nóng)歷、生肖等內(nèi)容,然后使用串口顯示出來。
8、中斷服務(wù)函數(shù)
一般來說,上面的Time_Display時間顯示每秒中更新一次,而根據(jù)前面的配置,RTC每秒會進(jìn)入一次中斷,本實驗中的RTC中斷服務(wù)函數(shù)如下。RTC的秒中斷服務(wù)函數(shù)只是簡單地對全局變量TimeDisplay置1,在main函數(shù)的while循環(huán)中會檢測這個標(biāo)志,當(dāng)標(biāo)志為1時,就調(diào)用Time_Display函數(shù)顯示一次時間,達(dá)到每秒鐘更新當(dāng)前時間的效果。
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
RTC_ClearITPendingBit(RTC_IT_SEC); //清中斷標(biāo)志
TimeDisplay = 1; //置位秒顯示更新任務(wù)標(biāo)志
RTC_WaitForLastTask(); //等待RTC操作完成
}
}
9、main函數(shù)
main函數(shù)的流程非常清晰,初始化了按鍵、串口等外設(shè)后,調(diào)用RTC_CheckAndConfig函數(shù)初始化RTC,若RTC是第一次初始化,就使用變量systmtime中的默認(rèn)時間配置,若之前已配置好RTC,那么RTC_CheckAndConfig函數(shù)僅同步時鐘系統(tǒng),便于獲取實時時間。在 while循環(huán)里檢查中斷設(shè)置的TimeDisplay是否置1,若置1了就調(diào)用Time_Display函數(shù),它的輸入?yún)?shù)是庫函數(shù)RTC_GetCounter的返回值,也就是RTC計數(shù)器里的時間戳,Time_Display函數(shù)把該時間戳轉(zhuǎn)化成北京時間顯示到串口上。
/**
* @brief 主函數(shù)
* @param 無
* @retval 無
*/
int main()
{
USART_Config();
Key_GPIO_Config();
RTC_NVIC_Config();/* 配置RTC秒中斷優(yōu)先級 */
RTC_CheckAndConfig(&systmtime);
while (1)
{
if (TimeDisplay == 1)//每過1s 更新一次時間
{
Time_Display( RTC_GetCounter(),&systmtime); //當(dāng)前時間
TimeDisplay = 0;
}
//按下按鍵,通過串口修改時間
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
struct rtc_time set_time;
Time_Regulate_Get(&set_time);//使用串口接收設(shè)置的時間,輸入數(shù)字時注意末尾要加回車
Time_Adjust(&set_time);//用接收到的時間設(shè)置RTC
BKP_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);//向備份寄存器寫入標(biāo)志
}
}
}
main函數(shù)中當(dāng)檢測到開發(fā)板上的KEY1被按下時,會調(diào)用Time_Regulate_Get函數(shù)通過串口獲取配置時間,然后把獲取得的時間輸入到Time_Adjust函數(shù)把該時間寫入到RTC計數(shù)器中,更新配置。Time_Regulate_Get函數(shù)的本質(zhì)是利用重定向到串口的C標(biāo)準(zhǔn)數(shù)據(jù)流輸入函數(shù)scanf獲取用戶輸入,若獲取得的數(shù)據(jù)符合范圍,則賦值到tm結(jié)構(gòu)體中,在main函數(shù)中再調(diào)用Time_Adjust函數(shù)把tm存儲的時間寫入到RTC計數(shù)器中。
-
寄存器
+關(guān)注
關(guān)注
31文章
5386瀏覽量
121462 -
開發(fā)板
+關(guān)注
關(guān)注
25文章
5162瀏覽量
98532 -
萬年歷
+關(guān)注
關(guān)注
3文章
189瀏覽量
24006 -
RTC
+關(guān)注
關(guān)注
2文章
548瀏覽量
67182
原文標(biāo)題:MCU微課堂|CKS32F107xx RTC(二)
文章出處:【微信號:中科芯MCU,微信公眾號:中科芯MCU】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論