STM32-RTC实时时钟

1 RTC实时时钟特征与原理

1.1 RTC简介

        RTC是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。RTC还包含用于管理低功耗模式的自动唤醒单元。在断电情况下 RTC仍可以独立运行,只要芯片的备用电源一直供电,RTC上的时间会一直走。
        RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
                ①设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟 
                ②设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。

        RTC实质是一个掉电后还继续运行的定时器,从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断)。但其高级指出也就在于掉电之后还可以正常运行。
        两个 32 位寄存器包含二进码十进数格式 (BCD) 的秒、分钟、小时( 12 或 24 小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。
        上电复位后,所有RTC寄存器都会受到保护,以防止可能的非正常写访问。
        无论器件状态如何(运行模式、低功耗模式或处于复位状态),只要电源电压保持在工作范围内,RTC使不会停止工作。

1.2 RTC特征

        1)可编程的预分频系数:分频系数最高为2²º;
        2)32位的可编程计数器,可用于较长时间段的测量;
        3)2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟频率的四分之一以上);
        4)可以选择以下三种RTC的时钟源:
                ①HSE时钟除以128;
                ②LSE振荡器时钟;
                ③LSI振荡器时钟;

        5)2个独立的复位类型:
                ①APB1接口由系统复位;
                ②RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位;

        6)3个专门的可屏蔽中断:
                ①闹钟中断,用来产生一个软件可编程的闹钟中断;
                ②秒中断,用来产生一个可编程的周期性中断信号(最长可达1秒);
                ③溢出中断,指示内部可编程计数器溢出并回转为0的状态。

1.3 RTC工作原理框图

        RTC由两个主要部分组成(参见下图)。第一部分(APB1接口):用来和APB1总线相连。通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值,闹钟值)。此单元还包含一组16位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线时钟驱动,用来与APB1总线接口。 
        第二部分(RTC核心):由一组可编程计数器组成,分成两个主要模块。第一个模块是RTC的预分频模块,它可编程产生最长为1秒的RTC时间基准TR_CLK。RTC的预分频模块包含了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。第二个模块是一个32位的可编程计数器,可被初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒,约合 136 年左右,作为一般应用,这已经是足够了的系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。

        注意:复位过程除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位。

1.4 RTC具体流程

        RTCCLK经过RTC_DIV预分频,RTC_PRL设置预分频系数,然后得到TR_CLK时钟信号,我们一般设置其周期为1s,RTC_CNT计数器计数,假如1970设置为时间起点为0s,通过当前时间的秒数计算得到当前的时间。RTC_ALR是设置闹钟时间,RTC_CNT计数到RTC_ALR就会产生计数中断,
        ①RTC_Second 为秒中断,用于刷新时间,
        ②
RTC_Overflow 是溢出中断。
        ③
RTC Alarm 控制开关机

1.5 RTC时钟选择

        RTC是一个独立的时钟源

        使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作.所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz,因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块.(在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式)。

1.6 读RTC寄存器

        RTC核完全独立于RTC APB1接口。 
        软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是,相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。RTC标志也是如此的。
        这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。下述几种情况下能够发生这种情形: 

                ①发生系统复位或电源复位 
                ②系统刚从待机模式唤醒(低功耗模式)
                ③系统刚从停机模式唤醒(低功耗模式)。
 
        所有以上情况中,
APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。 因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置‘1’。

1.7 配置RTC寄存器

        必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。
        另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是’1’时,才可以写入RTC寄存器。 
        配置过程:

                ①查询RTOFF位,直到RTOFF的值变为‘1’
                ②置CNF值为1,进入配置模式
                ③对一个或多个RTC寄存器进行写操作
                ④清除CNF标志位,退出配置模式
                ⑤查询RTOFF,直至RTOFF位变为’1’以确认写操作已经完成。

        仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTCCLK周期。

1.8 RTC标志的设置

        在每一个RTC核心的时钟周期中,更改RTC计数器之前设置RTC秒标志(SECF)。
        在计数器到达0x0000之前的最后一个RTC时钟周期中,设置RTC溢出标志(OWF)。
        在计数器的值到达闹钟寄存器的值加1(RTC_ALR+1)之前的RTC时钟周期中,设置RTC_Alarm和RTC闹钟标志(ALRF)。

        对RTC闹钟的写操作必须使用下述过程之一与RTC秒标志同步:
                ①使用RTC闹钟中断,并在中断处理程序中修改RTC闹钟和/或RTC计数器。
                ②等待RTC控制寄存器中的SECF位被设置,再更改RTC闹钟和/或RTC计数器。

        RTC秒和闹钟波形图示例,PR=0003,ALARM=00004

RTC溢出波形图示例,PR=0003

2 BKP备份寄存器

2.1 BKP简介

        备份寄存器是42个16位的寄存器,可用来存储84个字节的用户应用程序数据。他们处在备份域里,当Vdd电源被切断,他们仍然由Vbat维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。
        此外,BKP控制寄存器用来管理侵入检测和RTC校准功能。在本文中,使用备份寄存器来存储RTC的相关信息(标记时钟是否已经经过了配置)。
        复位后,对备份寄存器和RTC的访问被禁止,并且备份域被保护以防止可能存在的意外的写操作。执行以下操作可以使能对备份寄存器和RTC的访问。
                ①通过设置寄存器RCC_APB1ENR的PWREN和BKPEN位来打开电源和后备接口的时钟;
                ②电源控制寄存器(PWR_CR)的DBP位来使能对后备寄存器和RTC的访问。

2.2 BKP特性

        1)20字节数据后备寄存器(中容量和小容量产品),或84字节数据后备寄存器(大容量和互联型产品) 
        2)用来
管理防侵入检测并具有中断功能的状态/控制寄存器 
        3)用来存储RTC校验值的校验寄存器
        4)在PC13引脚(当该引脚不用于侵入检测时)上输出RTC校准时钟,RTC闹钟脉冲或者秒脉冲

2.3 BKP侵入检测

        当TAMPER引脚上的信号从0变成1或者从1变成0(取决于备份控制寄存器BKP_CR的TPAL位),会产生一个侵入检测事件。侵入检测事件将所有数据备份寄存器内容清除。然而为了避免丢失侵入事件,侵入检测信号是边沿检测的信号与侵入检测允许位的逻辑与,从而在侵入检测引脚被允许前发生的侵入事件也可以被检测到。
        ①当TPAL=0时:如果在启动侵入检测TAMPER引脚前(通过设置TPE位)该引脚已经为高电平,一旦启动侵入检测功能,则会产生一个额外的侵入事件(尽管在TPE位置’1’后并没有出现上升沿)。
        ②当TPAL=1时:如果在启动侵入检测引脚TAMPER前(通过设置TPE位)该引脚已经为低电平,一旦启动侵入检测功能,则会产生一个额外的侵入事件(尽管在TPE位置’1’后并没有出现下降沿)。

        设置BKP_CSR寄存器的TPIE位为’1’,当检测到侵入事件时就会产生一个中断。
        在一个侵入事件被检测到并被清除后,侵入检测引脚TAMPER应该被禁止。然后,在再次写入备份数据寄存器前重新用TPE位启动侵入检测功能。这样,可以阻止软件在侵入检测引脚上仍然有侵入事件时对备份数据寄存器进行写操作。这相当于对侵入引脚TAMPER进行电平检测。

2.4 RTC校准

        为方便测量,RTC时钟可以经64分频输出到侵入检测引脚TAMPER上。通过设置RTC校验寄存器(BKP_RTCCR)的CCO位来开启这一功能。
        通过配置CAL[6:0]位,此时钟可以最多减慢121ppm。
        关于RTC校准和如何提高精度,请看AN2604“STM32F101xx和STM32F103xx的RTC校准”

3 RTC寄存器描述

3.1 RTC控制寄存器高位(RTC_CRH)

        这些位用来屏蔽中断请求。注意:系统复位后所有的中断被屏蔽,因此可通过写RTC寄存器来确保在初始化后没有挂起的中断请求。当外设正在完成前一次写操作时(标志位RTOFF=0),不能对RTC_CRH寄存器进行写操作。
        RTC功能由这个控制寄存器控制。一些位的写操作必须经过一个特殊的配置过程来完成。
        作用:配置3个专门的可屏蔽中断(溢出中断、闹钟中断、秒中断)使能。

3.2 RTC控制寄存器低位(RTC_CRL)

        RTC的功能由这个控制寄存器控制。当前一个写操作还未完成时(RTOFF=0时),不能写RTC_CR寄存器。
        注意:
                ①任何标志位都将保持挂起状态,直到适当的RTC_CR请求位被软件复位,表示所请求的中断已经被接受。
                ② 在复位时禁止所有中断,无挂起的中断请求,可以对RTC寄存器进行写操作。
                ③ 当APB1时钟不运行时,OWF、ALRF、SECF和RSF位不被更新。
                ④OWF、ALRF、SECF和RSF位只能由硬件置位,由软件来清零。
                ⑤若ALRF=1且ALRIE=1,则允许产生RTC全局中断。如果在EXTI控制器中允许产生EXTI线 17中断,则允许产生RTC全局中断和RTC闹钟中断。
                ⑥若ALRF=1,如果在EXTI控制器中设置了EXTI线 17的中断模式,则允许产生RTC闹钟中断;如果在EXTI控制器中设置了EXTI线 17的事件模式,则这条线上会产生一个脉冲(不会产生RTC闹钟中断)。

        作用:RTC操作是否完成判断、配置模式判断、寄存器同步判断、3个中断的标志位。
        这个寄存器尤其重要(尤其是位5、位4、位3):
                ①写任何寄存器之前,必须判断第五位RTOFF即RTC操作位上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次,也就是判断RTOFF位是否置1
                ②写CNT、ALR、PRL寄存器,必须先配置第 4 位CNF配置标志位进入配置模式,以允许进入配置模式,修改完之后,设置CNF位为0退出配置模式
                ③读任何寄存器,必须先判断第 3 位RSF寄存器同步标志位,确定已经同步。我们在修改控制寄存器 RTC_CRH/CRL 之前,必须先判断该位,是否已经同步了,如果没有则等待同步

3.3 RTC预分频装载寄存器(RTC_PRLH、RTC_PRLL)

        预分频装载寄存器用来保存RTC预分频器的周期计数值。它们受RTC_CR寄存器的RTOFF位保护,仅当RTOFF值为’1’时允许进行写操作。

        RTC预分频装载寄存器高位(RTC_PRLH):

        RTC预分频装载寄存器低位(RTC_PRLL):

        作用:配置RTC预分频装载值,这个值是20bit长度。
        根据这个寄存器的值可以确定,TR_CLK和RTCCLK之间的关系公式:
                ①fTR_CLK=fRTCCLK/(PRL+1)
                ②如果输入时钟频率是32.768kHz(fRTCCLK,也就是以LSE作为时钟源),这个寄存器中写入7FFFh(32767)可获得周期为1秒钟的信号。

3.4 RTC预分频器余数寄存器(RTC_DIVH、RTC_DIVL)

        在TR_CLK的每个周期里,RTC预分频器中计数器的值都会被重新设置为RTC_PRL寄存器的值。用户可通过读取RTC_DIV寄存器,以获得预分频计数器的当前值,而不停止分频计数器的工作,从而获得精确的时间测量。此寄存器是只读寄存器,其值在RTC_PRL或RTC_CNT寄存器中的值发生改变后,由硬件重新装载。

        RTC预分频器余数寄存器高位(RTC_DIVH):

         RTC预分频器余数寄存器低位(RTC_DIVL):

         作用:获得预分频计数器的当前值,也就是从RTC预分频装载寄存器倒数到0之间的一个值(以RTCCLK为时钟)。

3.5 RTC计数器寄存器(RTC_CNTH、RTC_CNTL)

        RTC核有一个32位可编程的计数器,可通过两个16位的寄存器访问。计数器以预分频器产生的TR_CLK时间基准为参考进行计数。RTC_CNT寄存器用来存放计数器的计数值。他们受RTC_CR的位RTOFF写保护,仅当RTOFF值为’1’时,允许写操作。在高或低寄存器(RTC_CNTH或RTC_CNTL)上的写操作,能够直接装载到相应的可编程计数器,并且重新装载RTC预分频器。当进行读操作时,直接返回计数器内的计数值(系统时间)。

        RTC计数器寄存器高位(RTC_CNTH):

        RTC计数器寄存器低位(RTC_CNTL):

        作用:存放计数器内的计数值(以TR_CLK为时钟)。
        注意:由于RTC预分频器余数寄存器以RTCCLK为时钟,而RTC计数器寄存器以TR_CLK为时钟,而RTCCLK的时钟通常远远大于TR_CLK,所以利用RTC预分频器余数寄存器可以获得更准确的控制。比如,RTC计数器寄存器存储当前时间,精确到秒;但是利用由于RTC预分频器余数寄存器,可以在RTC预分频装载寄存器倒数到0的平均数处停下,从而达到0.5秒的更精确时间。

3.6 RTC闹钟寄存器(RTC_ALRH、RTC_ALRL)

        当可编程计数器的值与RTC_ALR中的32位值相等时,即触发一个闹钟事件,并且产生RTC闹钟中断。此寄存器受RTC_CR寄存器里的RTOFF位写保护,仅当RTOFF值为’1’时,允许写操作。

        RTC闹钟寄存器高位(RTC_ALRH):

        RTC闹钟寄存器低位(RTC_ALRL):

         作用:当RTC计数器寄存器的值与RTC闹钟寄存器的值相等的时候,触发一个闹钟事件,产生一个闹钟中断。

3.7 备份数据寄存器x(BKP_DRx) (x = 1 … 10)

RTC相关配置库函数

4.1 2个时钟源操作函数

void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);  //时钟源选择
void RCC_RTCCLKCmd(FunctionalState NewState);  //时钟使能

        作用:确定RTC的时钟源,使能RTC时钟(通常选用LSE时钟源)。

4.2 3个参数配置函数(计数值、预分频值、闹钟值)

void RTC_SetCounter(uint32_t CounterValue);  //设置计数器值:CNTH/CNTL
void RTC_SetPrescaler(uint32_t PrescalerValue);  //预分频配置:PRLH/PRLL
void RTC_SetAlarm(uint32_t AlarmValue);  //闹钟设置:ALRH/ALRL

        作用:配置预分频装载寄存器的值、计数器的值、闹钟配置。

4.3 1个中断配置函数

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);  //CRH配置

        作用:配置RTC中断的选择和使能。

4.4 2个配置模式函数

void RTC_EnterConfigMode(void);  //允许RTC配置:CRL位 CNF
void RTC_ExitConfigMode(void);  //退出配置模式:CRL位 CNF

        作用:前者允许RTC配置,后者退出配置模式。

4.5 2个同步函数

void RTC_WaitForLastTask(void);  //等待上次操作完成:CRL位RTOFF
void RTC_WaitForSynchro(void);  //等待时钟同步:CRL位RSF

        作用:前者等待上次操作完成(CRL寄存器的RTOFF位),后者等待时钟同步(CRL寄存器的RSF位)。

4.6 4个状态位函数

FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);

        作用:前两者获取(或清除)状态标志位,后两者为获取(或清除)中断状态标志位。

4.7 其他的相关函数

void PWR_BackupAccessCmd(FunctionalState NewState);  //BKP后备区域访问使能
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);  //使能PWR和BKP时钟
void RCC_LSEConfig(uint8_t RCC_LSE);  //开启LSE,RTC选择LSE作为时钟源

        作用:第一个函数使能BKP后备区域访问使能,第二个函数使能PWR和BKP时钟,第三个函数开启LSE时钟(这里为什么使用这几个函数?是在上文:BKP备份寄存器简介中讲到)。

void PWR_BackupAccessCmd();  //BKP后备区域访问使能
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);  //写BKP
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);  //读BKP寄存器

        作用:上面的PWR_BackupAccessCmd()函数使能BKP后备区域使能之后,就可以通过这两个函数来读BKP的寄存器,写BKP的寄存器。

RTC一般步骤

        1)使能PWR和BKP时钟。调用函数:RCC_APB1PeriphClockCmd();
        2)使能后备寄存器访问。调用函数:PWR_BackupAccessCmd();
        3)配置RTC时钟源,使能RTC时钟。调用函数:RCC_RTCCLKConfig();RCC_RTCCLKCmd();
        4)如果使用LSE,要打开LSE:RCC_LSEConfig(RCC_LSE_ON);
        5)设置RTC预分频系数。调用函数:RTC_SetPrescaler();
        6)设置时间。调用函数:RTC_SetCounter();
        7)开启相关中断(如果需要)。调用函数:RTC_ITConfig();
        8)编写中断服务函数。调用函数:RTC_IRQHandler();
        9)部分操作要等待写操作完成和同步。调用函数:RTC_WaitForLastTask();RTC_WaitForSynchro()。

        下面按照这个一般步骤来进行一个简单的RTC程序:

#ifndef __RTC_H
#define __RTC_H	    

//时间结构体
typedef struct 
{
	vu8 hour;
	vu8 min;
	vu8 sec;			
	//公历日月年周
	vu16 w_year;
	vu8  w_month;
	vu8  w_date;
	vu8  week;		 
}_calendar_obj;		
			 
extern _calendar_obj calendar;	//日历结构体
extern u8 const mon_table[12];	//月份日期数据表

void Disp_Time(u8 x,u8 y,u8 size);//在制定位置开始显示时间
void Disp_Week(u8 x,u8 y,u8 size,u8 lang);//在指定位置显示星期
u8 RTC_Init(void);        //初始化RTC,返回0,失败;1,成功;
u8 Is_Leap_Year(u16 year);//平年,闰年判断
u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);
u8 RTC_Get(void);         //更新时间   
u8 RTC_Get_Week(u16 year,u8 month,u8 day);
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//设置时间	
		 
#endif
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "rtc.h" 		    
	   
_calendar_obj calendar;//时钟结构体 
 
static void RTC_NVIC_Config(void)
{	
  NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;		//RTC全局中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	//先占优先级1位,从优先级3位
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;	//先占优先级0位,从优先级4位
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;		//使能该通道中断
	NVIC_Init(&NVIC_InitStructure);		//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}

//实时时钟配置
//初始化RTC时钟,同时检测时钟是否工作正常
//BKP->DR1用于保存是否第一次配置的设置
//返回0:正常
//其他:错误代码

u8 RTC_Init(void)
{
	//检查是不是第一次配置时钟
	u8 temp=0;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟   
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问  
	if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)		//从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
		{	 			
		BKP_DeInit();	//复位备份区域 	
		RCC_LSEConfig(RCC_LSE_ON);	//设置外部低速晶振(LSE),使用外设低速晶振
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)	//检查指定的RCC标志位设置与否,等待低速晶振就绪
			{
			temp++;
			delay_ms(10);
			}
		if(temp>=250)return 1;//初始化时钟失败,晶振有问题	    
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);		//设置RTC时钟(RTCCLK),选择LSE作为RTC时钟    
		RCC_RTCCLKCmd(ENABLE);	//使能RTC时钟  
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_WaitForSynchro();		//等待RTC寄存器同步  
		RTC_ITConfig(RTC_IT_SEC, ENABLE);		//使能RTC秒中断
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_EnterConfigMode();/// 允许配置	
		RTC_SetPrescaler(32767); //设置RTC预分频的值
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_Set(2015,1,14,17,42,55);  //设置时间	
		RTC_ExitConfigMode(); //退出配置模式  
		BKP_WriteBackupRegister(BKP_DR1, 0X5050);	//向指定的后备寄存器中写入用户程序数据
		}
	else//系统继续计时
		{

		RTC_WaitForSynchro();	//等待最近一次对RTC寄存器的写操作完成
		RTC_ITConfig(RTC_IT_SEC, ENABLE);	//使能RTC秒中断
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		}
	RTC_NVIC_Config();//RCT中断分组设置		    				     
	RTC_Get();//更新时间	
	return 0; //ok

}		 				    
//RTC时钟中断
//每秒触发一次  
//extern u16 tcnt; 
void RTC_IRQHandler(void)
{		 
	if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
	{							
		RTC_Get();//更新时间   
 	}
	if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
	{
		RTC_ClearITPendingBit(RTC_IT_ALR);		//清闹钟中断	  	
	  RTC_Get();				//更新时间   
  	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);//输出闹铃时间	
		
  	} 				  								 
	RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);		//清闹钟中断
	RTC_WaitForLastTask();	  	    						 	   	 
}
//判断是否是闰年函数
//月份   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;	
}	 			   
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//返回值: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;	    
}

//初始化闹钟		  
//以1970年1月1日为基准
//1970~2099年为合法年份
//syear,smon,sday,hour,min,sec:闹钟的年月日时分秒   
//返回值: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;	    
}


//得到当前的时间
//返回值: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;
}	 
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//输入参数:公历年月日 
//返回值:星期号																						 
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);
}			  
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"	
#include "usmart.h"	 
#include "rtc.h" 

 int main(void)
 {	 
 	u8 t=0;	
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
 	LED_Init();			     //LED端口初始化
	LCD_Init();			 	
	usmart_dev.init(SystemCoreClock/1000000);	//初始化USMART	
	RTC_Init();	  			//RTC初始化
	POINT_COLOR=RED;//设置字体为红色 
	LCD_ShowString(60,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(60,70,200,16,16,"RTC TEST");	
	LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(60,110,200,16,16,"2015/1/14");		
	//显示时间
	POINT_COLOR=BLUE;//设置字体为蓝色
	LCD_ShowString(60,130,200,16,16,"    -  -  ");	   
	LCD_ShowString(60,162,200,16,16,"  :  :  ");		    
	while(1)
	{								    
		if(t!=calendar.sec)
		{
			t=calendar.sec;
			LCD_ShowNum(60,130,calendar.w_year,4,16);									  
			LCD_ShowNum(100,130,calendar.w_month,2,16);									  
			LCD_ShowNum(124,130,calendar.w_date,2,16);	 
			switch(calendar.week)
			{
				case 0:
					LCD_ShowString(60,148,200,16,16,"Sunday   ");
					break;
				case 1:
					LCD_ShowString(60,148,200,16,16,"Monday   ");
					break;
				case 2:
					LCD_ShowString(60,148,200,16,16,"Tuesday  ");
					break;
				case 3:
					LCD_ShowString(60,148,200,16,16,"Wednesday");
					break;
				case 4:
					LCD_ShowString(60,148,200,16,16,"Thursday ");
					break;
				case 5:
					LCD_ShowString(60,148,200,16,16,"Friday   ");
					break;
				case 6:
					LCD_ShowString(60,148,200,16,16,"Saturday ");
					break;  
			}
			LCD_ShowNum(60,162,calendar.hour,2,16);									  
			LCD_ShowNum(84,162,calendar.min,2,16);									  
			LCD_ShowNum(108,162,calendar.sec,2,16);
			LED0=!LED0;
		}	
		delay_ms(10);								  
	};  
 }

6 STM32控制程序分析

        RTC_Init()函数:RTC初始化函数。
        按照之前的RTC一般步骤初始化RTC函数,这里需要注意的是,为了区分是否是第一次执行RTC_Init()函数,这里使用了一个flag(向BKP_DR1寄存器写入0x5050,当然写入其他的数字也都是可以的)。

if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)	//从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
{	 			
	//第一次执行RTC_Init
	BKP_WriteBackupRegister(BKP_DR1, 0X5050);	//向指定的后备寄存器中写入用户程序数据
}
else//系统继续计时
{
	//不是第一次执行RTC_Init
}

        为什么要区分是否第一次执行RTC_Init呢?因为如果由于断电等因素,程序中断,但是RTC时钟却还是在执行中;等恢复供电,重新启动程序,这个时候就不需要再对RTC时钟进行初始化了。
        同时,设置外部低速晶振(LSE),使用外设低速晶振。需要检查指定的RCC标志位设置与否,等待低速晶振就绪。
        这里时间的设置是:距离1970年1月1日0点0分0秒的时间距离。其中,RTC_Get()、RTC_Set()等函数的内容涉及到时间距离转换的各种算法,就不在本文的讨论范围了。

  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
SD2405AL实时时钟模块介绍: SD2400系列是一种具有内置晶振、支持IIC串行接口的高精度实时时钟芯片,CPU可使用该接口通过5位地址寻址来读写片内32字节寄存器的数据(包括时间寄存器、报警寄存器、控制寄存器、通用SRAM寄存器)。 SD2400系列内置晶振,该芯片可保证时钟精度为±5ppm(在25℃下),即年误差小于2.5 分钟;该芯片内置时钟精度调整功能,可以在很宽的范围内校正时钟的偏差(分辨力3ppm),通过外置或内置的数字温度传感器可设定适应温度变化的调整值,实现在宽温范围内高精度的计时功能。 SD2400系列内置的一次性工业级电池或充电电池可保证在外部掉电情况下时钟使用寿命为5~8年时间;内部具备电源切换电路,当芯片检测到主电源VDD掉到电池电压以下,芯片会自动转为由备电电池供电。 SD2400系列内置单路定时/报警中断输出,报警中断时间最长可设至100年;内置频率中断输出和倒计时中断输出。 SD2400系列采用了多种提高芯片可靠性的技术,可满足对实时时钟芯片的各种需要,是在选用高精度实时时钟时的理想选择。 该模块采用Gadgeteer接口,同时很好的兼容Arduino(UNO、MegaDue等)和Maple系列控制板,也可与其他微控制器协同使用。 SD2405AL实时时钟模块特性: 低功耗: 1.0μA 典型值(时钟电路部分,Ta=25℃)。 工作电压:3.3V~5.5V,工作温度:民用级0℃~70℃,工业级-40℃~85℃。 标准IIC总线接口方式, 时钟电路最高速度400KHZ(4.5V~5.5V)。 年、月、日、星期、时、分、秒的BCD码输入/输出,并可通过独立的地址访问各时间寄存器 闰年自动调整功能(从2000年~2099年)。 可选择12/24小时制式. 内置年、月、日、星期、时、分、秒共7字节的报警数据寄存器及1字节的报警允许寄存器。 内置12字节通用SRAM寄存器可用于存储用户的一般数据。 三种中断均可选择从INT脚输出,并具有两个中断标志位. 可设定并自动重置的单路报警中断功能(时间范围最长设至100年),年、月、日、星期、时、分、秒报警共有96种组合方式,并有单事件报警和周期性报警两种中断输出模式. 周期性频率中断输出:从32768Hz~1/16Hz……1秒共十五种方波脉冲. 自动重置的8位倒计时定时器,可选的4种时钟源(4096HZ、64HZ、1HZ、1/60HZ)。 内置晶振,出厂前已对时钟进行校准,时钟精度为±5ppm(在25℃±1℃下),即年误差小于2.5 分钟。 内置时钟精度数字调整功能,可通过程序来调整走时的快。用户采用外置或内置的温度传感器,设定适应温度变化的调整值,可实现在宽温范围内高精度的计时功能(在-10℃~50℃小于5 ppm, 在-40℃~85℃小于10ppm)。 内置备电自动切换功能 ,芯片依据不同的电压自动从VDD切换到VBAT或从VBAT切换到VDD。 在VBAT模式下,芯片具有中断输出允许或禁止的功能,可满足在备用电池供电时输出中断的需要。 内置的充电电池及充电电路,累计电池电量超过550mAh,电池使用寿命为5~8年时间;内置的一次性民用级电池使用寿命为3~5年,一次性工业级电池使用寿命为5~8年时间。 内置的16kbit~256kbit非易失性SRAM(C/D/E型),其读写次数为100亿次,且内部写延时小于300ns。 内置的2kbit~256kbitE2PROM(F/B/C/D/E型),其擦写次数100万次 内置IIC总线0.5秒自动复位功能(从Start命令开始计时),保证时钟数据的有效性及可靠性,避免总线挂死问题。 内置三个时钟数据写保护位, 避免对数据的误写操作,可更好地保护时钟数据。 内置VBAT模式IIC总线通信禁止功能,从而避免在电池供电时CPU对时钟操作所消耗的电池电量,也可避免在主电源上、下电的过程中因CPU的I/O端口所输出的不受控的杂波信号对时钟芯片的误写操作,进一步提高时钟芯片的可靠性。 内置上电复位电路及指示位;内置电源稳压,内部计时电压可低至1.5V。 芯片管脚抗静电(ESD)>4KV。 外形尺寸:36x31x14mm 实物购买链接:https://item.taobao.com/item.htm?spm=2013.1.20141001.2.LgLOhp&id=17280765860&scm=1007.10115.36023.100200300000000&pvid=5ade1258-3a58-432a-90dd-c2c12ae31961&idnum=0

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值