STM32学习笔记一一RTC实时时钟

前言:

为了方便查看博客,特意申请了一个公众号,附上二维码,有兴趣的朋友可以关注,和我一起讨论学习,一起享受技术,一起成长。

在这里插入图片描述


1. 简述

STM32 的实时时钟(RTC)是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

RTC 模块和时钟配置系统 (RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC,以防止对后备区域 (BKP) 的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP)写保护。

在这里插入图片描述

RTC 由两个主要部分组成(参见上图), 第一部分(APB1 接口)用来和 APB1 总线相连。此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。 APB1 接口由 APB1 总线时钟驱动,用来与 APB1 总线连接。

另一部分 (RTC 核心) 由一组可编程计数器组成,分成两个主要模块。第一个模块是 RTC 的预分频模块,它可编程产生 1 秒的 RTC 时间基准 TR_CLK。 RTC 的预分频模块包含了一个 20 位的可编程分频器 (RTC 预分频器)。如果在 RTC_CR 寄存器中设置了相应的允许位,则在每个TR_CLK 周期中 RTC 产生一个中断(秒中断)。第二个模块是一个 32 位的可编程计数器,可被初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒,约合 136 年左右。

RTC 还有一个闹钟寄存器 RTC_ALR,用于产生闹钟。系统时间按 TR_CLK 周期累加并与存储在 RTC_ALR 寄存器中的可编程时间相比较,如果 RTC_CR 控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。RTC 内核完全独立于 RTC APB1 接口,而软件是通过 APB1 接口访问 RTC 的预分频值、计数器值和闹钟值的。但是相关可读寄存器只在 RTC APB1 时钟进行重新同步的 RTC 时钟的上升沿被更新, RTC 标志也是如此。这就意味着,如果 APB1 接口刚刚被开启之后,在第一次的内部寄存器更新之前,从 APB1 上读取的 RTC 寄存器值可能被破坏了(通常读到 0)。因此,若在读取 RTC 寄存器曾经被禁止的 RTC APB1 接口,软件首先必须等待 RTC_CRL 寄存器的 RSF位(寄存器同步标志位, bit3)被硬件置 1。

2. RTC 寄存器介绍

2.1 RTC 的控制寄存器——RTC_CRH 寄存器

在这里插入图片描述该寄存器用来控制中断的。

2.2 RTC 的控制寄存器——RTC_CRL 寄存器

在这里插入图片描述RTC 用到的是该寄存器的 0、 3~5 这几个位,第 0 位是秒钟标志位,我们在进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写 0)。第 3 位为寄存器同步标志位,我们在修改控制寄存器 RTC_CRH/CRL 之前,必须先判断该位,是否已经同步了,如果没有则等待同步,在没同步的情况下修 RTC_CRH/CRL 的值是不行的。第 4 位为配置标位,在软件修改 RTC_CNT/RTC_ALR/RTC_PRL 的值的时候,必须先软件置位该位,以允许进入配置模式。第 5 位为 RTC 操作位,该位由硬件操作,软件只读。通过该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次操作。

2.3 RTC 预分频装载寄存器——RTC_PRLH 寄存器

这两个寄存器用来配置 RTC 时钟的分频数的,比如我们使用外部 32.768K 的晶振作为时钟的输入频率,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率。

在这里插入图片描述

2.4 RTC 预分频装载寄存器——RTC_PRLL 寄存器

在这里插入图片描述

2.5 RTC 预分频器余数寄存器——RTC_DIVH 寄存器

在这里插入图片描述

2.6 RTC 预分频器余数寄存器——RTC_DIVH 寄存器

在这里插入图片描述
这两个寄存器的作用就是用来获得比秒钟更为准确的时钟,比如可以得到 0.1 秒,或者 0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。

2.7 RTC 计数器寄存器——RTC_CNT 寄存器

该寄存器由 2 个 16 位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。在修改这个寄存器的时候要先进入配置模式。

在这里插入图片描述

2.8 RTC 计数器寄存器——RTC 闹钟寄存器

该寄存器也是由 2 个 16 位的寄存器组成 RTC_ALRH 和 RTC_ALRL。总共也是 32 位,用来标记闹钟产生的时间(以秒为单位),如果 RTC_CNT 的值与 RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。该寄存器的修改也要进入配置模式才能进行。

在这里插入图片描述

3. 备份寄存器介绍

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

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

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

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

一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM,不过这个 EEPROM 并不是真正的 EEPROM,而是需要电池来维持它的数据。

在这里插入图片描述
RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在 RTC 操作之前先要通过这个寄存器选择 RTC 的时钟源,然后才能开始其他的操作。

4. RTC 配置步骤

(1) 使能电源时钟和备份区域时钟

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

(2) 取消备份区写保护

PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问

(3) 复位备份区域,开启外部低速振荡器。

BKP_DeInit();//复位备份区域

(4) 选择 RTC 时钟,并使能

RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟(RCC_RTCCLKSource_LSI 和 RCC_RTCCLKSource_HSE_Div128)
RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟

(5) 设置 RTC 的分频,以及配置 RTC 时钟

在开启了 RTC 时钟之后,我们要做的是设置 RTC 时钟的分频数,通过 RTC_PRLH 和RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位(RTC_CRH 的 CNF 位),设置时间(其实就是设置RTC_CNTH 和 RTC_CNTL两个寄存器)。

RTC_EnterConfigMode();/// 允许配置
RTC_ExitConfigMode();//退出配置模式,更新配置
void RTC_SetPrescaler(uint32_t PrescalerValue);
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)//RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
void RTC_SetCounter(uint32_t CounterValue)最后在配置完成之后

(6) 更新配置,设置 RTC 中断分组

设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CRH 的 CNF
来实现。

RTC_ExitConfigMode();//退出配置模式,更新配置

在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)//往备份区域写用户数据
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)//读取备份区域指定寄存器

(7) 编写中断服务函数

流程图:

在这里插入图片描述

5. 程序实现

5.1 初始化
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)	//检查指定的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(1972,1,2,1,1,1);  //设置时间	
		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

}
static void RTC_NVICConfig(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寄存器
}

中断服务函数:

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_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);		//清闹钟中断
	RTC_WaitForLastTask();	  	    						 	   	 
}
5.2 RTC部分实现

闰年判断:

//判断是否是闰年函数
//月份   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;//月份从1开始,数组从0开始计数
	
	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寄存器的写操作完成  	
	RTC_Get();
	
	return 0;	    
}

得到当前时间:

//得到当前的时间
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
	static u16 daycnt=0;
	u32 timecount=0; 
	u32 temp=0;
	u16 temp1=0;
	
    timecount = RTC_GetCounter();	 
	//timecount = 86400*4+88;
 	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)//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_GetWeek(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);
}			  
对于星期如何转换,可参考如下链接:

1.RTC-由年月日计算星期几

2.基姆拉尔森计算公式

6. 附录:

STM32 时钟树:
在这里插入图片描述


参考:

1.[原子教程库函数实现]

2.STM32开发 – RTC详解

  • 24
    点赞
  • 207
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值