前言
咕咕咕了许久,终于又写了一篇。。。。这次代码写的时间明显超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