首页 > 技术文章 > STM32之RTC时钟

greatpumpkin 2020-09-30 08:01 原文

本文介绍如何使用STM32标准外设库驱动实时时钟RTC。

实时时钟RTC(Real Time Clock),是一个掉电后还能继续运行的定时器,一般用来运行时钟,掉电后需要额外的电池对RTC电路供电,电池正极接入V­BAT引脚,主电源VDD掉电后,电池通过V­BAT给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 }

 

源码下载:(不包括工程文件和库文件)

https://files.cnblogs.com/files/greatpumpkin/RTC.rar

推荐阅读