RTC实时时钟实验(下)

文章目录

  • 二、硬件设计
  • 三、软件设计



二、硬件设计

本实验用到的硬件资源有:
1) 指示灯 DS0
2) 串口
3) TFTLCD 模块
4) RTC
RTC 属于 STM32 内部资源,其配置也是通过软件设置好就可以了。不过 RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么必须确保开发板的电池有电。

三、软件设计

我们的工程中加入了 rtc.c 源文件和 rtc.h头文件,同时,引入了 stm32f10x_rtc.c 和 stm32f10x_bkp.c 库文件。

说明,首先是 RTC_Init,其代码如下:

//实时时钟配置
//初始化 RTC 时钟,同时检测时钟是否工作正常
//BKP->DR1 用于保存是否第一次配置的设置
//返回 0:正常
//其他:错误代码
u8 RTC_Init(void)
{
u8 temp=0;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |
RCC_APB1Periph_BKP, ENABLE); //使能 PWR 和 BKP 外设时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问
//检查是不是第一次配置时钟
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) //从指定的后备寄存器中
//读出数据:读出了与写入的指定数据不相乎
{ 
BKP_DeInit(); //③复位备份区域
RCC_LSEConfig(RCC_LSE_ON);  //设置外部低速晶振(LSE)
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) //检查指定的
//RCC 标志位设置与否,等待低速晶振就绪
{
temp++;
delay_ms(10);
}
if(temp>=250)return 1;//初始化时钟失败,晶振有问题 
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置 RTC 时钟
//(RTCCLK),选择 LSE 作为 RTC 时钟
RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟
RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成
RTC_WaitForSynchro(); //等待 RTC 寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成
RTC_EnterConfigMode(); // 允许配置
RTC_SetPrescaler(32767); //设置 RTC 预分频的值
RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成
RTC_Set(2009,12,2,10,0,55); //设置时间
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中
//写入用户程序数据 0x5050
}
else//系统继续计时
{
RTC_WaitForSynchro(); //等待最近一次对 RTC 寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成
}
RTC_NVIC_Config(); //RCT 中断分组设置 
RTC_Get(); //更新时间 
return 0; //ok
}

该函数用来初始化 RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍的 RTC 初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数RTC_Set(2014,3,8,22,10,55);来实现的,这里我们默认将时间设置为 2014 年 3 月 8 日 22 点 10 分55 秒。在设置好时间之后,我们通过语句 BKP_WriteBackupRegister(BKP_DR1, 0X5050);向
BKP->DR1 写入标志字 0X5050,用于标记时间已经被设置了。这样,再次发生复位的时候,该函数通过语句 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)判断 BKP->DR1 的值,来决定是不是需要重新设置时间,如果不需要设置,则跳过时间设置,仅仅使能秒钟中断一下,就进行中断分组,然后返回了。这样不会重复设置时间,使得我们设置的时间不会因复位或者断电而丢失。
该函数还有返回值,返回值代表此次操作的成功与否,如果返回 0,则代表初始化 RTC 成功,如果返回值非零则代表错误代码了。

然后是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_APB1PeriphClockCmd(RCC_APB1Periph_PWR |
RCC_APB1Periph_BKP, ENABLE); //使能 PWR 和 BKP 外设时钟
PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问
RTC_SetCounter(seccount); //设置 RTC 计数器的值
RTC_WaitForLastTask();  //等待最近一次对 RTC 寄存器的写操作完成
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_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 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_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
{ 
RTC_Get();//更新时间
}
if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
{
RTC_ClearITPendingBit(RTC_IT_ALR);  //清闹钟中断
}
RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);  //清闹钟中断
RTC_WaitForLastTask(); 
}

此部分代码比较简单,我们通过 RTC_GetITStatus 函数来判断发生的是何种中断,如果是秒钟中断,则执行一次时间的计算,获得最新时间。从而,我们可以在 calendar 里面读到时间、日期等信息。

接下来看看 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 里面读出当前的公历时间了。

最后来看看主函数代码:

int main(void)
{ 
u8 t;
delay_init(); //延时函数初始化 
uart_init(9600);  //串口初始化为 9600
LED_Init();  //初始化与 LED 连接的硬件接口
LCD_Init();  //初始化 LCD
usmart_dev.init(72);  //初始化 USMART
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(60,50,200,16,16,"Mini STM32");
LCD_ShowString(60,70,200,16,16,"RTC TEST"); 
LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(60,110,200,16,16,"2014/3/8");
while(RTC_Init()) //RTC 初始化 ,一定要初始化成功
{
LCD_ShowString(60,130,200,16,16,"RTC ERROR! "); delay_ms(800);
LCD_ShowString(60,130,200,16,16,"RTC Trying...");
} 
//显示时间
POINT_COLOR=BLUE;//设置字体为蓝色
LCD_ShowString(60,130,200,16,16," - - "); 
LCD_ShowString(60,162,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,162,calendar.hour,2,16); 
LCD_ShowNum(84,162,calendar.min,2,16);
LCD_ShowNum(108,162,calendar.sec,2,16);
LED0=!LED0;
} 
delay_ms(10);
};
}

这部分代码就不再需要详细解释了,在包含了 rtc.h 之后,通过判断 calendar.sec 是否改变来决定要不要更新时间显示。同时我们设置 LED0 每 2 秒钟闪烁一次,用来提示程序已经开始跑了。
为了方便设置时间,我们在 usmart_config.c 里面,修改 usmart_nametab 如下:

struct _m_usmart_nametab usmart_nametab[]=
{
#if USMART_USE_WRFUNS==1 //如果使能了读写操作
(void*)read_addr,"u32 read_addr(u32 addr)",
(void*)write_addr,"void write_addr(u32 addr,u32 val)", 
#endif 
(void*)delay_ms,"void delay_ms(u16 nms)",
(void*)delay_us,"void delay_us(u32 nus)",
(void*)RTC_Set,"u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)",
};

将 RTC_Set 加入了 usmart,同时去掉了上一章的一些函数(减少代码量),这样通过串口就可以直接设置 RTC 时间了。
至此,RTC 实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否正确了。


你可能感兴趣的:(STM32,单片机,stm32,嵌入式硬件)