程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

STM32F103 RTC配置

本节我们将介绍RTC,实际上有关RTC我们在学习S3C2440的时候已经详细介绍过《Mini2440裸机开发之RTC》。

一、RTC

RTC,英文全称Real Time Clock,中文就是实时时钟,是一个可以为系统提供精确的时间基准的元器件,RTC一般采用精度较高的晶振作为时钟源,有些RTC为了在主电源掉电时还可以工作,需要外加电池供电。

1.1 概述

在断电情况下RTC仍可以独立运行,只要芯片的外加电池一直供电,RTC上的时间会一直走。

RTC本质上是个独立的定时器。RTC模块拥有一个连续计数的计数器。从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断),但其高级之处也就在于掉电之后还可以正常运行。

两个32位寄存器包含二进码十进数格式 (BCD) 的秒、分钟、小时(1224小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为2829(闰年)、3031天。

上电复位后,所有RTC寄存器都会受到保护,以防止可能的非正常写访问。

无论器件状态如何(运行模式、低功耗模式或处于复位状态),只要电源电压保持在工作范围内,RTC不会停止工作。

1.2 特性

  • 可编程的预分频系数:分频系数高为\(2^{20}\)

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

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

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

    • HSE时钟除以128
    • LSE振荡器时钟;
    • LSI振荡器时钟;
  • 2个独立的复位类型:

    • APB1接口由系统复位;
    • RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位;
  • 3个专门的可屏蔽中断;

    • 闹钟中断,用来产生一个软件可编程的闹钟中断;
    • 秒中断,用来产生一个可编程的周期性中断信号(长可达1秒);
    • 溢出中断,指示内部可编程计数器溢出并回转为0的状态;

1.3 RTC方框图

RTC由两个主要部分组成。

  • 第一部分(APB1接口):用来和APB1总线相连,此单元还包含一组16位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线时钟驱动,用来与APB1总线连接;
  • 另一部分(RTC核心):由一组可编程计数器组成,分成两个主要模块;
    • 第一个模块是RTC的预分频模块(左下1):它可编程产生最长为1秒的RTC时间基准TR_CLKRTC的预分频模块包含了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个实时时钟(RTCTR_CLK周期中RTC产生一个中断(秒中断);
    • 第二个模块是一个32位的可编程计数器(左下2):可被初始化为当前的系统时间;系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。
1.3.1 RTCCLK选择

我们在《STM32F103系统时钟配置》中介绍了STM32的时钟框图;

img

RTCCLK可以选择以下三种作为时钟源;

  • HSE时钟除以128
  • LSE振荡器时钟;
  • LSI振荡器时钟;

使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作,所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz

因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块。在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式。

1.3.2 具体流程

RTCCLK经过RTC_DIV预分频,RTC_PRL设置预分频系数,然后得到TR_CLK时钟信号,我们一般设置其周期为1sRTC_CNT计数器计数,假如1970设置为时间起点为0s,通过当前时间的秒数计算得到当前的时间。

RTC_ALR是设置闹钟时间,RTC_CNT计数到RTC_ALR就会产生计数中断;

  • RTC_Second为秒中断,用于刷新时间;
  • RTC_Overflow是溢出中断;
  • RTC Alarm控制开关机。
1.3.3 RTC复位过程

除了RTC_PRLRTC_ALRRTC_CNTRTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。
RTC_PRLRTC_ALRRTC_CNTRTC_DIV寄存器仅能通过备份域复位信号复位。

系统复位后,禁止访问后备寄存器和RCT,防止对后卫区域(BKP)的意外写操作。

1.3.4 读RTC寄存器

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

1.3.5 配置RTC寄存器

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

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

二、RTC相关寄存器

RTC相关的寄存器主要有:

  • RTC控制寄存器:RTC_CRHRTC_CRL
  • RTC预分频装载寄存器: RTC_PRLHRTC_PRLL
  • RTC预分频余数寄存器:RTC_DIVHRTC_DIVL
  • RTC计数器寄存器:RTC_CNTHRTC_CNTL
  • RTC闹钟寄存器:RTC_ALRHRTC_ALRL

2.1 RTC控制寄存器

2.1.1 RTC_CRH

作用:配置3个专门的可屏蔽中断(溢出中断、闹钟中断、秒中断)使能。

注意:系统复位后所有的中断被屏蔽,因此可通过写RTC寄存器来确保在初始化后没有挂起的中断请求。当外设正在完成前一次写操作时(标志位RTOFF=0),不能对RTC_CRH寄存器进行写操作。

2.1.2 RTC_CRL

一般用到该寄存器的345位:

  • 3RSF):寄存器同步标志位,我们在修改控制寄存器RTC_CRH/RTC_CRL 之前,必须先判断该位,是否已经同步了,如果没有则等待同步;
  • 4CNF):配置标位,在软件修改RTC_CNT/RTC_ALR/RTC_PRL的值的时候,必须先软件置位该位,以允许进入配置模式;
  • 5RTOFF):RTC操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次,也就是判断RTOFF位是否置位。

3个位总结如下:

(1) 修改RTC_CRH/RTC_CRL寄存器,必须先判断RSF位,确定已经同步;

(2) 修改RTC_CNT/RTC_ALR/RTC_PRL的时候,必须先配置CNF位进入配置模式,修改完之后,设置CNF位为0退出配置模式。

(3) 同时在对RTC相关寄存器写操作之前,必须先判断RTOFF位,确定上次对RTC寄存器的操作是已经完成。

2.2 RTC预分频装载寄存器

预分频装载寄存器用来保存RTC预分频器的周期计数值。在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式。

我们使用外部32.768kHz的晶振作为RTC时钟的输入频率,那么就要设置这两个寄存器的值为327677FFF)以得到1s的计数频率。

2.2.1 RTC_PRLH
2.2.2 RTC_PRLL

2.3 RTC预分频余数寄存器

预分频余数寄存器用于获取比秒钟更准确的时钟。该寄存器的值是自减的,用于保存还需要多少时钟周期获取的一个秒信号。在一次秒钟更新后,由硬件重新加载,这两个寄存器和RTC_PRL的各位是一样的。

TR_CLK的每个周期里,RTC预分频器中计数器的值都会被重新设置为RTC_PRL寄存器的值。用户可通过读取RTC_DIV寄存器,以获得预分频计数器的当前值,而不停止分频计数器的工作,从而获得精确的时间测量。

此寄存器是只读寄存器,其值在RTC_PRLRTC_CNT寄存器中的值发生改变后,由硬件重新装载。

2.3.1 RTC_DIVH
2.3.2 RTC_DIVL

2.4 RTC计数器寄存器

存放计数器内的计数值,也就是用来记录时钟时间。

该寄存器由216位的寄存器组成RTC_CNTHRTC_CNTL,总共32位,当进行读操作时,直接返回计数器内的计数值(系统时间)。

在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式。

2.4.1 RTC_CNTH
2.4.2 RTC_CNTL

2.5 RTC闹钟寄存器

当可编程计数器RTC_CNT的值与RTC_ALR中的32位值相等时,即触发一个闹钟事件,并且产生RTC闹钟中断。

在修改这个寄存器的时候要先进入配置模式,即设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式。

2.5.1 RTC_ALRH
2.5.2 RTC_ALRL

2.6 BKP备份寄存器

备份寄存器是4216位的寄存器。可用来存储84个字节数据。

它们处在备份区域,当VDD电源切断,仍然由VBAT维持供电。

当系统在待机模式下被唤醒,或者系统复位或者电源复位,它们也不会复位。执行以下操作将使能对后备寄存器和RTC访问:

  • 设置寄存器RCC_APB1ENRPWRENBKPEN位,使能电源和后备时钟;
  • 设置寄存器PWR_CRDBP位,使能对RTC和后备寄存器的访问;
2.6.1 备份数据寄存器(BKP_DRx

一般用BKP来存储RTC的校验值或者记录一些重要的数据;

2.6.2 备份域控制寄存器 (RCC_BDCR)

备份域控制寄存器中(RCC_BDCR)的LSEONLSEBYPRTCSELRTCEN位处于备份域。

因此,这些位在复位后处于写保护状态,只有在电源控制寄存器(PWR_CR)中的DBP位置1后才能对这些位进行改动。

三、RTC源码

3.1 RTC初始化步骤

RTC配置流程如下:

(1) 时钟使能以及取消写保护;

  • 使能电源时钟和备份区域时钟:通过配置RCC_APB1ENR的位28和位27

    • 28PWREN):电源接口时钟使能;

    • 27BKPEN):备份接口时钟使能;

  • 取消备份区写保护:通过配置PWR_CRDBP位(位8)为1来使能对后备寄存器和RTC的访问;

(2) RTC时钟配置 (复位,取消复位,时钟源选择,使能)->该模块是在备份区域;

  • 备份区域软复位,复位结束;

    • 通过配置RCC_BDCR寄存器位161备份区域软复位;
    • 通过配置RCC_BDCR寄存器位160备份区域软复位结束;
  • 开启LSE

    • 通过配置RCC_BDCR寄存器位0使能外部低速振荡器;

    • 通过读取RCC_BDCR寄存器位1等待外部低速振荡器就绪;

  • 选择RTC时钟,并使能:通过配置RCC_BDCR的位[9:8]和位15

    • 位[9:8]:RTC是时钟源选择,配置为01选择LSE作为RTC时钟;

    • 15RTC时钟使能;

(3) 等待上一次RTC写操作完成:判断上一次RTC操作是否完成,通过等待RTC_CRL5RTOFF=1)实现;

(4) 等待RTC寄存器同步:判断RTC寄存器是否同步,通过等待RTC_CRL3RSF=1)实现;

(5) 如果使能时钟,可以配置RTC_CRH寄存器,比如;

  • 0:允许秒中断;
  • 1:允许闹钟中断;
  • 2:允许溢出中断;

(6) 进入配置模式:允许配置,通过RTC_CRL4CNF=1)实现;

(7) 配置RTC_CNTPRC_PRLPRC_ARL

  • 配置预分频装载寄存器RTC_PRL32767
  • 配置计数器寄存器RTC_CNT初始化时间;
  • 配置闹钟寄存器RTC_ALR

(8) 更新RTC:设置RTC_CRL4CNF=0)退出配置模式;

(9) 等待RTC更新完成,通过等待RTC_CRL5RTOFF=0)实现;

(10) 设置初始化完成标志;

  • BKP_DR1写入0x5050作为已经初始化过时钟的标志;下次开机或复位的时候先读取BKP_DR1,判断是否是0x5050来决定是不是要配置;

(11) 设置NVIC;

(12) 中断处理函数;

  • 设置中断服务函数(包括清除中断标志)。

3.2 源码实现

3.2.1 RTC_TypeDef

RTC寄存器结构RTC_TypeDef,在文件stm32f10x_map.h中定义如下:

/*------------------------ Real-Time Clock -----------------------------------*/
typedef struct
{
  vu16 CRH;	      // 控制寄存器高位 ;
  u16  RESERVED0; //
  vu16 CRL;		  // 控制寄存器低位 ;
  u16  RESERVED1; //
  vu16 PRLH;	  // 预分频装载寄存器高位 ;
  u16  RESERVED2; //
  vu16 PRLL;	  // 预分频装载寄存器低位 ;
  u16  RESERVED3; //
  vu16 DIVH;	  // 预分频分频因子寄存器高位 ;
  u16  RESERVED4; //
  vu16 DIVL;	  // 预分频分频因子寄存器低位 ;
  u16  RESERVED5; //
  vu16 CNTH;	  // 计数器寄存器高位 ;
  u16  RESERVED6; //
  vu16 CNTL;	  // 计数器寄存器低位 ;
  u16  RESERVED7; //
  vu16 ALRH;	  // 闹钟寄存器高位 ;
  u16  RESERVED8; //
  vu16 ALRL;	  // 闹钟寄存器低位 ;
  u16  RESERVED9; //
} RTC_TypeDef;

#ifdef _RTC
  #define RTC                 ((RTC_TypeDef *) RTC_BASE)
#endif /*_RTC */

在前面我们已经对RTC_TypeDef结构体中定义的大部分寄存器进行了详细的介绍,那么我们如何编码去初始化这些寄存器呢?

3.2.2 RTC_Init

RTC初始化函数RTC_Init定义如下:

/**********************************************************************************
 *
 *		初始化步骤
 *				  1.时钟使能 取消写保护
 *				  2.RTC时钟配置 (复位,取消复位,时钟源选择,使能)->该模块是在备份区域
 *                3.等待上一次RTC写操作完成
 *                4.等待RTC寄存器同步  
 *                5.中断使能     
 *				  6.进入配置模式
 *				  7.配置RTC_CNT,PRC_PRL,PRC_ARL
 *				  8.更新RTC
 *			      9.等待RTC更新完成
 *      开启成功返回0,否则返回1 
        备份域控制寄存器中(RCC_BDCR)的LSEON、LSEBYP、RTCSEL和RTCEN位处于备份域。
        由此,这些位在复位后被写保护,只有在电源控制寄存器(PWR_CR)中的DBP位置1之后才
        能对这些位进行改动
 *
 ***********************************************************************************/
 u8 RTC_Init(void)
 {
    u8 temp=0;
	if(BKP->DR1!=0X5050)            //第一次初始化RTC
	{
	    RCC->APB1ENR |=1<<28;           //POWER时钟使能 
	    RCC->APB1ENR |=1<<27;           //BKPEN时钟使能
	    PWR->CR      |=1<<8;            //取消写保护
		RCC->BDCR    |=1<<16;           //备份区域软复位
		RCC->BDCR    &=~(1<<16);        //复位结束  
		RCC->BDCR    |=1<<0;            //开启LSE
		while((!(RCC->BDCR&0X02))&&temp<250)   //等待LSE就绪
		{
		   temp++;
		   delay_ms(10);
		} 
		if(temp>=250)
		{
		   return 1;                    //初始化失败 
		}
		RCC->BDCR |=1<<8;               //LSE作为RTC时钟			(默认是无时钟的)
		RCC->BDCR |=1<<15;              //RTC时钟使能 
		while(!(RTC->CRL&(1<<5)));      //等待上一次RTC写操作完成   
		while(!(RTC->CRL&(1<<3)))
		    printf("tring.......\n");   //等待RTC寄存器同步
		RTC->CRH  |=1;                  //开启秒钟中断
		RTC->CRL  |=1<<4;               //CNF=1  进入配置模式
		RTC->PRLL   =32767;             
		RTC->PRLH  =0;
		RTC_Set(2015,12,3,13,25,10);    //初始化时间
		RTC->CRL  &=~(1<<4);            //CNF=0 更新RTC
		while(!(RTC->CRL&(1<<5)));      //等待RTC写操作完成
		BKP->DR1=0X5050;                //作为初始化完成的标志             
 	}
	else
	{
	    while(!(RTC->CRL&(1<<3)));      //等待RTC寄存器同步
		RTC->CRH     |=0X01;            //开启秒钟中断  			  CR寄存器使用的是APB1时钟而不是RTCCLK
		while(!(RTC->CRL&(1<<5)));      //等待上一次RTC写操作完成  
	}
	return 0;
}
3.2.3 RTC_Set

RTC时间设定函数RTC_Set定义如下:

u8 MonthTable[12]={31,28,31,30,31,30,31,31,30,31,30,31};           //存放每个月份的天数
//***************************闰年判定********************************************
u8  IsLeapYear(u16 year)
{
  if(((year%4==0)&&(year%100!=0))||(year%400==0))
  {
     return 1;
   }
  else
  {
    return 0;
  }  
}

/********************************************************************************
 *
 *		  Description:RTC时间设定
 *					   以2000年1月1日为基址					星期六
 *		  
 **********************************************************************************/
 void RTC_Set(u16 year,u8 month,u8 day,u8 hour,u8 min,u8 sec)
 {
    u16 t=0;                 //存储年份 
    u32 seccount;				//存储秒钟数
   	for(t=2000;t<year;t++)
	{
	   if(IsLeapYear(t))
	     seccount +=31622400;          //闰年
	   else
	     seccount +=31536000;		   //平年
	}
	for(t=0;t<(month-1);t++)
	{
	    seccount +=(u32)MonthTable[t]*86400;
		if(IsLeapYear(year)&&(t==1))
		       seccount +=86400;              //闰年二月29天 
	}
	seccount +=(u32)(day-1)*86400;        //天
	seccount +=(u32)(hour)*3600;          //时
	seccount +=(u32)(min)*60;             //分
	seccount +=(u32)(sec);                 //秒
	RCC->APB1ENR |=1<<28;           //POWER时钟使能
	RCC->APB1ENR |=1<<27;           //BKPEN时钟使能	 RTC模块和始终配置系统都在后备区域
	PWR->CR      |=1<<8;            //取消写保护
	RTC->CRL     |=1<<4;            //进入配置模式
	RTC->CNTL     =seccount&0xFFFF;
	RTC->CNTH     =seccount>>16;
	RTC->CRL     &=~(1<<4);         //更新
	while(!(RTC->CRL&(1<<5)));      //等待配置完成
 }
3.2.4 RTC_Get

RTC时间获取函数RTC_Get定义如下:

/********************************************************************************
 *
 *		  Description:RTC时间获取
 *					   以2000年1月1日为基址					星期六
 *		  
 **********************************************************************************/
 void RTC_Get(void)
 {
    u32 seccount;
	u16 daycount;
	u16 temp=2000;                   
	while(!(RTC->CRL&(1<<3)));      //等待RTC寄存器已经同步
    seccount   =RTC->CNTH;
	seccount <<=16;
	seccount  +=RTC->CNTL;          //获取此时秒钟数
	daycount   =seccount/86400;     //获取天数
	if(daycount>=1)	                //大于1天
	{
	    temp=2000;
		while(daycount>=365)         //闰年
		{
		   	 if(IsLeapYear(temp))
			 {
			     if(daycount>=366)
			        daycount -=366;
				 else
				    break;
			 }
			 else
			 { 
			    daycount -=365;		  //平年
			 }
			 temp++;
		}
		cale.year =temp;             //获取年份 
		temp =0;
		while(daycount>=28)
		{
		   if((IsLeapYear(cale.year))&&(temp==1))               //闰年二月
		   {
		      if(daycount>=29)
			     daycount -=29;
			  else
			      break;
		   }
		   else
		   {
		     if(daycount>=MonthTable[temp])
		        daycount -=MonthTable[temp];
			 else
			   break;
		   }
		   temp++;
		}
		cale.month =temp+1;               //得到月份
		cale.day   =daycount+1;           //得到日期 
	}
	temp =seccount%86400;                 //得到剩余秒钟数
	cale.hour =temp/3600;	              //得到小时 
	cale.min  =(temp%3600)/60;			  //得到分
	cale.sec  =(temp%3600)%60;			  //得到秒
	temp =daycount%7;
	switch(temp)
	{
	  case 0:
	      cale.week = Sunday;
		  break;
	  case 1:
	      cale.week = Monday;
		  break;
	  case 2:
	      cale.week = Tuesday;
		  break;
	  case 3:
	      cale.week = Wednesday;
		  break;
	  case 4:
	      cale.week = Thursday;
		  break;
	  case 5:
	      cale.week = Friday;
		  break;  
	  default:
	      cale.week = Saturday;
	} 
 }
3.2.5 RTC_IRQHandler

RTC中断处理函数RTC_IRQHandler定义如下:

/*******************************************************************************
* Function Name  : RTC_IRQHandler
* Description    : This function handles RTC global interrupt request.
* Input          : None
* Output         : None
* Return         : None
                 若ALRF=1且ALRIE=1,则允许产生RTC全局中断。如果在EXTI控制器中允许产生EXTI线 17
                 中断,则允许产生RTC全局中断和RTC闹钟中断。
*
*******************************************************************************/
void RTC_IRQHandler(void)
{
    if(RTC->CRL&0X01)		   //秒中断
	{
	  RTC_Get();              //获取当前时间
	  printf("当前时间为%d-%d-%d %d:%d",cale.year,cale.month,cale.day,cale.hour,cale.min);
	  switch(cale.week)
	  {
	  case 0:
	      printf("\tSunday\n");
		  break;
	  case 1:
	      printf("\tMonday\n");
		  break;
	  case 2:
	      printf("\tTuesday\n");
		  break;
	  case 3:
	      printf("\tWednesday\n");
		  break;
	  case 4:
	      printf("\tThursday\n");
		  break;
	  case 5:
	      printf("\tFriday\n");
		  break;  
	  default:
	      printf("\tSaturday\n");
	  } 
	  RTC->CRL &=~0X01;       //清中断
    }
	if(RTC->CRL&0X02)         //闹铃中断
	{
	  RTC->CRL &=~(0X01<<1);  //清中断 
	}
    if(RTC->CRL&0X04)         //溢出中断
	{
	  RTC->CRL &=~(0X01<<2);  //清中断 =
	}
	while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成
}

3.3 实现功能

这里我们实现一个很简单的功能,设置当前时间,每隔1s中断输出一次当前时间。

3.3.1 main函数实现
int main()
{
   u32 temp=0;
   STM32_Clock_Init(9);                              //系统时钟初始化
	
   STM32_NVIC_Init(2,USART1_IRQn,0,1);		        //串口中断优先级初始化,其中包括中断使能
   usart_init(USART_1,115200);				        //串口1初始化,波特率115200 映射到PA9 PA10
	
   STM32_NVIC_Init(2,RTC_IRQn,0,1);		            //RTC中断优先级初始化,其中包括中断使能
   while(RTC_Init());                               //RTC初始化
	
   // LED1初始化
   gpio_init(PA8,GPO_SpeedMax_50,HIGH);              //PA8接入LED1
	
   while(1)
   {
        RTC_Get(); 
   }
}
3.3.2 测试

编译程序并下载测试:

注意:如果串口输出中文为乱码,点击左上角【设置】-> 【选项】-> 【字符集编码】-> 【ANSIGBK)】.

四、源码下载

源码下载路径:stm32f103

参考文章

[1] STM32 RTC实时时钟,步骤超细详解,一文看懂RTC

posted @ 2024-12-14 17:04  大奥特曼打小怪兽  阅读(381)  评论(0)    收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步