20.3 软件设计 打开上一章的工程,首先在HARDWARE文件夹下新建一个RTC的文件夹。然后打开USER文件夹下的工程,新建一个rtc.c的文件和rtc.h的头文件,保存在RTC文件夹下,并将RTC文件夹加入头文件包含路径。 由于篇幅所限,rtc.c中的代码,我们不全部贴出了,这里针对几个重要的函数,进行简要说明,首先是RTC_Init,其代码如下: //实时时钟配置 //初始化RTC时钟,同时检测时钟是否工作正常 //BKP->DR1用于保存是否第一次配置的设置 //返回0:正常 //其他:错误代码 u8 RTC_Init(void) { //检查是不是第一次配置时钟 u8 temp=0; if(BKP->DR1!=0X5050)//第一次配置 { RCC->APB1ENR|=1<<28; //使能电源时钟 RCC->APB1ENR|=1<<27; //使能备份时钟 PWR->CR|=1<<8; //取消备份区写保护 RCC->BDCR|=1<<16; //备份区域软复位 RCC->BDCR&=~(1<<16); //备份区域软复位结束 RCC->BDCR|=1<<0; //开启外部低速振荡器 while((!(RCC->BDCR&0X02))&&temp<250)//等待外部时钟就绪 { temp++; delay_ms(10); }; if(temp>=250)return 1; //初始化时钟失败,晶振有问题 RCC->BDCR|=1<<8; //LSI作为RTC时钟 RCC->BDCR|=1<<15; //RTC时钟使能 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 while(!(RTC->CRL&(1<<3))); //等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 RTC->CRL|=1<<4; //允许配置 RTC->PRLH=0X0000; RTC->PRLL=32767; //时钟周期设置(有待观察,看是否跑慢了?)理论值:32767 RTC_Set(2012,9,7,13,16,55); //设置时间 RTC->CRL&=~(1<<4); //配置更新 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 BKP->DR1=0X5050; printf("FIRST TIME\n"); }else//系统继续计时 { while(!(RTC->CRL&(1<<3))); //等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 printf("OK\n"); } MY_NVIC_Init(0,0,RTC_IRQChannel,2); //优先级设置 RTC_Get();//更新时间 return 0; //ok } 该函数用来初始化RTC时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍的RTC初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数RTC_Set(2012,9,7,13,16,55);来实现的,这里我们默认将时间设置为2012年9月7日13点16分55秒。在设置好时间之后,我们向BKP->DR1写入标志字0X5050,用于标记时间已经被设置了。这样,再次发生复位的时候,该函数通过判断BKP->DR1的值,来决定是不是需要重新设置时间,如果不需要设置,则跳过时间设置,仅仅使能秒钟中断一下,就进行中断分组,然后返回了。这样不会重复设置时间,使得我们设置的时间不会因复位或者断电而丢失。 该函数还有返回值,返回值代表此次操作的成功与否,如果返回0,则代表初始化RTC成功,如果返回值非零则代表错误代码了。 介绍完RTC_Init,我们来介绍一下RTC_Set函数,该函数代码如下: //设置时钟 //把输入的时钟转换为秒钟 //以1970年1月1日为基准 //1970~2099年为合法年份 //返回值:0,成功;其他:错误代码. //平年的月份日期表 const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31}; //syear,smon,sday,hour,min,sec:年月日时分秒 //返回值:设置结果。0,成功;1,失败。 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->APB1ENR|=1<<28;//使能电源时钟 RCC->APB1ENR|=1<<27;//使能备份时钟 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)));//等待RTC寄存器操作完成 RTC_Get();//设置完之后更新一下数据 return 0; } 该函数用于设置时间,把我们输入的时间,转换为以1970年1月1日0时0分0秒当做起始时间的秒钟信号,后续的计算都以这个时间为基准的,由于STM32的秒钟计数器可以保存136年的秒钟数据,这样我们可以计时到2106年。 接着,我们介绍一下RTC_Get函数,该函数用于获取时间和日期等数据,其代码如下: //得到当前的时间,结果保存在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 {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; } 函数其实就是将存储在秒钟寄存器RTC->CNTH和RTC->CNTL中的秒钟数据转换为真正的时间和日期。该代码还用到了一个calendar的结构体,calendar是我们在rtc.h里面将要定义的一个时间结构体,用来存放时钟的年月日时分秒等信息。因为STM32的RTC只有秒钟计数器,而年月日,时分秒这些需要我们自己软件计算。我们把计算好的值保存在calendar里面,方便其他程序调用。 最后,我们介绍一下秒钟中断服务函数,该函数代码如下: //RTC时钟中断 //每秒触发一次 void RTC_IRQHandler(void) { if(RTC->CRL&0x0001) //秒钟中断 { RTC_Get(); //更新时间 //printf("sec:%d\r\n",calendar.sec); } if(RTC->CRL&0x0002) //闹钟中断 { RTC->CRL&=~(0x0002); //清闹钟中断 //printf("Alarm!\n"); } RTC->CRL&=0X0FFA; //清除溢出,秒钟中断标志 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 } 此部分代码比较简单,我们通过RTC->CRL的不同位来判断发生的是何种中断,如果是秒钟中断,则执行一次时间的计算,获得最新时间。从而,我们可以在calendar里面读到时间、日期等信息。 rtc.c的其他程序,这里就不再介绍了,请大家直接看光盘的源码。保存rtc.c,然后将rtc.c加入HARDWARE组下,在rtc.h里面输入如下代码: #ifndef __RTC_H #define __RTC_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; //日历结构体 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_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 从上面代码可以看到_calendar_obj结构体所包含的东西,是一个完整的公历信息,包括年、月、日、周、时、分、秒等7个元素。我们以后要知道当前时间,只需要通过RTC_Get函数,执行时钟转换,然后就可以从calendar里面读出当前的公历时间了。 |