基于51单片机和LCD1602实现的电子时钟

该博客围绕51单片机电子时钟展开,介绍其显示当前日期和时间、秒表、定时器和闹钟等功能,阐述各模块实现思路与代码,如用AT24C02断电保存时间、DS1302计算保存时间等,还提及实现中遇到的问题,最后表示此项目有助于深入理解相关器件。

一、功能

本电子时钟的功能是显示当前日期和时间、秒表、定时器和闹钟。以下是展示图。(在typora上写的时候图片是正的,贴上来之后就翻过来了,不改了)

  1. 当前日期和时间
    在这里插入图片描述

  2. 秒表
    在这里插入图片描述

  3. 定时器
    在这里插入图片描述

  4. 闹钟
    在这里插入图片描述

二、各模块实现思路以及代码实现

1. 当前日期和时间

实现思路是:用数组保存日期数据,将其写入到AT24C02中,以实现对时间的断电保存,因为AT24C02本质上就是一个EEPROM,所以可以实现断电保存。使用DS1302对时间数据进行计算和保存。第一次访问日期数据时,DS1302要先从AT24C02中读出先前保存好的数据,然后保存在时间寄存器中,再将时间数据用LCD1602显示出来。之后每次访问时间数据时,只需要从DS1302时间寄存器中读出即可。
AT24C02的用法是:先写好基于I2C协议传输一个位和一个字节的功能,写好对应的时序即可。再基于该功能写AT24C02传送一个帧的功能,注意好其帧的格式即可。(必须要严格检查好自己写的时序是否符合图中所给,很多问题就是因为时序不对。比如数据错乱,无数据显示等问题。)
在这里插入图片描述

DS1302的用法是在其时序规则内完成读写操作即可,表3给出的寄存器地址/定义就是DS1302单字节读/写指令中的前八位,注意读写指令是不同的。主机读数据时,每到一个下降沿,从机就会将一个数据放到IO口,此时主机在整个低电平期间都可以在该IO口中取得数据,再给一个上升沿,等待下一个数据到来。对DS1302的时钟寄存器进行写入前,要先关闭其写保护,即WP地址后要带全零。(读写都不难,但一定要注意时序)
在这里插入图片描述

AT24C02相关代码实现

#include <REGX52.H>
#include "LCD1602.H"
#include "Calendar.h"
#include "Delay.H"

// SCL和SDA是AT24C02的接口,为方便使用,先为其命名
sbit SCL = P2^1;
sbit SDA = P2^0;

// 在51开发板上AT24C02的读写地址,固定好的。
unsigned char AT24C02_WAddress = 0xA0;
unsigned char AT24C02_RAddress = 0xA1;

/**
  * @brief I2C协议初始化,调用以下任何函数之前都必须初始化I2C
  */
void I2C_Init()
{
	SDA = 1;
	SCL = 1;
}

/**
  * @brief I2C时序开始信号
  */
void I2C_Start()
{
	SDA = 1;
	SCL = 1;
	SDA = 0;
	SCL = 0;
}

/**
  * @brief I2C时序停止信号
  */
void I2C_Stop()
{
	SCL = 0;
	SDA = 0;
	SCL = 1;
	SDA = 1;
}

/**
  * @brief 主机向从机发送一个字节的数据
  * @param  char型数据
  */
void I2C_SendByte(unsigned char Byte)
{
	unsigned char i = 0;
	SCL = 0;
	for(i = 0; i < 8; i++)
	{
        // I2C发送数据时先发高位,而DS1302发送数据时先发低位。
		SDA = Byte & (0x80 >> i);
		SCL = 1;
		SCL = 0;
	}
}

/**
  * @brief 主机接收从机发来的数据
  * @retval char型数据
  */
unsigned char I2C_ReceiveByte()
{
	unsigned char res = 0x00;
	unsigned char i = 0;
	SDA = 1;
	SCL = 0;
	for(i = 0; i < 8; i++)
	{
		SCL = 1;
		if(SDA) res |= (0x80 >> i);
		SCL = 0;
	}
	return res;
}
/**
  * @brief 主机向从机发送一个确认帧
  * @param  一位确认数据
  */
void I2C_SendAck(bit ack)
{
	SCL = 0;
	SDA = ack;
	SCL = 1;
	SCL = 0;
}
/**
  * @brief 主机接收从机发来的一位确认
  * @retval 一位确认数据
  */
bit I2C_ReceiveAck()
{
	bit ack = 0;
	SCL = 0;
	SCL = 1;
	ack = SDA;
	SCL = 0;
	return ack;
}

/*
--------------------------------------------------------------------------------


--------------以上与I2C的时序有关,是AT24C02数据帧的组成成分--------------


---------------------------------------------------------------------------------
*/

/**
  * @brief 向EEPROM的地址Address中写入一个字节的数据Data
  * @param 两个无符号字符型数据:要写入的地址和要写入的数据。
  * (因为EEPROM在开发板上的地址已经默认,所以不需要特意作为参数)
  */
void AT24C02_Write(unsigned char Address, Data)
{
	I2C_Init();
	I2C_Start();
	I2C_SendByte(AT24C02_WAddress);
	I2C_ReceiveAck();
	I2C_SendByte(Address);
	I2C_ReceiveAck();
	I2C_SendByte(Data);
	I2C_ReceiveAck();
	I2C_Stop();
}
/**
  * @brief 读出在指定地址的数据
  * @param 八位无符号字符型地址数据
  * @retval 八位无符号字符型数据
  */
unsigned char AT24C02_Read(unsigned char Address)
{
	unsigned char res = 0x00;
	I2C_Init();
	I2C_Start();
	I2C_SendByte(AT24C02_WAddress);
	I2C_ReceiveAck();
	I2C_SendByte(Address);
	I2C_ReceiveAck();
	I2C_Start();
	I2C_SendByte(AT24C02_RAddress);
	I2C_ReceiveAck();
	res = I2C_ReceiveByte();
	I2C_SendAck(1);
	I2C_Stop();
	P2 = test;
	return res;
}
/**
  * @brief 将时间数据保存在EEPROM上
  * @param 八位无符号字符型数组
  */
void AT24C02_SetTime(unsigned char *Data)
{
	unsigned char i;
	for(i = 0; i < 7; i++)
	{
        // DS1302要从AT24C02中直接获取数据,而1302中的时间数据是以BCD码的格式保存,所以在保存在			24C02的时候就直接将十进制数据转换成BCD码
		Data[i] = BCD_DEC(0, Data[i]);
	}
	for(i = 0; i < 7; i++)
	{
        // 每次写入一定要有间隔,否则会导致写入数据发生错误。
		AT24C02_Write(i, Data[i]);
		Delay10ms(1);
	}
}
DS1302相关代码实现
#include <REGX52.H>
#include "LCD1602.H"
#include "Delay.h"
#include "AT24C02.H"
#include "Timer0.H"
#include "MOREFUNCTION.H"

// 与DS1302时序息息相关的几个端口
sbit CE = P3^5;
sbit IO = P3^4;
sbit SCLK = P3^6;

// 定义SEC写命令字,读命令就在该基础上加1。其他写命令字就在其基础上加2
#define SEC 0x80
#define WP 0x8e

// 用以显示时间数据的数组,数据所代表的含义按顺序依次是:秒、分、时、日、月、星期、年

unsigned char Calendar_Data[7];
// 第一次访问DS1302时要先从AT24C02中读取数据,该标志用于判断是否第一次访问DS1302
unsigned char AT_getTag = 0;

/**
  	* @brief BCD码和十进制的转换。DS1302中的时间数据以BCD码的形式来保存
    * @param 一位标志,1为BCD-》十进制,0为十进制-》BCD;一位无符号字符型数据
    * @retval 返回转换后的数据
    */
unsigned char BCD_DEC(bit tag, unsigned char num)
{
	// tag为1,BCD转为十进制
	if(tag) return num / 16 * 10 + num % 16;
	else return num / 10 * 16 + num % 10;
}
/**
  	* @brief 向DS1302的Command时间寄存器存入数据Data
    * @param 两个无符号字符型,一个是访问的寄存器地址,一个是要存入的数据
    */
void DS1302_WriteByte(unsigned char Command, Data)
{
	unsigned char i;
	CE = 0;
	SCLK = 0;
	CE = 1;
	
	for(i = 0; i < 8; i++)
	{
        // 先前已经提到过,DS1302以低位优先
        // 按照时序先输入命令,再输入数据
		IO = Command & (0x01 << i);
		SCLK = 1;
		SCLK = 0;
	}
	for(i = 0; i < 8; i++)
	{
		IO = Data & (0x01 << i);
		SCLK = 1;
		SCLK = 0;
	}
	CE = 0;
	IO = 0;
}
/**
  	* @brief 从DS1302的Command时间寄存器读出数据
    * @param 一个无符号字符型,访问的寄存器地址
    * @retval 从Command寄存器中读出的数据
    */
unsigned char DS1302_ReadByte(unsigned char Command)
{
	unsigned char i;
	unsigned char res = 0x00;
	CE = 0;
	SCLK = 0;
	CE = 1;
	
	for(i = 0; i < 8; i++)
	{
		SCLK = 0;
		IO = Command & (0x01 << i);
		SCLK = 1;
	}

	for(i = 0; i < 8; i++)
	{
		SCLK = 0;
		if(IO) res |= (0x01) << i;
		SCLK = 1;
		
	}
	CE = 0;
	IO = 0;
	return res;
}

/**
  	* @brief 从AT24C02中取出数据存到时钟寄存器中
    */
void DS1302_SetTime()
{
	unsigned char i;
	unsigned char Data[7];
	DS1302_WriteByte(WP, 0);
	for(i = 0; i < 7; i++)
	{
		Data[i] = AT24C02_Read(i);
	}

	for(i = 0; i < 7; i++)
	{
		DS1302_WriteByte(SEC+2*i, Data[i]);
		Delay10ms(1);
	}
}
/**
  	* @brief 从DS1302的时钟寄存器中读取数据
    */
void DS1302_Read()
{
	unsigned char i;
	for(i = 0; i < 7; i++)
	{
		// 判断是否为第一次读取DS1302中的时间数据,第一次就要访问AT24C02,然后就不需要了
		if(AT_getTag == 0)
		{
			DS1302_SetTime();
			AT_getTag = 1;
		}
		Calendar_Data[i] = DS1302_ReadByte(SEC+1+(2 * i));
	}
	for(i = 0; i < 7; i++)
	{
		Calendar_Data[i] = BCD_DEC(1, Calendar_Data[i]);
	}
}
/**
  	* @brief 将从DS1302的时钟寄存器中读取到的数据展示为时钟的形式
    */
void DS1302_showTime()
{
	while(1)
	{
		DS1302_Read();
		LCD_ShowString(1,1,"  -  -    ");
		LCD_ShowString(2,1,"  :  :    ");
		LCD_ShowNum(1,1,Calendar_Data[6],2);
		LCD_ShowNum(1,4,Calendar_Data[4],2);
		LCD_ShowNum(1,7,Calendar_Data[3],2);
		LCD_ShowNum(2,1,Calendar_Data[2],2);
		LCD_ShowNum(2,4,Calendar_Data[1],2);
		LCD_ShowNum(2,7,Calendar_Data[0],2);
        // 以下为检测按键以实现扩展秒表,定时器,闹钟功能。
        // 检测到P31按下,进入修改时间模式。
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			MF_controlTime(Calendar_Data);
		}
        // 检测到P30按下,进入秒表功能
		if(P3_0 == 0)
		{
			Delay10ms(2);
			while(P3_0 == 0);
			Delay10ms(2);
			MS_Stopwatch();
		}
        // 检测到P32按下,进入定时器功能
		if(P3_2 == 0)
		{
			Delay10ms(2);
			while(P3_2 == 0);
			Delay10ms(2);
			MS_Timer();
		}
        // 检测到P33按下,进入闹钟功能
		if(P3_3 == 0)
		{
			Delay10ms(2);
			while(P3_3 == 0);
			Delay10ms(2);
			MS_Clock(Calendar_Data[2], Calendar_Data[1], Calendar_Data[0]);
		}
	}
	
}

3. 修改时间功能代码实现

按下P31后,年的位置会开始闪烁,表明要对年进行修改,闪烁效果需要用到定时器0;按下P30后,会跳到下一个位置;按下P32后,会对当前闪烁位置的时间数据加1;按下P33后,将修改后的时间存入AT24C02并显示到LCD1602中。

闪烁效果补充:经过验证,LCD1602是将现实的数据保存在其内部寄存器中,能够在不断电的情况下永久显示已经显示过的内容,除非将寄存器的值覆盖掉。因此让对应位置闪烁的方法如下:
使用Timer0定时改变标志位tag,tag等于0时,将时间数据显示出来;tag不等于0时,让要修改的位置显示一个空白字符串,其他位置会保存显示过的时间数据。这样以来,定时器定时修改tag标志,就能让对应位置进行闪烁。

// 实现闪烁
unsigned char tag = 0;

/**
  	* @brief 用于手动对时间信息进行修改
    * @param 一个无符号字符型指针,是DS1302相关代码中用来展示时间数据的数组。
    */
void MF_controlTime(unsigned char *Calendar_Data)
{
	// temp是闪烁位置对应数据在数组中的下标
	unsigned char temp = 6;
	// pos是LCD1602上闪烁的位置
	unsigned char pos = 1;
    // 对定时器0进行初始化
	Timer0_Init();
	while(1)
	{
		// 再次按下第一个按键时退出修改模式
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			return;
		}
		// 按下第二个按键时,要修改的下一位进行闪烁
		if(P3_0 == 0)
		{
			Delay10ms(1);
			while(P3_0 == 0);
			Delay10ms(1);
			pos = pos + 3;
            // 年和月在数组中的位置中间还有一个星期,所以如果当前位是年,修改月时就要往后跳两格
			if(temp == 6) temp -= 2;
			else temp -= 1;
		}
		// 按下第三个按键时,对要修改为进行加1操作
		if(P3_2 == 0)
		{
			Delay10ms(2);
			while(P3_2 == 0);
			Delay10ms(2);
			Calendar_Data[temp] += 1;
		}
		// 按下第四个按键时,保存修改后的值并退出修改模式
		if(P3_3 == 0)
		{
			Delay10ms(1);
			while(P3_3 == 0);
			Delay10ms(1);
			// 将新数据保存到DS1302的时钟寄存器和AT24C02中
			AT24C02_SetTime(Calendar_Data);
			DS1302_SetTime();
			return;
		}
		
		// tag实现闪烁,tag不等于0时显示时间数据
		if(tag)
		{
			LCD_ShowNum(1,1,Calendar_Data[6],2);
			LCD_ShowNum(1,4,Calendar_Data[4],2);
			LCD_ShowNum(1,7,Calendar_Data[3],2);
			LCD_ShowNum(2,1,Calendar_Data[2],2);
			LCD_ShowNum(2,4,Calendar_Data[1],2);
			LCD_ShowNum(2,7,Calendar_Data[0],2);
		}
		// tag等于0时让对应位不显示
		else
		{
			if(pos <= 7)
			{
				LCD_ShowString(1, pos, "  ");
			}
			
			else if(pos > 16)
			{
				pos = 1;
				temp = 6;
			}
			else 
				LCD_ShowString(2, pos-9, "  ");
		}
	}
	return;
}
// 定时器0的中断处理函数
void Timer0_Routine() interrupt 1
{
	static unsigned int counter;
    // 1ms进入中断一次
	TL0 = 64536;
	TH0 = 64536;
	counter++;
	if(counter == 1)
	{
		counter = 0;
		tag = ~tag;
	}
}

4. 秒表功能代码实现

用定时器1实现,LCD中最低位是1s进位一次,中间位是60s进位一次,最高位99min置零。

// 秒表功能的参数,sec表示低两位,min表示中间两位,hour表示最高两位
unsigned char sw_sec = 0;
unsigned char sw_min = 0;
unsigned char sw_hour = 0;
// 用于秒表的标志
bit sw_tag = 0;
/**
  	* @brief 实现秒表功能
    */
void MS_Stopwatch()
{
	LCD_Init();
	Timer1_Init();
	LCD_ShowString(1,1,"Stopwatch");
	LCD_ShowString(2,1,"  :  :");
	while(1)
	{
		LCD_ShowNum(2,7,sw_sec,2);
		LCD_ShowNum(2,4,sw_min,2);
		LCD_ShowNum(2,1,sw_hour,2);
		// 按下P30后,退出秒表功能。如果退出前还在计时,那么停止计时再退出。
		if(P3_0 == 0)
		{
			Delay10ms(1);
			while(P3_0 == 0);
			Delay10ms(1);
			Timer1_Run(0);
			if(sw_tag == 1)
				sw_tag = ~sw_tag;
			return;
		}
        // 按下P32后,开始或者停止计时。对应到计时器1的中断处理函数中就是sw_tag=1时进入秒表的中断处理,			否则不进入
		if(P3_2 == 0)
		{
			Delay1ms(1);
			while(P3_2 == 0);
			Delay1ms(1);
			sw_tag = ~sw_tag;
		}
        // 按下P33后,对当前秒表数据进行清零
		if(P3_3 == 0)
		{
			Delay1ms(1);
			while(P3_3 == 0);
			Delay1ms(1);
			sw_sec = 0;
			sw_min = 0;
			sw_hour = 0;
		}
	}
}
// 定时器1部分代码,只有当sw_tag等于1时才进入秒表中断处理。
void Timer1_Routine() interrupt 3
{
	static unsigned int counter = 0;
	// 1ms加1
	TL1 = 54686%256;
	TH1 = 54686/256;
	if(sw_tag)
	{
		counter++;
		if(counter == 1)
		{
			counter = 0;
			sw_sec++;
			if(sw_sec == 100)
			{
				sw_sec = 0;
				sw_min++;
				if(sw_min == 60)
				{
					sw_hour++;
					sw_min = 0;
					if(sw_hour == 100)
						sw_hour = 0;
				}
			}
		}
	}
}
5. 定时器功能代码实现

定时器的实现思路是:秒表功能的稍微转化,从正计时变成倒计时而已,但多了一个修改当前时间的过程。因此需要用到定时器0和定时器1。倒计时到点后会显示出Timer’s up,可以按下P31清除。
但不知道是不是定时器1的问题,当定时器处于计时状态时,按下P32退出定时器功能再回到定时器后,定时器1的机器周期会变慢,原本1s改变一次的数值变成了6s改变一次。但我没有检测出原因,也没找到对应的解决方法。

// 定时器功能的参数,定义同上
unsigned char timer_sec = 0;
unsigned char timer_min = 0;
unsigned char timer_hour = 0;
// 为了实现设定时间到后按下P31清除Timer'up标志并恢复一开始设定的时间这个功能,可以新开temp变量来重复保存一份一开始设定的值。
unsigned char temp_timer_sec = 0;
unsigned char temp_timer_min = 0;
unsigned char temp_timer_hour = 0;

// 用于定时器的标志
bit timer_tag = 0;

// z_tag表示倒计时时间到。
bit z_tag = 0;

/**
  	* @brief 定时器修改时间函数
    */
void MF_controlTimer()
{
	unsigned char pos = 1;
	Timer0_Init();
	while(1)
	{
		// 再次按下第一个按键时退出修改模式
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			return;
		}
		// 按下第二个按键时,跳到要修改的下一位
		if(P3_0 == 0)
		{
			Delay10ms(1);
			while(P3_0 == 0);
			Delay10ms(1);
			pos = pos + 3;
			if(pos == 10)
				pos = 1;
		}
		// 按下第三个按键时,对要修改位进行加1操作
		if(P3_2 == 0)
		{
			Delay10ms(2);
			while(P3_2 == 0);
			Delay10ms(2);
			if(pos == 1) 
			{
				timer_hour++;
				if(timer_hour == 100)
					timer_hour = 0;
				temp_timer_hour = timer_hour;
			}
			else if(pos == 4) 
			{
				timer_min++;
				if(timer_min == 60)
					timer_min = 0;
				temp_timer_min = timer_min;
			}
			else if(pos == 7) 
			{
				timer_sec++;
				if(timer_sec == 60)
					timer_sec = 0;
				temp_timer_sec = timer_sec;
			}
			
		}
		// 按下第四个按键时,开始计时
		if(P3_3 == 0)
		{
			Delay10ms(1);
			while(P3_3 == 0);
			Delay10ms(1);
			timer_tag = 1;
			z_tag = 0;
			return;
		}
		
		// tag实现闪烁,tag不等于0时显示时间数据
		if(tag)
		{
			LCD_ShowNum(2,1,timer_hour,2);
			LCD_ShowNum(2,4,timer_min,2);
			LCD_ShowNum(2,7,timer_sec,2);
		}
		// tag等于0时让对应位不显示
		else
		{
			LCD_ShowString(2, pos, "  ");
		}
	}
}
/**
  	* @brief 定时器函数,用以倒计时
    */
void MS_Timer()
{
	LCD_Init();
	Timer1_Init();
	LCD_ShowString(1,1,"           ");
	LCD_ShowString(1,1,"Timer");
	LCD_ShowString(2,1,"  :  :");
	LCD_ShowNum(2,7,timer_sec,2);
	LCD_ShowNum(2,4,timer_min,2);
	LCD_ShowNum(2,1,timer_hour,2);
	while(1)
	{
		// 按下P30后,将当前显示数据清零,但只能在定时器停止计时时可以清零数据。
		if(P3_0 == 0)
		{
			if(timer_tag == 0)
			{
				Delay10ms(1);
				while(P3_0 == 0);
				Delay10ms(1);
				timer_sec = 0;
				timer_min = 0;
				timer_hour = 0;
			}
		}
		
		// 没开始定时时是调整对应位置加1
		// 开始定时时是退出计时态但保持设置的数值
		// 只能用一次的定时器,退出计时态再回去的话定时器会变慢
		if(P3_2 == 0)
		{
			Delay10ms(2);
			while(P3_2 == 0);
			Delay10ms(2);
			if(timer_tag)
			{
				timer_tag = 0;
			}
			return;
		}
		// 未定时时进入修改模式
		// 定时时是time's up的清除并恢复开始时设定的时间
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			if(timer_tag == 0)
				MF_controlTimer();
			else
			{
				timer_tag = 0;
				z_tag = 0;
				LCD_ShowString(1,7,"             ");
				timer_sec = temp_timer_sec;
				timer_min = temp_timer_min;
				timer_hour = temp_timer_hour;
			}
		}
		// 开始定时后开始或停止计时
		// 未开始定时时无效
		if(P3_3 == 0)
		{
			Delay10ms(2);
			while(P3_3 == 0);
			Delay10ms(2);
			if(timer_sec != 0 || timer_min != 0 || timer_hour != 0)
				timer_tag = ~timer_tag;
		}
		LCD_ShowNum(2,7,timer_sec,2);
		LCD_ShowNum(2,4,timer_min,2);
		LCD_ShowNum(2,1,timer_hour,2);
		if(z_tag == 1)
		{
			LCD_ShowString(1,7,"Time's up!");
			Delay10ms(10);
		}
	}
}
// 定时器0的中断处理函数,用的是跟修改时间函数那个一样的。
void Timer0_Routine() interrupt 1
{
	static unsigned int counter;
	TL0 = 64536;
	TH0 = 64536;
	counter++;
	if(counter == 1)
	{
		counter = 0;
		tag = ~tag;
	}
}

// 定时器1部分代码,只有当timer_tag等于1时才进入秒表中断处理。
void Timer1_Routine() interrupt 3
{
	static unsigned int counter = 0;
	// 1ms加1
	TL1 = 54686%256;
	TH1 = 54686/256;
	if(timer_tag)
	{
		counter++;
		if(counter >= 1000)
		{
			counter = 0;
			if(timer_hour > 0)
			{
				if(timer_min > 0)
				{
					if(timer_sec > 0)
						timer_sec--;
					else
					{
						timer_min--;
						timer_sec = 59;
					}
				}
				else
				{
					timer_hour--;
					timer_min = 59;
					timer_sec = 59;
				}
			}
			else
			{
				if(timer_min > 0)
				{
					if(timer_sec > 0)
						timer_sec--;
					else
					{
						timer_min--;
						timer_sec = 59;
					}
				}
				else
				{
					if(timer_sec > 0)
					{
						timer_sec-=1;
                        // 当所有位置都减为0时,认为倒计时结束,将结束标志z_tag设置为1
						if(timer_sec == 0)
							z_tag = 1;
					}
				}
			}
		}
	}
}
6. 闹钟功能代码实现

闹钟功能实际上非常不完善,没有实现闹钟边计时时间也一起在走。比如:使用闹钟功能时,显示时间功能就没法用了,因为他俩共用一个定时器。
闹钟实现思路:以当前时间作为参数传入闹钟函数,用户再设定一个闹钟值,实际上就是计算从进入闹钟函数正计时到用户设定时间的这一段时间,其实还是正计时。

// 保存用户设定值
unsigned char clock_sec = 0;
unsigned char clock_min = 0;
unsigned char clock_hour = 0;
// 保存进入闹钟函数的值
unsigned char temp_clock_sec = 0;
unsigned char temp_clock_min = 0;
unsigned char temp_clock_hour = 0;

/**
  	* @brief 修改闹钟数据的函数
    */
void MS_conClock()
{
	unsigned char pos = 1;
	Timer0_Init();
	while(1)
	{
		// 再次按下第一个按键时退出修改模式
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			return;
		}
		// 按下第二个按键时,跳到要修改的下一位
		if(P3_0 == 0)
		{
			Delay10ms(1);
			while(P3_0 == 0);
			Delay10ms(1);
			pos = pos + 3;
			if(pos == 10)
				pos = 1;
		}
		// 按下第三个按键时,对要修改位进行加1操作
		if(P3_2 == 0)
		{
			Delay10ms(2);
			while(P3_2 == 0);
			Delay10ms(2);
			if(pos == 1) 
			{
				clock_hour++;
				if(clock_hour == 24)
					clock_hour = 0;
			}
			else if(pos == 4) 
			{
				clock_min++;
				if(clock_min == 60)
					clock_min = 0;
			}
			else if(pos == 7) 
			{
				clock_sec++;
				if(clock_sec == 60)
					clock_sec = 0;
			}
			
		}
		// 按下第四个按键时,设定闹钟
		if(P3_3 == 0)
		{
			Delay10ms(1);
			while(P3_3 == 0);
			Delay10ms(1);
            // clock_tag等于1时开始倒计时
			clock_tag = 1;
			return;
		}
		// tag实现闪烁,tag不等于0时显示时间数据
		if(tag)
		{
			LCD_ShowNum(2,1,clock_hour,2);
			LCD_ShowNum(2,4,clock_min,2);
			LCD_ShowNum(2,7,clock_sec,2);
		}
		// tag等于0时让对应位不显示
		else
		{
			LCD_ShowString(2, pos, "  ");
		}
	}
}

/**
  	* @brief 闹钟函数
    * @param 三个无符号字符型,分别是当前时间的时分秒
    */
void MS_Clock(unsigned char hour, min, sec)
{
	temp_clock_sec = sec;
	temp_clock_min = min;
	temp_clock_hour = hour;
	LCD_Init();
	Timer1_Init();
	LCD_ShowString(1,1,"           ");
	LCD_ShowString(1,1,"Clock");
	LCD_ShowString(2,1,"  :  :");
    // 进入闹钟功能时先显示全零,等待用户设置
	LCD_ShowNum(2,7,clock_sec,2);
	LCD_ShowNum(2,4,clock_sec,2);
	LCD_ShowNum(2,1,clock_sec,2);
	while(1)
	{
		// 不处于计时状态时将当前闹钟数值清零
		if(P3_0 == 0)
		{
			if(clock_tag == 0)
			{
				Delay10ms(1);
				while(P3_0 == 0);
				Delay10ms(1);
				clock_sec = 0;
				clock_min = 0;
				clock_hour = 0;
			}
		}
	
		// 进入修改模式
		if(P3_1 == 0)
		{
			Delay10ms(2);
			while(P3_1 == 0);
			Delay10ms(2);
			MS_conClock();
		}
		LCD_ShowNum(2,7,clock_sec,2);
		LCD_ShowNum(2,4,clock_min,2);
		LCD_ShowNum(2,1,clock_hour,2);
		if(time_s_up == 1)
		{
			LCD_ShowString(1,7,"Time's up!");
			Delay10ms(10);
		}
	}
}
void Timer0_Routine() interrupt 1
{
	static unsigned int counter;
	TL0 = 64536;
	TH0 = 64536;
	counter++;
	if(counter == 1)
	{
		counter = 0;
		tag = ~tag;
	}
}

void Timer1_Routine() interrupt 3
{
	static unsigned int counter = 0;
	// 1ms加1
	TL1 = 54686%256;
	TH1 = 54686/256;
	if(clock_tag)
	{
		counter++;
		if(counter == 100)
		{
			counter = 0;
			temp_clock_sec++;
            // 进入闹钟函数的时间正计时到达用户设定时间时将闹钟计时停止,将到时标志time_s_up设为1.
            // 因为在每个数据改变后都有可能到达设定时间,所以改变数据后都需要检查是否到时。
			if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
			{
				clock_tag = 0;
				time_s_up = 1;
			}
			if(temp_clock_sec == 60)
			{
				temp_clock_sec = 0;
				temp_clock_min++;
				if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
				{
					clock_tag = 0;
					time_s_up = 1;
				}

				if(temp_clock_min == 60)
				{
					temp_clock_hour++;
					temp_clock_min = 0;
					if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
					{
						clock_tag = 0;
						time_s_up = 1;
					}

					if(temp_clock_hour == 100)
						temp_clock_hour = 0;
					if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
					{
						clock_tag = 0;
						time_s_up = 1;
					}

				}
			}
		}
	}
}

三、总结

这个电子时钟做起来难度不是很大,但是能让我对定时器、中断、LCD1602、AT24C02和DS1302的理解和使用更深一层,因此还是很有益处的,接下来就是做蓝牙小车了,希望能学到更多东西,然后进入STM32的学习。

  // 因为在每个数据改变后都有可能到达设定时间,所以改变数据后都需要检查是否到时。
			if(temp_clock_sec == clock_sec &&  temp_clock_min == clock_min && temp_clock_hour == clock_hour)
			{
				clock_tag = 0;
				time_s_up = 1;
			}
			if(temp_clock_sec == 60)
			{
				temp_clock_sec = 0;
				temp_clock_min++;
				if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
				{
					clock_tag = 0;
					time_s_up = 1;
				}

				if(temp_clock_min == 60)
				{
					temp_clock_hour++;
					temp_clock_min = 0;
					if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
					{
						clock_tag = 0;
						time_s_up = 1;
					}

					if(temp_clock_hour == 100)
						temp_clock_hour = 0;
					if(temp_clock_sec == clock_sec && temp_clock_min == clock_min && temp_clock_hour == clock_hour)
					{
						clock_tag = 0;
						time_s_up = 1;
					}

				}
			}
		}
	}

三、总结

这个电子时钟做起来难度不是很大,但是能让我对定时器、中断、LCD1602、AT24C02和DS1302的理解和使用更深一层,因此还是很有益处的,接下来就是做蓝牙小车了,希望能学到更多东西,然后进入STM32的学习。

<think>嗯,用户这次的问题聚焦在51单片机数字时钟的复位功能实现上。结合之前的引用内容,用户应该是个电子爱好者或相关专业学生,正在做一个带LCD1602显示的时钟项目。ta已经掌握了基本的时钟显示时间调整功能,现在需要解决系统复位的问题——这通常是项目调试阶段的实际需求。 用户提到的三个引用很有价值:引用[1]提供了整个系统的框架设计,引用[2]详细说明了DS1302芯片的操作方法,而引用[3]则展示了定时器中断配置的关键代码。特别要注意引用[3]里那个定时器初始化函数,这正是复位功能需要配合的关键部分。 复位功能需要考虑硬件软件两个层面。硬件上最好加个复位按键,但光靠硬件复位会让整个系统重启,丢失当前时间数据——这不是用户想要的。所以核心应该放在软件层面实现:通过独立按键触发复位中断,同时保持DS1302时钟芯片的持续运行。这里有个关键点,DS1302自带锂电池供电,不受单片机复位影响,这为软件复位提供了基础。 具体实现时,要特别注意寄存器操作顺序。比如退出复位状态时要先拉高CE引脚再拉高SCLK,否则可能写坏寄存器。显示复位后,LCD1602需要重新初始化,但不需要清屏——直接刷新显示当前时间更高效。另外,为了避免按键抖动导致多次复位,代码里应该加个20ms的延时消抖。 用户可能没意识到复位功能其实包含两个需求:一是复位系统状态(比如关闭闹钟),二是保持时钟持续运行。因此在回复时需要强调DS1302的特殊性——它就像个独立运行的电子表,单片机复位时它仍在后台计时。 最后要提醒用户测试复位时的电流波动问题。突然复位可能导致LCD显示异常,建议在代码里加入显示缓冲区的强制刷新。那些复用P0口的设计更要小心,复位时数据冲突可能烧IO口,最好加个74HC245驱动。 </think>为51单片机LCD1602数字时钟添加复位功能,需综合考虑**硬件复位按键连接****软件复位逻辑处理**。以下是具体实现方法及步骤: --- ### **一、硬件复位电路设计** 1. **独立复位按键** 在单片机`RST`引脚连接轻触开关(推荐10kΩ上拉电阻 + 104电容滤波),实现硬件复位: ```plaintext +5V ──┬── R1 (10kΩ) │ ┌┴┐ │ │ SW (复位按键) └┬┘ ├───────── RST (单片机复位引脚) │ ┌┴┐ C1 (0.1μF) └┬┘ │ GND ``` - **功能**:按下按键强制单片机重启,清除所有寄存器状态。 2. **软件复位按键**(推荐方案) 将按键连接到普通I/O口(如`P3.2`),通过软件检测实现部分复位: ```c sbit Reset_Key = P3^2; // 定义复位按键引脚 ``` --- ### **二、软件复位功能实现** #### **1. 硬件复位(全系统重启)** - 直接使用硬件复位电路,无需额外代码。 - **缺点**:时钟数据丢失(需重新从DS1302读取)。 #### **2. 软件复位(数据保持)** **步骤**: **(1) 初始化按键检测** ```c void Key_Init() { Reset_Key = 1; // 置高电平准备检测 } ``` **(2) 检测按键并执行复位** 在主循环或定时器中添加检测逻辑: ```c void main() { Init_All(); // 初始化系统 while(1) { if (Reset_Key == 0) { // 按键按下(低电平触发) delay_ms(20); // 消抖 if (Reset_Key == 0) { Soft_Reset(); // 执行软复位 } while(!Reset_Key); // 等待按键释放 } // 其他任务... } } ``` **(3) 软复位函数实现** ```c void Soft_Reset() { // 1. 复位显示状态 LCD1602_Init(); // 重新初始化LCD LCD1602_WriteString("Reset..."); // 2. 复位时钟显示(从DS1302重新读取时间) DS1302_GetTime(&hour, &min, &sec); Update_Display(); // 刷新显示 // 3. 关闭闹钟等附加功能(若有) Alarm_Enabled = 0; } ``` --- ### **三、复位功能优化建议** 1. **DS1302数据保持** - DS1302自带备用电池,单片机复位时其时钟持续运行,无需重新设置时间。 - 软复位后直接读取DS1302即可恢复时间显示[^1][^2]。 2. **复位状态指示** - 在LCD显示"Reset"提示(如持续1秒),增强交互性。 3. **抗干扰设计** - 按键检测需**消抖处理**(延时20ms)。 - 避免复位期间操作DS1302(防止数据冲突)。 --- ### **四、代码整合示例(关键部分)** ```c sbit Reset_Key = P3^2; // 复位按键连接P3.2 void main() { Timer0_Init(); // 初始化定时器(引用[3]方法) LCD1602_Init(); // 初始化LCD DS1302_Init(); // 初始化时钟芯片 while(1) { // 检测复位按键 if (Reset_Key == 0) { delay_ms(20); if (Reset_Key == 0) { LCD1602_Clear(); LCD1602_WriteString("System Reset"); delay_ms(1000); // 显示复位提示 Soft_Reset(); // 执行软复位 } while(!Reset_Key); // 等待按键释放 } // 正常显示时间(每秒刷新) if (time_update_flag) { DS1302_GetTime(&hour,&min,&sec); Display_Time(hour,min,sec); time_update_flag = 0; } } } void Soft_Reset() { LCD1602_Init(); // 重置LCD // 无需重置DS1302,直接读取最新时间 DS1302_GetTime(&hour,&min,&sec); Display_Time(hour,min,sec); // 立即显示时间 } ``` --- ### **注意事项** 1. **硬件复位**:会导致整个系统重启,适用于严重错误恢复。 2. **软件复位**:更灵活,可自定义复位范围(如仅复位显示/闹钟),推荐优先采用[^2][^3]。 3. **DS1302操作**:复位期间避免进行写操作,防止数据损坏。 > **提示**:若需彻底重置DS1302时间,可添加组合键功能(如长按复位键3秒进入时间设置模式)。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值