基于STM32的RTC实时时钟实验

基于STM32的RTC实时时钟实验

RTC是什么?

STM32的RTC外设,实质是一个掉电后还继续运行的定时器,从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断).但是从掉电还能继续运行来看,它是STM32中唯一一个具有这个功能的外设(RTC外设的复杂之处不在于它的定时,而在于它掉电还可以继续运行的特性)。

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

STM32系统时钟源简介

系统时钟包括了:

1. HSE高速外部时钟(常用8MHz无源晶振);

2. PLL时钟源(来源有HSE和HSI/2,一般选HSE作为时钟来源);

3. PLL时钟PLLCLK(通过设置PLL的倍频因子,一般8Mx9=72MHz,72MHz是官方推荐稳定运行时钟,最高128MHz);

4. 系统时钟SYSCLK(一般SYSCLK=PLLCLK=72MHz);

5. AHB总线时钟HCLK(是系统时钟SYSCLK经过AHB分频器分频后得到的时钟,也就是APB总线时钟,一般设置1分频,HCLK=SYSSCLK=72MHz);

6. APB2总线时钟HCLK2(APB2总线时钟PCLK2由 HCLK经过高速APB2预分频余数器得到,分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器CFGR的位13-11:PPRE2[2:0]决定,一般设置为 1 分频,即 PCLK2 = HCLK =72M);

7. APB1总线时钟HCLK1(APB1 总线时钟 PCLK1 由 HCLK 经过低速 APB 预分频余数器得到,HCLK1 属于低速的总线时钟,最高为 36M,这里只需粗线条的设置好 APB1 的时钟即可。

RTC的时钟系统

RTC的时钟来源有三个:

① 外部有源晶体震荡时钟源(32.768KHz);

② 内置RC无源震荡源(约为40KHz);

③ 外部无源高速震荡时钟(约62.5KHz)。

RTC的晶振

任何实时时钟的核心都是晶振,晶振频率为32768Hz(LSE时钟)。它为分频计数器提供精确的与低功耗的实基信号。它可以用于产生秒、分、时、日等信息。为了确保时钟长期的准确性,晶振必须正常工作,不能够收到干扰。RTC的晶振又分为:外部晶振和内置晶振。

RTC内部设备工作原理

RTC核心部分

RTC核心设备包括“预分频余数模块”与“计数器模块”。说白了,RTC核心设备的独立工作功能就是“自己按照设定的预分频余数因子,一个周期计数一次,计数值存在32位的计数器中”。

APB1接口部分

RTC核心设备虽然可以根据设定的参数自己独立运行,但是RTC的中断和标志位是由APB1接口部分来操作的。

APB1接口设备包括“CR控制寄存器”,这个寄存器是32位的,也就是说CR寄存器分为两个16位寄存器CRL与CRH寄存器,,这两个寄存器的功能为“控制RTC的中断”与“置位RTC的状态标志位”。

RTC寄存器简介

CR控制寄存器

CR寄存器是由CRL与CRH两个16位寄存器组成的,由APB1总线控制,因此当RTC独立运行时,也就是开发板的电源断电时,CR寄存器是无法发挥其作用的。

“允许中断标志位”可以进行写操作。

 

这些位的相应信息如下:

功能

置位/复位操作

RTOFF

看看上一次对RTC的操作是否完成

硬件置位/硬件复位

CNF

是否进行写操作

软件置位/软件复位

RSF

APB1时钟是否与RTC时钟同步

硬件置位/软件复位

OWF/ALRF/SECF

相应动作对应的标志位

硬件置位/软件复位

注:

① 我们一般使用APB1总线对RTC操作之前,先将RSF复位以清除原来的残余信息,然后等待置位,一旦置位就说明APB1与RTC时钟已同步我们可以进行写操作;

② 读操作之前一定要等待RSF时钟同步标志位置1,才可以进行读出正确的数据。

PRL重装载寄存器

 

重加载寄存器中的值在预分频余数计数器的值递减至0后,重新装载进入预分频余数寄存器。

DIV预分频余数寄存器

 

RTC预分频余数寄存器的作用就是获取更加精准的时间,工作原理如下:

 

我们看到:预加载余数寄存器每1s被自动重装载一次,即每1s减至0。

假如:我们此时读取的DIV预加载余数寄存器的值为0x3FFF,说明此时自上次重装载已经过去了0.5s,我们得到了比1s精确度更高的时间,这就是DIV预加载余数寄存器的作用。除此之外,DIV预加载余数寄存器还可以获得0.01s,0.001s这样更加精确的时间。

CNT计数器寄存器

 

ARL闹钟寄存器

 

如何对RTC进行写操作?

必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。

另外,对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是’1’时,才可以写入RTC寄存器。

寄存器配置步骤如下:

① 等待上次对RTC的操作结束 等待RTOFF位置1;

② 取消写保护/进行配置模式 将CNF标志位置1;

③ 对一个或多个RTC寄存器进行写操作;

④ 写保护/取消配置模式 将CNF标志位复位(仅当CNF标志位被清除时,写操作才能进行,这个过程至少需要3个RTC时钟周期);

⑤ 查询RTOFF,直至RTOFF位变为’1’以确认写操作已经完成。

如何进行RTC数据的读操作?

APB1总线时钟复位的几种情况:

电源/系统被复位

系统刚从待机模式中被唤醒

系统刚从停机模式中被唤醒

APB1复位就说明“APB1总线时钟与RTC时钟不再同步”。

若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待 RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置’1’。

RTC的复位操作

除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复

位或电源复位进行异步复位。

RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位。

在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护。

当VDD电源被切断,后备区域与RTC核心部分仍然由VBAT维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。

关于RTC的疑难问题解析

为什么要等待APB1与RTC内部时钟同步后,我们对RTC中寄存器写操作才有效?

 

因为RTC时钟源和APB1接口的时钟源不同,一个来自32.768K晶振,一个来自8M晶振,他们的时钟一般会有一个差异的,所以才需要等待同步。

为什么RTC的时钟最准确(RTC时钟为何是准确的32768Hz)?

 

① RTC时间是以振荡频率来计算的。故它不是一个时间器而是一个计数器。而一般的计数器都是16位的。又因为时间的准确性很重要,故震荡次数越低,时间的准确性越低。所以必定是个高次数,即2^15=32768;

② 32768Hz=2^15即分频2^15次后为1Hz,周期=1s;

③ 经过工程师的经验总结32768Hz,时钟最准确;

④ 规范和统一。

为什么RTC和APB1有一些关联,他俩不是完全独立的吗?

不是的,我们的代码调试代码下载只能下载到STM32核心芯片中我们要通过STM32芯片来控制RTC设备就必须让RTC与APB1之间有接口。

为什么叫“秒标志位”?

因为RTC采用的是32.768KHz的晶振,PLR重装载寄存器(20位)的取值可以为32767,也就是说RTC可以没经过1s来置位一次“秒标志位”。我们通常的日历是以秒为最小的时间单位,因此RTC也可以提供我们日历的功能,但是这些事件是“xxxx秒”的形式出现的,需要我们根据“时秒分”的关系去进行换算。

注:其实PLR重装载寄存器的值可以是[0,2^20-1]之间所有的数值,因此我们也可以设定更小的计数单位进行计数。

为什么读数据/命令时需要等待RSF(时钟同步标志位)置1,而写命令/数据时则不用?

RTC内核完全独立于APB1接口,软件通过APB1接口对RTC相关寄存器访问。但是相关寄存器只在RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。所以软件必须先等待寄存器同步标志位(RTC_CRL的RSF位)被硬件置1才读。

我们要读取数据就读取寄存器当前值,因此我们必须等待RTC时钟的上升沿在将数据读取到APB1总线中去,而写操作不同,我们不需要读取任何RTC寄存器的信息,因此写操作没必要等待时钟同步。

APB1总线时钟与RTC时钟同步是什么意思?

由于时钟源不同,因此APB1总线时钟不可能与RTC时钟完全重合,我们读取的原理如下:

 

我们只需要在下一个RTC时钟上升沿到来之前将RTC寄存器中的数据读取到APB1总线上即可以实现“数据同步读取”。

RTC如何实现日历功能?

要实现日历功能首先需要具备两个条件:间隔相同的计数单位(计数器+1所需时间)+初始计数时间(计数器的初始值)。

例如:我要从计数器=10000时开始计数并且我们计数器+1的时间为1s,如果我们一年之后计数器的值=36000,我们可以得知RTC实时时钟连续计数了26000*1s=26000s。

LSE时钟被旁路是什么意思?

 

所谓旁路模式,是指无需上面提到的使用外部晶体时所需的芯片内部时钟驱动组件,直接从外界导入时钟信号,犹如芯片内部的驱动组件被旁路了。

”晶振/时钟被旁路“ 是指将芯片内部的用于外部晶体起振和功率驱动等的部分电路和XTAL_OUT引脚断开,这时使用的外部时钟是有源时钟或者其他STM32提供的CCO输出等时钟信号,直接单线从XTAL_IN输入,这样即使外部有晶体也震荡不起来了。

RTC固件库库函数解析

void RTC_EnterConfigMode(void)

进入RTC配置模式

void RTC_ExitConfigMode(void)

退出RTC配置模式

uint32_t  RTC_GetCounter(void)

获得RTC中32位可编程计数器的值

void RTC_SetCounter(uint32_t CounterValue)

设置RTC中32位可编程计数器的初始值

void RTC_SetPrescaler(uint32_t PrescalerValue)

设置PRL重加载寄存器的值

void RTC_SetAlarm(uint32_t AlarmValue)

设置闹钟值用于和计数器值比较以置位闹钟标志位

uint32_t  RTC_GetDivider(void)

获得预分频余数寄存器的剩余值

void RTC_WaitForLastTask(void)

等待RTOFF位(RTC操作完成标志位)值1说明前一次对RTC的操作已经完成

void RTC_WaitForSynchro(void)

等待APB1与RTC时钟同步

FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG)

获得RTC中寄存器的相应标志位

void RTC_ClearFlag(uint16_t RTC_FLAG)

软件清除RTC中寄存器的相应标志位

ITStatus RTC_GetITStatus(uint16_t RTC_IT)

判断中断类型的函数

void RTC_ClearITPendingBit(uint16_t RTC_IT)

清除相应的中断悬挂标志(中断标志位硬件置1软件清0)

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)

RTC中断配置函数

RTC时钟源配置函数

void RCC_RTCCLKConfig(uint32_t  CLKSource)

RTC时钟源选择函数

void RCC_RTCCLKCmd(FunctionalState NewState)

RTC时钟源使能

RCC_LSEConfig()

LSE时钟配置函数(LSE=32.678KHz)

RTC备份区域(BKP)操作函数

PWR_BackupAccessCmd()

后备区域访问使能函数

uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)

读出BKP备份区域数据的函数

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)

向BKP备份区域写入数据的函数

BKP备份寄存器

备份寄存器是 42 个 16 位的寄存器(战舰开发板就是大容量的),可用来存储 84 个字节的 用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。 即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。

备份区域(BKP_CR)控制寄存器

 

注释的意思是“当我们需要失能入侵事件时,我们只需使得TPE=0即可“。

备份区域状态寄存器

 

备份数据(BKP_DR)寄存器

 

备份控制/状态寄存器(BKP_CSR)

 

备份控制寄存器(BKP_CR)

 

注释的意思是说“当我们想要关闭侵入检测引脚时,我们只将TPE位置0就OK了”。

图片大意如下:

① TPAL=0时,低电平和下降沿脉冲都可以充当入侵信号的事件;

② TPAL=1时,高电平和上升沿脉冲都可以充当入侵信号的事件。

RTC时钟校准寄存器(BKP_RTCCR) 

RTC校准有两种方式,分别在“用ppm值校准”和“定时器校准”,这两种方式分别在AN2604.pdf,AN2821.pdf被提及。

按照AN2604.pdf描述的原理,RTC 的校准值应在0-127之间,可实现的校准误差对应为0-121ppm,相当于每30天跑快的秒数为0-314s。

Ppm值计算公式:ppm误差=偏差/基准值*10^6。

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

 

注意:这里的复位方式“是通过PC13(TAMPER)引脚进行后备区域BKP复位的”。

侵入事件检测引脚

注:此引脚高电平不得超过3.3V。

RTC复位后,如何对后备区域进行操作?

复位后,对备份寄存器和RTC的访问被禁止,并且备份域被保护以防止可能存在的意外的写操作。执行以下操作可以使能对备份寄存器和RTC的访问:

① 通过设置寄存器RCC_APB1ENR的PWREN和BKPEN位来打开电源和后备接口的时钟;

② 电源控制寄存器(PWR_CR)的DBP位来使能对后备寄存器和RTC的访问。

关于BKP备份寄存器的疑难问题解析?

STM32的入侵检测是干什么用的?

你的数据是保存在RAM里的;但是一掉电RAM里的数据就没了;有一块地方,后备电池相关的一块RAM的数据却放不掉(除非电池没电了);还有一个方法可以自动清掉这一部分RAM(寄存器组)这就是入侵事件。

后备存储区BKP有什么用?

你的系统上电后你输入一个密码;这个密码就保存在后备寄存器组中;只要电池有电,这个密码一直保存完好;你的系统每次开机后检测这个密码是否正确,如果不正确说明有两种可能发生的事情:“电池没电了”或者“后备存储区坏掉了“。

事件标志位与中断标志位的区别?

在STM32中“中断标志位“置位的条件是“事件标志位置位+中断允许标志位置位“。我们要知道,当符合中断的条件全部具备,中断触发后,我们一定要清除“中断标志位与事件标志位”这两个位,与单纯的清除事件标志位不同。

#define ADC_IT_EOC                                 ((uint16_t)0x0220)  
#define ADC_IT_AWD                                 ((uint16_t)0x0140)  
#define ADC_IT_JEOC                                ((uint16_t)0x0480)  

这是定义的中断位,可以产生中断:

#define ADC_FLAG_AWD                               ((uint8_t)0x01)  
#define ADC_FLAG_EOC                               ((uint8_t)0x02)  
#define ADC_FLAG_JEOC                              ((uint8_t)0x04)  
#define ADC_FLAG_JSTRT                             ((uint8_t)0x08)  
#define ADC_FLAG_STRT                              ((uint8_t)0x10)  

这是定义的标志位,二者对比可以发现有的标志位不能产生中断,此外,中断标志位置位包括“事件标志位置位+中断标志位置位”。

RTC输出时钟校准原理?

计算ppm误差,ppm代表比例误差,ppm是百万分之一的意思。

例如,当距离为1公里的时候,比例误差为5mm。 对于一台测距精度为(5+5ppm*D)mm的全站仪或者测距仪,当被测量距离为1公里时,仪器的测距精度为5mm+5ppm*1(公里)=10mm。

为方便测量,RTC时钟可以经64分频输出到侵入检测引脚TAMPER上。通过设置RTC校验寄存 器(BKP_RTCCR)的CCO位来开启这一功能。RTC时钟经过64分频输出到PC13(TAMPER)引脚上的时钟为32767Hz/64=511.968Hz(RTC时钟源为32768Hz),但是如果实测TAMPER引脚输出的频率为511.982Hz,那么RTC对输出时钟进行如下修正:

(511.982Hz-511.968Hz)/ 511.968Hz *10^6 = 27.35ppm,则误差为27.35ppm,我们可以查询AN2604.pdf,可以得知此时我们选择28ppm;

2^20个时钟延误1个时钟所造成的ppm值计算

AN2604.pdf中说,若校准值为1,则RTC 校准时,每2的20次方个时钟周期扣除1个时钟脉冲。这相当于0.954ppm(1/2^20*10^6 = 0.954)。而校准值最大为127,所以最大可以减慢121ppm(0.954ppm*127 = 121)。所以这个校准表就是由简单的乘除运算得来的,当然要使用浮点运算才可以得到准确结果。

由此,我们可以计算出28ppm对应的2^20个周期中延误周期的数量为

BKP后备区域的功能预览

① 20字节数据后备寄存器(中容量和小容量产品),或84字节数据后备寄存器(大容量和互联型产品) ;

② 用来管理防侵入检测并具有中断功能的状态/控制寄存器;

③ 用来存储RTC校验值的校验寄存器;

④ 在PC13引脚(当该引脚不用于侵入检测时)上输出RTC校准时钟,RTC闹钟脉冲或者秒脉冲。

BKP固件库函数解析

函数名

功能

void BKP_DeInit(void)

将后备存储区初始化为默认值

void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel)

入侵信号检测引脚配置

void BKP_TamperPinCmd(FunctionalState NewState)

入侵信号检测引脚使能

void BKP_ITConfig(FunctionalState NewState)

入侵信号中断配置

void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource)

RTC时钟脉冲输出配置

void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue)

设置RTC的时钟校准值

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)

数据写入

uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)

数据读出

FlagStatus BKP_GetFlagStatus(void)

读出RTC事件标志位的状态

void BKP_ClearFlag(void)

清除RTC所有的事件标志位

ITStatus BKP_GetITStatus(void)

获取RTC中断状态(到底是触发了哪一个中断)

void BKP_ClearITPendingBit(void)

清除所有的RTC中断标志位

BKP_DeInit复位函数的作用

 

外设时钟使能,复位外设的总线时钟,再清除复位外设的总线时钟,可以继续配置(读写)外设,就如同如下所述:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // 外设时钟使能

RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, ENABLE);  // 复位外设的总线时钟
  
RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, DISABLE); // 清除复位外设的总线时钟
  
USART_Init(USART1, &USART_InitStructure); // 重新初始化

RTC完整代码展示

Rtc.c

#include "rtc.h"  
#include "usart.h"  
#include "delay.h"  
#include "stm32f10x.h"  
  
_calendar_obj calendar;//时钟结构体   
  
u8 RTC_initConfig()  
{  
    u8 temp = 0;  
    NVIC_InitTypeDef NVIC_InitStructure;  
      
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP|RCC_APB1Periph_PWR, ENABLE); // 使能APB1总线上的BKP与PWR的时钟  
    PWR_BackupAccessCmd(ENABLE); // 取消后备区域写保护  
    delay_init(); // delay函数初始化  
      
    NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;  
    NVIC_Init(&NVIC_InitStructure); // 配置RTC的NVIC中断通道  
      
    if(BKP_ReadBackupRegister(BKP_DR1) == 0x5050) // 首次执行程序段  
    {  
        BKP_DeInit(); // BKP外设时钟复位  
        RCC_LSEConfig(RCC_LSE_ON); // LSE低速时钟使能  
          
        while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && temp < 250)  
        {  
            temp++;  
            delay_ms(10);  
        }  
        if(temp>=250) return 1;  
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);  
        RCC_RTCCLKCmd(ENABLE);  
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_EnterConfigMode(); // 进入RTC配置模式  
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_SetPrescaler(32768-1); // 1s溢出一次  
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_ITConfig(RTC_IT_OW|RTC_IT_SEC,ENABLE); // RTC中断配置  
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_Set(2015,1,14,17,42,55);  //将时间转化为以秒为单位的数值加载到32位可编程计数器当中      
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_ExitConfigMode(); // 退出配置模式,并且执行在此之前写入的命令  
          
        BKP_WriteBackupRegister(BKP_DR1,0x5050); // 向BKP_DR1(16位寄存器)寄存器写入0x5050这个16位数据  
    }  
    else // 再次进行执行的程序段(系统/电源复位后执行)  
    {  
        RTC_WaitForLastTask(); // 等待RTC操作完成  
        RTC_WaitForSynchro(); // 等待APB1与RTC时钟同步  
          
        RTC_ITConfig(RTC_IT_OW|RTC_IT_SEC,ENABLE); // 系统/电源复位后执行后,RTC的CR寄存器被复位因此需要重新配置RTC中断  
          
        RTC_WaitForLastTask(); // 等待RTC操作完成  
    }  
    return 0;  
}  
  
//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);  
} 

 

Rtc.h

#ifndef _RTC_H  
#define _RTC_H  
  
#include "sys.h"  
  
//时间结构体  
typedef struct   
{  
    vu8 hour;  
    vu8 min;  
    vu8 sec;              
    //公历日月年周  
    vu16 w_year;  
    vu8  w_month;  
    vu8  w_date;  
    vu8  week;         
}_calendar_obj;  
  
u8 RTC_initConfig();  
u8 Is_Leap_Year(u16 year);  
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);  
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);  
  
#endif  

Main.c

#include "delay.h"  
#include "sys.h"  
#include "lcd.h"  
#include "usart.h"     
#include "rtc.h"  
  
extern _calendar_obj calendar;//时钟结构体   
  
 int main(void)  
 {     
    u8 t=0;  
    delay_init();            //延时函数初始化      
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级  
    uart_init(115200);      //串口初始化为115200  
    LCD_Init();       
    RTC_initConfig(); // 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);  
        }     
        delay_ms(10);                                   
    };    
 } 

 

RTC代码解析

① 我们要通过APB1总线对RTC后备区域进行操作,无非就是想读取后备区域的数据,因此,我们此时应该将APB1总线时钟供给主电源让其为后备区域提供稳定的电能,并且使能APB1的接口部分:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);    //使能PWR和BKP外设时钟     
PWR_BackupAccessCmd(ENABLE);    //使能后备寄存器访问   

② 复位外设并且配置RTC时钟

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时钟   

③ 等待操作结束并且APB1与RTC时钟同步

RTC_WaitForLastTask();  //等待最近一次对RTC寄存器的写操作完成  
RTC_WaitForSynchro();       //等待RTC寄存器同步  

  

④ 进行写操作,配置CR控制寄存器(由于之前全是在配置RTC的时钟源,并没有对RTC进行任何写操作,因此操作时钟时无需等待时钟同步与RTC操作完成)

RTC_EnterConfigMode();/// 允许配置   

⑤ 进行写操作,配置CR控制寄存器(由于之前全是在配置RTC的时钟源,并没有对RTC进行任何写操作,因此操作时钟时无需等待时钟同步与RTC操作完成)

RTC_ITConfig(RTC_IT_SEC, ENABLE);       //使能RTC秒中断  

⑥ 进行写操作,配置重装载寄存器

RTC_SetPrescaler(32767); //设置RTC预分频的值——1s溢出一次  

⑦ 将设定的初始计数时间转化为以秒为单位的数值,并加载进入32位可编程计数器中

RTC_Set(2015,1,14,17,42,55);  //正点原子封装的函数用于设置时间,本质上就是计算出来一个值将此值赋给32位可编程计数器

⑧ 退出配置模式

RTC_ExitConfigMode(); //退出配置模式  

⑨ 想BKP备份寄存器内写入数据

BKP_WriteBackupRegister(BKP_DR1, 0X5050);   //向指定的后备寄存器中写入用户程序数据(小于16位的数据,因为寄存器为16位的)

注:这里当我们使能了“APB1总线上的后备区域”和“APB1总线上的主电源时钟”,我们就直接可以对BKP进行读写操作,不同于RTC操作。

备份区域BKP与RTC的工作注意事项

BKP是后备存储区,那里有42个16位寄存器用于存储高达84个字节的数据,但是BKP与RTC并没有共性,也就是说RTC与BKP在主电源断开后仍共用一个备用电源来储存各自寄存器中的值,但是对这些寄存器中的值进行修改就要用到它们与APB1总线的接口,用APB1接口修改各自寄存器中的值的操作注意事项如下:

① 只有给BKP备份区域接通主电源并且接通BKP与APB1总线的接口,我们才可以通过APB1总线对BKP备份区域进行数据的读写操作;

② 每次对RTC进行操作,一定要进行“等待上一次RTC操作完成”和“等待APB1和RTC时钟同步”;

③ 完成对RTC操作后一定要退出操作,也就是RTC_CRL.CNF置0,只有这样前面写入的操作才会被执行。

STM32编程小技巧

如果有while循环等待命令该怎么办?

while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)    //检查指定的RCC标志位设置与否,等待低速晶振就绪  
{  
    temp++;  
    delay_ms(10);  
}  
if(temp>=250)return 1;//初始化时钟失败,晶振有问题 

其实,当我们遇到这种循环等待时,为了防止进入死循环,我们要规定循环的最大次数,并且如果循环了MAX次还没有成功完成操作,那么就返回一个可以代表具体错误的信息,例如:

if(temp>=250)return 1;//初始化时钟失败,晶振有问题 

猜你喜欢

转载自blog.csdn.net/weixin_45590473/article/details/108809123