蓝桥杯第八届赛题----电子钟程序设计范例

本文档详细介绍了如何设计一个基于DS1302和DS18B20的电子时钟,涵盖了硬件驱动、数码管显示、温度测量、闹钟设置等功能。通过状态数控制数码管显示内容,实现时间、温度和闹钟模式的切换。同时,文中还涉及了按键扫描、数码管闪烁和LED灯光闪烁的实现。整个系统通过定时器中断进行管理,确保了各个功能的流畅运行。
摘要由CSDN通过智能技术生成

前言

咕咕咕了许久,终于又写了一篇。。。。这次代码写的时间明显超5个小时了。。我是真的拉。。。

赛题要求

基本要求分析

制作一个电子时钟,要求数码管显示DS1302的获取时间以及DS18B20的温度,并使用四个按键进行控制。
如图所示,所需的硬件基础驱动有:
1、LED灯光开启与关闭(74HC138与74HC573操控)
2、数码管动态刷新(74HC138与74HC573操控)(定时器中断控制)
3、DS18B20计算温度(单总线通信协议)
4、DS1302写入读出时间(SPI总线通信协议)
5、按键扫描与消抖
在这里插入图片描述
看完硬件,我们再来看功能
在这里插入图片描述
首先是初始化,需要用到三八译码器和锁存器对蜂鸣器、继电器等设备进行关闭。
我们知道DS1302是BCD码,所以我们可以设置Time[]数组作为DS1302的数据,并初始化时间为{0x50,0x59,0x23,…}
在这里插入图片描述
此种格式显示,只需要DS1302的小时,分钟,秒的部分,以及DS18B20求出的整数部分即可!
在这里插入图片描述
根据此按键功能分析,我们可以设置一个八位的状态数,按键通过改变这个状态数的值来控制数码管显示。比如当这个状态数是0xa0,显示温度,是0xf1时小时的时间可以进行设置。
为此,可以设计一个Time_Con的八位数据作为状态数
在这里插入图片描述
不理解的话继续往下看,代码部分我会对这个状态数详细介绍。

在这里插入图片描述
该功能我们需要用到定时器中断与标志位,不能直接再主循环中使用。添加闹钟闪烁状态数。

赛题中所需的基础技能

1、基本硬件

1、LED灯光开启与关闭(74HC138与74HC573操控)
2、数码管动态刷新(74HC138与74HC573操控)(定时器中断控制)
3、DS18B20计算温度(单总线通信协议)
4、DS1302写入读出时间(SPI总线通信协议)
5、按键扫描与消抖

2、基本功能逻辑

1、运用锁存器和三八译码器函数控制各个部件
2、设置状态数通过状态选择控制不同功能
3、用定时器中断设置标志位控制灯光闪烁

代码部分

初始化

基本外设初始化

这算是老生常谈的基本了。
首先就是必要的138译码器选择函数和关闭蜂鸣器函数

void Sel_HC138(unsigned char n)
{
	switch(n)
	{
		case 4:P2 = (P2&0x1f)|0x80;break;//LED
		case 5:P2 = (P2&0x1f)|0xa0;break;//BUZZ and RELAY
		case 6:P2 = (P2&0x1f)|0xc0;break;//数码管正极
		case 7:P2 = (P2&0x1f)|0xe0;break;//数码管阴极
		case 0:P2 = (P2&0x1f);break;
		default:break;
	}
}
void Close_BUZZ()
{
	Sel_HC138(5);
	P0 = 0x00;
	Sel_HC138(0);
}

之后初始定时器0中断

DS1302的初始化

蓝桥杯的程序中,会提前将DS1302的驱动给我们,而DS1302的初始化也会含在文件中

void DS1302_Config()
{
	unsigned char i;
	Write_Ds1302_Byte( 0x8e, 0x00);
	for(i = 0; i < 7; i++)
	{
		Write_Ds1302_Byte( Write_DS1302_adrr[i], Timer[i]);
	}
	Write_Ds1302_Byte( 0x8e, 0x80);
}

这里注意,我们是将该文件中的Timer[i]作为初始化的值直接用在DS1302中,可Timer[i]还有一个十分重要的功能不能忘掉,这个数组不仅仅是初始化要用,因为官方自带的读取DS1302内部数据的函数也是将数据传给了Timer[i]这个数组!!如下

void Read_DS1302_Timer()
{
	unsigned char i;
	for(i = 0; i < 7; i++)
	{
		Timer[i] = Read_Ds1302_Byte( Read_DS1302_adrr[i] );
	}
}

因此,为了方便后面按键改变Timer[i]的值,我们需要将官方所给的DS1302的驱动文件中的Timer[]数组变成可以用在其它页面的全局变量!!!
c语言中,全局变量前加上extern声明,便可以使该数据在所有页面可用,但是,extern声明后的变量,不能在本页面初始化,但可以在其他页面初始化。
如:
//-------DS1302.c中----------
extern unsigned char Timer[7]; //正确
extern unsigned char Timer[7] = {0x50, 0x59, 0x23, 0x19, 0x02, 0x05, 0x21};//错误
//-------main.c中---------
unsigned char Timer[7] = {0x50, 0x59, 0x23, 0x19, 0x02, 0x05, 0x21};//正确

完成对Timer[i]的设置后,我们就可以将官方文件中的DS1302_Config()函数复制进主函数对DS1302进行初始化。

DS18B20初始化

和DS1302一样,DS18B20也有着官方给的读入读出代码,但和DS1302不一样,DS18B20的数据获取与分析需要我们自己写一个函数。
流程应该为
1、复位DS18B20。
2、向DS18B20中写入0xCC指令,跳过ROM操作。
3、向DS18B20中写入0x44指令,开始温度转换。
4、延时700ms左右,等待温度转换成功。
5、再次复位。
6、再次向DS18B20中写入0xCC指令,跳过ROM操作。
7、向写入0xBE指令,读取高速暂存器。
8、读取温度第八位。
9、读取温度高八位。
10、按要求处理温度数据。
代码为:

unsigned int Cal_DS13B20()
{
	unsigned char LSB,MSB;
	unsigned int temp;
	init_ds18b20();		      		//DS18B20复位	
	Write_DS18B20(0xCC);	      //跳过ROM操作指令
	Write_DS18B20(0x44);        //开始温度转换
	Delay_OneWire(100);         //延时700ms左右,等待温度转换完成
	
	init_ds18b20();
	Write_DS18B20(0xCC);	      //跳过ROM操作指令	
	Write_DS18B20(0xBE);	      //开始读取高速暂存器		
	
	LSB = Read_DS18B20();	      //读取温度数据的低8位
	MSB = Read_DS18B20();	      //读取温度数据的高8位
	init_ds18b20();		      		//DS18B20复位
	
	temp = MSB;
	temp = (temp<<8)|LSB;         //将LSB和MSB整合成为一个16位的整数
	temp >>= 4;					  //因为题目要求只需要整数部分即可
	return temp;
}

闹钟函数初始化

设置0.2s时间和5s时间以及LED灯闪烁的标志位,再设置一个与Timer[]相似的闹钟数组,这里我设置的是Timer_BUZZ[]。
设置判断Timer_BUZZ[]是否与读出的Timer[]内数据相同的函数。

void BUZZ_Judge()
{
	unsigned char i;
	for(i=0;i<3;i++)
	{
		if(Timer_BUZZ[i] != Timer[i])
			return;
	}
	LED_Time = 1;    //LED闪烁标志位,等于1代表开启
}

各功能的实现

此时我们可以得出主函数大致

void main()
{
	Close_BUZZ();  			//关闭蜂鸣器
	Init_T0(); 		 		//初始化定时器0
	DS1302_Config();  		//初始化DS1302
	
	while(1)
	{
		BUZZ_Judge();		//判断时间是否与闹钟时间相同
		T = Cal_DS13B20();	//计算温度
		KeyAction();		//检测按键(基于金沙滩的按键检测扫描)
		if(ms200 == 1)		//每隔200ms进入一次
		{
			ms200 = 0;				//置位
			Read_DS1302_Timer();    //读取当前传感器时间
			SMG_Cal();				//计算对应数码管时间
		}
	}
}

经过初始化,我们已经得到了DS18B20和DS1302各个传感器的数据,之后只需要再while(1)的大循环中不停的取得数据就可以,但是数码管只有8个,每次只能显示一组数据,所以我们需要状态值来改变数码管的显示状态!

状态数详解

设置char型数据Time_Con作为不同状态下的指示
在这里插入图片描述

实时时钟设置模式:数码管进入时钟设置模式。并咋瓦鲁多
0xf1代表小时段数码管闪烁,并可以对其进行显示。
0xf2代表分钟段数码管闪烁,并可以对其进行显示。
0xf3代表秒段数码管闪烁,并可以对其进行显示。
0xf4时,向DS1302写入设置好的时间,Time_Con归零,回到时间正常流动的模式。
闹钟设置模式:数码管键入闹钟设置模式,显示闹钟设定时间
0xe1代表小时段数码管闪烁,并可以对其进行显示。
0xe2代表分钟段数码管闪烁,并可以对其进行显示。
0xe3代表秒段数码管闪烁,并可以对其进行显示。
0xe4时,Time_Con归零,回到时间正常流动的模式。
温度查看模式:数码管进入温度显示状态。
默认时钟流动模式:也就是什么也不干的正常模式。

我们将数码管数据计算的函数变成这样

void SMG_Cal()
{
	if(((Time_Con>>4)!=0x0e)&&((Time_Con>>4)!=0x0a))//显示时间
	{
		SMG_Show[0] = SMG_Num[Timer[0]&0x0f];
		SMG_Show[1] = SMG_Num[(Timer[0]&0x7f)>>4];
		SMG_Show[2] = 0xBF;
		SMG_Show[3] = SMG_Num[Timer[1]&0x0f];
		SMG_Show[4] = SMG_Num[Timer[1]>>4];
		SMG_Show[5] = 0xBF;
		SMG_Show[6] = SMG_Num[Timer[2]&0x0f];
		SMG_Show[7] = SMG_Num[Timer[2]>>4];
	}
	else if((Time_Con>>4)==0x0e)			//显示闹钟
	{
		SMG_Show[0] = SMG_Num[Timer_BUZZ[0]&0x0f];
		SMG_Show[1] = SMG_Num[(Timer_BUZZ[0]&0x7f)>>4];
		SMG_Show[2] = 0xBF;
		SMG_Show[3] = SMG_Num[Timer_BUZZ[1]&0x0f];
		SMG_Show[4] = SMG_Num[Timer_BUZZ[1]>>4];
		SMG_Show[5] = 0xBF;
		SMG_Show[6] = SMG_Num[Timer_BUZZ[2]&0x0f];
		SMG_Show[7] = SMG_Num[Timer_BUZZ[2]>>4];
	}
	else if((Time_Con>>4)==0x0a)		//显示温度
	{
		SMG_Show[0] = 0xc6;
		SMG_Show[1] = SMG_Num[T%10];
		SMG_Show[2] = SMG_Num[(T/10)%10];
		SMG_Show[3] = 0xff;
		SMG_Show[4] = 0xff;
		SMG_Show[5] = 0xff;
		SMG_Show[6] = 0xff;
		SMG_Show[7] = 0xff;
	}
}

即可根据数据的改变而改变数码管显示内容。(因为懒所以省略基本数码管动态刷新教程)

由于状态改变导致各种状态的工作内容可能发生冲突,所以我们需要再主函数中加入限制。
比如:设置时间的模式里如果一直读取传感器内部的数值,则改好的Timer[]数组会被原本的数组替代。

void main()
{
	Close_BUZZ();  			//关闭蜂鸣器
	Init_T0(); 		 		//初始化定时器0
	DS1302_Config();  		//初始化DS1302
	
	while(1)
	{
		if((Time_Con>>4)!=0x0f) //没有进入设置状态时
			BUZZ_Judge();		//判断时间是否与闹钟时间相同
		T = Cal_DS13B20();		//计算温度
		KeyAction();			//检测按键
		if(ms200 == 1)
		{
			ms200 = 0;
			if((Time_Con>>4)!=0x0f) 	//没有进入设置状态时
				Read_DS1302_Timer();   	//读取当前传感器时间
			SMG_Cal();					//计算对应数码管时间
		}
	}
}

按键功能

按键动态扫描(基于金沙滩)这里就不说了,想看的可以站内查询。这里只说一下四种按键需要实现的工能代码罢。真不是我懒

按键7:

Time_Con在0x00状态时,按键7需要进入时钟设置模式,关闭DS1302的时间流动,此时改变Time_Con值为0xf1,若在再按则+1。
当按到最后一下时即Time_Con == 0xf4时,将此时设置好的Timer[i]值写入DS1302中,并打开时间流动。

Time_Con = (Time_Con|0xf0)+1;					//设置Time_Con状态
Write_Ds1302_Byte(0x8e, 0x00); 					//允许写入数据
Write_Ds1302_Byte(0x80, Timer[0]|0x80); 		//时钟震荡停止
if(Time_Con >= 0xf4)
{
	Write_Ds1302_Byte(0x80, Timer[0]&0x7f);
	for(i = 1; i < 7; i++)
	{
		Write_Ds1302_Byte( Write_DS1302_adrr[i], Timer[i]);
	}
	Write_Ds1302_Byte(0x8e, 0x80); //重新写入Timer值并开始震荡退出设置模式
	Time_Con = 0x00; 							 //重置状态数字
}
按键6:

设置闹钟状态,状态数改为0xen,此时数码管显示的内容就是闹钟时间。

Time_Con = (Time_Con|0xe0)+1;    //设置闹钟状态
if(Time_Con>=0xe4)
{
	Time_Con = 0x00;
}
按键5:

默认状态按着无效,但是进入闹钟设置状态,可以根据Time_Con的值进行对对应数码管的值进行加一运算。BUZZ代表闹钟,Time代表时钟

if((Time_Con>>4) == 0x0f)	  //当进入时间设置模式时
	Time_Add(Time_Con);		  //时钟增加当前时间的数值
else if((Time_Con >>4) == 0x0e)
	BUZZ_Add(Time_Con);       //闹钟

根据Time_Con的后四位进行不同BCD转时间计数的加法运算

void Time_Add(unsigned char Time_Con)   //设置模式时间增加函数
{
	switch(Time_Con)
	{
		case 0xf1:
			Timer[2]++;										//按下按键小时时间增加1
				if(Timer[2] >= 0x24)				//当所得值为24
				{
					Timer[2] = 0x00;					//全部归零
				}
			if((Timer[2]&0x0f) == 0x0a)     //当后四位值变成10
			{
				Timer[2] = Timer[2]&0xf0;		//后四位归零
				Timer[2] += 16;							//前四位进1
			}
		break;//小时  Time[2]
		case 0xf2:
			Timer[1]++;										//按下按键分钟时间增加1
			if((Timer[1]&0x0f) == 0x0a)     //当后四位值变成10
			{
				Timer[1] = Timer[1]&0xf0;		//后四位归零
				Timer[1] += 16;							//前四位进1
				if(Timer[1] >= 0x60)
					Timer[1] = 0x00;
			}
		break;//分钟	Time[1]
		case 0xf3:
			Timer[0]++;										//按下按键秒时间增加1
			if((Timer[0]&0x0f) == 0x0a)   //当后四位值变成10
			{
				Timer[0] = Timer[0]&0xf0;		//后四位归零
				Timer[0] += 16;							//前四位进1
				if(Timer[0] >= 0x60)
					Timer[0] = 0x00;
			}
		break;//秒		Time[0]
		default:break;
	}
}
void BUZZ_Add(unsigned char Time_Con)   //设置模式时间增加函数
{
	switch(Time_Con)
	{
		case 0xe1:
			Timer_BUZZ[2]++;										//按下按键小时时间增加1
				if(Timer_BUZZ[2] >= 0x24)				//当所得值为24
				{
					Timer_BUZZ[2] = 0x00;					//全部归零
				}
			if((Timer_BUZZ[2]&0x0f) == 0x0a)     //当后四位值变成10
			{
				Timer_BUZZ[2] = Timer_BUZZ[2]&0xf0;		//后四位归零
				Timer_BUZZ[2] += 16;							//前四位进1
			}
		break;//小时  Time[2]
		case 0xe2:
			Timer_BUZZ[1]++;										//按下按键分钟时间增加1
			if((Timer_BUZZ[1]&0x0f) == 0x0a)     //当后四位值变成10
			{
				Timer_BUZZ[1] = Timer_BUZZ[1]&0xf0;		//后四位归零
				Timer_BUZZ[1] += 16;							//前四位进1
				if(Timer_BUZZ[1] >= 0x60)
					Timer_BUZZ[1] = 0x00;
			}
		break;//分钟	Time[1]
		case 0xe3:
			Timer_BUZZ[0]++;										//按下按键秒时间增加1
			if((Timer_BUZZ[0]&0x0f) == 0x0a)   //当后四位值变成10
			{
				Timer_BUZZ[0] = Timer_BUZZ[0]&0xf0;		//后四位归零
				Timer_BUZZ[0] += 16;							//前四位进1
				if(Timer_BUZZ[0] >= 0x60)
					Timer_BUZZ[0] = 0x00;
			}
		break;//秒		Time[0]
		default:break;
	}
}
按键4:

此处Sub为减法,方法与按键5所示一至,不过多阐述。

在普通模式下按下此按键可以显示温度,Time_Con更改状态为温度显示状态。

if((Time_Con>>4) == 0x0f)
	Time_Sub(Time_Con);
else if((Time_Con >>4) == 0x0e)
	BUZZ_Sub(Time_Con);
else if(Time_Con == 0x00)
	Time_Con = 0xa0;

按键抬起时更换为普通状态,我们可以直接在按键消抖函数(基于金沙滩)中添加判断抬起的if语句。

void KeyAction()
{
	static unsigned char backup[4] = {1,1,1,1};
	unsigned char i;
	for(i=0;i<4;i++)
	{
		if(backup[i] != KeySta[i])
		{
			if(backup[i] == 0)    //当前一状态按下,这次改变的状态就是抬起
			{
				if((i==2)&&((Time_Con>>4)==0xa0))//若改变的时第二个按键并且状态数为温度显示状态时
				{
					Time_Con = 0x00;//重置状态,使其变回原样
				}
			}
			else
				Action(KeyNum[i]);//若并非前一状态为抬起,现在状态就是按下,执行按键动作函数。
			backup[i] = KeySta[i];
		}
	}
}

设置数码管闪烁功能

由于数码管闪烁间隔一秒,放在主函数中影响其它进程,所以我们将其放入定时器中断中。
基于之前数码管和按键的动态扫描,我们已经设置每隔1ms进入一次中断,所以只需设置0.2s的标志位(便于之后闹钟灯光闪烁)与0.2s经过5次的标志位。

static unsigned char s_5 = 0;
static unsigned char i = 0;
i++;
	if(i >= 200)        //每隔200ms执行一次
	{
		i = 0;
		ms200 = 1;			//0.2s时间执行完毕
		s_5++;          //5次0.2s函数
		if(s_5 == 5)		//当1s时间完毕
		{
			s_5 = 0;			//时间归零
			F_1s = ~F_1s; //F_1s取反
		}
	}

每当1s之后,设置的F_1s标志位取反。
由于只要当Time_Con处于设置状态而非显示状态时,数码管都要闪烁,所以我们干脆在数码管扫描,注意是扫描函数中判断当前Time_Con是否是设置模式,若是设置模式,则根据Time_Con的后四位和设置的1s标志位决定哪个灯的亮灭

void SMGshow(unsigned char index)
{
	Sel_HC138(6);
	P0 = (0x80>>index);
	if(F_1s == 1) //1s间隔
	{
		switch(Time_Con&0x0f)  //选择第几个数码管
		{
			case 0x03:P0 = P0&0x3f;break; //关闭对应数码管
			case 0x02:P0 = P0&0xe7;break;
			case 0x01:P0 = P0&0xfc;break;
			default:break;
		}
	}
	Sel_HC138(7);
	P0 = SMG_Show[index];
	Sel_HC138(0);
}

闹钟灯光闪烁功能

由于灯光闪烁持续五秒,放在主函数中影响其它进程,所以我们将其放入定时器中断中。
于是,完整的中断函数。

void it0() interrupt 1
{
	static unsigned char s_5 = 0;
	static unsigned char i = 0;
	TH0 = (65535 - 1000)/256;
	TL0 = (65535 - 1000)%256;
	
	KeyScan();
	SMGshow(i%8);
	i++;
	if(i >= 200)        //每隔200ms执行一次
	{
		if(LED_Time)      //当LED闪烁模式开启
		{
			LED_ON();       //LED灯光闪烁函数
			F_5s++;
			if(F_5s == 25)  //当到达5s时
			{
				LED_Time = 0;	//LED闪烁时间结束
				F_5s = 0;			//5s计数归零
				LED_OFF();		//关闭LED!
			}
		}
		else	//当LED闪烁标志位为零,关闭灯光!
		{
			F_5s = 0;			//5s计数归零
			LED_OFF();		//关闭LED!
		}
		i = 0;
		ms200 = 1;			//0.2s时间执行完毕
		s_5++;          //5次0.2s函数
		if(s_5 == 5)		//当1s时间完毕
		{
			s_5 = 0;			//时间归零
			F_1s = ~F_1s; //F_1s取反
		}
	}
}

又因为闹钟闪烁时,不论按什么按键,都是关闭闪烁,所以我们就继续对按键函数下刀

void KeyAction()
{
	static unsigned char backup[4] = {1,1,1,1};
	unsigned char i;
	for(i=0;i<4;i++)
	{
		if(backup[i] != KeySta[i])
		{
			if(backup[i] == 1)		//按键按下时
			{
				if(LED_Time == 1)   //若LED闪烁标志位位1代表现在正在闪烁
				{
					LED_Time = 0;	//标志位打为0,每1ms的中断检测到标志位为0,自动启动LED_OFF()函数关闭灯光
				}
				else
				Action(KeyNum[i]);
			}
			else if(backup[i] == 0)
			{
				if((i==2)&&((Time_Con>>4)==0xa0))
				{
					Time_Con = 0x00;
				}
			}
			backup[i] = KeySta[i];
		}
	}
}

总结

第八届赛题总体上来说不算难,都是一些很基础的地方,我觉得难点在于BCD码转化为十进制的加减运算,而重点在于对状态数的设计。

源码

度娘网盘
链接:https://pan.baidu.com/s/1hWJC9nT68sRoy5SgQktQiw
提取码:3l5e

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值