基于51单片机的可调时钟(开源)

Brief:

        应用DS1302实现在LCD1602上显示时间,包括年月日,时分秒以及星期几的英文缩写,并具有时间可调的功能,调节当前位时闪烁。利用独立按键1实现模式切换,按键2设置时间位选择,按键3当前时间位+1,按键4当前时间位-1,调节时间具有边界判断和自动校正功能,同时会显示当前的工作模式。

        长文预警,三个模块分别为DS1302读取和显示写入的时间设置并修改当前时间

一、DS1302

1、简介和引脚介绍

RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片

DS1302  是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。 由此得知时间位的越界校正在芯片内部实现,我们只需要写入和读取时间就能实现时钟显示

 引脚介绍

 数据通过I/O引脚与内部数据寄存器进行数据交换

寄存器定义

寄存器中的时间数据均使用BCD码保存,前两列为读和写的地址为16进制,也就是命令字

WP为写保护,最高位为1时打开写保护,为0时关闭 

命令字 

 最高位固定为1,最低位为0时为写数据,为1时读取数据

2、读写时间数据步骤和原理

读写步骤

  • 关闭写保护(输入命令字0x8E,写入0x00) 
  • 向I/O端口输入命令字(地址)字节,确定读/写的数据。

        例如:命令字为0x81,就是告诉芯片我要读取存放秒的寄存器里的数据

                   命令字为0x80,就是高速芯片要往存放秒的寄存器写入数据,下一个字节就是要写入的数据;  

  • 输入要写入的数据 ;或者读取对应地址寄存器里的数据
  • 打开(也可以不打开)写保护

举例:输入(命令字,数据)为(0x82,0x24)就是把分钟的数据置为24(寄存器里是BCD码)

           输入(0x81),就是接下来要读取分钟当前的数据

由于寄存器内部以BCD码存储时间数据,而我们使用LCD1602显示是十进制数,因此我们统一定义写入的时间为十进制数,写入的时候转换为BCD码,读取的时候再将BCD码转为十进制保存显示BCD码转十进制:DEC=BCD/16*10+BCD%16;2BCD

十进制转BCD码:BCD=DEC/10*16+DEC%10;2BCD

读写数据时序定义

 写数据 :

  1. 首先将CE置高电平允许读写
  2. 输入的第一位数据到达后在时钟第一个上升沿将其写入,随后将时钟置0
  3. 下一个数据到达,继续给时钟上升沿将其读入,随后置0
  4. 第一个字命令字节写入完毕,确定在哪个寄存器写入数据
  5. 下一个字节为要写入的数据,写入方式和前面一样
  6. CE置0关闭读写
/**
  * @brief DS1302的写数据功能,前八位为要写入的地址,后八位为要写入的数据
  * @param  Commend 命令字确定要写的地址  Data 要写入的数据
  * @retval 无
  */
void DS1302_WriteByte(unsigned char Commend, Data)
{
	unsigned char i;
	DS1302_CE = 1;        //芯片开启读写
	for(i=0;i<8;i++)
	{
		DS1302_IO = Commend & (0x01<<i);  //移位寄存,从低位开始写地址
		DS1302_SCLK = 1;				//上升沿写入
		DS1302_SCLK = 0;				//写入后置零
	}
	for(i=0;i<8;i++)
	{
		DS1302_IO = Data & (0x01<<i);  //移位寄存,从低位开始写数据
		DS1302_SCLK = 1;				//上升沿写入
		DS1302_SCLK = 0;				//写入后置零
	}
	DS1302_CE = 0;	      //关闭读写功能
}      //关闭读写功能

 读数据 :

  1. 首先将CE置高电平允许读写
  2. 字命令字节写入方式相同,但读数据比写数据少一个时钟周期,时序应为,在第八个时钟上升沿完成第一个字节的写入,在将第八个时钟高电平置0的下降沿开始读取第一位数据,并由单片机将其置于寄存器内给用户保存,在下一个时钟下降沿读取第二位以此类推
  3. 在每一个对应时序把读取到的数据保存
  4. CE置0关闭读写,写入的字命令清0,等待下一次读取在写入对应地址
/**
  * @brief DS1302的读数据功能,前八位为要写入的地址,确定后八位要读的数据在哪
  *			前八个上升沿写地址,后八个下降沿读数据
  * @param  Commend 命令字确定要读数据的地址
  * @retval  读取到的一个字节数据
  */
unsigned char DS1302_ReadByte(unsigned char Commend)
{
	unsigned char Data = 0x00;   //局部变量用于存储读出的数据
	unsigned char i;	
	Commend |= 0x01;
	DS1302_CE = 1;	    
	for(i=0;i<8;i++)
	{
		DS1302_IO = Commend & (0x01<<i);  //移位寄存,从低位开始写地址
		DS1302_SCLK = 0;          
		DS1302_SCLK = 1;
	}
		for(i=0;i<8;i++)
	{		
		DS1302_SCLK = 1;          
		DS1302_SCLK = 0;				  //下降沿读取一位
		if(DS1302_IO){Data |= (0x01<<i);}   //读取位为1的时候给Data对应位1
	}
	DS1302_CE = 0;			//关闭读写功能	
	DS1302_IO = 0;          //置零等下一次读取
	return Data;            //返回读取的数据
}

这两个是主要的读写函数,下面介绍实现完全实现读写的其他相关函数 

3、DS1302读写相关声明、定义和函数

类型声明和定义

和之前一样 按对应地址自定义声明DS1302的串行时钟、使能、数据输入输出; 宏定义,要写入的地址; 存放要写入时间数据的数组。便于移植和维护,增加可读性

#include <REGX52.H>

//自定义声明DS1302的串行时钟、使能、数据输入输出
sbit DS1302_SCLK = P3^6;
sbit DS1302_CE = P3^5;
sbit DS1302_IO = P3^4;

//宏定义写入地址
#define DS1302_SECOND  0x80
#define DS1302_MINITE  0x82
#define DS1302_HOUR    0x84
#define DS1302_DATE    0x86
#define DS1302_MONTH   0x88
#define DS1302_DAY     0x8A
#define DS1302_YAER    0x8C
#define DS1302_WP      0x8E

char DS1302_Time[] = {22, 12, 5, 0, 21, 58, 1}; //数组存放设置时间

初始化函数DS1302_Init(void)

由于单片机一上电会把时钟和CE使能给置高电平,因此我们要正常使用需要先手动将这俩置0

/**
  * @brief 初始化DS1302的串行时钟和使能端输入
  * @param  无
  * @retval 无
  */
void DS1302_Init(void)
{
	DS1302_SCLK = 0;  //单片机一上电赋高电平,这里手动清零
	DS1302_CE = 0;    //不允许读写状态
}

 设置时间函数DS1302_SetTime(void)

调用写数据函数依次将时间数据写入DS1302,涉及前面提到的,将十进制数转换为BCD码写入

/**
  * @brief 写入存放在数组的时间数据
  * @param  无
  * @retval 无
  */
void DS1302_SetTime(void)
{
	DS1302_WriteByte(DS1302_WP,0x00);//关闭写保护
	
	DS1302_WriteByte(DS1302_YAER,DS1302_Time[0]/10*16+DS1302_Time[0]%10);//写入时十进制转BCD码
	DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
	DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
	DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
	DS1302_WriteByte(DS1302_MINITE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
	DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
	DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);

//	DS1302_WriteByte(DS1302_WP,0x80);//打开写保护
}

最后这句打开写保护可加可不加,因为我后面要加入修改当前时间的功能所以这里我注释掉了

读取时间函数DS1302_ReadTime(void)

调用读数据函数将时间数据依次读取并存放在数组里,用于显示在LCD1602,读取时将读取到的BCD码转换为十进制数再保存

/**
  * @brief 读取时间数据存放在数组
  * @param  无
  * @retval 无
  */
void DS1302_ReadTime(void)
{
	unsigned char Temp;  //中间变量
	
	Temp = DS1302_ReadByte(DS1302_YAER);
	DS1302_Time[0] = Temp/16*10 + Temp%16; //读取时BCD码转十进制便于显示
	Temp = DS1302_ReadByte(DS1302_MONTH);
	DS1302_Time[1] = Temp/16*10 + Temp%16; 
	Temp = DS1302_ReadByte(DS1302_DATE);
	DS1302_Time[2] = Temp/16*10 + Temp%16; 
	Temp = DS1302_ReadByte(DS1302_HOUR);
	DS1302_Time[3] = Temp/16*10 + Temp%16; 
	Temp = DS1302_ReadByte(DS1302_MINITE);
	DS1302_Time[4] = Temp/16*10 + Temp%16; 
	Temp = DS1302_ReadByte(DS1302_SECOND);
	DS1302_Time[5] = Temp/16*10 + Temp%16; 
	Temp = DS1302_ReadByte(DS1302_DAY);
	DS1302_Time[6] = Temp/16*10 + Temp%16; 
	
}

以上就是DS1302使用的所有代码,记得自行封装~

二、显示当前时间

有了使用DS1302写入和读取时间的函数,我们只需要在主函数里调用写入数据函数,再在while循环中不断调用读取时间函数,然后将当前时间显示在LCD1602即可

其中显示星期几需要将对应周一到周日的数字(1~7)转换为英文字符串显示,比较简单

//存放周一到周日的显示字符
unsigned char Day[][6] = {"Mon. ", "Tues.", "Wed. ", "Thur.", "Fri. ", "Sat. ", "Sun. "};



/**
  * @brief 将周内对应数字转化为英文字符串缩写显示
  * @param  无
  * @retval 无
  */
void TranserShowStr_Day(char DayNum)
{
	switch(DayNum)    //将数字转化为对应字符串显示
	{
		case 1: LCD_ShowString(1,12,Day[0]);break;
		case 2: LCD_ShowString(1,12,Day[1]);break;
		case 3: LCD_ShowString(1,12,Day[2]);break;
		case 4: LCD_ShowString(1,12,Day[3]);break;
		case 5: LCD_ShowString(1,12,Day[4]);break;
		case 6: LCD_ShowString(1,12,Day[5]);break;
		case 7: LCD_ShowString(1,12,Day[6]);break;	
	}	
}

将时间数据(年月日时分秒)显示在LCD1602的函数

/**
  * @brief 显示当前的时间包括:年月日时分秒
  * @param  无
  * @retval 无
  */
void ShowTime(void)   //显示年月日时分秒
{
	LCD_ShowNum(1,1,DS1302_Time[0],2);  //显示年
	LCD_ShowChar(1,3,'-');
	LCD_ShowNum(1,4,DS1302_Time[1],2);  //显示月
	LCD_ShowChar(1,6,'-');
	LCD_ShowNum(1,7,DS1302_Time[2],2);	//显示日		
	LCD_ShowNum(2,1,DS1302_Time[3],2);	//显示时
	LCD_ShowChar(2,3,':');				
	LCD_ShowNum(2,4,DS1302_Time[4],2);	//显示分
	LCD_ShowChar(2,6,':');			
	LCD_ShowNum(2,7,DS1302_Time[5],2);	//显示秒
}

主函数比较简单,注意先初始化DS1302和LCD1602

#include <REGX52.H>
#include"DS1302.h"
#include"LCD1602.h"

void main()
{
    //初始化
	LCD_Init();      
	DS1302_Init();	
	DS1302_SetTime();	 //给DS1302写入时间
	while(1)
	{

        	DS1302_ReadTime();
			TranserShowStr_Day(DS1302_Time[6]);  //将数字转化为对应字符串显示
			ShowTime();                 //显示年月日时分秒
    }
}

到这里就完成了将时间数据写入DS1302并实时读取并将时间显示在LCD1602的功能

 三、设置并修改当前时间

 梳理一下思路

  1. 首先需要按键检测功能完成模式切换,定义Mode在0、1切换
  2. Mode为1时进入设置时间模式,不再读取时间,仅显示进入模式切换时的时间和当前模式
  3. 按键检测为2,时间位索引+1,对应为存放时间数组的索引,用于选择要修改的时间位
  4. 确定了要修改的时间位后按键检测3号键按下,对应时间位数据+1,4号键对应数据-1
  5. 数据增加和减少时的越界判定和校正
  6. 显示修改后的时间
  7. 完成修改后,切换回显示时间模式的时候同时调用设置时间函数将修改后的时间数据写入DS1302并显示
  8. 最后加上在设置时间模式下选中时间位闪烁效果,用于指示当前修改的时间位,在加入这一效果之前可以先用LCD1602把时间位索引显示出来,也可以知道当前修改时间位,闪烁效果更为优化和普遍

思考方向如此,但具体实现以下面为准 

1、按键检测和模式切换

独立按键检测,一号键切换工作模式

//定义键码、设置时间位索引、设置时间闪烁标志、模式(显示时间/设置时间)
unsigned char KeyNum, TimeSet_Sel, TimeSetFlag, Mode;  

void main()
{
    //初始化
	LCD_Init();      
	DS1302_Init();	
	DS1302_SetTime();	 //给DS1302写入时间
	Timer0Init();		 //定时器初始化
	while(1)
	{
		KeyNum = Key();   //接收键码
		if(KeyNum == 1)
		{
			Mode++;
			if(Mode>1)      //使按下按键1切换工作模式
			{
				Mode = 0;   //切换回显示时间模式
				DS1302_SetTime();  //将已修改的时间数据写入芯片
			}
			if(Mode == 1)TimeSet_Sel == 0;    //每次切换到设置时间模式索引清0,固定从“年”开始闪烁
		}
   ......
	if(KeyNum == 2)         //2号按键选择设置的时间位
	{
		TimeSet_Sel++;
		TimeSet_Sel %= 7;	//索引在0~6
	}
	if(KeyNum == 3)         //3号按键使当前时间位+1
	{
		DS1302_Time[TimeSet_Sel]++;
		BoundedCheckUp();   //越上界判断和校正
	}
	if(KeyNum == 4)			//4号键使当前时间位-1
	{
		DS1302_Time[TimeSet_Sel]--;
		BoundedCheckDown();	//越下界判断和校正
	}	

独立按键检测函数

/**
  * @brief 独立按键检测获取键码
  * @param  无
  * @retval 按下按键键码,范围0-4,无按键按下返回0
  */

unsigned char Key()
{
	unsigned char KeyNumber = 0;
	if(P3_1 == 0){Delay(20);while(!P3_1);Delay(20);KeyNumber = 1;}
	if(P3_0 == 0){Delay(20);while(!P3_0);Delay(20);KeyNumber = 2;}
	if(P3_2 == 0){Delay(20);while(!P3_2);Delay(20);KeyNumber = 3;}
	if(P3_3 == 0){Delay(20);while(!P3_3);Delay(20);KeyNumber = 4;}
	
	return KeyNumber;
}

2、 工作模式内容

Mode为0,显示时间模式,正常显示时间;Mode为1,调用TimeSet()函数,相关内容为不再读取时间,进入时保持显示切换时的时间和当前模式 名称

		switch(Mode)
		{
			case 0: LCD_ShowString(2,13,"Show");break;
			case 1: LCD_ShowString(2,13,"Set ");break;
		}
		if(!Mode)    //模式0,显示时间
		{
			DS1302_ReadTime();
			TranserShowStr_Day(DS1302_Time[6]);  //将数字转化为对应字符串显示
			ShowTime();                 //显示年月日时分秒
		}
		else          //模式1,设置时间
			TimeSet();

3、TimeSet()函数

/**
  * @brief 模式1,修改设置当前时间
  * @param  无
  * @retval 无
  */
void TimeSet(void)          //设置时间函数
{
	if(KeyNum == 2)         //2号按键选择设置的时间位
	{
		TimeSet_Sel++;
		TimeSet_Sel %= 7;	//索引在0~6
	}
	if(KeyNum == 3)         //3号按键使当前时间位+1
	{
		DS1302_Time[TimeSet_Sel]++;
		BoundedCheckUp();   //越上界判断和校正
	}
	if(KeyNum == 4)			//4号键使当前时间位-1
	{
		DS1302_Time[TimeSet_Sel]--;
		BoundedCheckDown();	//越下界判断和校正
	}	
	ShowSetTime(); 			//显示当前修改后的时间	
	if(TimeSet_Sel == 6 &&  TimeSetFlag ==1)  //选中时0.5s闪烁,未选中正常显示
		LCD_ShowString(1,12,"     ");
	else
		TranserShowStr_Day(DS1302_Time[6]);    //显示字符缩写是星期几
}

2号键修改的时间位索引+1,3号键按下,对应时间位数据+1,4号键对应数据-1。

4、越界判断和校正

增加时越上界校正

 DS1302显示年份范围为2000~2099,这里只显示0~99,当增加到99在增加变为0

同理月份12月增加变为1,日增加需要考虑大小月,4年一闰的2月为29天(400一闰无需考虑)

小时显示模式是AM,0~23,分秒均为0~59,星期几是1~7,都需要进行逻辑判断,超过置为初值

/**
  * @brief 越上界判断和校正
  * @param  无
  * @retval 无
  */
void BoundedCheckUp(void)   //越上界判断函数
{
	//越上界校正
	if(DS1302_Time[0]>99)DS1302_Time[0] = 0;		//年份越界判断
	if(DS1302_Time[1]>12)DS1302_Time[1] = 1;		//月份越界判断
	//天数越界判断	
	if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
	{
		if(DS1302_Time[2]>30)DS1302_Time[2] = 1;	 //小月
	}
	else if(DS1302_Time[1]==2)
	{
		if(DS1302_Time[0]%4 == 0)       //4年一闰2月29天,时钟寿命仅100年不需要考虑400年一闰
		{
			if(DS1302_Time[2]>29)DS1302_Time[2] = 1;
		}
		else
		{
			if(DS1302_Time[2]>28)DS1302_Time[2] = 1;  //平年2月28天
		}		
	}		
	else 
	{
		if(DS1302_Time[2]>31)DS1302_Time[2] = 1;  //大月
	}	
					
	if(DS1302_Time[3]>23)DS1302_Time[3] = 0;   //小时越界判断	
	if(DS1302_Time[4]>59)DS1302_Time[4] = 0;   //分越界判断
	if(DS1302_Time[5]>59)DS1302_Time[5] = 0;   //秒越界判断	
	if(DS1302_Time[6]>7)DS1302_Time[6] = 1;    //周内越界判断	
}

减小时越下界校正

思路和越上界校正几乎一样,只有判断日期的时候,有特殊情况:当月份从大月调成小月的时候,如果这时候日期是31需要将其置为1,同理调到2月也需要这个操作。

说白了就是越下界判断在日期这还要加上越上界判断和校正

/**
  * @brief 越下界判断和校正
  * @param  无
  * @retval 无
  */
void BoundedCheckDown(void)   //越下界判断函数
{
	//越下界校正
	if(DS1302_Time[0]<0)DS1302_Time[0] = 99;		//年份越界判断
	if(DS1302_Time[1]<1)DS1302_Time[1] = 12;		//月份越界判断
	//天数越界判断	
	if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
	{
		if(DS1302_Time[2]<1)DS1302_Time[2] = 30;	 //小月
		if(DS1302_Time[2]>30)DS1302_Time[2] = 1;	 //小月
	}
	else if(DS1302_Time[1]==2)
	{
		if(DS1302_Time[0]%4 == 0)       //4年一闰2月29天,时钟寿命仅100年不需要考虑400年一闰
		{
			if(DS1302_Time[2]<1)DS1302_Time[2] = 29;
			if(DS1302_Time[2]>29)DS1302_Time[2] = 1;			
		}
		else
		{
			if(DS1302_Time[2]<1)DS1302_Time[2] = 28;  //平年2月28天
			if(DS1302_Time[2]>28)DS1302_Time[2] = 1;  //平年2月28天
		}		
	}		
	else 
	{
		if(DS1302_Time[2]<1)DS1302_Time[2] = 31;  //大月
		if(DS1302_Time[2]>31)DS1302_Time[2] = 1;  //大月
	}	
					
	if(DS1302_Time[3]<0)DS1302_Time[3] = 23;   //小时越界判断	
	if(DS1302_Time[4]<0)DS1302_Time[4] = 59;   //分越界判断
	if(DS1302_Time[5]<0)DS1302_Time[5] = 59;   //秒越界判断	
	if(DS1302_Time[6]<1)DS1302_Time[6] = 7;    //周内越界判断
}

 5、LCD显示修改后的时间

在TimeSet()函数里修改时间后就马上显示

没加闪烁效果之前可直接调用之前的显示年月日时分秒的函数ShowTime(void)和显示星期几的函数TranserShowStr_Day(char DayNum),加入了闪烁效果就需要调用具有闪烁功能的显示函数了

ShowTime(); 			//显示当前修改后的时间
TranserShowStr_Day(DS1302_Time[6]);    //显示字符缩写是星期几

6、当前索引位闪烁效果 

需要定义一个标志位TimeSetFlag,当它为0的时候正常显示当前索引位,当它为1时,当前索引位的位置显示相同长度的空字符串

具有闪烁效果的显示时间(年月日时分秒)函数ShowSetTime(void)

当同时满足时间位被选中和 标志位为1的时候,当前索引位的位置显示相同长度的空字符串,否则正常显示数据

/**
  * @brief 设置时间模式下显示当前的时间包括:年月日时分秒,设置位闪烁
  * @param  无
  * @retval 无
  */
void ShowSetTime(void)   //显示年月日时分秒
{
	//特殊符号显示
	LCD_ShowChar(1,3,'-');
	LCD_ShowChar(1,6,'-');
	LCD_ShowChar(2,3,':');
	LCD_ShowChar(2,6,':');	
	
	if(TimeSet_Sel == 0 &&  TimeSetFlag ==1)
		LCD_ShowString(1,1,"  ");
	else
		LCD_ShowNum(1,1,DS1302_Time[0],2);  //显示年
	
	if(TimeSet_Sel == 1 &&  TimeSetFlag ==1)
		LCD_ShowString(1,4,"  ");
	else
		LCD_ShowNum(1,4,DS1302_Time[1],2);  //显示月
	
	if(TimeSet_Sel == 2 &&  TimeSetFlag ==1)
		LCD_ShowString(1,7,"  ");
	else
		LCD_ShowNum(1,7,DS1302_Time[2],2);	//显示日	

	if(TimeSet_Sel == 3 &&  TimeSetFlag ==1)
		LCD_ShowString(2,1,"  ");
	else
		LCD_ShowNum(2,1,DS1302_Time[3],2);	//显示时	

	if(TimeSet_Sel == 4 &&  TimeSetFlag ==1)
		LCD_ShowString(2,4,"  ");
	else
		LCD_ShowNum(2,4,DS1302_Time[4],2);	//显示分					

	if(TimeSet_Sel == 5 &&  TimeSetFlag ==1)
		LCD_ShowString(2,7,"  ");
	else
		LCD_ShowNum(2,7,DS1302_Time[5],2);	//显示秒	
}

利用定时器中断系统使0.5秒标志位取非,从而实现被选中时间位闪烁

1ms的定时器初始化函数

#include <REGX52.H>

/**
  * @brief 定时器初始化,计时时长1MS
  * @param  无
  * @retval 无
  */
  
void Timer0Init()
{
	TMOD = TMOD & 0XF0;    //低四位置零,高四位不变
	TMOD = TMOD | 0X01;    //最低位置1,高四位不变
	TR0 = 1;
	TF0 = 0;
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	ET0 = 1;
	EA = 1;
	PT0 = 0;
}

中断函数

每次进入中断函数计数+1,到达500给标志位取非,即0.5s闪烁

void Timer0_Routine()   interrupt 1
{	static unsigned int T0Count;
	T0Count++;
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	if(T0Count == 500)
	{
		T0Count = 0;    
		TimeSetFlag = !TimeSetFlag;   //0时熄灭、1时闪烁
	}	
}

对于设置星期几的闪烁

只需在TimeSet调用TranserShowStr_Day(char DayNum)时完成和前面年月日时分秒相同的操作

	if(TimeSet_Sel == 6 &&  TimeSetFlag ==1)  //选中时0.5s闪烁,未选中正常显示
		LCD_ShowString(1,12,"     ");
	else
		TranserShowStr_Day(DS1302_Time[6]);    //显示字符缩写是星期几

7、将修改后的时间数据写入DS1302

只需要在从设置时间模式切换回显示时间模式的时候调用设置时间函数

整个程序大多使用的是全局变量,在修改数据时很方便,避免了各种函数之间的参数传递很繁琐

		if(KeyNum == 1)
		{
			Mode++;
			if(Mode>1)      //使按下按键1切换工作模式
			{
				Mode = 0;   //切换回显示时间模式
				DS1302_SetTime();  //将已修改的时间数据写入芯片
			}
			if(Mode == 1)TimeSet_Sel == 0;    //每次切换到设置时间模式索引清0,固定从“年”开始闪烁
		}

完整的主程序

#include <REGX52.H>
#include"DS1302.h"
#include"LCD1602.h"
#include"Delay.h"
#include"Key.h"
#include"Timer0Init.h"


//存放周一到周日的显示字符
unsigned char Day[][6] = {"Mon. ", "Tues.", "Wed. ", "Thur.", "Fri. ", "Sat. ", "Sun. "};
//定义键码、设置时间位索引、设置时间闪烁标志、模式(显示时间/设置时间)
unsigned char KeyNum, TimeSet_Sel, TimeSetFlag, Mode;  

/**
  * @brief 将周内对应数字转化为英文字符串缩写显示
  * @param  无
  * @retval 无
  */
void TranserShowStr_Day(char DayNum)
{
	switch(DayNum)    //将数字转化为对应字符串显示
	{
		case 1: LCD_ShowString(1,12,Day[0]);break;
		case 2: LCD_ShowString(1,12,Day[1]);break;
		case 3: LCD_ShowString(1,12,Day[2]);break;
		case 4: LCD_ShowString(1,12,Day[3]);break;
		case 5: LCD_ShowString(1,12,Day[4]);break;
		case 6: LCD_ShowString(1,12,Day[5]);break;
		case 7: LCD_ShowString(1,12,Day[6]);break;	
	}	
}


/**
  * @brief 显示当前的时间包括:年月日时分秒
  * @param  无
  * @retval 无
  */
void ShowTime(void)   //显示年月日时分秒
{
	LCD_ShowNum(1,1,DS1302_Time[0],2);  //显示年
	LCD_ShowChar(1,3,'-');
	LCD_ShowNum(1,4,DS1302_Time[1],2);  //显示月
	LCD_ShowChar(1,6,'-');
	LCD_ShowNum(1,7,DS1302_Time[2],2);	//显示日		
	LCD_ShowNum(2,1,DS1302_Time[3],2);	//显示时
	LCD_ShowChar(2,3,':');				
	LCD_ShowNum(2,4,DS1302_Time[4],2);	//显示分
	LCD_ShowChar(2,6,':');			
	LCD_ShowNum(2,7,DS1302_Time[5],2);	//显示秒
}

/**
  * @brief 设置时间模式下显示当前的时间包括:年月日时分秒,设置位闪烁
  * @param  无
  * @retval 无
  */
void ShowSetTime(void)   //显示年月日时分秒
{
	//特殊符号显示
	LCD_ShowChar(1,3,'-');
	LCD_ShowChar(1,6,'-');
	LCD_ShowChar(2,3,':');
	LCD_ShowChar(2,6,':');	
	
	if(TimeSet_Sel == 0 &&  TimeSetFlag ==1)
		LCD_ShowString(1,1,"  ");
	else
		LCD_ShowNum(1,1,DS1302_Time[0],2);  //显示年
	
	if(TimeSet_Sel == 1 &&  TimeSetFlag ==1)
		LCD_ShowString(1,4,"  ");
	else
		LCD_ShowNum(1,4,DS1302_Time[1],2);  //显示月
	
	if(TimeSet_Sel == 2 &&  TimeSetFlag ==1)
		LCD_ShowString(1,7,"  ");
	else
		LCD_ShowNum(1,7,DS1302_Time[2],2);	//显示日	

	if(TimeSet_Sel == 3 &&  TimeSetFlag ==1)
		LCD_ShowString(2,1,"  ");
	else
		LCD_ShowNum(2,1,DS1302_Time[3],2);	//显示时	

	if(TimeSet_Sel == 4 &&  TimeSetFlag ==1)
		LCD_ShowString(2,4,"  ");
	else
		LCD_ShowNum(2,4,DS1302_Time[4],2);	//显示分					

	if(TimeSet_Sel == 5 &&  TimeSetFlag ==1)
		LCD_ShowString(2,7,"  ");
	else
		LCD_ShowNum(2,7,DS1302_Time[5],2);	//显示秒	
}

/**
  * @brief 越上界判断和校正
  * @param  无
  * @retval 无
  */
void BoundedCheckUp(void)   //越上界判断函数
{
	//越上界校正
	if(DS1302_Time[0]>99)DS1302_Time[0] = 0;		//年份越界判断
	if(DS1302_Time[1]>12)DS1302_Time[1] = 1;		//月份越界判断
	//天数越界判断	
	if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
	{
		if(DS1302_Time[2]>30)DS1302_Time[2] = 1;	 //小月
	}
	else if(DS1302_Time[1]==2)
	{
		if(DS1302_Time[0]%4 == 0)       //4年一闰2月29天,时钟寿命仅100年不需要考虑400年一闰
		{
			if(DS1302_Time[2]>29)DS1302_Time[2] = 1;
		}
		else
		{
			if(DS1302_Time[2]>28)DS1302_Time[2] = 1;  //平年2月28天
		}		
	}		
	else 
	{
		if(DS1302_Time[2]>31)DS1302_Time[2] = 1;  //大月
	}	
					
	if(DS1302_Time[3]>23)DS1302_Time[3] = 0;   //小时越界判断	
	if(DS1302_Time[4]>59)DS1302_Time[4] = 0;   //分越界判断
	if(DS1302_Time[5]>59)DS1302_Time[5] = 0;   //秒越界判断	
	if(DS1302_Time[6]>7)DS1302_Time[6] = 1;    //周内越界判断	
}

/**
  * @brief 越下界判断和校正
  * @param  无
  * @retval 无
  */
void BoundedCheckDown(void)   //越下界判断函数
{
	//越下界校正
	if(DS1302_Time[0]<0)DS1302_Time[0] = 99;		//年份越界判断
	if(DS1302_Time[1]<1)DS1302_Time[1] = 12;		//月份越界判断
	//天数越界判断	
	if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
	{
		if(DS1302_Time[2]<1)DS1302_Time[2] = 30;	 //小月
		if(DS1302_Time[2]>30)DS1302_Time[2] = 1;	 //小月
	}
	else if(DS1302_Time[1]==2)
	{
		if(DS1302_Time[0]%4 == 0)       //4年一闰2月29天,时钟寿命仅100年不需要考虑400年一闰
		{
			if(DS1302_Time[2]<1)DS1302_Time[2] = 29;
			if(DS1302_Time[2]>29)DS1302_Time[2] = 1;			
		}
		else
		{
			if(DS1302_Time[2]<1)DS1302_Time[2] = 28;  //平年2月28天
			if(DS1302_Time[2]>28)DS1302_Time[2] = 1;  //平年2月28天
		}		
	}		
	else 
	{
		if(DS1302_Time[2]<1)DS1302_Time[2] = 31;  //大月
		if(DS1302_Time[2]>31)DS1302_Time[2] = 1;  //大月
	}	
					
	if(DS1302_Time[3]<0)DS1302_Time[3] = 23;   //小时越界判断	
	if(DS1302_Time[4]<0)DS1302_Time[4] = 59;   //分越界判断
	if(DS1302_Time[5]<0)DS1302_Time[5] = 59;   //秒越界判断	
	if(DS1302_Time[6]<1)DS1302_Time[6] = 7;    //周内越界判断
}


/**
  * @brief 模式1,修改设置当前时间
  * @param  无
  * @retval 无
  */
void TimeSet(void)          //设置时间函数
{
	if(KeyNum == 2)         //2号按键选择设置的时间位
	{
		TimeSet_Sel++;
		TimeSet_Sel %= 7;	//索引在0~6
	}
	if(KeyNum == 3)         //3号按键使当前时间位+1
	{
		DS1302_Time[TimeSet_Sel]++;
		BoundedCheckUp();   //越上界判断和校正
	}
	if(KeyNum == 4)			//4号键使当前时间位-1
	{
		DS1302_Time[TimeSet_Sel]--;
		BoundedCheckDown();	//越下界判断和校正
	}	
	ShowSetTime(); 			//显示当前修改后的时间	
	if(TimeSet_Sel == 6 &&  TimeSetFlag ==1)  //选中时0.5s闪烁,未选中正常显示
		LCD_ShowString(1,12,"     ");
	else
		TranserShowStr_Day(DS1302_Time[6]);    //显示字符缩写是星期几
}


void main()
{
    //初始化
	LCD_Init();      
	DS1302_Init();	
	DS1302_SetTime();	 //给DS1302写入时间
	Timer0Init();		 //定时器初始化
	while(1)
	{
		KeyNum = Key();   //接收键码
		if(KeyNum == 1)
		{
			Mode++;
			if(Mode>1)      //使按下按键1切换工作模式
			{
				Mode = 0;   //切换回显示时间模式
				DS1302_SetTime();  //将已修改的时间数据写入芯片
			}
			if(Mode == 1)TimeSet_Sel == 0;    //每次切换到设置时间模式索引清0,固定从“年”开始闪烁
		}
		switch(Mode)
		{
			case 0: LCD_ShowString(2,13,"Show");break;
			case 1: LCD_ShowString(2,13,"Set ");break;
		}
		if(!Mode)    //模式0,显示时间
		{
			DS1302_ReadTime();
			TranserShowStr_Day(DS1302_Time[6]);  //将数字转化为对应字符串显示
			ShowTime();                 //显示年月日时分秒
		}
		else          //模式1,设置时间
			TimeSet();
	} 
}


void Timer0_Routine()   interrupt 1
{	static unsigned int T0Count;
	T0Count++;
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	if(T0Count == 500)
	{
		T0Count = 0;    
		TimeSetFlag = !TimeSetFlag;   //0时熄灭、1时闪烁
	}	
}
	

四、遇到的问题

1、设置时间函数最后的开启写保护那行代码要去掉,不然在模式1里改了的数据不能被写进DS1302

2、刚开始闪烁很粗糙,借鉴了高手的写法

3、设置模式下只是改变了保存时间数组里的数据,并没有写入到DS1302里,所以刚开始设置完返回显示模式也不会有变化

五、可以继续优化的地方

  • 增加设置闹钟功能,调用蜂鸣器
  • 大部分代码写的简单粗暴,代码量很大,部分地方可以考虑优化精简一下
  • 用的是普中51开发板,后续可以考虑用系统板和用到的外设连接做一个拿得出手的小巧时钟

六、作品演示

基于51单片机的可调时钟功能演示

准确延时,数码管显示!用于电子时钟 #include <reg51.h> //*** 函数定义 *** void long_delay(void); // 长延时 void short_delay(void); // 短延时 void delay10ms(unsigned char); // 延时10MS void write7279(unsigned char, unsigned char);// 写入到HD7279 unsigned char read7279(unsigned char);//从HD7279读入 void send_byte(unsigned char); // 发送一个字节 unsigned char receive_byte(void); //接收一个字节 void init_timer(); /*定时器T0初始化*/ void conv(); /*时、分、秒单元及走时单元转换*/ void dirve(); /*时间显示程序*/ void time_adj(); /*时间调整设置*/ unsigned char digit[6]; unsigned char j; unsigned int tmr; unsigned long wait_cnter; unsigned char hour=0,min=0,sec=0; /*时、分、秒单元清零*/ unsigned char deda=0; /*5mS计数单元清零*/ bit sign; //设置标志位 sbit cs=P1^5; // cs at P1.5 sbit clk=P1^4; // clk 连接于 P1.4 sbit dat=P1^3; // dat 连接于 P1.3 sbit set=P3^7; // key 连接于 P3.7 //****** HD7279A 指令 ****** #define CMD_RESET 0xa4 #define CMD_TEST 0xbf #define DECODE0 0x80 #define DECODE1 0xc8 #define CMD_READ 0x15 #define UNDECODE 0x90 #define RTL_CYCLE 0xa3 #define RTR_CYCLE 0xa2 #define RTL_UNCYL 0xa1 #define RTR_UNCYL 0xa0 #define ACTCTL 0x98 #define SEGON 0xe0 #define SEGOFF 0xc0 #define BLINKCTL 0x88 //*** 主函数 *** main() { for (tmr=0;tmr<0x2000;tmr++); // 上电延时 send_byte(CMD_RESET); // 复位HD7279A //****************************************** // 测试指令演示 //****************************************** send_byte(CMD_TEST); // 指令测试 for (j=0;j<3;j++) // 延时哟3秒? { delay10ms(100); } send_byte(CMD_RESET); // 清除显示 //********************************************** //时间显示 //********************************************** init_timer();/*定时器T0初始化*/ while(1) { if(set==0) time_adj(); conv(); /*时、分、秒单元及走时单元转换*/ dirve(); /*时、分、秒单元显示*/ } } /*定时器T0 5mS初始化*/ void init_timer() { TMOD=0x01; TH0=-(4800/256); TL0=-(4800%256); IE=0x82; TR0=1; } /*5mS定时中断服务子函数*/ void zd(void) interrupt 1 { TH0=-(4800/256); TL0=-(4800%256); deda++; } /*时、分、秒单元及走时单元转换*/ void conv() { if(deda>=200){sec++;deda=0;} if(sec==60){min++;sec=0;} if(min==60){hour++;min=0;} if(hour==24){hour=0;} } void dirve() { digit[0]=sec%10; //计数个位 write7279(DECODE0,digit[0]); //显示个位 digit[1]=0x80|(sec/10); //计数十位 write7279(DECODE0+1,digit[1]); //显示十位 digit[2]=0x80|(min%10); //计数百位 write7279(DECODE0+2,digit[2]); //显示百位 digit[3]=0x80|(min/10); //计数千位 write7279(DECODE0+3,digit[3]); //显示千位 digit[4]=0x80|(hour%10); //计数万位 write7279(DECODE0+4,digit[4]); //显示万位 digit[5]=hour/10; //计数十万位 write7279(DECODE0+5,digit[5]); } void write7279(unsigned char cmd, unsigned char dta) { send_byte (cmd); send_byte (dta); } unsigned char read7279(unsigned char command) { send_byte(command); return(receive_byte()); } void send_byte( unsigned char out_byte) { unsigned char i; cs=0; long_delay(); for (i=0;i<8;i++) { if (out_byte&0x80) { dat=1; } else { dat=0; } clk=1; short_delay(); clk=0; short_delay(); out_byte=out_byte*2; } dat=0; } unsigned char receive_byte(void) { unsigned char i, in_byte; dat=1; // 设置为输入状态 long_delay(); for (i=0;i<8;i++) { clk=1; short_delay(); in_byte=in_byte*2; if (dat) { in_byte=in_byte|0x01; } clk=0; short_delay(); } dat=0; return (in_byte); } void long_delay(void) { unsigned char i; for (i=0;i<0x30;i++); } void short_delay(void) { unsigned char i; for (i=0;i<8;i++); } // ********************* 延时 n*10ms ********************** void delay10ms(unsigned char time) { unsigned char i; unsigned int j; for (i=0;i<time;i++) { for(j=0;j<0x390;j++) { } } } /*时间调整程序*/ void time_adj() { if(set==0) //有键按下,判断按键时间 { delay10ms(200); //1s延时程序 if(set==0) //大于1s,进入时间设置 { ET0=0; TR0=0; //关定时器0 while(set==0) dirve(); //等键释放 // ET1=1;TR1=1; con=0xF3; //开定时器1,让调整位闪烁 write7279(BLINKCTL,0xF3); do { while(set!=0) dirve(); //等待按键 delay10ms(100); if(set!=0) //小于0.5s,进入分钟设置 { while(set==0) dirve(); //等待按键释放 sign=1; min++; if(min==60)min=0; //分钟加1 dirve(); } else sign=0; //大于0.5s,进入小时设置 }while(sign); while(set==0)dirve(); //等待按键释放 // con=0xCF; write7279(BLINKCTL,0xCF); do { while(set!=0) dirve(); //等待按键 delay10ms(100); if(set!=0) //小于0.5s,进入小时设置 { while(set==0) dirve(); //等待按键释放 sign=1; hour++; //小时加1 if(hour==24)hour=0; dirve(); } else sign=0; //大于0.5s,结束时间设置 }while(sign); // ET1=0;TR1=0; //关定时器1 ET0=1;TR0=1; //开定时器0 write7279(BLINKCTL,0xFF); } else //小于1s,进入省电模式 { ET0=1; TR0=1; //开定时器0 while(set!=0); //等待按键 do { dirve(); //有键按下,调显示程序消抖 }while(set!=0); //是干扰则继续循环 } while(set==0) dirve(); //等待按键释放 } else ; //无键按下,跳出按键扫描程序 }
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值