RTC介绍
RTC和后备寄存器通过一个开关供电,在Voo有效时该开关选择Voo供电,否则由VBAT引脚供电。后备寄存器(10个16位的寄存器)可以用于在关闭Voo时,保存20个字节的用户应用数据。RTC和后备寄存器不会被系统或电源复位源复位;当从待机模式唤醒时,也不会被复位。
实时时钟具有–组连续运行的计数器,可以通过适当的软件提供日历时钟功能,还具有闹钟中断和阶段性中断功能。RTC的驱动时钟可以是一个使用外部晶体的32.768kHz的振荡器、内部低功耗RC振荡器或高速的外部时钟经128分频。内部低功耗RC振荡器的典型频率为40kHz。为补偿天然晶体的偏差,可以通过输出一个512Hz的信号对RTC的时钟进行校准。RTC具有一个32位的可编程计数器,使用比较寄存器可以进行长时间的测量。有一“个20位的预分频器用于时基时钟,默认情况下时钟为32.768kHz时,它将产生一一个1秒长的时间基准。
RTC初始化过程
有如下几种函数类型
void RTC_First_Config(void);//首次启用RTC的设置
void RTC_Config(void);//实时时钟初始化
u8 Is_Leap_Year(u16 year);//判断是否是闰年函数
u8 RTC_Get(void);//读出当前时间值
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//写入当前时间
u8 RTC_Get_Week(u16 year,u8 month,u8 day);//按年月日计算星期
其中首次启用RTC的设置存在的必要性,RTC不仅使用主电源供电而且使用后备电源供电,当主电源也断电之后,后备电源可能会供电,但如果后被电源断电之后,后备存储器存储的内容丢失,RTC内部的相关寄存器值会丢失,这时上电之后需要对RTC的所有寄存器设置,让RTC处于走时工作状态。综上所述,如果RTC的后备电源有过断电情况(所有数据均丢失)或者第一次使用RTC时需要使用首次RTC设置。
具体代码如下:
void RTC_First_Config(void){ //首次启用RTC的设置
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//启用PWR和BKP的时钟(from APB1)
PWR_BackupAccessCmd(ENABLE);//后备域解锁
BKP_DeInit();//备份寄存器模块复位
RCC_LSEConfig(RCC_LSE_ON);//外部32.768KHZ晶振开启
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);//等待稳定
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//RTC时钟源配置成LSE(外部低速晶振32.768KHZ)
RCC_RTCCLKCmd(ENABLE);//RTC开启
RTC_WaitForSynchro();//开启后需要等待APB1时钟与RTC时钟同步,才能读写寄存器
RTC_WaitForLastTask();//读写寄存器前,要确定上一个操作已经结束
RTC_SetPrescaler(32767);//设置RTC分频器,使RTC时钟为1Hz,RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1)
RTC_WaitForLastTask();//等待寄存器写入完成
//当不使用RTC秒中断,可以屏蔽下面2条
// RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能秒中断
// RTC_WaitForLastTask();//等待写入完成
}
其中可以通过设置
RTC_SetPrescaler(32767);//设置RTC分频器,使RTC时钟为1Hz,RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1)
中的参数来进行时钟的矫正、微调。
void RTC_Config(void){ //实时时钟初始化
//在BKP的后备寄存器1中,存了一个特殊字符0xA5A5
//第一次上电或后备电源掉电后,该寄存器数据丢失,表明RTC数据丢失,需要重新配置
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5){//判断寄存数据是否丢失
RTC_First_Config();//重新配置RTC
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5
}else{
//若后备寄存器没有掉电,则无需重新配置RTC
//这里我们可以利用RCC_GetFlagStatus()函数查看本次复位类型
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET){
//这是上电复位
}
else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET){
//这是外部RST管脚复位
}
RCC_ClearFlag();//清除RCC中复位标志
//虽然RTC模块不需要重新配置,且掉电后依靠后备电池依然运行
//但是每次上电后,还是要使能RTCCLK
RCC_RTCCLKCmd(ENABLE);//使能RTCCLK
RTC_WaitForSynchro();//等待RTC时钟与APB1时钟同步
//当不使用RTC秒中断,可以屏蔽下面2条
// RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能秒中断
// RTC_WaitForLastTask();//等待操作完成
}
这里值得注意的是,0xA5A5是作为标志检查位而写入的,当后备电源断电之后,RTC相关寄存器和后备寄存器的值必定会丢失,这时可以根据此来判断是否需要进行首次RTC设置,进行首次RTC值设置之后,还应该对相应的一个后备寄存器写入该值,以便下次进行检查,代码中还有上电复位和外部SET复位,可向相应的if语句中写入上电复位和外部SET复位对应的不同效果,而且作为初学者,我们只需要调用一般的初始化函数即可,因为该函数中已经包含首次RTC设置的判断语句,除非强制首次RTC设置。另外初学者只要学会调用下面两条程序即可
u8 RTC_Get(void); //读出当前时间值
{ //写入当前时间(1970~2099年有效),
u16 t;
u32 seccount=0;
if(syear<2000||syear>2099)return 1;//syear范围1970-2099,此处设置范围为2000-2099
for(t=1970;t<syear;t++){ //把所有年份的秒钟相加
if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
else seccount+=31536000; //平年的秒钟数
}
smon-=1;
for(t=0;t<smon;t++){ //把前面月份的秒钟数相加
seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数
}
seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
seccount+=(u32)hour*3600;//小时秒钟数
seccount+=(u32)min*60; //分钟秒钟数
seccount+=sec;//最后的秒钟加上去
RTC_First_Config(); //重新初始化时钟
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5
RTC_SetCounter(seccount);//把换算好的计数器值写入
RTC_WaitForLastTask(); //等待写入完成
return 0; //返回值:0,成功;其他:错误代码.
}
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec); //写入当前时间
{//读出当前时间值 //返回值:0,成功;其他:错误代码.
static u16 daycnt=0;
u32 timecount=0;
u32 temp=0;
u16 temp1=0;
timecount=RTC_GetCounter();
temp=timecount/86400; //得到天数(秒钟数对应的)
if(daycnt!=temp){//超过一天了
daycnt=temp;
temp1=1970; //从1970年开始
while(temp>=365){
if(Is_Leap_Year(temp1)){//是闰年
if(temp>=366)temp-=366;//闰年的秒钟数
else {temp1++;break;}
}
else temp-=365; //平年
temp1++;
}
ryear=temp1;//得到年份
temp1=0;
while(temp>=28){//超过了一个月
if(Is_Leap_Year(ryear)&&temp1==1){//当年是不是闰年/2月份
if(temp>=29)temp-=29;//闰年的秒钟数
else break;
}else{
if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
else break;
}
temp1++;
}
rmon=temp1+1;//得到月份
rday=temp+1; //得到日期
}
temp=timecount%86400; //得到秒钟数
rhour=temp/3600; //小时
rmin=(temp%3600)/60; //分钟
rsec=(temp%3600)%60; //秒钟
rweek=RTC_Get_Week(ryear,rmon,rday);//获取星期
return 0;
}
其中写时间函数是需要将对应的时间值按照形参列表写入RTC中的32位计数器,返回值若为0,则代表写入成功。读函数则是对RTC中32位计数器中的值读出来,按照一定的算法存储在全局变量中,需要注意的是,读函数将对应时间的对于的周也读了出来,因此初学者不必调用计算周的函数因为该函数已经在读函数中调用,此外计算闰年的函数也不需要调用,读和写函数中均调用了闰年判断函数,和写函数一样,若返回值为0,则表示读出成功。
闰年函数和周函数如下:
//判断是否是闰年函数
//月份 1 2 3 4 5 6 7 8 9 10 11 12
//闰年 31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year){
if(year%4==0){ //必须能被4整除
if(year%100==0){
if(year%400==0)return 1;//如果以00结尾,还要能被400整除
else return 0;
}else return 1;
}else return 0;
}
u8 RTC_Get_Week(u16 year,u8 month,u8 day){ //按年月日计算星期(只允许1901-2099年)//已由RTC_Get调用
u16 temp2;
u8 yearH,yearL;
yearH=year/100;
yearL=year%100;
// 如果为21世纪,年份数加100
if (yearH>19)yearL+=100;
// 所过闰年数只算1900年之后的
temp2=yearL+yearL/4;
temp2=temp2%7;
temp2=temp2+day+table_week[month-1];
if (yearL%4==0&&month<3)temp2--;
return(temp2%7); //返回星期值
}