本文介紹如何使用STM32標准外設庫驅動實時時鍾RTC。
實時時鍾RTC(Real Time Clock),是一個掉電后還能繼續運行的定時器,一般用來運行時鍾,掉電后需要額外的電池對RTC電路供電,電池正極接入VBAT引腳,主電源VDD掉電后,電池通過VBAT給RTC電路供電,使得時鍾可以繼續運行,確保設備重新上電時,時鍾不丟失。
本文適合對單片機及C語言有一定基礎的開發人員閱讀,MCU使用STM32F103VE系列。
RTC分為兩部分,初始化和操作。
1. 初始化
初始化分兩步:通用中斷、RTC
1.1. 通用中斷:優先級分組、中斷源、優先級、使能
- 優先級分組:設定合適的優先級分組
- 中斷源:選擇指定的中斷源:RTC_IRQn
- 優先級:設定合適的優先級,一般實時時鍾優先級可以設的高一些。
- 使能:調用庫函數即可。
1.2. RTC
分為兩種情況:一種是第一次配置RTC,另外一種是配置好之后的重新上電之后的初始化。
1.2.1. 第一次配置RTC
1) 使能外設時鍾(PWR和BKP)
2) 使能后備寄存器訪問
3) 復位備份區域
4) 設置低速外部時鍾信號LSE使用外部低速晶振
5) 等待低速外部時鍾信號LSE就緒
6) 設置RTC使用低速外部時鍾信號LSE
7) 使能RTC時鍾
8) 等待RTC寄存器寫操作完成
9) 等待RTC寄存器同步完成
10) 設置RTC預分頻值
11) 等待RTC寄存器寫操作完成
12) 設置一個時鍾初始日期時間
13) 給指定的后備寄存器寫入一個特定值
14) 使能RTC秒中斷
15) 等待RTC寄存器寫操作完成
1.2.2. 配置好之后的重新上電之后的初始化
1) 使能外設時鍾(PWR和BKP)
2) 使能后備寄存器訪問
3) 等待RTC寄存器同步完成
4) 使能RTC秒中斷
5) 等待RTC寄存器寫操作完成
2. 操作
操作主要分為讀取、設置、時間轉換和秒中斷函數。
RTC是一個每秒加1的計數器,其計數器的值表示了當前的時間,該計數器是一個32位的寄存器,其最大值為232,約為136年,該計數器值又被稱作UNIX時間戳,而計數器為0時代表的時間為1970年1月1日0時0分0秒,又被稱作UNIX時間元年,時間每過1秒,該計數器值加1。舉例來說,2000年1月1日0時0分0秒對應的計數器值為946,684,800。但要注意的是,UNIX時間戳為格林威治時間,比北京時間晚8小時。因此該值對應的北京時間為2000年1月1日8時0分0秒。如果認定0代表北京時間1970年1月1日0時0分0秒,那么就不必考慮8小時的時差,這樣程序處理起來更為簡單且不易出錯。
讀取操作就是將計數器的值讀取出來,調用庫函數RTC_GetCounter()即可返回當前計數器的值。
寫入操作就是更新當前計數器的值,調用庫函數RTC_SetCounter()即可更新當前計數器的值,注意調用RTC_SetCounter()完畢后需要調用RTC_WaitForLastTask()等待RTC寄存器寫操作完成。
因為通過查看計數器的值無法直觀得知當前的時間信息,因此需要時間轉換函數將計數器值與年月日時分秒值進行互相轉換。一個是將當前計數器的值轉換為年月日時分秒值,對應的函數為RTC_Convert ();另一個是將年月日時分秒值轉換為計數器值,對應的函數為RTC_Set(),該函數也包含了寫入計數器值操作。
RTC可以設置一個秒中斷函數,該函數每秒中斷一次,對應的中斷服務函數為RTC_IRQHandler(),該函數執行時可以將當前計數器值讀取出來,用作程序其他部分更新時間用,因為計數器的累計是自動進行的,因此無需在此函數中累加計數器值。
完整代碼(僅自己編寫的部分)
1 #include "RTC.h" 2 #include "delay.h" 3 4 #include <stdio.h> 5 6 #define BKP_VALUE 0x5A5A 7 8 Calendar_s calendar; //日歷結構體 9 vu32 rtcCounter; //RTC計數值 10 11 void RTC_NVIC_Config(void) 12 { 13 NVIC_InitTypeDef NVIC_InitStructure; 14 15 /* 嵌套向量中斷控制器組選擇 */ 16 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 17 NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; 18 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; 19 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 20 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 21 NVIC_Init(&NVIC_InitStructure); 22 } 23 24 //判斷是否是閏年函數 25 //輸入:年份 26 //輸出:該年份是不是閏年.1,是.0,不是 27 uint8_t isLeapYear(uint16_t year) 28 { 29 30 if(year % 4 == 0) //必須能被4整除 31 { 32 if(year % 100 == 0) 33 { 34 if(year % 400 == 0){ 35 return 1;//如果以00結尾,還要能被400整除 36 }else{ 37 return 0; 38 } 39 }else{ 40 return 1; 41 } 42 }else{ 43 return 0; 44 } 45 } 46 47 //月份數據表 48 uint8_t const table_week[12]={0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5}; //月修正數據表 49 //平年的月份日期表 50 const uint8_t mon_table[12]={31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 51 52 //獲得現在是星期幾 53 //功能描述:輸入公歷日期得到星期(只允許1901-2099年) 54 //輸入參數:公歷年月日 55 //返回值:星期號 56 uint8_t RTC_GetWeek(uint16_t year, uint8_t month, uint8_t day) 57 { 58 uint16_t temp2; 59 uint8_t yearH, yearL; 60 61 yearH = year / 100; 62 yearL = year % 100; 63 // 如果為21世紀,年份數加100 64 if(yearH > 19){ 65 yearL += 100; 66 } 67 // 所過閏年數只算1900年之后的 68 temp2 = yearL + yearL / 4; 69 temp2 = temp2 % 7; 70 temp2 = temp2 + day + table_week[month - 1]; 71 72 if((yearL % 4 == 0) && (month < 3)){ 73 temp2--; 74 } 75 76 return temp2 % 7; 77 } 78 79 //得到當前的時間 80 //返回值:0,成功;其他:錯誤代碼. 81 uint8_t RTC_Convert(Calendar_s * pCalendar) 82 { 83 static uint16_t daycnt = 0; 84 uint32_t timecount = 0; 85 uint32_t temp = 0; 86 uint16_t temp1 = 0; 87 88 if(pCalendar == NULL){ 89 return 1; 90 } 91 92 timecount = rtcCounter; 93 94 temp = timecount / 86400; //得到天數(一天對應的秒數) 95 96 if(daycnt != temp) //超過一天了 97 { 98 daycnt = temp; 99 temp1 = 1970; //從1970年開始 100 while(temp >= 365) 101 { 102 if(isLeapYear(temp1)) //是閏年 103 { 104 if(temp >= 366){ 105 temp -= 366; //閏年的天數 106 }else{ 107 temp1++; 108 break; 109 } 110 }else{ 111 temp -= 365; //平年 112 } 113 temp1++; 114 } 115 pCalendar->year = temp1;//得到年份 116 117 temp1 = 0; 118 while(temp >= 28) //超過了一個月 119 { 120 if(isLeapYear(pCalendar->year) && (temp1 == 1)){ //當年是不是閏年/2月份 121 if(temp >= 29){ 122 temp -= 29; //閏年的秒鍾數 123 }else{ 124 break; 125 } 126 }else{ 127 if(temp >= mon_table[temp1]){ 128 temp -= mon_table[temp1]; //平年 129 }else{ 130 break; 131 } 132 } 133 temp1++; 134 } 135 pCalendar->month = temp1 + 1; //得到月份 136 pCalendar->day = temp + 1; //得到日期 137 } 138 temp = timecount % 86400; //得到秒鍾數 139 pCalendar->hour = temp / 3600; //小時 140 pCalendar->min = (temp % 3600) / 60; //分鍾 141 pCalendar->sec = (temp % 3600) % 60; //秒鍾 142 pCalendar->week = RTC_GetWeek(pCalendar->year, pCalendar->month, pCalendar->day); //獲取星期 143 144 return 0; 145 } 146 147 //得到當前的時間 148 //返回值:0,成功;其他:錯誤代碼. 149 void RTC_Get(void) 150 { 151 static uint16_t daycnt = 0; 152 uint32_t timecount = 0; 153 uint32_t temp = 0; 154 uint16_t temp1 = 0; 155 156 timecount = RTC_GetCounter(); 157 temp = timecount / 86400; //得到天數(一天對應的秒數) 158 159 if(daycnt != temp) //超過一天了 160 { 161 daycnt = temp; 162 temp1 = 1970; //從1970年開始 163 while(temp >= 365) 164 { 165 if(isLeapYear(temp1)) //是閏年 166 { 167 if(temp >= 366){ 168 temp -= 366; //閏年的天數 169 }else{ 170 temp1++; 171 break; 172 } 173 }else{ 174 temp -= 365; //平年 175 } 176 temp1++; 177 } 178 calendar.year = temp1;//得到年份 179 180 temp1 = 0; 181 while(temp >= 28) //超過了一個月 182 { 183 if(isLeapYear(calendar.year) && (temp1 == 1)){ //當年是不是閏年/2月份 184 if(temp >= 29){ 185 temp -= 29; //閏年的秒鍾數 186 }else{ 187 break; 188 } 189 }else{ 190 if(temp >= mon_table[temp1]){ 191 temp -= mon_table[temp1]; //平年 192 }else{ 193 break; 194 } 195 } 196 temp1++; 197 } 198 calendar.month = temp1 + 1; //得到月份 199 calendar.day = temp + 1; //得到日期 200 } 201 temp = timecount % 86400; //得到秒鍾數 202 calendar.hour = temp / 3600; //小時 203 calendar.min = (temp % 3600) / 60; //分鍾 204 calendar.sec = (temp % 3600) % 60; //秒鍾 205 calendar.week = RTC_GetWeek(calendar.year, calendar.month, calendar.day); //獲取星期 206 } 207 208 //設置時鍾 209 //把輸入的時鍾轉換為秒鍾 210 //以1970年1月1日為基准 211 //1970~2099年為合法年份 212 //返回值:0,成功;其他:錯誤代碼. 213 uint8_t RTC_Set(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec) 214 { 215 uint16_t i; 216 uint32_t seccount=0; 217 218 if((year < 1970) || (year > 2099)){ 219 return 1; 220 } 221 222 for(i = 1970; i < year;i++) //把所有年份的秒鍾相加 223 { 224 if(isLeapYear(i)){ 225 seccount += 31622400;//閏年的秒鍾數 226 }else{ 227 seccount += 31536000; //平年的秒鍾數 228 } 229 } 230 month -= 1; 231 for(i = 0; i < month; i++) //把前面月份的秒鍾數相加 232 { 233 seccount += (uint32_t)mon_table[i] * 86400; //月份秒鍾數相加 234 if(isLeapYear(year) && (i == 1)){ 235 seccount += 86400; //閏年2月份增加一天的秒鍾數 236 } 237 } 238 seccount += (uint32_t)(day - 1) * 86400;//把前面日期的秒鍾數相加 239 seccount += (uint32_t)hour * 3600; //小時秒鍾數 240 seccount += (uint32_t)min * 60; //分鍾秒鍾數 241 seccount += sec; //最后的秒鍾加上去 242 243 RTC_SetCounter(seccount); //設置RTC計數器的值 244 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成 245 246 return 0; 247 } 248 249 //實時時鍾配置 250 //初始化RTC時鍾 251 //BKP->DR1用於保存是否第一次配置的設置 252 //返回0:正常 253 //其他:錯誤代碼 254 uint8_t RTC_Init(void) 255 { 256 uint8_t temp = 0; 257 258 RTC_NVIC_Config(); //RCT中斷分組設置 259 260 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外設時鍾 261 PWR_BackupAccessCmd(ENABLE); //使能后備寄存器訪問 262 263 //檢查是不是第一次配置時鍾 264 if (BKP_ReadBackupRegister(BKP_DR1) != BKP_VALUE){ //從指定的后備寄存器中讀出數據 265 BKP_DeInit(); //復位備份區域 266 267 RCC_LSEConfig(RCC_LSE_ON); //設置外部低速晶振(LSE),使用外設低速晶振 268 while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) //檢查指定的RCC標志位設置與否,等待低速晶振就緒 269 { 270 if(temp++ >= 250){ 271 return 1; //初始化時鍾失敗,晶振有問題 272 } 273 delay_ms(10); 274 } 275 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //設置RTC時鍾(RTCCLK),選擇LSE作為RTC時鍾 276 277 RCC_RTCCLKCmd(ENABLE); //使能RTC時鍾 278 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成 279 RTC_WaitForSynchro(); //等待RTC寄存器同步 280 281 RTC_SetPrescaler(32767); //設置RTC預分頻的值 282 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成 283 284 RTC_Set(2020, 8, 24, 16, 30, 10); //設置時間 285 286 BKP_WriteBackupRegister(BKP_DR1, BKP_VALUE); //向指定的后備寄存器中寫入用戶程序數據 287 }else{ //系統繼續計時 288 RTC_WaitForSynchro(); //等待最近一次對RTC寄存器的寫操作完成 289 } 290 291 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中斷 292 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成 293 294 RTC_Get(); //更新時間 295 296 return 0; 297 } 298 299 //RTC時鍾中斷,每秒觸發一次,更新一次時間 300 void RTC_IRQHandler(void) 301 { 302 if (RTC_GetITStatus(RTC_IT_SEC) != RESET) //秒鍾中斷 303 { 304 RTC_ClearITPendingBit(RTC_IT_SEC); //清秒鍾中斷 305 RTC_WaitForLastTask(); 306 // RTC_Get(); //更新時間 307 rtcCounter = RTC_GetCounter(); 308 } 309 }