STM32-RTC實時時鍾-毫秒計時實現


OS:Windows 64

Development kit:MDK5.14

IDE:UV4

MCU:STM32F103C8T6

1、RTC時鍾簡介

  STM32 的實時時鍾(RTC)是一個獨立的定時器,在相應軟件配置下,可提供時鍾日歷的功能。 詳細資料請參考ALIENTEK的官方文檔——《STM32F1開發指南(精英版-庫函數版)》,以下為博主摘錄要點:

 

  • RTC 模塊和時鍾配置系統(RCC_BDCR 寄存器)在后備區域 ,系統復位后,會自動禁止訪問后備寄存器和 RTC ,所以在要設置時間之前, 先要取消備份區域(BKP)的寫保護
  • RTC 內核完全獨立於 RTC APB1 接口,而軟件是通過 APB1 接口訪問 RTC 的預分頻值、計數器值和鬧鍾值,因此需要等待時鍾同步,寄存器同步標志位(RSF)會硬件置1
  • RTC相關寄存器包括:控制寄存器(CRH、CRL)、預分頻裝載寄存器(PRLH、PRLL)、預分頻器余數寄存器(DIVH、DIVL)、計數寄存器(CNTH、CNTL)、鬧鍾寄存器(ALRH、ALRL)
  • STM32備份寄存器,存RTC校驗值和一些重要參數,最大字節84,可由VBAT供電
  • 計數器時鍾頻率:RTCCLK頻率/(預分頻裝載寄存器值+1)

2、軟硬件設計

  由於RTC是STM32芯片自帶的時鍾資源,所以自主開發的時候只需要在設計時加上晶振電路和紐扣電池即可。編程時在HARDWARE文件夾新建 rtc.c、rtc.h 文件。

3、時鍾配置與函數編寫

  為了使用RTC時鍾,需要進行配置和時間獲取,基本上按照例程來寫就可以了。為避免零散,我將附上完整代碼。函數說明如下:

rtc.c中需要編寫的函數列表
RTC_Init(void) 配置時鍾
RTC_NVIC_Config(void) 中斷分組
RTC_IRQHandler(void) 秒中斷處理
RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 設置時間
RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 鬧鍾設置
RTC_Get(void) 獲取時鍾
RTC_Get_Week(u16 year,u8 month,u8 day) 星期計算
Is_Leap_Year(u16 year) 閏年判斷

 

  事實上,以上函數並不都要,鬧鍾沒有用到的話就不要,秒中斷也可以不作處理,看項目需求。

 1 #include "sys.h"
 2 #include "delay.h"
 3 #include "rtc.h"             
 4        
 5 _calendar_obj calendar;//時鍾結構體 
 6  
 7 static void RTC_NVIC_Config(void)  8 {  9  NVIC_InitTypeDef NVIC_InitStructure;  10     NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;        //RTC全局中斷
 11     NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;    //先占優先級1位,從優先級3位
 12     NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;    //先占優先級0位,從優先級4位
 13     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;        //使能該通道中斷
 14     NVIC_Init(&NVIC_InitStructure);        //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器
 15 }  16 
 17 //實時時鍾配置  18 //初始化RTC時鍾,同時檢測時鍾是否工作正常  19 //BKP->DR1用於保存是否第一次配置的設置  20 //返回0:正常  21 //其他:錯誤代碼
 22 
 23 u8 RTC_Init(void)  24 {  25     //檢查是不是第一次配置時鍾
 26     u8 temp=0;  27     RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);    //使能PWR和BKP外設時鍾 
 28     PWR_BackupAccessCmd(ENABLE);    //使能后備寄存器訪問 
 29     if (BKP_ReadBackupRegister(BKP_DR1) != 0x5051)        //從指定的后備寄存器中讀出數據:讀出了與寫入的指定數據不相乎
 30  {  31         BKP_DeInit();    //復位備份區域 
 32         RCC_LSEConfig(RCC_LSE_ON);    //設置外部低速晶振(LSE),使用外設低速晶振
 33         while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)    //檢查指定的RCC標志位設置與否,等待低速晶振就緒
 34  {  35             temp++;  36             delay_ms(10);  37  }  38         if(temp>=250)return 1;//初始化時鍾失敗,晶振有問題 
 39         RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);        //設置RTC時鍾(RTCCLK),選擇LSE作為RTC時鍾 
 40         RCC_RTCCLKCmd(ENABLE);    //使能RTC時鍾 
 41         RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成
 42         RTC_WaitForSynchro();        //等待RTC寄存器同步 
 43         RTC_ITConfig(RTC_IT_SEC, ENABLE);        //使能RTC秒中斷
 44         RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成
 45         RTC_EnterConfigMode();/// 允許配置 
 46         RTC_SetPrescaler(32767); //設置RTC預分頻的值
 47         RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成
 48         RTC_Set(2018,4,2,17,37,00);  //設置時間 
 49         RTC_ExitConfigMode(); //退出配置模式 
 50         BKP_WriteBackupRegister(BKP_DR1, 0X5051);    //向指定的后備寄存器中寫入用戶程序數據
 51  }  52     else//系統繼續計時
 53  {  54 
 55         RTC_WaitForSynchro();    //等待最近一次對RTC寄存器的寫操作完成
 56         RTC_ITConfig(RTC_IT_SEC, ENABLE);    //使能RTC秒中斷
 57         RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成
 58  }  59     RTC_NVIC_Config();//RCT中斷分組設置 
 60     RTC_Get();//更新時間 
 61     return 0; //ok
 62 
 63 }  64 //RTC時鍾中斷  65 //每秒觸發一次  66 //extern u16 tcnt; 
 67 void RTC_IRQHandler(void)  68 {  69 // if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒鍾中斷  70 // {  71 // RTC_Get();//更新時間  72 // }  73 // if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//鬧鍾中斷  74 // {  75 // RTC_ClearITPendingBit(RTC_IT_ALR); //清鬧鍾中斷  76 // RTC_Get(); //更新時間  77 //      //printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//輸出鬧鈴時間  78 //        
 79 // } 
 80     RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);        //清鬧鍾中斷
 81  RTC_WaitForLastTask();  82 }  83 
 84 
 85 //判斷是否是閏年函數  86 //月份 1 2 3 4 5 6 7 8 9 10 11 12  87 //閏年 31 29 31 30 31 30 31 31 30 31 30 31  88 //非閏年 31 28 31 30 31 30 31 31 30 31 30 31  89 //輸入:年份  90 //輸出:該年份是不是閏年.1,是.0,不是
 91 u8 Is_Leap_Year(u16 year)  92 {  93     if(year%4==0) //必須能被4整除
 94  {  95         if(year%100==0)  96  {  97             if(year%400==0)return 1;//如果以00結尾,還要能被400整除 
 98             else return 0;  99         }else return 1; 100     }else return 0; 101 } 102 
103 
104 //設置時鍾 105 //把輸入的時鍾轉換為秒鍾 106 //以1970年1月1日為基准 107 //1970~2099年為合法年份 108 //返回值:0,成功;其他:錯誤代碼. 109 //月份數據表 
110 const u8  table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正數據表 
111 const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};//平年的月份日期表
112 
113 u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 114 { 115  u16 t; 116     u32 seccount=0; 117     if(syear<1970||syear>2099)return 1; 118     for(t=1970;t<syear;t++)    //把所有年份的秒鍾相加
119  { 120         if(Is_Leap_Year(t))seccount+=31622400;//閏年的秒鍾數
121         else seccount+=31536000;              //平年的秒鍾數
122  } 123     smon-=1; 124     for(t=0;t<smon;t++)       //把前面月份的秒鍾數相加
125  { 126         seccount+=(u32)mon_table[t]*86400;//月份秒鍾數相加
127         if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//閏年2月份增加一天的秒鍾數 
128  } 129     seccount+=(u32)(sday-1)*86400;//把前面日期的秒鍾數相加 
130     seccount+=(u32)hour*3600;//小時秒鍾數
131     seccount+=(u32)min*60;     //分鍾秒鍾數
132     seccount+=sec;//最后的秒鍾加上去
133 
134     RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);    //使能PWR和BKP外設時鍾 
135     PWR_BackupAccessCmd(ENABLE);    //使能RTC和后備寄存器訪問 
136     RTC_SetCounter(seccount);    //設置RTC計數器的值
137 
138     RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成 
139     return 0; 140 } 141 
142 //初始化鬧鍾 143 //以1970年1月1日為基准 144 //1970~2099年為合法年份 145 //syear,smon,sday,hour,min,sec:鬧鍾的年月日時分秒 146 //返回值:0,成功;其他:錯誤代碼.
147 u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 148 { 149  u16 t; 150     u32 seccount=0; 151     if(syear<1970||syear>2099)return 1; 152     for(t=1970;t<syear;t++)    //把所有年份的秒鍾相加
153  { 154         if(Is_Leap_Year(t))seccount+=31622400;//閏年的秒鍾數
155         else seccount+=31536000;              //平年的秒鍾數
156  } 157     smon-=1; 158     for(t=0;t<smon;t++)       //把前面月份的秒鍾數相加
159  { 160         seccount+=(u32)mon_table[t]*86400;//月份秒鍾數相加
161         if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//閏年2月份增加一天的秒鍾數 
162  } 163     seccount+=(u32)(sday-1)*86400;//把前面日期的秒鍾數相加 
164     seccount+=(u32)hour*3600;//小時秒鍾數
165     seccount+=(u32)min*60;     //分鍾秒鍾數
166     seccount+=sec;//最后的秒鍾加上去 167     //設置時鍾
168     RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);    //使能PWR和BKP外設時鍾 
169     PWR_BackupAccessCmd(ENABLE);    //使能后備寄存器訪問 170     //上面三步是必須的!
171     
172  RTC_SetAlarm(seccount); 173  
174     RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成 
175     
176     return 0; 177 } 178 
179 
180 //得到當前的時間 181 //返回值:0,成功;其他:錯誤代碼.
182 u8 RTC_Get(void) 183 { 184     static u16 daycnt=0; 185     u32 timecount=0; 186     u32 temp=0; 187     u16 temp1=0; 188     timecount=RTC_GetCounter(); 189      temp=timecount/86400;   //得到天數(秒鍾數對應的)
190     if(daycnt!=temp)//超過一天了
191  { 192         daycnt=temp; 193         temp1=1970;    //從1970年開始
194         while(temp>=365) 195  { 196             if(Is_Leap_Year(temp1))//是閏年
197  { 198                 if(temp>=366)temp-=366;//閏年的秒鍾數
199                 else {temp1++;break;} 200  } 201             else temp-=365;      //平年 
202             temp1++; 203  } 204         calendar.w_year=temp1;//得到年份
205         temp1=0; 206         while(temp>=28)//超過了一個月
207  { 208             if(Is_Leap_Year(calendar.w_year)&&temp1==1)//當年是不是閏年/2月份
209  { 210                 if(temp>=29)temp-=29;//閏年的秒鍾數
211                 else break; 212  } 213             else 
214  { 215                 if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
216                 else break; 217  } 218             temp1++; 219  } 220         calendar.w_month=temp1+1;    //得到月份
221         calendar.w_date=temp+1;      //得到日期 
222  } 223     temp=timecount%86400;             //得到秒鍾數 
224     calendar.hour=temp/3600;         //小時
225     calendar.min=(temp%3600)/60;     //分鍾 
226     calendar.sec=(temp%3600)%60;     //秒鍾
227     calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//獲取星期
228     calendar.msec=(32767-RTC_GetDivider())*    1000/32767; 229     return 0; 230 } 231 
232 
233 //獲得現在是星期幾 234 //功能描述:輸入公歷日期得到星期(只允許1901-2099年) 235 //輸入參數:公歷年月日 236 //返回值:星期號 
237 u8 RTC_Get_Week(u16 year,u8 month,u8 day) 238 { 239  u16 temp2; 240  u8 yearH,yearL; 241     
242     yearH=year/100;    yearL=year%100; 243     // 如果為21世紀,年份數加100 
244     if (yearH>19)yearL+=100; 245     // 所過閏年數只算1900年之后的 
246     temp2=yearL+yearL/4; 247     temp2=temp2%7; 248     temp2=temp2+day+table_week[month-1]; 249     if (yearL%4==0&&month<3)temp2--; 250     return(temp2%7); 251 }              
rtc.c
 1 #include "sys.h"
 2 
 3 //時間結構體
 4 typedef struct 
 5 {  6     vu8 hour;//vu8
 7  vu8 min;  8  vu8 sec;  9  vu16 msec; 10     
11     //公歷日月年周
12  vu16 w_year; 13  vu8 w_month; 14  vu8 w_date; 15  vu8 week; 16 }_calendar_obj; 17 
18 extern _calendar_obj calendar;    //日歷結構體
19 extern u8 const mon_table[12];    //月份日期數據表
20 
21 u8 RTC_Init(void);        //初始化RTC,返回0,失敗;1,成功;
22 u8 Is_Leap_Year(u16 year);//平年,閏年判斷 23 
24 //u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);
25 u8 RTC_Get(void);         //更新時間 
26 u8 RTC_Get_Week(u16 year,u8 month,u8 day); 27 u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//設置時間 
rtc.h

4、秒鍾計時原理

  使用外部32.768KHz的晶振作為時鍾的輸入頻率,設置預分頻裝載寄存器的值為32767,根據計算公式,剛好可以得到1秒的計數頻率。時間基准設置為1970年1月1日0時0分0秒,后續的時間都以這個為基准進行計算。RTC計數器是32位的,理論上可以記錄136年左右的時間。(注意不必在秒中斷里更新時間)

5、毫秒計時原理

  如果要獲取到毫秒級的時鍾怎么辦?在我的項目中就有這樣的要求。事實上,獲取毫秒時鍾也非常簡單。

  查閱開發指南,RTC預分頻器余數寄存器(RTC_DIVH、RTC_DIVL),這兩個寄存器的作用就是用來獲得比秒鍾更為准確的時鍾。 該寄存器的值自減的,用於保存還需要多少時鍾周期獲得一個秒信號。在一次秒鍾更新后,由硬件重新裝載。這兩個寄存器和 RTC 預分頻裝載寄存器位數是一樣的。也就是說,如果預分頻裝載寄存器的值為32767,那么余數寄存器就會在每一次秒更新時由硬件重新裝載為32767,然后向下計數,計數到0表示一秒,也即1000ms。

  因此,我們在時鍾結構體中添加msec成員

 1 //時間結構體
 2 typedef struct 
 3 {  4     vu8 hour;//vu8
 5  vu8 min;  6  vu8 sec;  7  vu16 msec;  8     
 9     //公歷日月年周
10  vu16 w_year; 11  vu8 w_month; 12  vu8 w_date; 13  vu8 week; 14 }_calendar_obj;    

  獲取毫秒時間

1 calendar.msec=(32767-RTC_GetDivider())*1000/32767;

6、修改時間  

  如果RTC時鍾在使用的過程中不准了(我遇到的情況大概是掉電跑了2個月,重新測試的時候差了2分鍾左右),可以重新校准時鍾。我們在備份區域 BKP_DR1 中寫入 0X5051 ,下次開機(或復位)的時候,先讀取 BKP_DR1 的值,然后判斷是否是 0X5051決定是不是要配置。 如果要修改時間,請將0x5051改為其它數據,修改RTC_Set函數實參,再重新燒寫一下程序即可。

if (BKP_ReadBackupRegister(BKP_DR1) != 0x5051) { ... RTC_EnterConfigMode();/// 允許配置 
    RTC_SetPrescaler(32767); //設置RTC預分頻的值
    RTC_WaitForLastTask();    //等待最近一次對RTC寄存器的寫操作完成
    RTC_Set(2018,4,2,17,37,00);  //設置時間 
    RTC_ExitConfigMode(); //退出配置模式 
    BKP_WriteBackupRegister(BKP_DR1, 0X5051);    //向指定的后備寄存器中寫入用戶程序數據 
}

 

 

 



 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM