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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、功能

本电子时钟的功能是显示当前日期和时间、秒表、定时器和闹钟。以下是展示图。(在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的学习。

51单片机配合LCD1602液晶显示模块实现闹钟功能的代码通常涉及到以下几个步骤: 1. **初始化硬件**:首先需要配置LCD1602的基本通信,包括数据线、地址线以及读写控制线。 ```c #include <reg52.h> void LCD_Init(void); ``` 2. **设置闹钟时间**:通过程序设定闹钟的时间点,这可以是一个定时器中断,用于定期检查是否到了设定的时间。 ```c void SetAlarmTime(unsigned char hour, unsigned char minute); ``` 3. **显示时间**:编写函数用于从内存读取当前时间并显示到LCD上。 ```c void DisplayTime(void); ``` 4. **闹钟唤醒处理**:当达到预设闹钟时间时,触发中断,更新LCD显示,并可能有声音提示或其他动作。 ```c ISR(TIMER1_VECTOR) { if (is_alarm_time()) { LCD_Cmd(_LCD_ENTRY_MODE_2); // 设置光标闪烁 DisplayTime(); // 执行其他闹钟唤醒操作 } } ``` 5. **LCD指令集**:LCD1602有自己的指令集,包括行选择、列选择、清屏等基本操作。 ```c #define LCD_Cmd(cmd) _sendCommand(cmd) #define LCD_Init() { ... LCD_Cmd(_LCD_FUNCTIONSET | _LCD_CLEARDISP); ... } // _sendCommand 函数发送LCD指令 void _sendCommand(unsigned char cmd); ``` 注意,由于篇幅限制,这里只是一个简化的概述,实际代码会更复杂,包含错误检测、异常处理等部分。同时,你需要了解具体的51单片机型号(如8051或ATmega等)及其对应的库函数,因为不同型号的处理器对硬件资源的访问可能会有所不同。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值