参考资料:STM32中文参考手册;正点原子STM32开发指南
实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器。修改计数器的值可以重新设置系统当前的时间和日期。 RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后,RTC的设置和时间维持不变。 系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
● 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟
● 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
● 可编程的预分频系数:分频系数高为220。
● 32位的可编程计数器,可用于较长时间段的测量。
● 2个分离的时钟:用于APB1接口的PCLK1和RTC时钟(RTC时钟的频率必须小于PCLK1时钟 频率的四分之一以上)。
● 可以选择以下三种RTC的时钟源:
─ HSE时钟除以128;
─ LSE振荡器时钟;
─ LSI振荡器时钟
● 2个独立的复位类型:
─ APB1接口由系统复位;
─ RTC核心(预分频器、闹钟、计数器和分频器)只能由后备域复位
● 3个专门的可屏蔽中断:
─ 闹钟中断,用来产生一个软件可编程的闹钟中断。
─ 秒中断,用来产生一个可编程的周期性中断信号(长可达1秒)。
─ 溢出中断,指示内部可编程计数器溢出并回转为0的状态。
RTC由两个主要部分组成(参见下图)。
第一部分(APB1接口)用来和APB1总线相连。此单元还包 含一组16位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线 时钟驱动,用来与APB1总线接口。
另一部分(RTC核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是RTC的预分频模块,它可编程产生长为1秒的RTC时间基准TR_CLK。RTC的预分频模块包含了一个20位的可编程分频器(RTC预分频器)。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。第二个模块是一个32位的可编程计数器,可被初始 化为当前的系统时间。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时 间相比较,如果RTC_CR控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断
在看配置步骤之前我自己是偏向于看寄存器版本的,更能理解实际的过程,但是我们常常使用库函数方式,因为进行了封装比较方便。
1.使能电源时钟和备份区域时钟
这也是很多配置过程的第一步,可以通过RCC_APB1ENR寄存器来设置。在中文参考手册中是设置寄存器RCC_APB1ENR的PWREN和BKPEN位
寄存器方式:RCC_APB1ENR=1<<28;
RCC_APB1ENR=1<<27;
库函数方式:RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |RCC_APB1Periph_BKP, ENABLE);
2.取消备份区写保护
要向备份区写入数据先要取消备份区写保护(写保护在每次硬复位之后被使能),否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。
设置寄存器PWR_CR(电源控制寄存器)的DBP位,使能对后备寄存器和RTC的访问。
寄存器方式:PWR->CR|=1<<8;
库函数方式:PWR_BackupAccessCmd(ENABLE);
3.复位备份区域,开启外部低速振荡器
在取消备份区域写保护之后,可以先对这个区域复位,可以清除前面的设置,然后可以使能外部低速振荡器,这里一般要先判断RCC_BDCR(备份域控制寄存器)的LSERDY位来确定低速振荡器已经就绪。
寄存器方式:
RCC->BDCR|=1<<16; //备份区域软件复位
RCC->BDCR&=~(1<<16); //备份区域软件复位结束
RCC->BDCR|=1<<0;//开启外部低速振荡器
RCC->BDCR|=0X02; //外部低速LSE就绪
库函数方式:
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON);//设置外部低速晶振
RCC_GetFlagStatus(RCC_FLAG_LSERDY) = RESET; //外部低速LSE就绪
说明:在最后一步外部低速LSE就绪,一般是在if中用于判断,用==
号。
4.选择RTC时钟,并使能
这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSE(32.768K 的外部晶振)作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟,为什么选这个时钟??是通过时钟树决定的,RTC时钟可以有三个来源
寄存器方式:
RCC->BDCR|=1<<8;
RCC->BDCR|=1<<15;
库函数方式:
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
5.设置RTC的分频,以及配置RTC时钟
在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL (RTC预分频装载寄存器)来设置,但是在设置RTC时钟分频数时,要先检查RTC_CR寄存器的RTOFF位。
预分频装载寄存器用来保存RTC预分频器的周期计数值。它们受RTC_CR寄存器的RTOFF位保护,仅当RTOFF值为’1’时允许进行写操作。
然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位,设置时间或者设置闹钟。
寄存器方式:
while(RTC->CRL&=(1<<5));//检查RTC寄存器的RTOFF位
while(!(RTC->CRL&(1<<3))); //等待 RTC 寄存器同步
RTC->CRH|=0X01; //允许秒中断
RTC->CRH|=0X02; //允许闹钟中断
while(!(RTC->CRL&(1<<5)));//等待 RTC 寄存器操作完成
RTC->CRL|=1<<4; //允许配置
RTC->PRLH=0X0000;
RTC->PRLL=32767; //时钟周期设置 理论值:32767
库函数方式:
RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成
RTC_WaitForSynchro(); //等待 RTC 寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
RTC_WaitForLastTask(); //等待 RTC 寄存器操作完成
RTC_EnterConfigMode(); // 允许配置
RTC_SetPrescaler(32767); //设置 RTC 预分频的值
6.更新配置,设置 RTC 中断
在设置完时钟之后,我们将配置更新,这里还是通过 RTC_CRH 的 CNF 来实现。在这之后 我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化过时钟了,下次开机(或复位) 的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来决定是不是要配置,避免重复配置。接着我们配置 RTC 的秒钟中断,并进行分组。
寄存器方式:
RTC->CRL&=~(1<<4); //配置更新
while(!(RTC->CRL&(1<<5))); //等待 RTC 寄存器操作完成
BKP->DR1=0X5050; //标记已经配置过
库函数方式:
RTC_WaitForLastTask(); //等待 RTC 寄存器操作完成
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中写入用户程序数据 0x5050
在退出配置模式之前可以进行时间设置
7.编写中断服务函数
设置时间函数RTC_Set()
该函数用于设置时间,把我们输入的时间,转换为以 1970 年 1 月 1 日 0 时 0 分 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;
}
用于获取时间和日期等数据函数RTC_Get()
函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据转换为真正的时间和日期。该代码还用到了一个 calendar 的结构体。 因为 STM32 的 RTC 只有秒钟计数器,而年月日,时分秒这些需要我们自己软件计算。把计算好的值保存在 calendar 里面,方便其他程序调用。
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;
//得到当前的时间,结果保存在 calendar 结构体里面
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
static u16 daycnt=0;
u32 timecount=0;
u32 temp=0;
u16 temp1=0;
timecount=RTC->CNTH; //得到计数器中的值(秒钟数)
timecount<<=16;
timecount+=RTC->CNTL;
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 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;
}
秒钟中断服务函数
//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();
}
最后是想要用按键调整时间,后面再改成TFTLCD试试吧,main函数如下,按键写在外部中断里面:
int main(void)
{
u8 t=0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
LCD_Init();
EXTIX_Init();
usmart_dev.init(SystemCoreClock/1000000); //初始化USMART
RTC_Init(); //RTC初始化
//显示时间
POINT_COLOR=BLUE;//设置字体为蓝色
LCD_ShowString(60,130,200,16,16," - - ");
LCD_ShowString(60,170,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,170,calendar.hour,2,16);
LCD_ShowNum(84,170,calendar.min,2,16);
LCD_ShowNum(108,170,calendar.sec,2,16);
LED0=!LED0;
}
delay_ms(10);
}
}
u8 cnt=0;
//外部中断0服务程序
void EXTI0_IRQHandler(void)
{
delay_ms(10);//消抖
if(WK_UP==1) //WK_UP按键
{
if(cnt<4)
{
cnt++;
}
else
{
cnt=0;
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位
}
//外部中断3服务程序
void EXTI3_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY1==0&&cnt==0) //年
{
RTC_Set(calendar.w_year+1,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
}
else if(KEY1==0&&cnt==1) //月
{
RTC_Set(calendar.w_year,calendar.w_month+1,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
}
else if(KEY1==0&&cnt==2) //日
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date+1,calendar.hour,calendar.min,calendar.sec);
}
else if(KEY1==0&&cnt==3) //时
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour+1,calendar.min,calendar.sec);
}
else if(KEY1==0&&cnt==4) //分
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min+1,calendar.sec);
}
EXTI_ClearITPendingBit(EXTI_Line3); //清除LINE3上的中断标志位
}
void EXTI4_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY0==0&&cnt==0) //年
{
RTC_Set(calendar.w_year-1,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
}
else if(KEY0==0&&cnt==1) //月
{
if(calendar.w_month>0)
{
RTC_Set(calendar.w_year,calendar.w_month-1,calendar.w_date,calendar.hour,calendar.min,calendar.sec);
}
}
else if(KEY0==0&&cnt==2) //日
{
if(calendar.w_date>0)
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date-1,calendar.hour,calendar.min,calendar.sec);
}
}
else if(KEY0==0&&cnt==3) //时
{
if(calendar.hour>0)
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour-1,calendar.min,calendar.sec);
}
}
else if(KEY0==0&&cnt==4) //分
{
if(calendar.min>0)
{
RTC_Set(calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min-1,calendar.sec);
}
}
EXTI_ClearITPendingBit(EXTI_Line4); //清除LINE4上的中断标志位
}
因为在减时间的时候会出现bug,目前还没看出什么毛病,所以就不允许时间跨度递减,比如从2020-1-1把月份减一变成2019-12-31可能就会出现bug,导致时钟紊乱,其他日,时,分也是。
在这里运用KEY0对时间减,KEY1对时间加,KEY_UP换位,这里用到了cnt这个标志位,初始是cnt=0,表示调整年,cnt=1,表示调整月,依此类推,但是不对秒进行调整,原因是当按键触发中断的时候要先运行中断函数对时间进行调整,导致秒中断被打断,所以秒会不准。
这是毕业设计里面的一小部分,后面再把毕业设计里面的东西再总结一遍吧,其实想用TFTLCD屏幕直接数字修改时间,后面再改改。
有错误的话欢迎指出来呀