基于51单片机和DS3231时钟模块、LCD1602(I2C通信)模块的可调时钟+温度测量+计时+闹钟

news2025/1/7 18:20:56

目录

  • 系列文章目录
  • 前言
  • 一、效果展示
  • 二、原理分析
  • 三、各模块代码
    • 1、延时函数
    • 2、定时器0
    • 3、定时器1
    • 4、独立按键
    • 5、DS3231时钟模块
    • 6、LCD1602模块(PCF8574T驱动)
  • 四、主函数
  • 总结

系列文章目录


前言

之前做过一个类似的,用到了很多外设,包括:独立按键、LCD1602液晶显示屏、DHT11湿度传感器、DS18B20温度传感器、DS1302时钟模块、AT24C02存储模块、无源蜂鸣器模块,自己日常也在用,但是感觉用到的外设太多了,有点凌乱。所以,利用精度更加高的DS3231时钟模块做了一个外设少一点的,除了不能测湿度,其他功能跟原来的一样。

有两个版本:
①普中开发板版本
②最小开发板版本

本文代码对应的是普中开发板版本。

两个版本用到的单片机都是:STC89C52RC。
普中开发板版本用了板载的蜂鸣器,最小开发板版本用了一个无源蜂鸣器模块。

效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。

一、效果展示

1、普中开发板版本

(1)正常走时模式
在这里插入图片描述

(2)正常走时模式(有闹钟生效的显示)
在这里插入图片描述

(3)正计时
在这里插入图片描述

(4)倒计时
在这里插入图片描述

(5)设置闹钟
在这里插入图片描述

(5)设置时间
在这里插入图片描述

(6)闹钟响(右半屏显示闹钟时间,并且对应的闹钟时间会闪烁)
在这里插入图片描述

(7)倒计时结束(闹钟响,第二行闪烁)
在这里插入图片描述

2、最小开发板版本

在这里插入图片描述

二、原理分析

DS3231时钟芯片除了计时精确外,也能测量温度,还能存储闹钟数据,无需再接DS18B20温度传感器和AT24C02存储芯片,所接外设减少了很多。

单片机与DS3231时钟芯片的通信跟单片机与DS1302时钟芯片的通信方式,不能说很类似,只能说一模一样,所以如果会用DS1302,那肯定也会用DS3231。

本案例所用到的DS3231时钟模块跟LCD1602(PCF8574T驱动)模块都是通过I2C协议通信,本代码没有共用相同的I2C模块,而是DS3231和LCD1602都有自己的I2C通信的底层函数,虽然有点重复,但是移植DS3231或LCD1602的代码的时候不用复制多两个文件。而且如果共用相同的I2C模块需要DS3231和LCD1602的SDA接在相同的IO口,SCL也接在相同的IO口,需要接多一个I2C扩展板,比较麻烦,现在只是做一个多功能可调时钟,IO口很充裕,无需省IO口。

本案例用的是无源蜂鸣器,通过定时器1(Timer1)以1KHz的频率翻转IO口。如果用有源蜂鸣器,无需频繁翻转IO口,在定时器中控制导通的时长就行了。

按键检测通过定时器,每隔20ms检测一次按键状态,这样就不用进行消抖处理,也不会阻塞主函数的程序运行。有短按、长按、松手检测。独立按键模块中,通过计数让按键长按200ms才被检测出是长按,这样可以防止短按的按键码获取不到的情况。需要注意的是,如果在下一次检测按键状态前主循环中第二次获取按键码的话,一定返回0,所以在main函数的while(1)中的某些代码需要加适当的延时,延时要大于定时器检测按键的周期。

为了防止频繁进入中断,影响主函数的运行,定时器0的定时设置为了10ms。

LCD1602模块用到了一些自定义的字符,需要写入自定义字符的字模到CGRAM中,总共可以自定义8个字符,本案例只用到了5个。

调整时间模式中进行了越界判断,调整日期时有大小月的判断,也有平年二月、闰年二月的判断,如果日期越界,会将日调成1号。此模式中,如果不按K3或K4调整时间的话,是可以正常走时的,按K3或K4调整时间后,松手瞬间会将设置后的时间写入DS3231芯片,再从DS3231芯片中不停地读取时间。星期用英文的前三个字母来表示。

三、各模块代码

1、延时函数

h文件

#ifndef	__DELAY_H__
#define	__DELAY_H__
 
void Delay(unsigned int xms);
 
#endif

c文件

/**
  * @brief	延时函数,晶振@11.0592MHz时,输入参数延时的单位是1ms
  * @param	xms 延时的时间,范围:0~65535
  * @retval	无
  */
void Delay(unsigned int xms)
{
	unsigned char i,j;
	while(xms)
	{
		i=2;
		j=199;
		do
		{
			while(--j);
		} while(--i);
		xms--;
	}
}

2、定时器0

h文件

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);

#endif

c文件

#include <REGX52.H>

/**
  * @brief	定时器0初始化
  * @param  无
  * @retval 无
  */
void Timer0_Init(void)
{
	TMOD&=0xF0;	//设置定时器模式(高四位不变,低四位清零)
	TMOD|=0x01;	//设置定时器模式(通过低四位设为“定时器0工作方式1”的模式)
	TL0=0x66;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TH0=0xFC;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TF0=0;	//清除TF0标志
	TR0=1;	//定时器0开始计时
	ET0=1;	//打开定时器0中断允许
	EA=1;	//打开总中断
	PT0=0;	//当PT0=0时,定时器0为低优先级,当PT0=1时,定时器0为高优先级
}

/*定时器中断函数模板
void Timer0_Routine() interrupt 1	//定时器0中断函数
{
	static unsigned int T0Count;	//定义静态变量
	TL0=0x66;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TH0=0xFC;	//设置定时初值,定时1ms,晶振@11.0592MHz
	T0Count++;
	if(T0Count>=1000)
	{
		T0Count=0;
		
	}
}
*/

3、定时器1

h文件

#ifndef __TIMER1_H__
#define __TIMER1_H__
 
void Timer1_Init(void);

#endif

c文件

#include <REGX52.H>

/**
  * @brief  定时器1初始化
  * @param  无
  * @retval 无
  */
void Timer1_Init(void)
{
	TMOD&=0x0F;	//设置定时器模式(低四位不变,高四位清零)
	TMOD|=0x10;	//设置定时器模式(通过高四位设为“定时器1工作方式1”的模式)
	TL1=0x66;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TH1=0xFC;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TF1=0;	//清除TF1标志
	TR1=1;	//定时器1开始计时
	ET1=1;	//打开定时器1中断允许
	EA=1;	//打开总中断
	PT1=1;	//当PT1=0时,定时器1为低优先级,当PT1=1时,定时器1为高优先级
}

/*定时器中断函数模板
void Timer1_Routine() interrupt 3	//定时器1中断函数
{
	static unsigned int T1Count;	//定义静态变量
	TL1=0x66;	//设置定时初值,定时1ms,晶振@11.0592MHz
	TH1=0xFC;	//设置定时初值,定时1ms,晶振@11.0592MHz
	T1Count++;
	if(T1Count>=1000)
	{
		T1Count=0;
		
	}
}
*/

4、独立按键

h文件

#ifndef __KEYSCAN_H__
#define __KEYSCAN_H__

unsigned char Key(void);
void Key_Tick(void);

#endif

c文件

#include <REGX52.H>

sbit Key1=P3^1;
sbit Key2=P3^0;
sbit Key3=P3^2;
sbit Key4=P3^3;

unsigned char KeyNumber;

/**
  * @brief  获取独立按键键码
  * @param  无
  * @retval 按下按键的键码,范围:0,1~13,0表示无按键按下
  */
unsigned char Key(void)
{
	unsigned char KeyTemp=0;
	KeyTemp=KeyNumber;
	KeyNumber=0;	//主程序中获取键码值之后键码值清零,在下一次定时器扫描按键之前再次获取键码值,一定会返回0
	return KeyTemp;
}

/**
  * @brief  获取当前按键的状态,无消抖及松手检测
  * @param  无
  * @retval 按下的按键,范围:0~5,无按键按下时返回值为0
  */
unsigned char Key_GetState()
{
	unsigned char KeyValue=0;
	
	if(Key1==0){KeyValue=1;}
	if(Key2==0){KeyValue=2;}
	if(Key3==0){KeyValue=3;}
	if(Key4==0){KeyValue=4;}

	if(Key1==0 && Key2==0){KeyValue=5;}
	
	return KeyValue;
}


/**
  * @brief  按键驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Key_Tick(void)
{
	static unsigned char NowState,LastState;
	static unsigned int KeyCount;
	LastState=NowState;	//按键状态更新
	NowState=Key_GetState();	//获取当前按键状态
	
	//如果上个时间点按键未按下,这个时间点按键按下,则是按下瞬间
	if(LastState==0)
	{
		switch(NowState)
		{
			case 1:KeyNumber=1;break;
			case 2:KeyNumber=2;break;
			case 3:KeyNumber=3;break;
			case 4:KeyNumber=4;break;
			default:break;
		}
	}
	
	//如果上个时间点按键按下,这个时间点按键按下,则是一直按住按键
	if(LastState && NowState)
	{
		KeyCount++;
		if(KeyCount>=10)	//按下超过200ms才被检测为长按(定时器中断函数中每隔20ms检测一次按键)
		{
			if(LastState==1 && NowState==1){KeyNumber=5;}
			if(LastState==2 && NowState==2){KeyNumber=6;}
			if(LastState==3 && NowState==3){KeyNumber=7;}
			if(LastState==4 && NowState==4){KeyNumber=8;}
		}
	}
	else
	{
		KeyCount=0;
	}
	
	//如果上个时间点按键按下,这个时间点按键未按下,则是松手瞬间
	if(NowState==0)
	{
		switch(LastState)
		{
			case 1:KeyNumber=9;break;
			case 2:KeyNumber=10;break;
			case 3:KeyNumber=11;break;
			case 4:KeyNumber=12;break;
			default:break;
		}
	}
	
	//如果同时按下Key1和Key2
	if(NowState==5 && LastState==5){KeyNumber=13;}
}

5、DS3231时钟模块

h文件

#ifndef	__DS3231_H__
#define	__DS3231_H__
 
extern char DS3231_Time[];
extern char DS3231_Alarm[];
void DS3231_I2C_Start(void);
void DS3231_I2C_Stop(void);
void DS3231_I2C_SendByte(unsigned char Byte);
unsigned char DS3231_I2C_ReceiveByte(unsigned char AckBit);
void DS3231_WriteByte(unsigned char WordAddress,Data);
unsigned char DS3231_ReadByte(unsigned char WordAddress);
void DS3231_SetTime(void);
void DS3231_ReadTime(void);
void DS3231_SetAlarm(void);
void DS3231_ReadAlarm(void);
void DS3231_ConvertT(void);
unsigned char DS3231_CheckBusy(void);
float DS3231_ReadT(void);

#endif

c文件

#include <REGX52.H>

#define DS3231_ADDRESS 0xD0	//DS3231的I2C地址

//DS3231引脚定义
sbit	DS3231_SDA=P1^0;
sbit	DS3231_SCL=P1^1;

//DS3231的时间地址:年,月,日,时,分,秒,星期
unsigned char code DS3231_TimeAddress[7]={0x06,0x05,0x04,0x02,0x01,0x00,0x03,};
//DS3231的闹钟地址:A1日期/星期,A1时,A1分,A1秒,A2日期/星期,A2时,A2分
unsigned char code DS3231_AlarmAddress[7]={0x0A,0x09,0x08,0x07,0x0D,0x0C,0x0B,};
//时间数组:年,月,日,时,分,秒,星期
char DS3231_Time[]={24,10,24,9,22,23,4};	//时间设置的初始值
//闹钟数组:A1日期/星期,A1时,A1分,A1秒,A2日期/星期,A2时,A2分
char DS3231_Alarm[]={1,6,7,0,1,6,7};	//闹钟设置的初始值

/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void DS3231_I2C_Start(void)
{
	DS3231_SDA=1;
	DS3231_SCL=1;
	DS3231_SDA=0;
	DS3231_SCL=0;
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void DS3231_I2C_Stop(void)
{
	DS3231_SDA=0;
	DS3231_SCL=1;
	DS3231_SDA=1;
}

/**
  * @brief  I2C发送一个字节
  * @param  Byte 要发送的字节
  * @retval 无
  */
void DS3231_I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		DS3231_SDA=Byte&(0x80>>i);
		DS3231_SCL=1;
		DS3231_SCL=0;
	}
	DS3231_SCL=1;	//额外一个时钟,不处理应答信号
	DS3231_SCL=0;
}

/**
  * @brief  I2C接收一个字节
  * @param  AckBit 要发送的应答位,0为应答,1为非应答
  * @retval 接收到的一个字节数据
  */
unsigned char DS3231_I2C_ReceiveByte(unsigned char AckBit)
{
	unsigned char i,Byte=0x00;
	DS3231_SDA=1;
	for(i=0;i<8;i++)
	{
		DS3231_SCL=1;
		if(DS3231_SDA){Byte|=(0x80>>i);}
		DS3231_SCL=0;
	}
	DS3231_SDA=AckBit;	//发送应答位
	DS3231_SCL=1;
	DS3231_SCL=0;
	return Byte;
}

/**
  * @brief  DS3231写入一个字节
  * @param  WordAddress 要写入字节的地址
  * @param  Data 要写入的数据
  * @retval 无
  */
void DS3231_WriteByte(unsigned char WordAddress,Data)
{
	DS3231_I2C_Start();
	DS3231_I2C_SendByte(DS3231_ADDRESS);
	DS3231_I2C_SendByte(WordAddress);
	DS3231_I2C_SendByte(Data);
	DS3231_I2C_Stop();
}

/**
  * @brief  DS3231读取一个字节
  * @param  WordAddress 要读出字节的地址
  * @retval 读出的数据
  */
unsigned char DS3231_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;
	DS3231_I2C_Start();
	DS3231_I2C_SendByte(DS3231_ADDRESS);
	DS3231_I2C_SendByte(WordAddress);
	DS3231_I2C_Start();
	DS3231_I2C_SendByte(DS3231_ADDRESS|0x01);
	Data=DS3231_I2C_ReceiveByte(1);
	DS3231_I2C_Stop();
	return Data;
}

/**
  * @brief  DS3231设置时间,调用之后,DS3231_Time数组的数字会被设置到DS3231中
  * @param  无
  * @retval 无
  */
void DS3231_SetTime(void)
{
	unsigned char i;
	for(i=0;i<7;i++)	//依次写入:年,月,日,时,分,秒,星期
	{
		DS3231_WriteByte(DS3231_TimeAddress[i],DS3231_Time[i]/10*16+DS3231_Time[i]%10);	//十进制转换为BCD码
	}
}

/**
  * @brief  DS3231读取时间,调用之后,DS3231中的数据会被读取到DS3231_Time数组中
  * @param  无
  * @retval 无
  */
void DS3231_ReadTime(void)
{
	unsigned char Temp,i;
	for(i=0;i<7;i++)	//依次读取:年,月,日,时,分,秒,星期
	{
		Temp=DS3231_ReadByte(DS3231_TimeAddress[i]);
		DS3231_Time[i]=Temp/16*10+Temp%16;	//BCD码转十进制后读取
	}
}

/**
  * @brief  DS3231设置闹钟数据
  * @param  无
  * @retval 无
  */
void DS3231_SetAlarm(void)
{
	unsigned char i;
	for(i=0;i<7;i++)	//依次写入:A1日期/星期,A1时,A1分,A1秒,A2日期/星期,A2时,A2分
	{
		DS3231_WriteByte(DS3231_AlarmAddress[i],DS3231_Time[i]/10*16+DS3231_Time[i]%10);	//十进制转换为BCD码
	}
}

/**
  * @brief  DS3231读取闹钟数据
  * @param  无
  * @retval 无
  */
void DS3231_ReadAlarm(void)
{
	unsigned char Temp,i;
	for(i=0;i<7;i++)	//依次读取:A1日期/星期,A1时,A1分,A1秒,A2日期/星期,A2时,A2分
	{
		Temp=DS3231_ReadByte(DS3231_AlarmAddress[i]);
		DS3231_Alarm[i]=Temp/16*10+Temp%16;	//BCD码转十进制后读取
	}
}

/**
  * @brief  DS3231转换温度
  * @param  无
  * @retval 无
  */
void DS3231_ConvertT(void)
{
	unsigned char Data;
	Data=DS3231_ReadByte(0x0E);
	DS3231_WriteByte(0x0E,Data|0x20);
}

/**
  * @brief  DS3231检查是否已转换完温度
  * @param  无
  * @retval 无
  */
unsigned char DS3231_CheckBusy(void)
{
	unsigned char BusyState;
	BusyState=DS3231_ReadByte(0x0F);
	BusyState&=0x04;
	return BusyState;
}

/**
  * @brief  DS3231读取温度
  * @param  无
  * @retval 无
  */
float DS3231_ReadT(void)
{
	unsigned char TM,TL;
	int Temp;
	float T;
	TM=DS3231_ReadByte(0x11);
	TL=DS3231_ReadByte(0x12);
	Temp=(TM<<8)|TL;
	Temp>>=6;
	T=Temp/4.0;
	return T;
}

6、LCD1602模块(PCF8574T驱动)

h文件

#ifndef	__LCD1602_I2C_H__
#define	__LCD1602_I2C_H__
 
void PCF8574T_WriteByte(unsigned char Byte);
void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_Init();
void LCD_SetCursor(unsigned char Line,unsigned char Column);
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_MakeChar(void);
void LCD_Clear(void);
void LCD_MoveLeft(void);
void LCD_MoveRight(void);

#endif

c文件

#include <REGX52.H>

#define	PCF8574T_ADDRESS	0x4E

sbit LCD1602_SDA=P1^2;
sbit LCD1602_SCL=P1^3;

//自定义字符,最多8个
unsigned char code CGRAMData[]={
0x07,0x05,0x07,0x00,0x00,0x00,0x00,0x00,	//摄氏度的圆圈
0x04,0x0C,0x04,0x04,0x0E,0x00,0x00,0x00,	//小1
0x0E,0x02,0x0E,0x08,0x0E,0x00,0x00,0x00,	//小2
0x00,0x0A,0x1F,0x1F,0x0E,0x04,0x00,0x00,	//小心形
0x00,0x0A,0x00,0x00,0x11,0x0E,0x00,0x00,	//小笑脸
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无
};

/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void LCD1602_I2C_Start(void)
{
	LCD1602_SDA=1;
	LCD1602_SCL=1;
	LCD1602_SDA=0;
	LCD1602_SCL=0;
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void LCD1602_I2C_Stop(void)
{
	LCD1602_SDA=0;
	LCD1602_SCL=1;
	LCD1602_SDA=1;
}

/**
  * @brief  I2C发送一个字节
  * @param  Byte 要发送的字节
  * @retval 无
  */
void LCD1602_I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		LCD1602_SDA=Byte&(0x80>>i);
		LCD1602_SCL=1;
		LCD1602_SCL=0;
	}
	LCD1602_SCL=1;	//额外一个时钟,不处理应答信号
	LCD1602_SCL=0;
}

/**
  * @brief  PCF8574T写入一个字节
  * @param  Byte 要写入的字节
  * @retval 无
  */
void PCF8574T_WriteByte(unsigned char Byte)
{
	LCD1602_I2C_Start();
	LCD1602_I2C_SendByte(PCF8574T_ADDRESS);
	LCD1602_I2C_SendByte(Byte);
	LCD1602_I2C_Stop();
}

/**
  * @brief  LCD1602写指令
  * @param  Command 要写的指令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	unsigned char Temp;	//临时变量保存处理后的数据
	Temp=Command|0x0F;	//准备写入指令的高四位
	PCF8574T_WriteByte(Temp&0xFC);	//xxxx 1(控制背光) 1(EN) 0(RW) 0(RS),EN=1
	PCF8574T_WriteByte(Temp&0xF8);	//xxxx 1(控制背光) 0(EN) 0(RW) 0(RS),EN=0,下降沿写入指令高四位
	Temp=(Command<<4)|0x0F;	//准备写入指令的低四位
	PCF8574T_WriteByte(Temp&0xFC);
	PCF8574T_WriteByte(Temp&0xF8);
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	unsigned char Temp;
	Temp=Data|0x0F;
	PCF8574T_WriteByte(Temp&0xFD);
	PCF8574T_WriteByte(Temp&0xF9);
	Temp=(Data<<4)|0x0F;
	PCF8574T_WriteByte(Temp&0xFD);
	PCF8574T_WriteByte(Temp&0xF9);
}

void LCD_Init()
{
	LCD_WriteCommand(0x28);	//发送第一个指令的高四位的时候LCD就已经知道选择的是四位数据接口的模式
	PCF8574T_WriteByte(0x0C);	//低四位暂时没什么用,如果急着发送下一个指令,则第一个指令的低四位和第二个指令的高四位会组成一个字节
	PCF8574T_WriteByte(0x08);	//所以不能急着发送下一个指令,要再使能一次再写其他指令
	LCD_WriteCommand(0x28);	//四位数据接口,两行显示,5*7点阵
	LCD_WriteCommand(0x0C);	//显示开,光标关,闪烁关
	LCD_WriteCommand(0x06);	//数据读写操作后,光标自动加一,画面不动
	LCD_WriteCommand(0x01);	//光标复位,清屏
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else if(Line==2)
	{
		LCD_WriteCommand(0x80|(Column-1+0x40));
	}
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i,SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData(SingleNumber+'0');
		}
		else
		{
			LCD_WriteData(SingleNumber-10+'A');
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
	}
}

/**
  * @brief  在CGRAM中写入8个自定义字符的64个字节,每个5*8点阵横向取模,使用每个字节的低5位
  * @param  无
  * @retval 无
  */
void LCD_MakeChar(void)
{
	unsigned char i;
	LCD_WriteCommand(0x40);	//起始地址为0x40
	for(i=0;i<64;i++)
	{
		LCD_WriteData(CGRAMData[i]);
	}
}

/**
  * @brief  LCD1602的光标复位,清屏
  * @param  无
  * @retval 无
  */
void LCD_Clear(void)
{
	LCD_WriteCommand(0x01);
}

/**
  * @brief  LCD1602的屏幕向左移动一个字符位,光标不动
  * @param  无
  * @retval 无
  */
void LCD_MoveLeft(void)
{
	LCD_WriteCommand(0x18);
}

/**
  * @brief  LCD1602的屏幕向右移动一个字符位,光标不动
  * @param  无
  * @retval 无
  */
void LCD_MoveRight(void)
{
	LCD_WriteCommand(0x1C);
}

四、主函数

main.c

/*
by甘腾胜@20250104
操作和功能:可以在B站搜索“甘腾胜”或“gantengsheng”查看
单片机:STC89C52RC
晶振:12T@11.0592MHz
外设:独立按键、无源蜂鸣器、DS3231时钟模块、LCD1602液晶显示屏(PCF8574T驱动,I2C通讯)
注意:计时不准的话,可以修改定时器0所赋初值进行补偿

操作说明:

	K1	K2	K3	K4	

一、正常走时模式:
【K1】切换模式
【K2】长按:显示闹钟时间;松手:返回正常走时模式
【K3】控制闹钟1生不生效
【K4】控制闹钟2生不生效
【K2&K1】先按K2再按K1:显示作者和编程日期等;松手:返回正常走时模式
二、计时模式
【K1】切换模式
【K2】计时暂停时:切换计时模式/选择要设置的倒计时的时间(时分秒)
【K3】正/倒计时暂停时:清空计时数据;倒计时设置时间时:设置倒计时的时间(数值只能加,不能减),短按加1,长按连续增加
【K4】计时中:开始/暂停/继续;设置倒计时时间且数值均为零时:切换为正计时并开始计时
三、设置闹钟模式:
【K1】切换模式
【K2】切换选择
【K3】减,短按减1,长按连续减小
【K4】增,短按加1,长按连续增加
四、设置闹钟模式:
【K1】切换模式
【K2】切换选择要设置的时间
【K3】减,短按减1,长按连续减小(设置秒的时候,清零)
【K4】增,短按加1,长按连续增加(设置秒的时候,清零)

*/

#include <REGX52.H>
#include "Delay.h"
#include "KeyScan.h"
#include "DS3231.h"
#include "LCD1602_I2C.h"
#include "Timer0.h"
#include "Timer1.h"

sbit Buzzer=P2^5;	//蜂鸣器端口
#define AlarmTimeLength 120	//闹钟响的时长

unsigned char KeyNum;	//保存键码值的变量
unsigned char FlashFlag;	//设置时间时闪烁的标志,1:不显示,0:显示
unsigned char Mode;	//时间模式,0:正常走时模式,1:计时模式,2:设置闹钟模式,3:设置时间模式
unsigned char TimeSelect;	//时间选择变量
unsigned char ReadTimeFlag;	//读取时间的标志,1:读取,0:不读取
unsigned char ReadTempFlag;	//读取温度的标志,1:读取,0:不读取
float T;	//存储温度数据的变量
unsigned char ExecuteOnceFlag;	//各模式中只执行一次的标志,用于清屏和显示静态字符
char TimeCount[4];	//存储计时的变量,时、分、秒、十毫秒
unsigned char TimeIsUpFlag;	//计时模式倒计时时间到了的标志,1:时间到了,0:时间未到
unsigned char TimeCountModeFlag=1;	//计时模式标志,1:正计时,0:倒计时
unsigned char TimeCountRunFlag;	//计时启停的标志,1:启动,0:暂停
char AlarmClock[4];	//存储两个闹钟时间的变量,闹钟1-时,闹钟1-分,闹钟2-时,闹钟2-分
unsigned char Alarm1RunFlag,Alarm2RunFlag;	//闹钟生效的标志,1:闹钟生效,0:闹钟不生效
unsigned char KeypressFlag;	//按键按下标志,1:有按键按下,0:无按键按下
unsigned char KeypressSoundTime;	//按键声时长
unsigned char Alarm1MatchFlag,Alarm2MatchFlag;	//闹钟到了的标志,1:走时到了闹钟的时间,0:走时未到闹钟的时间
unsigned char AlarmSoundCount;	//闹钟时长计数,控制闹钟响的方式
unsigned int AlarmSoundTime;	//闹钟响的时长,控制闹钟响的时长
unsigned char NormalShowFlag;	//闹钟停止时恢复正常走时显示的标志,1:恢复正常走时的显示,0:不用恢复原来显示
unsigned char AlarmFlag;	//闹钟响的标志,1:闹钟响,0:闹钟不响
unsigned char LastKeyNum;	//上次获取的键码值(用来控制正常走时模式长按K2显示闹钟时间和同时按K2、K1显示作者信息)
unsigned char NowKeyNum;	//本次获取的键码值(用来控制正常走时模式长按K2显示闹钟时间和同时按K2、K1显示作者信息)
unsigned char SwitchDisplayFlag;	//正常走时模式长按K2和同时按下K2、K1切换显示的标志,
									//1:切换显示,0:不重复显示静态字符,进入函数后会清屏和使此标志清零
unsigned char ChangedFlag;	//计时模式和设置闹钟模式有变动(有按下K2、K3或K4)的标志,1:有变动,0:无变动

void TimeShow(void)	//时间显示功能
{
	if(Mode==3 && FlashFlag==1 && TimeSelect==0){LCD_ShowString(1,1,"  ");}	//设置时间时闪烁
	else{LCD_ShowNum(1,1,DS3231_Time[0],2);}	//显示年
	if(Mode==3 && FlashFlag==1 && TimeSelect==1){LCD_ShowString(1,4,"  ");}
	else{LCD_ShowNum(1,4,DS3231_Time[1],2);}	//显示月
	if(Mode==3 && FlashFlag==1 && TimeSelect==2){LCD_ShowString(1,7,"  ");}
	else{LCD_ShowNum(1,7,DS3231_Time[2],2);}	//显示日
	if(AlarmFlag==0)	//如果闹钟不响,才显示周
	{
		if(Mode==3 && FlashFlag==1 && TimeSelect==3){LCD_ShowString(1,12,"   ");}
		else
		{		
			switch(DS3231_Time[6])
			{
				case 1 : LCD_ShowString(1,12,"Mon");break;
				case 2 : LCD_ShowString(1,12,"Tue");break;
				case 3 : LCD_ShowString(1,12,"Wen");break;
				case 4 : LCD_ShowString(1,12,"Thu");break;
				case 5 : LCD_ShowString(1,12,"Fri");break;
				case 6 : LCD_ShowString(1,12,"Sat");break;
				case 7 : LCD_ShowString(1,12,"Sun");break;
			}
		}	//显示周
	}
	if(Mode==3 && FlashFlag==1 && TimeSelect==4){LCD_ShowString(2,1,"  ");}
	else{LCD_ShowNum(2,1,DS3231_Time[3],2);}	//显示时
	if(Mode==3 && FlashFlag==1 && TimeSelect==5){LCD_ShowString(2,4,"  ");}
	else{LCD_ShowNum(2,4,DS3231_Time[4],2);}	//显示分
	if(Mode==3 && FlashFlag==1 && TimeSelect==6){LCD_ShowString(2,7,"  ");}
	else{LCD_ShowNum(2,7,DS3231_Time[5],2);}	//显示秒
	if(AlarmFlag==0)	//如果闹钟不响
	{	//小心形闪烁
		if(DS3231_Time[5]%2 && Mode==0){LCD_ShowChar(1,11,3);LCD_ShowChar(1,15,3);}	//小心形
		else {LCD_ShowChar(1,11,0x20);LCD_ShowChar(1,15,0x20);}	//无显示
	}
}

void TimeCheck(void)	//时间越界判断,防止向DS1302写入错误数据
{
		if(DS3231_Time[0]<0){DS3231_Time[0]=99;}	//年越界判断,范围:0~99
		if(DS3231_Time[0]>99){DS3231_Time[0]=0;}
		if(DS3231_Time[1]<1){DS3231_Time[1]=12;}	//月越界判断,,范围:1~12
		if(DS3231_Time[1]>12){DS3231_Time[1]=1;}
		if(DS3231_Time[1]==1 || DS3231_Time[1]==3 || DS3231_Time[1]==5 || DS3231_Time[1]==7 || 
			DS3231_Time[1]==8 || DS3231_Time[1]==10 || DS3231_Time[1]==12)	//日越界判断,区分大小月、闰年2月、平年2月
		{
			if(DS3231_Time[2]<1){DS3231_Time[2]=31;}	//大月
			if(DS3231_Time[2]>31){DS3231_Time[2]=1;}
		}
		else if(DS3231_Time[1]==4 || DS3231_Time[1]==6 || DS3231_Time[1]==9 || DS3231_Time[1]==11)
		{
			if(DS3231_Time[2]<1){DS3231_Time[2]=30;}	//小月
			if(DS3231_Time[2]>30){DS3231_Time[2]=1;}
		}
		else if(DS3231_Time[1]==2)
		{
			if(DS3231_Time[0]%4==0)
			{
				if(DS3231_Time[2]<1){DS3231_Time[2]=29;}	//闰年2月
				if(DS3231_Time[2]>29){DS3231_Time[2]=1;}
			}
			else
			{
				if(DS3231_Time[2]<1){DS3231_Time[2]=28;}	//平年2月
				if(DS3231_Time[2]>28){DS3231_Time[2]=1;}
			}
		}
		if(DS3231_Time[3]<0){DS3231_Time[3]=23;}	//时越界判断,范围:0~24
		if(DS3231_Time[3]>23){DS3231_Time[3]=0;}
		if(DS3231_Time[4]<0){DS3231_Time[4]=59;}	//分越界判断,范围:0~59
		if(DS3231_Time[4]>59){DS3231_Time[4]=0;}	
		if(DS3231_Time[6]<1){DS3231_Time[6]=7;}		//周越界判断,范围:1~7
		if(DS3231_Time[6]>7){DS3231_Time[6]=1;}		
}

void TimeUpdateDisplay(void)	//设置时按下(短按或长按)K3和K4更新显示时间
{
	LCD_ShowNum(1,1,DS3231_Time[0],2);	//显示年
	LCD_ShowNum(1,4,DS3231_Time[1],2);	//显示月
	LCD_ShowNum(1,7,DS3231_Time[2],2);	//显示日
	switch(DS3231_Time[6])	//显示周
	{
		case 1 : LCD_ShowString(1,12,"Mon");break;
		case 2 : LCD_ShowString(1,12,"Tue");break;
		case 3 : LCD_ShowString(1,12,"Wen");break;
		case 4 : LCD_ShowString(1,12,"Thu");break;
		case 5 : LCD_ShowString(1,12,"Fri");break;
		case 6 : LCD_ShowString(1,12,"Sat");break;
		case 7 : LCD_ShowString(1,12,"Sun");break;
	}
	LCD_ShowNum(2,1,DS3231_Time[3],2);	//显示时
	LCD_ShowNum(2,4,DS3231_Time[4],2);	//显示分
	LCD_ShowNum(2,7,DS3231_Time[5],2);	//显示秒
}

void AlarmStop(void)	//闹钟停止
{
	AlarmSoundCount=0;	//闹钟时间计数清零
	AlarmSoundTime=0;	//闹钟时间计时清零
	Alarm1MatchFlag=0;	//闹钟1标志清零
	Alarm2MatchFlag=0;	//闹钟2标志清零
	if(AlarmFlag==1){NormalShowFlag=1;}	//如果通过按键取消闹钟,则切换为正常走时的显示
	AlarmFlag=0;	//闹钟响的标志清零
	Buzzer=1;	//蜂鸣器低电平导通,防止不响的时候导通
	TimeIsUpFlag=0;	//时间到了的标记清零(倒计时模式)
}

void main()
{
	DS3231_ConvertT();	//转换温度
	LCD_Init();	//LCD1602初始化
	Timer0_Init();	//定时器0初始化
	Timer1_Init();	//定时器1初始化
	LCD_MakeChar();	//向LCD1602的CGRAM中写入自定义字符数据
	
	DS3231_ReadAlarm();	//从DS3231中读出闹钟数据
	AlarmClock[0]=DS3231_Alarm[1];
	AlarmClock[1]=DS3231_Alarm[2];
	AlarmClock[2]=DS3231_Alarm[5];
	AlarmClock[3]=DS3231_Alarm[6];
	Alarm1RunFlag=DS3231_Alarm[0];	//闹钟1是否生效的标志
	Alarm2RunFlag=DS3231_Alarm[4];	//闹钟2是否生效的标志
	if(AlarmClock[0]>=24 || AlarmClock[0]<0){AlarmClock[0]=0;}	//判断闹钟时间是否越界,如果越界,则清零
	if(AlarmClock[1]>=60 || AlarmClock[1]<0){AlarmClock[1]=0;}
	if(AlarmClock[2]>=24 || AlarmClock[2]<0){AlarmClock[2]=0;}
	if(AlarmClock[3]>=60 || AlarmClock[3]<0){AlarmClock[3]=0;}
	if(Alarm1RunFlag>1){Alarm1RunFlag=0;}	//判断闹钟是否生效的标志是否越界,如果越界,则清零
	if(Alarm2RunFlag>1){Alarm2RunFlag=0;}
	
	ExecuteOnceFlag=1;	//上电正常走时模式清屏和显示静态字符

	while(1)
	{
		KeyNum=Key();	//获取键码值

		if(KeyNum)	//如果键码值不为零
		{
			if(KeyNum==1)	//如果K1短按
			{
				KeypressFlag=1;	//按键声标志置1
				AlarmStop();	//闹钟停止
				if(ChangedFlag==1 && (Mode==1 || Mode==2))
				{	//如果计时模式或设置闹钟模式有变动
					Mode=0;	//按下模式键则返回正常走时模式
					ChangedFlag=0;	//计时模式或设置闹钟模式有变动的标志清零
					ExecuteOnceFlag=1;
				}
				else
				{
					Mode++;	//转换模式
					Mode%=4;	//越界清零
				}
				if(Mode){ExecuteOnceFlag=1;}	//设置时间模式切换到正常走时模式不用清屏
				TimeSelect=0;	//时间选择变量清零
				if(TimeCount[0]==0 && TimeCount[1]==0 && TimeCount[2]==0 && TimeCount[3]==0)
				{	//切换到计时模式时,如果没在倒计时,则切换为正计时模式
					TimeCountModeFlag=1;
				}
			}
			
			if(KeyNum==2)	//如果K2短按
			{
				KeypressFlag=1;	//按键声标志置1
				AlarmStop();	//闹钟停止
				if(Mode==1)	//如果是模式1(计时模式)
				{
					ChangedFlag=1;	//计时模式有变动
					if(TimeCountRunFlag==0)	//停止计时才能切换正计时和倒计时模式
					{
						TimeSelect++;	//时间选择变量自增
						TimeSelect%=4;	//越界清零
						
						//按下K2,如果TimeSelect为0,是正计时模式,如果TimeSelect为1/2/3,是倒计时模式
						if(TimeSelect){TimeCountModeFlag=0;}
						else {TimeCountModeFlag=1;}
					}
				}
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					ChangedFlag=1;	//设置闹钟模式有变动
					TimeSelect++;	//时间选择变量自增
					TimeSelect%=4;	//越界清零
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					TimeSelect++;	//时间选择变量自增
					TimeSelect%=7;	//越界清零
				}
			}
			
			if(KeyNum==3)	//如果K3短按
			{
				KeypressFlag=1;	//按键声标志置1
				AlarmStop();	//闹钟停止
				if(Mode==0)	//如果是模式0(正常走时模式)
				{
					Alarm1RunFlag=!Alarm1RunFlag;	//更改闹钟1是否生效的标志的值
					DS3231_WriteByte(0x0A,Alarm1RunFlag);	//将闹钟设置状态保存到DS3231
					if(Alarm1RunFlag){LCD_ShowChar(1,9,1);}	//显示小1,表示闹钟1生效了
					else {LCD_ShowChar(1,9,0x20);}	//无显示,表示闹钟1不生效
				}
				if(Mode==1)	//如果是模式1(计时模式)
				{
					ChangedFlag=1;	//计时模式有变动
					if(TimeCountRunFlag==0)
					{	//停止计时才能修改时间或清零
						if(TimeCountModeFlag)	//如果是正计时
						{
							TimeCount[0]=0;	//计时数据清零
							TimeCount[1]=0;
							TimeCount[2]=0;
							TimeCount[3]=0;			
						}
						else	//如果是倒计时
						{
							switch(TimeSelect)
							{
								case 0:	//倒计时暂停后,按K3将数据清零
									TimeCount[0]=0;
									TimeCount[1]=0;
									TimeCount[2]=0;
									TimeCount[3]=0;	
									break;
								case 1:	//按键有限,设置倒计时的时间只能加,不能减
									TimeCount[0]++;if(TimeCount[0]>=100){TimeCount[0]=0;}break;	//最大计时,99h59m59s
								case 2:TimeCount[1]++;if(TimeCount[1]>=60){TimeCount[1]=0;}break;
								case 3:TimeCount[2]++;if(TimeCount[2]>=60){TimeCount[2]=0;}break;
								default:break;
							}
							LCD_ShowNum(2,1,TimeCount[0],2);	//更新显示
							LCD_ShowNum(2,4,TimeCount[1],2);
							LCD_ShowNum(2,7,TimeCount[2],2);
							LCD_ShowNum(2,10,TimeCount[3],2);
							Delay(200);	//短按K3调节时间后暂不闪烁
						}
					}
				}
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					ChangedFlag=1;	//设置闹钟模式有变动
					switch(TimeSelect)	//时间数值减小
					{
						case 0:
							AlarmClock[0]--;
							if(AlarmClock[0]<0){AlarmClock[0]=23;}	//限制闹钟时间范围
							LCD_ShowNum(1,8,AlarmClock[0],2);	//更新显示
							break;
						case 1:
							AlarmClock[1]--;
							if(AlarmClock[1]<0){AlarmClock[1]=59;}
							LCD_ShowNum(1,11,AlarmClock[1],2);
							break;
						case 2:
							AlarmClock[2]--;
							if(AlarmClock[2]<0){AlarmClock[2]=23;}
							LCD_ShowNum(2,8,AlarmClock[2],2);
							break;
						case 3:
							AlarmClock[3]--;
							if(AlarmClock[3]<0){AlarmClock[3]=59;}
							LCD_ShowNum(2,11,AlarmClock[3],2);
							break;
						default:break;
					}
					Delay(200);	//防止短按松手后检测不出松手的键码值
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					switch(TimeSelect)	//时间数值减小
					{
						case 0:DS3231_Time[0]--;break;
						case 1:DS3231_Time[1]--;break;
						case 2:DS3231_Time[2]--;break;
						case 3:DS3231_Time[6]--;break;
						case 4:DS3231_Time[3]--;break;
						case 5:DS3231_Time[4]--;break;
						case 6:DS3231_Time[5]=0;break;	//按下按键3,秒的数值清零
					}
					TimeCheck();	//检查时间值是否越界
					TimeUpdateDisplay();	//更新显示
					Delay(200);	//防止短按松手后检测不出松手的键码值
				}
			}
			
			if(KeyNum==4)	//如果K4短按
			{
				KeypressFlag=1;	//按键声标志置1
				AlarmStop();	//闹钟停止
				if(Mode==0)	//如果是模式0(正常走时模式)
				{
					Alarm2RunFlag=!Alarm2RunFlag;	//更改闹钟2是否生效的标志的值
					DS3231_WriteByte(0x0D,Alarm1RunFlag);	//将闹钟设置状态保存到DS3231
					if(Alarm2RunFlag){LCD_ShowChar(2,9,2);}	//显示小2,表示闹钟2生效了
					else {LCD_ShowChar(2,9,0x20);}	//无显示,表示闹钟2不生效
				}
				if(Mode==1)	//如果是模式1(计时模式)
				{
					ChangedFlag=1;	//计时模式有变动
					if(TimeCountModeFlag){TimeCountRunFlag=!TimeCountRunFlag;}	//如果是正计时,切换启动和暂停的状态
					else if(TimeCount[0]==0 && TimeCount[1]==0 && TimeCount[2]==0 && TimeCount[3]==0)	//如果是倒计时
					{	//如果所有数据均为0,则转变成正计时模式
						TimeSelect=0;
						TimeCountModeFlag=1;
						TimeCountRunFlag=1;
					}
					else	//如果数据不都为零
					{
						TimeSelect=0;	//启动后再暂停,使时分秒的显示不闪烁
						TimeCountRunFlag=!TimeCountRunFlag;	//切换启动和暂停的状态
					}
				}
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					ChangedFlag=1;	//设置闹钟模式有变动
					switch(TimeSelect)	//时间数值增加
					{
						case 0:
							AlarmClock[0]++;
							if(AlarmClock[0]>=24){AlarmClock[0]=0;}	//限制闹钟时间范围
							LCD_ShowNum(1,8,AlarmClock[0],2);	//更新显示
							break;
						case 1:
							AlarmClock[1]++;
							if(AlarmClock[1]>=60){AlarmClock[1]=0;}
							LCD_ShowNum(1,11,AlarmClock[1],2);
							break;
						case 2:
							AlarmClock[2]++;
							if(AlarmClock[2]>=24){AlarmClock[2]=0;}
							LCD_ShowNum(2,8,AlarmClock[2],2);
							break;
						case 3:
							AlarmClock[3]++;
							if(AlarmClock[3]>=60){AlarmClock[3]=0;}
							LCD_ShowNum(2,11,AlarmClock[3],2);
							break;
						default:break;
					}
					Delay(200);	//防止短按松手后检测不出松手的键码值
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					switch(TimeSelect)	//时间数值增加
					{
						case 0:DS3231_Time[0]++;break;
						case 1:DS3231_Time[1]++;break;
						case 2:DS3231_Time[2]++;break;
						case 3:DS3231_Time[6]++;break;
						case 4:DS3231_Time[3]++;break;
						case 5:DS3231_Time[4]++;break;
						case 6:DS3231_Time[5]=0;break;
					}
					TimeCheck();	//检查时间值是否越界
					TimeUpdateDisplay();	//更新显示
					Delay(200);	//防止短按松手后检测不出松手的键码值
				}
			}
			
			if(KeyNum==7)	//如果K3长按
			{
				if(Mode==1)	//如果是模式1(计时模式)
				{
					if(TimeCountModeFlag==0 && TimeCountRunFlag==0)	//倒计时设置时间(暂停状态才能修改)
					{
						switch(TimeSelect)
						{
							case 1:
								TimeCount[0]++;	//按键有限,设置倒计时的时间只能加,不能减
								if(TimeCount[0]>=100){TimeCount[0]=0;}	//最大计时,99h59m59s
								LCD_ShowNum(2,1,TimeCount[0],2);	//更新显示
								break;
							case 2:
								TimeCount[1]++;
								if(TimeCount[1]>=60){TimeCount[1]=0;}
								LCD_ShowNum(2,4,TimeCount[1],2);
								break;
							case 3:
								TimeCount[2]++;
								if(TimeCount[2]>=60){TimeCount[2]=0;}
								LCD_ShowNum(2,7,TimeCount[2],2);
								break;
							default:break;
						}
						LCD_ShowNum(2,10,TimeCount[3],2);
						Delay(50);	//防止数值变化过快,也防止长按按键时数据闪烁
					}
				}
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					switch(TimeSelect)	//时间数值减小
					{
						case 0:
							AlarmClock[0]--;
							if(AlarmClock[0]<0){AlarmClock[0]=23;}	//限制闹钟时间范围
							LCD_ShowNum(1,8,AlarmClock[0],2);	//更新显示
							break;
						case 1:
							AlarmClock[1]--;
							if(AlarmClock[1]<0){AlarmClock[1]=59;}
							LCD_ShowNum(1,11,AlarmClock[1],2);
							break;
						case 2:
							AlarmClock[2]--;
							if(AlarmClock[2]<0){AlarmClock[2]=23;}
							LCD_ShowNum(2,8,AlarmClock[2],2);
							break;
						case 3:
							AlarmClock[3]--;
							if(AlarmClock[3]<0){AlarmClock[3]=59;}
							LCD_ShowNum(2,11,AlarmClock[3],2);
							break;
						default:break;
					}
					Delay(50);	//防止数值变化过快,也防止长按按键时数据闪烁
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					switch(TimeSelect)	//时间数值减小
					{
						case 0:DS3231_Time[0]--;break;
						case 1:DS3231_Time[1]--;break;
						case 2:DS3231_Time[2]--;break;
						case 3:DS3231_Time[6]--;break;
						case 4:DS3231_Time[3]--;break;
						case 5:DS3231_Time[4]--;break;
						case 6:DS3231_Time[5]=0;break;
					}
					TimeCheck();	//检查时间值是否越界
					TimeUpdateDisplay();	//更新显示
					Delay(50);	//防止数值变化过快,也防止长按按键时数据闪烁
				}
			}
			
			if(KeyNum==8)	//如果K4长按
			{
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					switch(TimeSelect)	//时间数值增加
					{
						case 0:
							AlarmClock[0]++;
							if(AlarmClock[0]>=24){AlarmClock[0]=0;}	//限制闹钟时间范围
							LCD_ShowNum(1,8,AlarmClock[0],2);	//更新显示
							break;
						case 1:
							AlarmClock[1]++;
							if(AlarmClock[1]>=60){AlarmClock[1]=0;}
							LCD_ShowNum(1,11,AlarmClock[1],2);
							break;
						case 2:
							AlarmClock[2]++;
							if(AlarmClock[2]>=24){AlarmClock[2]=0;}
							LCD_ShowNum(2,8,AlarmClock[2],2);
							break;
						case 3:
							AlarmClock[3]++;
							if(AlarmClock[3]>=60){AlarmClock[3]=0;}
							LCD_ShowNum(2,11,AlarmClock[3],2);
							break;
						default:break;
					}
					Delay(50);	//防止数值变化过快,也防止长按按键时数据闪烁
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					switch(TimeSelect)	//时间数值增加
					{
						case 0:DS3231_Time[0]++;break;
						case 1:DS3231_Time[1]++;break;
						case 2:DS3231_Time[2]++;break;
						case 3:DS3231_Time[6]++;break;
						case 4:DS3231_Time[3]++;break;
						case 5:DS3231_Time[4]++;break;
						case 6:DS3231_Time[5]=0;break;
					}
					TimeCheck();	//检查时间值是否越界
					TimeUpdateDisplay();	//更新显示
					Delay(50);	//防止数值变化过快,也防止长按按键时数据闪烁
				}
			}
			
			if(KeyNum==11 || KeyNum==12)	//如果K3或K4松开(松开瞬间)
			{
				if(Mode==2)	//如果是模式2(设置闹钟模式)
				{
					switch(TimeSelect)	//将修改后的闹钟时间写入DS3231
					{
						case 0:
							DS3231_WriteByte(0x09,AlarmClock[0]/10*16+AlarmClock[0]%10);
							break;
						case 1:
							DS3231_WriteByte(0x08,AlarmClock[1]/10*16+AlarmClock[1]%10);
							break;
						case 2:
							DS3231_WriteByte(0x0C,AlarmClock[2]/10*16+AlarmClock[2]%10);
							break;
						case 3:
							DS3231_WriteByte(0x0B,AlarmClock[3]/10*16+AlarmClock[3]%10);
							break;
						default:break;
					}
				}
				if(Mode==3)	//如果是模式3(设置时间模式)
				{
					DS3231_SetTime();	//将修改后的时间写入DS1302
				}
			}
			
			LastKeyNum=NowKeyNum;
			NowKeyNum=KeyNum;	//更新获取的键码值
			
			//如果上次键码值不是6,本次键码值是6,说明按键2长按,显示闹钟时间
			if(LastKeyNum!=6 && NowKeyNum==6 && Mode==0){SwitchDisplayFlag=1;}	//用于长按K2瞬间清屏和显示静态字符
			//如果上次键码值是6,本次键码值不是6,说明按键2由长按变成松开
			if(LastKeyNum==6 && NowKeyNum!=6 && Mode==0){ExecuteOnceFlag=1;}	//用于松手瞬间清屏和恢复显示
			//如果上次键码值不是13,本次键码值是13,说明同时按下K1、K2(要先按K2,再按K1),显示作者信息和写程序时间
			if(LastKeyNum!=13 && NowKeyNum==13 && Mode==0){SwitchDisplayFlag=1;}	//用于同时按K2和K1瞬间清屏和显示静态字符
			//如果上次键码值是13,本次键码值不是13,说明不同时按下K1、K2了
			if(LastKeyNum==13 && NowKeyNum!=13 && Mode==0){ExecuteOnceFlag=1;}	//用于松手瞬间清屏和恢复显示
		}
		
		if(DS3231_Time[3]==AlarmClock[0] && DS3231_Time[4]==AlarmClock[1] && DS3231_Time[5]==0)	//判断走时是否到了闹钟时间
		{
			Alarm1MatchFlag=1;	//闹钟1到了的标志置1
			Alarm2MatchFlag=0;	//如果闹钟2正在响,则闹钟2停止
			AlarmSoundTime=0;	//闹钟计时清零
		}
		if(DS3231_Time[3]==AlarmClock[2] && DS3231_Time[4]==AlarmClock[3] && DS3231_Time[5]==0)	//判断走时是否到了闹钟时间
		{
			Alarm2MatchFlag=1;	//闹钟2到了的标志置1
			Alarm1MatchFlag=0;	//如果闹钟1正在响,则闹钟1停止
			AlarmSoundTime=0;	//闹钟计时清零	
		}
		
		if((Alarm1RunFlag==1 && Alarm1MatchFlag==1) || (Alarm2RunFlag==1 && Alarm2MatchFlag==1) || TimeIsUpFlag==1)
		{
			AlarmFlag=1;	//闹钟响(到闹钟时间或倒计时结束)的标志置1
		}
		
		if(Mode==0 || Mode==3)	//正常走时模式和设置时间模式
		{
			if(KeyNum!=7 && KeyNum!=8)	//如果设置时间模式长按K3或K4则不执行此模式中的代码,防止闪烁
			{
				if((Mode==0 && KeyNum==6) || (Mode==0 && KeyNum==13))	//如果正常走时模式K2长按或先按K2再按K1
				{
					if(SwitchDisplayFlag)
					{
						if(KeyNum==6)	//如果K2长按则显示闹钟
						{
							SwitchDisplayFlag=0;	//切换显示标志清零(全部是静态字符,长按按K2,函数内程序只执行1次)
							LCD_Clear();	//清屏
							LCD_ShowString(1,1,"Alarm1");
							LCD_ShowString(2,1,"Alarm2");
							LCD_ShowChar(1,10,':');
							LCD_ShowChar(2,10,':');
							LCD_ShowNum(1,8,AlarmClock[0],2);	//显示时(闹钟1)
							LCD_ShowNum(1,11,AlarmClock[1],2);	//显示分(闹钟1)
							LCD_ShowNum(2,8,AlarmClock[2],2);	//显示时(闹钟2)
							LCD_ShowNum(2,11,AlarmClock[3],2);	//显示分(闹钟2)
						}
						else	//如果先按K2再按K1
						{	//同时按下K2和K1则显示作者信息和编程日期
							SwitchDisplayFlag=0;	//切换显示标志清零(全部是静态字符,同时按下K2和K1,函数内程序只执行1次)
							LCD_Clear();	//清屏
							LCD_ShowString(1,1,"by gantengsheng  Never give up!");	//显示作者信息和编写时间
							LCD_ShowString(2,1,"at 20250104      You are the best!");
							LCD_ShowChar(1,36,3);	//显示小心形
							LCD_ShowChar(2,36,3);	//显示小心形
							LCD_ShowChar(1,38,4);	//显示小笑脸
							LCD_ShowChar(2,38,4);	//显示小笑脸
						}
					}
					if(KeyNum==13){Delay(500);LCD_MoveLeft();}	//移屏效果,500ms移动一次
					Delay(25);	//定时器扫描按键的周期是20ms,要延时一小段时间,否则显示不正常
								//在下次扫描按键前,如果再次获取键码值,得到的键码值是0,如果不加延时,下次就会执行下面else的内容
				}
				else	//如果不是长按K2和同时按K2、K1
				{
					if(ExecuteOnceFlag)	//切换模式前,此if内容只执行一次
					{
						ExecuteOnceFlag=0;
						LCD_Clear();	//清屏
						LCD_ShowString(1,1,"  -  -  ");	//显示静态字符
						LCD_ShowString(2,1,"  :  :  ");
						LCD_ShowChar(2,13,'.');
						LCD_ShowChar(2,15,0);	//在2行15列显示CGRAM中编码0x00的自定义字符(摄氏度的圈)
						LCD_ShowChar(2,16,'C');
						if(Alarm1RunFlag){LCD_ShowChar(1,9,1);}	//显示小1,表示闹钟1生效了
						else {LCD_ShowChar(1,9,0x20);}	//无显示,表示闹钟1不生效
						if(Alarm2RunFlag){LCD_ShowChar(2,9,2);}	//显示小2,表示闹钟2生效了
						else {LCD_ShowChar(2,9,0x20);}	//无显示,表示闹钟2不生效
					}
					if(AlarmFlag)
					{	//如果闹钟响了,则对应的闹钟图标(小1和小2)和闹钟时间闪烁
						if(Alarm1MatchFlag && FlashFlag)	//如果是闹钟1响
						{
							LCD_ShowString(1,9,"        ");	//清空上右半屏
						}
						else	//如果是闹钟1
						{
							if(Alarm1RunFlag){LCD_ShowChar(1,9,1);}	//显示小1,表示闹钟1生效了
							else {LCD_ShowChar(1,9,0x20);}	//无显示,表示闹钟1不生效
							LCD_ShowChar(1,10,0x20);
							LCD_ShowChar(1,13,':');
							LCD_ShowNum(1,11,AlarmClock[0],2);
							LCD_ShowNum(1,14,AlarmClock[1],2);
							LCD_ShowChar(1,16,0x20);
						}
						if(Alarm2MatchFlag && FlashFlag)	//如果是闹钟2响
						{
							LCD_ShowString(2,9,"        ");	//清空下右半屏
						}
						else
						{
							if(Alarm2RunFlag){LCD_ShowChar(2,9,2);}	//显示小2,表示闹钟2生效了
							else {LCD_ShowChar(2,9,0x20);}	//无显示,表示闹钟2不生效
							LCD_ShowChar(2,10,0x20);
							LCD_ShowChar(2,13,':');
							LCD_ShowNum(2,11,AlarmClock[2],2);
							LCD_ShowNum(2,14,AlarmClock[3],2);
							LCD_ShowChar(2,16,0x20);
						}
					}
					if(NormalShowFlag)	//闹钟响完之后恢复原来显示
					{
						NormalShowFlag=0;	//恢复正常走时显示的标志清零
						LCD_ShowString(1,10,"       ");	//清空右半屏和显示静态字符
						LCD_ShowString(2,10,"   .   ");	//清空右半屏和显示静态字符
						LCD_ShowChar(2,15,0);	//在2行15列显示CGRAM中编码0x00的自定义字符(摄氏度的圈)
						LCD_ShowChar(2,16,'C');
						if(Alarm1RunFlag){LCD_ShowChar(1,9,1);}	//显示小1,表示闹钟1生效了
						else {LCD_ShowChar(1,9,0x20);}	//无显示,表示闹钟1取消了
						if(Alarm2RunFlag){LCD_ShowChar(2,9,2);}	//显示小2,表示闹钟2生效了
						else {LCD_ShowChar(2,9,0x20);}	//无显示,表示闹钟2取消了
					}
					if(ReadTimeFlag && KeyNum!=3 && KeyNum!=4)	//如果读取时间标志为1
					{	//短按K3或K4不读取时间,防止数据未写入就读取设置时间前的时间,等松手后数据写入DS1302再读取时间
						ReadTimeFlag=0;	//读取时间标志清零
						DS3231_ReadTime();	//读取时间
						TimeShow();	//显示时间
					}
					if(ReadTempFlag && KeypressFlag==0 && AlarmFlag==0)	//如果读取温度标志为1
					{	//闹钟响时无需显示温度
						ReadTempFlag=0;	//读取温度标志清零
						if(DS3231_CheckBusy()==0)
						{
							T=DS3231_ReadT();
							DS3231_ConvertT();
						}					
						if(T<0)	//如果温度小于0
						{
							LCD_ShowChar(2,10,'-');	//显示负号
							T=-T;	//将温度变为正数
						}
						else	//如果温度大于等于0
						{
							LCD_ShowChar(2,10,'+');	//显示正号
						}
						LCD_ShowNum(2,11,T,2);	//显示温度整数部分
						LCD_ShowNum(2,14,(unsigned long)(T*10)%10,1);	//显示温度小数部分,显示1位小数
					}
				}
			}
		}
		
		if(Mode==1)	//计时模式
		{
			if(KeyNum!=7)	//如果长按K3调整倒计时的时间时不执行此模式中的代码,防止闪烁
			{
				if(ExecuteOnceFlag)
				{
					ExecuteOnceFlag=0;
					LCD_Clear();	//清屏
					LCD_ShowString(1,1,"TimeCount:");	//显示静态字符
				}
				if(TimeCountModeFlag)	//如果是正计时
				{
					LCD_ShowString(1,12,"UP  ");	//显示计时模式
					LCD_ShowChar(2,3,':');
					LCD_ShowChar(2,6,':');
					LCD_ShowNum(2,1,TimeCount[0],2);	//显示时
					LCD_ShowNum(2,4,TimeCount[1],2);	//显示分
					LCD_ShowNum(2,7,TimeCount[2],2);	//显示秒
					LCD_ShowNum(2,10,TimeCount[3],2);	//显示十毫秒
				}
				else if(TimeIsUpFlag==0)	//如果是倒计时,且还没计时完
				{
					LCD_ShowString(1,12,"Down");	//显示计时模式
					LCD_ShowChar(2,3,':');
					LCD_ShowChar(2,6,':');
					if(TimeSelect==1 && FlashFlag==1 && TimeCountRunFlag==0){LCD_ShowString(2,1,"  ");}	//设置倒计时时间时闪烁
					else {LCD_ShowNum(2,1,TimeCount[0],2);}	//显示时
					if(TimeSelect==2 && FlashFlag==1 && TimeCountRunFlag==0){LCD_ShowString(2,4,"  ");}
					else {LCD_ShowNum(2,4,TimeCount[1],2);}	//显示分
					if(TimeSelect==3 && FlashFlag==1 && TimeCountRunFlag==0){LCD_ShowString(2,7,"  ");}
					else {LCD_ShowNum(2,7,TimeCount[2],2);}	//显示秒
					LCD_ShowNum(2,10,TimeCount[3],2);	//显示十毫秒
				}
				else if(FlashFlag==1)	//如果倒计时结束,则第二行闪烁
				{
					LCD_ShowString(2,1,"           ");	//清空第二行
				}
				else
				{
					LCD_ShowString(2,1,"00:00:00 00");	//第二行显示
				}
			}
		}

		if(Mode==2)	//设置闹钟模式
		{
			if(KeyNum!=7 && KeyNum!=8)	//如果长按K3或K4则不执行此模式中的代码,防止闪烁
			{
				if(ExecuteOnceFlag)
				{
					ExecuteOnceFlag=0;	//静态字符显示标志清零
					LCD_Clear();	//清屏
					LCD_ShowString(1,1,"Alarm1   :  ");	//显示静态字符
					LCD_ShowString(2,1,"Alarm2   :  ");	//显示静态字符
				}
				if(TimeSelect==0 && FlashFlag==1){LCD_ShowString(1,8,"  ");}
				else {LCD_ShowNum(1,8,AlarmClock[0],2);}	//显示时(闹钟1)
				if(TimeSelect==1 && FlashFlag==1){LCD_ShowString(1,11,"  ");}
				else {LCD_ShowNum(1,11,AlarmClock[1],2);}	//显示分(闹钟1)
				if(TimeSelect==2 && FlashFlag==1){LCD_ShowString(2,8,"  ");}
				else {LCD_ShowNum(2,8,AlarmClock[2],2);}	//显示时(闹钟2)
				if(TimeSelect==3 && FlashFlag==1){LCD_ShowString(2,11,"  ");}
				else {LCD_ShowNum(2,11,AlarmClock[3],2);}	//显示分(闹钟2)
			}
		}
	}
}

void Timer0_Routine() interrupt 1	//定时器0中断函数
{
	static unsigned int T0Count0,T0Count1,T0Count2,T0Count3,T0Count4,T0Count5;	//定义静态变量
	TL0=0x00;	//设置定时初值,定时10ms,晶振@11.0592Hz
	TH0=0xDC;	//设置定时初值,定时10ms,晶振@11.0592Hz
	if(TimeCountRunFlag)	//如果启动标志为1,则进行计时
	{
		if(TimeCountModeFlag)	//正计时
		{
			T0Count0++;
			if(T0Count0>=1)
			{
				T0Count0=0;
				TimeCount[3]++;
				if(TimeCount[3]>=100)	//进位
				{
					TimeCount[3]=0;
					TimeCount[2]++;
					if(TimeCount[2]>=60)
					{
						TimeCount[2]=0;
						TimeCount[1]++;
						if(TimeCount[1]>=60)
						{
							TimeCount[1]=0;
							TimeCount[0]++;
							if(TimeCount[0]>=100)	//最大计时,99h59m59s
							{
								TimeCount[0]=0;
							}
						}
					}
				}
			}
		}
		else	//倒计时
		{
			T0Count0++;
			if(T0Count0>=1)
			{
				T0Count0=0;
				TimeCount[3]--;
				if(TimeCount[3]<0)
				{
					if(TimeCount[0]==0 && TimeCount[1]==0 && TimeCount[2]==0)
					{
						TimeCount[3]=0;
						TimeCountRunFlag=0;	//暂停计时
						TimeIsUpFlag=1;	//倒计时结束
					}
					else
					{
						TimeCount[3]=99;
						TimeCount[2]--;
						if(TimeCount[2]<0)
						{
							TimeCount[2]=59;
							TimeCount[1]--;
							if(TimeCount[1]<0)
							{
								TimeCount[1]=59;
								TimeCount[0]--;
							}
						}
					}
				}
			}
		}
	}
	T0Count1++;
	T0Count2++;
	T0Count3++;
	T0Count4++;
	T0Count5++;
	if(T0Count1>=50)	//设置时闪烁周期为1s
	{
		T0Count1=0;
		FlashFlag=!FlashFlag;
	}
	if(T0Count2>=2)	//每隔20ms检测一次按键
	{
		T0Count2=0;
		Key_Tick();
	}
	if(T0Count3>=10)	//每隔100ms读取一次时间
	{
		T0Count3=0;
		ReadTimeFlag=1;
	}
	if(T0Count4>=30)	//每隔300ms读取一次温度
	{
		T0Count4=0;
		ReadTempFlag=1;
	}
}

void Timer1_Routine() interrupt 3	//定时器1中断函数
{
	static unsigned char T1Count;	//定义静态变量
	TL1=0x33;	//设置定时初值,定时500us,晶振@11.0592Hz
	TH1=0xFE;	//设置定时初值,定时500us,晶振@11.0592Hz
	if(KeypressFlag)	//如果有按键按下
	{
		Buzzer=!Buzzer;
		KeypressSoundTime++;
		if(KeypressSoundTime==200)	//按键声响100ms
		{
			KeypressSoundTime=0;	//按键声计时清零
			KeypressFlag=0;	//按键按下标志置0
			Buzzer=1;	//蜂鸣器低电平导通,防止不响的时候导通
		}
	}
	if(AlarmFlag)	//闹钟1或闹钟2或倒计时结束
	{
		if(AlarmSoundCount==1 || AlarmSoundCount==3){Buzzer=!Buzzer;}	//第1个和第3个100ms翻转蜂鸣器,让它响
		T1Count++;
		if(T1Count>=200)	//100ms
		{
			T1Count=0;
			AlarmSoundCount++;
			if(AlarmSoundCount==10)
			{
				AlarmSoundCount=0;
				AlarmSoundTime++;	//约每隔1s钟AlarmSoundTime自增
				if(AlarmSoundTime==AlarmTimeLength){AlarmStop();}	//时间到达设定的长度就停止闹钟
			}
		}
	}
}

总结

感觉还是比较实用,睡觉前设置好闹钟放在桌面,可以有效防止迷迷糊糊按掉手机闹钟导致迟到。而且DS3231时钟模块比DS1302时钟模块的走时要准很多,设置好时间后,可以很久都不用调整时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2272064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通义视觉推理大模型QVQ-72B-preview重磅上线

Qwen团队推出了新成员QVQ-72B-preview&#xff0c;这是一个专注于提升视觉推理能力的实验性研究模型。提升了视觉表示的效率和准确性。它在多模态评测集如MMMU、MathVista和MathVision上表现出色&#xff0c;尤其在数学推理任务中取得了显著进步。尽管如此&#xff0c;该模型仍…

企业级Nosql数据库和Redis集群

一、关系数据库和Nosql数据库 关系数据库 定义&#xff1a;关系数据库是建立在关系模型基础上的数据库。它使用表格&#xff08;关系&#xff09;来存储数据&#xff0c;通过行和列的形式组织信息。例如&#xff0c;一个简单的学生信息表可能有 “学号”“姓名”“年龄”“班级…

Ant Design中Flex布局、Grid布局和Layout布局详解

好的&#xff0c;我们来更详细地探讨 Ant Design 中的 Flex布局、Grid布局 和 Layout布局 的特点、用法、适用场景&#xff0c;以及如何灵活运用它们来构建页面。下面将从各个方面进行更深入的分析&#xff0c;并提供具体的实例。 VueFlex布局实现响应式布局 1. Flex布局 概念…

基于FPGA的SNN脉冲神经网络之IM神经元verilog实现,包含testbench

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 vivado2019.2 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff0…

健身房管理系统多身份

本文结尾处获取源码。 本文结尾处获取源码。 本文结尾处获取源码。 一、相关技术 后端&#xff1a;Java、JavaWeb / Springboot。前端&#xff1a;Vue、HTML / CSS / Javascript 等。数据库&#xff1a;MySQL 二、相关软件&#xff08;列出的软件其一均可运行&#xff09; I…

三甲医院等级评审八维数据分析应用(四)--数据质量管理篇

一、引言 1.1 研究背景与意义 在医疗卫生领域,医院评审是衡量医院综合实力、保障医疗服务质量的重要手段。其中,三甲评审作为我国医院评审体系中的最高级别,对医院的管理、医疗技术、服务质量等各方面都设定了严格标准。医务科作为医院医疗质量管理的核心部门,肩负着协调…

Solidity合约编写(一)

Solidity IDE地址&#xff1a;Remix - Ethereum IDE 点击进入后在contract文件夹下创建合约 合约代码如下&#xff1a; // SPDX-License-Identifier: MIT pragma solidity ^0.8.26;contract SimpleStorage{bool hasFavorNumtrue;uint256 favorNum5;string favorNums"fiv…

嵌入式系统(将软件嵌入到硬件里面)

目录 Linux起源 查看操作系统的版本 查看内核的版本&#xff1a; 内核系统架构 系统关机或重启命令 关机&#xff1a; 重启&#xff1a; linux下的软件安装 两种软件包管理机制&#xff1a; deb软件包分为两种&#xff1a; 软件包的管理工具&#xff1a;dpkg apt 1…

会员制电商创新:开源 AI 智能名片与 2+1 链动模式的协同赋能

摘要&#xff1a;本文聚焦于电商领域会员制的关键作用&#xff0c;深入探讨在传统交易模式向数字化转型过程中&#xff0c;如何借助开源 AI 智能名片以及 21 链动模式商城小程序&#xff0c;实现对会员数据的精准挖掘与高效利用&#xff0c;进而提升企业的营销效能与客户洞察能…

Day29 - 大模型RAG,检索增强生成

如何调用第三方大模型 阿里大模型 1. 获取百炼平台 api key 【阿里云】-【产品】-【人工智能与机器学习】-【大模型服务平台百炼】 2. 参考LangChain文档 https://python.langchain.com/docs/introduction/ 3. 连接阿里大模型 """1. 引入模型 "&quo…

谷粒商城项目125-spring整合high-level-client

新年快乐! 致2025年还在努力学习的你! 你已经很努力了&#xff0c;今晚就让自己好好休息一晚吧! 在后端中选用哪种elasticsearch客户端&#xff1f; elasticsearch可以通过9200或者9300端口进行操作 1&#xff09;9300&#xff1a;TCP spring-data-elasticsearch:transport-…

c语言的文件操作与文件缓冲区

目录 C语言文件操作函数汇总 简单介绍文件 为什么使用文件 什么是文件 文件名 二进制文件和文本文件 流和标准流 流 标准流 文件指针 文件的打开和关闭 文件的顺序读写 顺序读写函数介绍 文件的随机读写 fseek ftell rewind 文件读取结束的判定 文件缓冲区 缓…

http源码分析

一、HttpURLConnection http连接池源码分析 二、HttpClient 连接池&#xff0c;每个路由最大连接数 三、OkHttp okhttp的连接池与socket连接

【工具整理】WIN换MAC机器使用工具整理

最近公司电脑升级&#xff0c;研发同学统一更换了 Mac Book Pro 笔记版电脑&#xff0c;整理一下安装了那些软件以及出处&#xff0c;分享记录下&#xff5e; 知识库工具 1、语雀 网址&#xff1a;语雀&#xff0c;为每一个人提供优秀的文档和知识库工具 语雀 个人花园&…

【GUI-pyqt5】QWidget类

1. 描述 所有可视空间的基类是一个最简单的空白控件控件是用户界面的最小元素 接收各种事件&#xff08;鼠标、键盘&#xff09;绘制在桌面上&#xff0c;显示给用户看 每个控件都是矩形的&#xff0c;它们按z轴顺序排序控件由其父控件和前面的控件剪切没有父控件的控件&#…

SpringBoot Maven 项目 pom 中的 plugin 插件用法整理

把 SpringBoot Maven 项目打包成 jar 文件时&#xff0c;我们通常用到 spring-boot-maven-plugin 插件。 前面也介绍过&#xff0c;在 spring-boot-starter-parent POM 和 spring-boot-starter POM 中都有插件的管理&#xff0c;现在我们就撸一把构建元素中插件的用法。 一、…

springboot实战纪实-课程介绍

教程介绍 Spring Boot是由Pivotal团队提供的一套开源框架&#xff0c;可以简化spring应用的创建及部署。它提供了丰富的Spring模块化支持&#xff0c;可以帮助开发者更轻松快捷地构建出企业级应用。 Spring Boot通过自动配置功能&#xff0c;降低了复杂性&#xff0c;同时支持…

源代码编译安装X11及相关库、vim,配置vim(2)

一、编译安装vim 编译时的cofigure选项如下.只有上一步的X11的包安装全了&#xff08;具体哪些是必须的&#xff0c;哪些是多余的没验证&#xff09;&#xff0c;configure才能认为X的库文件和头文件是可以用的 ./configure --prefixpwd/mybuild \--x-includes/path/to/X11/m…

直接插入排序、折半插入排序、2路插入排序、希尔排序

本篇是排序专栏博客的第一篇&#xff0c;主要探讨以 “插入” 为核心思想的排序算法该如何实现 文章目录 一、前言二、直接插入排序1. 算法思想与操作分析2. 代码实现version 1version 2 3. 复杂度分析 三、折半插入排序1. 算法思想与操作分析2. 代码实现3. 复杂度分析 四、2路…

Ansible之批量管理服务器

文章目录 背景第一步、安装第二步、配置免密登录2.1 生成密钥2.2 分发公钥2.3 测试无密连接 背景 Ansible是Python强大的服务器批量管理 第一步、安装 首先要拉取epel数据源&#xff0c;执行以下命令 yum -y install epel-release安装完毕如下所示。 使用 yum 命令安装 an…