RTC 时间、闹钟

实时时钟RTC是一个独立的定时器。RTC模块拥有一个连续计数的计数器,在软件配置下,可以提供时钟日历的功能。修改计数器的值可以重新设置当前时间和日期 RTC还包含用于管理低功耗模式的自动唤醒单元。

在掉电情况下 RTC仍可以独立运行 只要芯片的备用电源一直供电,RTC上的时间会一直走。相对于通用定时器TIM 外设,它十分简单,只有很纯粹的计时和触发中断的功能;但从掉电还继续运行的角度来说,它却是32 中唯一一个具有如此强大功能的外设。所以RTC外设的复杂之处并不在于它的定时功能,而在于它掉电还继续运行的特性。

以上所说的掉电,是指主电源VDD 断开的情况,为了RTC 外设掉电继续运行,必须接上锂电池给STM32 的RTC、备份发卡通过VBAT 引脚供电。当主电源VDD 有效时,由VDD 给RTC 外设供电;而当VDD 掉电后,由VBAT 给RTC 外设供电。但无论由什么电源供电,RTC 中的数据都保存在属于RTC 的备份域中,若主电源VDD 和VBAT 都掉电,那么备份域中保存的所有数据将丢失。备份域除了RTC 模块的寄存器,还有42 个16 位的寄存器可以在VDD 掉电的情况下保存用户程序的数据,系统复位或电源复位时,这些数据也不会被复位。

RCT特征:

● 可编程的预分频系数:分频系数高为220。

● 32位的可编程计数器,可用于较长时间段的测量。

● 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟 频率的四分之一以上)。

可以选择以下三种RTC的时钟源:

● HSE时钟除以128;

● LSE振荡器时钟;

● LSI振荡器时钟

2个独立的复位类型:

● APB1接口由系统复位;

● RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位

3个专门的可屏蔽中断:

● 1.闹钟中断,用来产生一个软件可编程的闹钟中断。

● 2.秒中断,用来产生一个可编程的周期性中断信号(长可达1秒)。

● 3.溢出中断,指示内部可编程计数器溢出并回转为0的状态。

结构框图:

UNIX时间戳

由于RTC_CNT是32位寄存器,可存储的最大值为2^32-1,即这样计时,它将在136年时溢出。假如某个时刻读取到计数器的数值为X = 606024*2,即两天时间的秒数,而假设又知道计数器是在2011 年1 月1 日的0 时0 分0 秒置0 的,那么就可以根据计数器的这个相对时间数值,计算得这个X 时刻是2011 年1 月3 日的0 时0 分0 秒了。而计数器则会在(2011+136) 年左右溢出,也就是说到了(2011+136)年时,如果我们还在使用这个计数器提供时间的话就会出现问题。在这个例子中,定时器被置0 的这个时间被称为计时元年,相对计时元年经过的秒数称为时间戳,也就是计数器中的值。

大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX 时间戳和UNIX 计时元年。UNIX 计时元年被设置为格林威治时间1970 年1 月1 日0 时0 分0 秒,大概是为了纪念UNIX 的诞生的时代吧,而UNIX 时间戳即为当前时间相对于UNIX 计时元年经过的秒数。因为unix 时间戳主要用来表示当前时间或者和电脑有关的日志时间(如文件创立时间,log 发生时间等),考虑到所有电脑文件不可能在1970 年前创立,所以用unix 时间戳很少用来表示1970 前的时间。在这个计时系统中,使用的是有符号的32 位整型变量来保存UNIX 时间戳的,即实际可用计数位数比我们上面例子中的少了一位,少了这一位,UNIX 计时元年也相对提前了,这个计时方法在2038 年1 月19 日03 时14 分07 秒将会发生溢出,这个时间离我们并不远。由于UNIX 时间戳被广泛应用到各种系统中,溢出可能会导致系统发生严重错误,届时,很可能会重演一次“千年虫”的问题,所以在设计预期寿命较长的设备需要注意。

BKP备份寄存器
1备份寄存器是42个16位的寄存器。可用来存储84个字节数据。2它们处在备份区域,当VDD电源切断,仍然由Vear维持供
电。
3当系统在待机模式下被唤醒,或者系统复位或者电源复位,它们也
不会复位。
4执行以下操作将使能对后备寄存器和 RTC 访问:
设置寄存器RCC APB1ENR的 PWREN 和 BKPEN位,使能电源和后备时钟。
设置寄存器 PWR CR的 DBP位,使能对 RTC和后备寄存器的访问。

一共有 42 个 16 位备份寄存器。常用来保存一些系统配置信息和相关标志位。

RTC相关寄存器
1 、RTC控制寄存器(RTC_CRH,RTC_CRL)
2、 RTC预分频装载寄存器(RTC_ PRLH, RTC_PRLL)
3、RTC预分频余数寄存器(RTC_ DIVH,RTC_DIVL)

4、RTC计数器寄存器(RTC_CNTH,RTC_CNTL)
5、RTC闹钟寄存器(RTC_ALRH, RTC_ALRL)

RTC控制寄存器:

① 修改 CRH/CRL 寄存器,必须先判断 RSF 位,确定已经同步。
② 修改 CNT,ALR,PRL的时候,必须先配置CNF 位进入配置模式,修改完之后,设置 CNF位为 0 退出配置模式
③ 同时在对 RTC 相关寄存器写操作之前,必须判断上一次写操作已经结束,也就是判断 RTOFF 位是否置位。

配置RTC寄存器
必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRRTC_CNT、RTC_ALR寄存器。
另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询!RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是11'!时,才可以写入RTC寄存器。
配置过程:
1.查询RTOFF位,直到RTOFF的值变为'1’2.置CNF值为1,进入配置模式
3.对一个或多个RTC寄存器进行写操作4.清除CNF标志位,退出配置模式
5.查询RTOFF,直至RTOFF位变为'1'以确认写操作已经完成。
仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。
 

读RTC寄存器
RTC核完全独立于RTC APB1接口。
软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。RTC标志也是如此的。
这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。下述几种情况下能够发生这种情形:
发生系统复位或电源复位
系统刚从待机模式唤醒(参见第4.3节:低功耗模式)。系统刚从停机模式唤醒(参见第4.3节:低功耗模式)。
所有以上情况中,APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置'1'。

RTC 配置一般步骤
1、使能 PWR 和 BKP 时钟
2、使能后备寄存器访问

3、配置RTC时钟源,使能RTC时钟;(如果使用: LSE,要打开)

4、设置RTC 预分频系数
5、设置时间
6、开启相关中断(如果需要)

7、编写中断服务函数

8、部分操作要等待写操作完成和同步。
等待最近一次对RTC寄存器的写操作完成;等待RTC 寄存器同步
 

RTC的时间和闹钟寄存器都是以秒钟为计数单位的,所以要把时间换算成秒钟,获取时间需要把秒钟转换成日期。

判断是否是闰年

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 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5};     
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
    u16 t;
    u32 seccount=0;
    if(syear<1970||syear>2099)return 1;       
    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;//最后的秒钟加上去

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP,     ENABLE);    //使能PWR和BKP外设时钟  
    PWR_BackupAccessCmd(ENABLE);    //使能RTC和后备寄存器访问 
    RTC_SetCounter(seccount);    //设置RTC计数器的值

    RTC_WaitForLastTask();    //等待最近一次对RTC寄存器的写操作完成      
    return 0;        
}

设置闹钟

u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
    u16 t;
    u32 seccount=0;
    if(syear<1970||syear>2099)return 1;       
    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;//最后的秒钟加上去                 
    //设置时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP,            ENABLE);    //使能PWR和BKP外设时钟   
    PWR_BackupAccessCmd(ENABLE);    //使能后备寄存器访问  
    //上面三步是必须的!
    
    RTC_SetAlarm(seccount);
 
    RTC_WaitForLastTask();    //等待最近一次对RTC寄存器的写操作完成      
    
    return 0;        
}

获取时间

u8 RTC_Get(void)
{
    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++;  
        }   
        calendar.w_year=temp1;//得到年份
        temp1=0;
        while(temp>=28)//超过了一个月
        {
            if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2月份
            {
                if(temp>=29)temp-=29;//闰年的秒钟数
                else break; 
            }
            else 
            {
                if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
                else break;
            }
            temp1++;  
        }
        calendar.w_month=temp1+1;    //得到月份
        calendar.w_date=temp+1;      //得到日期 
    }
    temp=timecount%86400;             //得到秒钟数          
    calendar.hour=temp/3600;         //小时
    calendar.min=(temp%3600)/60;     //分钟    
    calendar.sec=(temp%3600)%60;     //秒钟
    calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);// 获取星期   
    return 0;
}

获取星期

u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{    
    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);
}