实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。
STM32 的 RTC 外设,实质是一个掉电后还继续运行的定时器。所以 RTC 外设的复杂之处并不在于它的定时功能,而在于它掉电还继续运行的特性,而所谓掉电是指主电源 VDD 断开的情况,为了 RTC 外设掉电继续运行,必须接上锂电池给 STM32 的 RTC、备份发卡通过 VBAT 引脚供电。当主电源 VDD 有效时,由VDD给 RTC 外设供电;而当 VDD掉电后,由 VBAT给RTC 外设供电。
开发板中提供了一个钮扣电池插槽,可以接入型号为 CR1220 的钮扣电池,该型号的钮扣电池电压为 3.2V,图中的 BAT54C 双向二极管可切换输入到 STM32 备份域电源引脚 VBAT 的供电,当主电源正常供电时,由稳压器输出的 3.3V 供电,当主电源掉电时,由钮扣电池供电。
对时钟不熟的请看《STM32系统时钟超详解》
从 RTC 的定时器特性来说,它是一个 32 位的计数器,只能向上计数。它使用的时钟源有三种:
1.高速外部时钟的 128 分频(HSE/128)
2.低速内部时钟 LSI
3.低速外部时钟 LSE;
使 HSE 分频时钟或 LSI 的话,在主电源 VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证 RTC 正常工作。
因此 RTC 一般使用低速外部时钟 LSE,在设计中,频率通常为实时时钟模块中常用的 32.768KHz,这是因为 32768 = 2的15次方,分频容易实现,所以它被广泛应用到 RTC 模块。在主电源 VDD有效的情况下(待机),RTC 还可以配置闹钟事件使 STM32 退出待机模式
● 可编程的预分频系数:分频系数最高为2的20次方。
● 32位的可编程计数器,可用于较长时间段的测量。
● 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟频率的四分之一以上)。
● 可以选择以下三种RTC的时钟源:
─ HSE时钟除以128;
─ LSE振荡器时钟;
─ LSI振荡器时钟。
● 2个独立的复位类型:
─ APB1接口由系统复位;
─ RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位(详见6.1.3节)。
● 3个专门的可屏蔽中断:
─ 闹钟中断,用来产生一个软件可编程的闹钟中断。
─ 秒中断,用来产生一个可编程的周期性中断信号(最长可达1秒)。
─ 溢出中断,指示内部可编程计数器溢出并回转为0的状态。
框图中浅灰色的部分都是属于备份域的,在 VDD 掉电时可在 VBAT 的驱动下继续运行。这部分仅包括 RTC 的分频器,计数器,和闹钟控制器。
若 VDD 电源有效,RTC 可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和 RTC_Alarm(闹钟中断)。
从结构图可以分析到,其中的定时器溢出事件无法被配置为中断。若 STM32 原本处于待机状态,可由闹钟事件或 WKUP 事件(外部唤醒事件,属于 EXTI 模块,不属于 RTC)使它退出待机模式。闹钟事件是在计数器 RTC_CNT 的值等于闹钟寄存器 RTC_ALR 的值时触发的
RTC有两个部分组成
第一部分:
RTC由两个主要部分组成(参见下图)。第一部分(APB1接口)用来和APB1总线相连。此单元还包含一组16位寄存器,可通过APB1总线对其进行读写操作(就是STM32对RTC的寄存器进行读写操作)。APB1接口由APB1总线时钟驱动,用来与APB1总线接口。
第二部分(RTC核心)由一组可编程计数器组成,分成两个主要模块。
第一个模块:
RTC的预分频模块,它可编程产生最长为1秒的RTC时间基准TR_CLK。RTC的预分频模块包含了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。
该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器位数是一样的。也就是说,如果预分频装载寄存器的值为32767,那么余数寄存器就会在每一次秒更新时由硬件重新装载为32767,然后向下计数,计数到0表示一秒,也即1000ms。
获取毫秒时间:
先用下面库函数获取RTC_DIV的值
毫秒时间:( 32767-RTC_GetDivider() )/32767*1000;
计算方式就是每个计数32767为1000ms也就是1秒钟,32767-RTC_DIV的值就等于计数了多少次。
第二个模块:
一个32位的可编程计数器,可被初始化为当前的系统时间。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断。
它的计数器RTC_CNT 的 32 位由 RTC_CNTL 和 RTC_CNTH 两个寄存器组成,分别保存定时计数值的低 16 位和高 16 位。在配置 RTC 模块的时钟时,通常把输入的 32768Hz 的 RTCCLK 进行32768 分频得到实际驱动计数器的时钟 TR_CLK = RTCCLK/32768= 1 Hz,计时周期为 1 秒,计时器在 TR_CLK 的驱动下计数,即每秒计数器 RTC_CNT 的值加 1。
一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒,
约合 136 年左右,作为一般应用,这已经是足够了。
RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。
系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
● 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
● 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问
这就是为什么要使能电源接口时钟,因为要配置它的寄存器
除了RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器外,所有的系统寄存器都由系统复位或电源复位进行异步复位。
RTC_PRL、RTC_ALR、RTC_CNT和RTC_DIV寄存器仅能通过备份域复位信号复位。
备份域复位
备份区域拥有两个专门的复位,它们只影响备份区域(见图4)。
当以下事件中之一发生时,产生备份区域复位。
软件复位,备份区域复位可由设置备份域控制寄存器 (RCC_BDCR)(见6.3.9节)中的BDRST位产生。
RTC核完全独立于RTC APB1接口。软件通过APB1接口访问RTC的预分频值、计数器值和闹钟值。但是相关的可读寄存器只在与RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。
RTC标志也是如此的。这意味着,如果APB1接口曾经被关闭,而读操作又是在刚刚重新开启APB1之后,则在第一次的内部寄存器更新之前,从APB1上读出的RTC寄存器数值可能被破坏了(通常读到0)。
意思是在第一次通过 APB1 接口访问 RTC 时,因为时钟频率的差异(APB1的时钟频率比RTC的时钟频率高的多),所以必须等待 APB1 与 RTC 外设同步,确保被读取出来的 RTC 寄存器值是正确的。若在同步之后,一直没有关闭 APB1 的 RTC 外设接口,就不需要再次同步了。
下述几种情况下能够发生这种情形:
● 发生系统复位或电源复位
● 系统刚从待机模式唤醒
● 系统刚从停机模式唤醒(参见第4.3节:低功耗模式)。
所有以上情况中,APB1接口被禁止时(复位、无时钟或断电)RTC核仍保持运行状态。
因此,若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置’1’
1.必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。
2.对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。
可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是’1’时,才可以写入RTC寄存器。
配置过程:
对一个或多个RTC寄存器进行写操作
清除CNF标志位,退出配置模式
查询RTOFF,直至RTOFF位变为’1’以确认写操作已经完成。
仅当CNF标志位被清除时(要退出配置模式写操作才开始),写操作才能进行,这个过程至少需要3个RTCCLK周期
但是写RTC_PRL、RTC_CNT、RTC_ALR库函数自动带了进入配置模式写完退出配置模式,所以我们写下一个寄存器之前一定要等待上一次写操作完成
在屏幕上显示当前时间日期(年月日),和时分秒。
1.UNIX 时间戳
大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX 时间戳和 UNIX 计时元年。
定时器被置 0 的这个时间被称为计时元年,相对计时元年经过的秒数称为时间戳,也就是计数器中的值。UNIX 计时元年被设置为格林威治时间 1970 年 1 月 1 日 0 时 0 分 0 秒,大概是为了纪念 UNIX 的诞生的时代吧,而UNIX 时间戳即为当前时间相对于 UNIX 计时元年经过的秒数。
通俗点讲:在1970年 1 月 1 日 0 时 0 分 0 秒,计数器寄存器RTC_CNT的值为0,我们需要先将当前时间与1970年 1 月 1 日 0 时 0 分 0 秒的差值算出经过了多少秒钟,将这个值写入计数器寄存器中,当使能RTC时钟的时候则则计数器从当前时间开始计时,计数器的值会每隔一秒加1,我们只需一直读取计数器寄存器中的值将它秒钟转化为实时时间(从1970年1月1日0时0分0秒开始计算)。再将转化的时间显示到屏幕上这样就实现了一个简易的时钟。
主要的任务就是将计数器的秒钟数换算成当前时间(主要考虑闰年与非闰年)
相当于1970 1 月 1 日 0 时 0 分 0 秒 ,计数器寄存器中的值为0,然后当前去读取计数器寄存器的值,就能获得一共经历了多少秒,然后换算成当前时间。
RCT外设配置过程
1.使能PWR和BKP时钟:RCC_APB1PeriphClockCmd();
2.使能后备寄存器访问: PWR_BackupAccessCmd();
1、2步使能对后备寄存器和RTC的访问。
3.配置RTC时钟源,使能RTC时钟:
1)开启LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
2)等待LSE使能完成
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) ==RESET)
3)LSE作为RTC的时钟源
RCC_RTCCLKConfig();
4)使能RTC时钟
RCC_RTCCLKCmd();
4.设置RTC预分频系数:RTC_SetPrescaler();
5. 设置时间:RTC_SetCounter();
6.开启相关中断(如果需要):RTC_ITConfig();
7.编写中断服务函数:RTC_IRQHandler();
8.部分操作要等待写操作完成和同步。
RTC_WaitForLastTask();//等待最近一次对RTC寄存器的写操作完成
RTC_WaitForSynchro(); //等待RTC寄存器同步
rtc.c
#include "rtc.h"
_calendar_obj calendar;//时钟结构体
static void RTC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; //RTC全局中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能该通道中断
NVIC_Init(&NVIC_InitStructure);
}
void RTC_Init(void)
{
//使能电源和后备接口时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);
//使能对后备寄存器和RTC的访问
PWR_BackupAccessCmd(ENABLE);
if( BKP_ReadBackupRegister(BKP_DR1) !=0x5050 )
{
//复位备份区域
BKP_DeInit();
//开启LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
//等待就绪
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) ==RESET)
//LSE作为RTC的时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//使能RTC时钟
RCC_RTCCLKCmd(ENABLE);
//等待时钟同步
RTC_WaitForSynchro();
//等待上一次写操作完成
RTC_WaitForLastTask();
//使能RTC秒中断
RTC_ITConfig(RTC_IT_SEC, ENABLE);
//等待最近一次对RTC寄存器的写操作完成
RTC_WaitForLastTask();
//进入配置模式
RTC_EnterConfigMode();
//设置分频
RTC_SetPrescaler(32767);
//等待上一次写操作完成
RTC_WaitForLastTask();
//配置时间
RTC_Set(2022,8,14,15,00,59);
//退出配置模式
RTC_ExitConfigMode();
BKP_WriteBackupRegister(BKP_DR1,0x5050 );
}
else
{
RTC_WaitForSynchro(); //等待最近一次对RTC寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
}
RTC_NVIC_Config();
RTC_Get();//更新时间
}
//RTC时钟中断
//每秒触发一次
//extern u16 tcnt;
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
{
RTC_Get();//更新时间
}
RTC_WaitForLastTask();
RTC_ClearITPendingBit(RTC_IT_SEC); //清闹钟中断
}
//判断是否是闰年函数
//月份 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寄存器的写操作完成
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();
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 "stm32f10x.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 RTC_Init(void); //初始化RTC,返回0,失败;1,成功;
u8 Is_Leap_Year(u16 year);//平年,闰年判断
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 /* __RTC_H */
main.c
#include "stm32f10x.h"
#include "./usart/bsp_usart.h"
#include "./lcd/bsp_ili9341_lcd.h"
#include "./flash/bsp_spi_flash.h"
#include "rtc.h"
static void Delay ( __IO uint32_t nCount );
int main(void)
{
//LCD 初始化
ILI9341_Init ();
/* USART config */
USART_Config();
RTC_Init();
//其中0、3、5、6 模式适合从左至右显示文字,
//不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
//其中 6 模式为大部分液晶例程的默认显示方向
ILI9341_GramScan ( 6 );
LCD_SetFont(&Font8x16);
LCD_SetColors(RED,BLACK);
ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
while ( 1 )
{
char dispBuff_T1[100];
char dispBuff_T2[100];
// ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
sprintf(dispBuff_T1,"%0.2d-%0.2d-%0.2d",calendar.w_year,calendar.w_month,calendar.w_date);
// ILI9341_Clear(8*5,LINE(8),LCD_X_LENGTH-8*5,HEIGHT_CH_CHAR);
//显示年月日
ILI9341_DispString_EN_CH(8*5,LINE(7),dispBuff_T1);
sprintf(dispBuff_T2,"%0.2d-%0.2d-%0.2d",calendar.hour,calendar.min,calendar.sec);
// ILI9341_Clear(8*5,LINE(8),LCD_X_LENGTH-8*5,HEIGHT_CH_CHAR);
//显示时分秒
ILI9341_DispString_EN_CH(8*5,LINE(8),dispBuff_T2);
switch(calendar.week)
{
case 0:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期日");
break;
case 1:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期一");
break;
case 2:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期二");
break;
case 3:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期三");
break;
case 4:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期四");
break;
case 5:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期五");
break;
case 6:
ILI9341_DispString_EN_CH(8*5,LINE(9),"星期六");
break;
default:
break;
}
}
}
主要是将秒钟数换算成时间,时间换算成秒钟数,其实就是注意一下闰年与非闰年,看一遍程序应该懂了。
以下来自百度:
闰年产生原因:
最根本的原因是:地球绕太阳运行的周期为365天5小时48分46秒(合365.24219天),即一回归年(tropical year)。公历的平年只有365天,比回归年短约0.2422天,所余下的时间约为每四年累积一天,故在第四年的2月末加1天,使当年的时间长度变为366天,这一年就是闰年。现行公历中每400年有97个闰年。按照每四年一个闰年计算,平均每年就要多算出0.0078天,这样,每128年就会多算出1天,经过400年就会多算出3天多。因此,每400年中要减少3个闰年。所以公历规定:年份是整百数时,必须是400的倍数才是闰年;不是400的倍数的世纪年,即使是4的倍数也不是闰年。
这就是通常说的:四年一闰,百年不闰,四百年再闰。例如:2000年是闰年,2100年则是平年。
普通闰年:公历年份是4的倍数,且不是100的倍数的,为闰年(如2004年、2020年等就是闰年)。
世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年(如1900年不是闰年,2000年是闰年)
闰年首先要是4的倍数,但不能是100的倍数
如果是100的倍数,必须要是400的倍数才是闰年
公历只分闰年和平年,平年有365天,闰年有366天(2月中多一天:29天)
//判断是否是闰年函数
//月份 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;
}