目录
本文将按如下内容介绍RTC及其使用步骤
- RTC时钟简介
- 相关寄存器
- 相关代码
- 几个注意点
- 运行结果
RTC时钟简介
STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数 的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当 前的时间和日期。
RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式 唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC, 以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP) 写保护。
RTRTC 由两个主要部分组成, 第一部分(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_CRH
该寄存器比较简单,这里,我们使用的是秒中断,所以相应的位需要设置为1。
控制寄存器 RTC_CRL
- 位0,秒钟标志位。进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断,然后通过软件清0
- 位3,寄存器同步标志位。修改控制寄存器之前,必须先判断该位,是否已经同步,如果没有则等待同步
- 位4,配置标志位。软件修改RTC_CNT / RTC_ALR / RTC_PRL的值的时候,必须先置位该位
- 位5,RTC操作位。由硬件操作,软件只读。通过该位,可以判断上次对RTC寄存器的操作是否已经完成,如果没有完成,则需要等待完成才能开始下一次的操作
RTC预分频装载寄存器 RTC_PRLH
用来配置RTC时钟的分频数的,比如我们使用外部32.768k的晶振作为时钟的输入频率,我们要设置这两个寄存器的值为32767,以得到一秒钟的计数频率。
RTC预分频装载寄存器 RTC_PRLL
RTC计数器寄存器RTC_CNT
该寄存器由 2 个 16 位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。一般情况下,可以计算232 = 4,294,967,296 s,大概136年,目前情况下,是完成够使用的。
备份寄存器
因为我们要使用到备份寄存器来存储RTC的相关信息(主要用来标记时钟是否已经经过了配置)。如果配置过了,那么下一次的时候就不再配置。
备份寄存器是 42 个 16 位的寄存器(战舰开发板就是大容量的),可用来存储 84 个字节的 用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。 即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。
备份区域控制寄存器RCC_BDCR
这里我们选择外部低速LSE来作为我们的RTC时钟。
RTC一般配置步骤
使能电源时钟和备份区域时钟
//我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM,
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
取消备份写保护
要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据。我们需要用到向备份区域写入一个字节,来标记时钟已经 配置过了,这样避免每次复位之后重新配置时钟。
PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问
复位备份区域,开启外部低速振荡器
在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个 操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要 看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。
备份区域复位的函数是:
BKP_DeInit();//复位备份区域
开启外部低速振荡器的函数是
RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器
选择 RTC 时钟,并使能
这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI 作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟。
RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟
设置 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); //设置 RTC 时钟分频数
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); //是设置秒中断允许,RTC 使能中断
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断,第一个参数是设置秒中断的类型.
void RTC_SetCounter(uint32_t CounterValue);//设置 RTC 计数值
更新配置,设置 RTC 中断分组
在设置完时钟之后,我们将配置更新同时退出配置模式
RTC_ExitConfigMode();//退出配置模式,更新配置
在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化 过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来 决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。 往备份区域写用户数据的函数是
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data); //寄存器标号和数据
BKP_WriteBackupRegister(BKP_DR1, 0X5050);
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR); //读取寄存器的数据
相关函数
头文件定义
//时间结构体
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);//设置时间
RTC初始化
u8 RTC_Init(void)
{
//检查是不是第一次配置时钟
u8 temp=0;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKR外设时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问,是用来存储一些重要的数据
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); //设置RTC时钟,选用LSE作为RTC的时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
RTC_WaitForLastTask(); //等待上一次写操作完成
RTC_WaitForSynchro(); //等待RTC寄存器同步,如果不同步,则对寄存器的访问是无效的。
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待上一次写操作完成
RTC_EnterConfigMode(); //允许配置
RTC_SetPrescaler(32767); //设置RTC预分频的值
RTC_WaitForLastTask(); //等待上一次写操作完成
RTC_Set(2015,1,14,17,42,55); //设置时间,y-m-d h-m-s
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定后备寄存器中写入用户数据0x5050,下次读写需要。
}
else//系统继续计时
{
RTC_WaitForSynchro(); //等待RTC寄存器同步,如果不同步,则对寄存器的访问是无效的。
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待上一次写操作完成
}
RTC_NVIC_Config();//RCT中断分组设置
RTC_Get();//更新时间
return 0; //ok
}
RTC_Get
//得到当前时间,结果保存在calendar结构体里面
//返回值0成功,其他失败
u8 RTC_Get(void)
{
static u16 daycnt=0;
u32 timecount=0;
u32 temp=0;
u16 temp1=0;
timecount=RTC_GetCounter(); //得到计数器中的值(秒钟数)
temp=timecount/86400; //得到天数。一天60*60*24 = 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; //平年
//temp是天数,-366,-365表示从0天开始。
temp1++;
}
calendar.w_year=temp1;//当前年份
temp1=0;
//temp是当前的天数,比如32天,那么就需要转换成哪一个月,哪一天。
//const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
//temp1++ = 1.w_month = temp1+1 = 2,w_date = temp+1 = 2,应该是从1开始显示。
while(temp>=28)//超过一个月
{
if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当前是闰年且为2月份
{
if(temp>=29)temp-=29;//闰年2月份,29天,所以减去29.
else break;
}
else
{
//temp>=当前月天数,那么temp - 当前月天数,表示新的一月开始
if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
else break;
}
temp1++; //月数+1
}
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;
}
RTC_Get_Week
//传入年月日,来获取星期。
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5};
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{
u16 temp2;
u8 yearH,yearL;
yearH=year/100; yearL=year%100; //yearH=20,yearL=19
if (yearH>19)yearL+=100; //yearL = 119
temp2=yearL+yearL/4; //temp2 = 119 + 29 = 148
temp2=temp2%7; //temp2 = 148%7 = 1
temp2=temp2+day+table_week[month-1];//temp2 = 1 + 2 + 5 = 8
if (yearL%4==0&&month<3)temp2--;
return(temp2%7); //8%7=1
}
/*
//2019-9-2,星期一 蔡勒公式
w 星期 对7取余,0是星期天
c 世纪数(前两位数) 20
y 年 (后两位数) 19
m 月 (m>=3,m<=14,也就是1,2月份要看成上一月的13,14月份来看) 9
d 日 2
w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1
y = 19,[y/4]=3,[c/4]=5,2c=40,[26(m+1)/10]=26,d=2
w = 19 + 4 + 5 - 40 + 27 = 15%7 = 1,星期一
*/
RTC_Set
//月份表,这里是年月日转换成秒数。
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)
{
//2019-9-2 10:00:00
//计数器只认秒,所以要转换成秒。
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;//如果是闰年的话,加上60*60*24*366
else seccount+=31536000; //否则加上60*60*24*365
}
smon-=1; //月份-1计算,因为从0开始
for(t=0;t<smon;t++) //计算月的时间
{
seccount+=(u32)mon_table[t]*86400;//60*60*24.
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年,额外+1天的秒数.
}
seccount+=(u32)(sday-1)*86400;//前面一天的秒数
seccount+=(u32)hour*3600;//当前秒数
seccount+=(u32)min*60; //分钟的秒数
seccount+=sec;//秒数
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能BKR和APB1的时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问,是用来存储一些重要的数据
RTC_SetCounter(seccount); //改变寄存器的值
RTC_WaitForLastTask(); //等待上一次的写操作完成
return 0;
}
RTC_IRQHandler
//RTC中断处理函数.
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();
}
几个注意点
- 通过RTC_CRL寄存器中的第5位,RTC操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC寄存器的操作是否已经完成,如果没有,我们必须等待上一次操作结束后才能开始下一次的操作。
- 关于预分频系数,这里我们设置RTC预分频装载寄存器的值为32767,因为我们使用外部32.768k的晶振作为时钟的输入频率。这里,给的公式是fTR_CLK = fRTCCLK/(PRL[19:0]+1) = 32.768k/(32767+1) = 1s,也就是配置计数1s,也可以使用别的寄存器,如RTC预分频器余数计数器,可以用来获得比秒钟更为准确的时钟,如0.1s,0.01s等等。
- 关于寄存器同步问题。在没有同步的情况下修改 RTC_CRH / RTC_CRL的值是不行的。
- 每一秒产生一次秒中断,在中断服务函数里面,有闹钟的中断和秒的中断,所以我们需要作出判断。通过函数RTC_Get(),我们来更新结构体的数值,然后显示在LCD上。
- 关于配置模式,由控制寄存器CRL的位4可以知道,该位由软件置1以进入配置模式,从而允许向RTC_CNT(计数寄存器)、RTC_ALR(闹钟寄存器)、或RTC_PRL(预分频寄存器)写入数据。只有当此位被置1并重新由软件清0后,才会执行此操作。