1、 项目介绍(设计内容)
基于AT89S52单片机的多功能电子万年历的硬件结构和软硬件设计方法。本设计由数据显示模块、温度采集模块、时间处理模块和调整设置模块四个模块组成。系统以AT89S52单片机为控制器,以串行时钟日历芯片DS1302记录日历和时间,它可以对年、月、日、时、分、秒进行计时,还具有闰年补偿等多种功能。温度采集选用DS18B20芯片,万年历采用直观的数字显示,数据显示采用1602液晶显示模块,可以在LCD1602上同时显示年、月、日、周日、时、分、秒,还具有时间校准等功能。
2、 项目的总体设计
系统的功能往往决定了系统采用的结构,经过成本,性能,功耗等多方面的考虑决定用三个8位74LS164串行接口外接LED显示器,RESPACK-8对单片机AT89S52进行供电,时间芯片DS1302连接单片机AT89S52。从而实现电子万年历的功能。
按照系统设计的要求,初步确定系统由电源模块、时钟模块、显示模块、键盘接口模块、温度测量模块和闹钟模块共六个模块组成,电路系统构成框图如图1所示。
图1 硬件电路框图
图2 主程序流程图
3、 设计思路方法及实现步骤(包括硬件设计和软件设计两个部分)
3.1 Proteus仿真图
单片机电子万年历的制作有多种方法,可供选择的器件和运用的技术也有很多种。所以,系统的总体设计方案应在满足系统功能的前提下,充分考虑系统使用的环境,所选的结构要简单使用、易于实现,器件的选用着眼于合适的参数、稳定的性能、较低的功耗以及低廉的成本。
系统的功能往往决定了系统采用的结构,经过成本,性能,功耗等多方面的考虑决定用三个8位74LS164串行接口外接LED显示器,RESPACK-8对单片机AT89S52进行供电,时间芯片DS1302连接单片机AT89S52。从而实现电子万年历的功能。
按照系统设计的要求,初步确定系统由电源模块、时钟模块、显示模块、键盘接口模块、温度测量模块和闹钟模块共六个模块组成,电路系统构成框图如图1所示。
图3 硬件电路框图
3.2 DS1302读写程序设计
本系统的时间读取主要来源于单片机对DS1302的操作,在硬件上时钟芯片DS1302与单片机的连接需要三条线,即SCLK(7)、I/O(6)、RST(5),具体连接图见系统硬件设计原理图。读取写程序设计如下:
函 数 名:RTInputByte()
功 能:实时时钟写入一字节
说 明:往DS1302写入1Byte数据 (内部函数)
入口参数:d 写入的数据
返 回 值:无
void RTInputByte(uchar d)
{
uchar i;
ACC = d;
for(i=8; i>0; i--)
{
T_IO = ACC0; /*相当于汇编中的 RRC */
T_CLK = 1;
T_CLK = 0;
ACC = ACC >> 1;
}
函 数 名:RTOutputByte()
功 能:实时时钟读取一字节
说 明:从DS1302读取1Byte数据 (内部函数)
入口参数:无
返 回 值:ACC
uchar RTOutputByte(void)
{
uchar i;
for(i=8; i>0; i--)
{
ACC = ACC >>1; /*相当于汇编中的 RRC */
ACC7 = T_IO;
T_CLK = 1;
T_CLK = 0;
}
return(ACC);
}
函 数 名:W1302()
功 能:往DS1302写入数据
说 明:先写地址,后写命令/数据 (内部函数)
调 用:RTInputByte() , RTOutputByte()
入口参数:ucAddr: DS1302地址, ucData: 要写的数据
返 回 值:无
void W1302(uchar ucAddr, uchar ucDa)
{
T_RST = 0;
T_CLK = 0;
T_RST = 1;
RTInputByte(ucAddr); /* 地址,命令 */
RTInputByte(ucDa); /* 写1Byte数据*/
T_CLK = 1;
T_RST = 0;
}
函 数 名:R1302()
功 能:读取DS1302某地址的数据
说 明:先写地址,后读命令/数据 (内部函数)
调 用:RTInputByte() , RTOutputByte()
入口参数:ucAddr: DS1302地址
返 回 值:ucData :读取的数据
uchar R1302(uchar ucAddr)
{
uchar ucData;
T_RST = 0;
T_CLK = 0;
T_RST = 1;
RTInputByte(ucAddr); /* 地址,命令 */
ucData = RTOutputByte(); /* 读1Byte数据 */
T_CLK = 1;
T_RST = 0;
return(ucData);
}
DS1302与微处理器进行数据交换时,首先由微处理器向电路发送命令字节,命令字节最高位MSB(D7)必须为逻辑 1,如果D7=0,则禁止写DS1302,即写保护;D6=0,指定时钟数据,D6=1,指定RAM数据;D5~D1指定输入或输出的特定寄存器;最低位LSB(D0)为逻辑0,指定写操作(输入),D0=1,指定读操作(输出) 。
3.3 完整代码
#include<reg52.h>
#include <intrins.h>
#define uint unsigned int
#define uchar unsigned char
char a,miao,shi,fen,ri,yue,nian,keynum;
int temp;//,year1,month1,day1;
#define h1 0x80 //LCD第一行的初始化位置
#define h2 0x80+0x40 //LCD第二行初始化位置
//定义1602相关管脚
sbit rs=P1^2;
sbit en=P1^0;
sbit rw=P1^1;
//DS1302芯片的管脚定义
sbit DSIO=P1^5;
sbit SCLK=P1^4;
sbit RST=P1^6;
sbit ACC0=ACC^0;//设置累加器
sbit ACC7=ACC^7;
//按键
sbit key1=P3^2;
sbit key2=P3^3;
sbit key3=P3^4;
void delay2(uint s)//延时,用于温度程序部分
{
while(s--);//区分i,用s表示
}
void delay(uint z)//延时函数
{
uint x,y;
for(x=z;x>0;x--)
for(y=110;y>0;y--);
}
void writecom(uchar com)//写入指令函数
{
rs=0;
rw=0;
P0=com;
delay2(1);
en=1;
delay2(1);
en=0;
}
void writedata(uchar dat)//写入数据函数
{
rs=1;
rw=0;
P0=dat;
delay2(1);
en=1;
delay2(1);
en=0;
}
void print(uchar a3,uchar *str)//写字符串函数
{
writecom(a3|0x80);
while(*str!='\0')
{
//delay(100);
writedata(*str++);
}
*str=0;
}
void lcdinit()
{
writecom(0x38);//设置为两行显示,8位显示
writecom(0x0c);//开显示,不显示光标
writecom(0x06);//光标右移
writecom(0x01);//清屏
}
void write_1302(uchar addr, uchar dat)
{
uchar n;
RST = 0;
_nop_();
SCLK = 0;//先将SCLK置低电平。
_nop_();
RST = 1; //然后将RST(CE)置高电平。
_nop_();
for (n=0; n<8; n++)//开始传送八位地址命令
{
DSIO = addr & 0x01;//数据从低位开始传送
addr >>= 1;
SCLK = 1;//数据在上升沿时,DS1302读取数据
_nop_();
SCLK = 0;
_nop_();
}
for (n=0; n<8; n++)//写入8位数据
{
DSIO = dat & 0x01;
dat >>= 1;
SCLK = 1;//数据在上升沿时,DS1302读取数据
_nop_();
SCLK = 0;
_nop_();
}
RST = 0;//传送数据结束
_nop_();
}
uchar read_1302(uchar addr )//从1302读数据函数,指定读取数据来源地址
{
uchar n,dat,dat1;
RST = 0;
_nop_();
SCLK = 0;//先将SCLK置低电平。
_nop_();
RST = 1;
_nop_();
for(n=0; n<8; n++)//开始传送八位地址命令
{
DSIO = addr & 0x01;
addr >>= 1;
SCLK = 1;
_nop_();
SCLK = 0;
_nop_();
}
_nop_();
for(n=0; n<8; n++)//读取8位数据
{
dat1 = DSIO;
dat = (dat>>1) | (dat1<<7);
SCLK = 1;
_nop_();
SCLK = 0;
_nop_();
}
RST = 0;
_nop_();
SCLK = 1;
_nop_();
DSIO = 0;
_nop_();
DSIO = 1;
_nop_();
return dat;
}
uchar turnBCD(uchar bcd)//BCD码转换为十进制函数
{
return((bcd>>4)*10+(bcd&0x0F));
}
void ds1302_init()//1302时钟芯片初始化函数
{
RST=0;
SCLK=0;
write_1302(0x8e,0x00);//允许写
write_1302(0x8e,0x80);//打开保护
}
//时分秒显示函数
void writetime(uchar add,uchar dat)//写入时分秒
{
uchar gw,sw;
gw=dat%10;//取得个位数
sw=dat/10;//取得十位数
writecom(h2+add);//第二行显示
writedata(0x30+sw);//显示该数字
writedata(0x30+gw);
}
//年月日显示函数
void writeday(uchar add,uchar dat)//写入年月日函数
{
uchar gw,sw;
gw=dat%10;//取得个位数字
sw=dat/10;//取得十位数字
writecom(h1+add);//在第一行显示
writedata(0x30+sw);
writedata(0x30+gw);//显示
}
//按键扫描函数
void keyscan()
{
if(key1==0)
{
delay(5);
if(key1==0)
{
while(!key1);
keynum++;
if(keynum>=8)
keynum=1;
switch(keynum)
{
case 1:TR0=0;
writecom(h2+0x0b);//秒的位置
writecom(0x0f);//设置为光标闪烁
temp=(miao)/10*16+(miao)%10;//秒化为bcd码
write_1302(0x8e,0x00);
write_1302(0x80,0x80|temp);//秒数据写入
write_1302(0x8e,0x80);
break;
case 2:writecom(h2+8);
break;
case 3:writecom(h2+5);
break;
case 4:writecom(h1+0x0c);
break;
case 5:writecom(h1+0x09);
break;
case 6:writecom(h1+0x06);
break;
case 7:writecom(0x0c);
TR0=1;//重新打开定时器
temp=(miao)/10*16+(miao)%10;
write_1302(0x8e,0x00);
write_1302(0x80,0x00|temp);//写入秒
write_1302(0x8e,0x80);
break;
}
}
}
if(keynum!=0)//当设置键按下时才能操作
{
if(key2==0)
{
delay(5);
if(key2==0)
{
while(!key2);
switch(keynum)
{
case 1:miao++;//
if(miao>=60) miao=0;
writetime(0x0a,miao);
temp=(miao)/10*16+(miao)%10;//转换为bcd码
write_1302(0x8e,0x00);//允许写
write_1302(0x80,temp);// 写入秒
write_1302(0x8e,0x80);//打开保护
writecom(h2+0x0b);//液晶模式为写入后自动右移,在此返回原来位置
break;
case 2:fen++;
if(fen>=60) fen=0;
writetime(0x07,fen);
temp=(fen)/10*16+(fen)%10;
write_1302(0x8e,0x00);
write_1302(0x82,temp);
write_1302(0x8e,0x80);
writecom(h2+0x08);
break;
case 3:shi++;
if(shi>=24) shi=0;
writetime(0x04,shi);
temp=(shi)/10*16+(shi)%10;
write_1302(0x8e,0x00);
write_1302(0x84,temp);
write_1302(0x8e,0x80);
writecom(h2+0x05);
break;
case 4:ri++;
if(ri>=32) ri=1;
writeday(0x0b,ri);
temp=(ri)/10*16+(ri)%10;
write_1302(0x8e,0x00);
write_1302(0x86,temp);
write_1302(0x8e,0x80);
writecom(h1+0x0c);
break;
case 5:yue++;
if(yue>=13) yue=1;
writeday(0x08,yue);
temp=(yue)/10*16+(yue)%10;
write_1302(0x8e,0x00);
write_1302(0x88,temp);
write_1302(0x8e,0x80);
writecom(h1+0x09);
break;
case 6:nian++;
if(nian>=100) nian=0;
writeday(0x05,nian);
temp=(int)((nian)/10*16+(nian)%10);
write_1302(0x8e,0x00);
write_1302(0x8c,temp);
write_1302(0x8e,0x80);
writecom(h1+0x06);
break;
}
}
}
//以下是减的函数
if(key3==0)
{
delay(5);//消除抖动
if(key3==0)
{
while(!key3);
switch(keynum)
{
case 1:miao--;
if(miao<0) miao=59;//减到-1返回59
writetime(0x0a,miao);//在十位数写入
temp=(miao)/10*16+(miao)%10;//转换为bcd码
write_1302(0x8e,0x00);//允许写
write_1302(0x80,temp);//写入秒
write_1302(0x8e,0x80);//打开保护
writecom(h2+0x0b);//返回个位位置
break;
case 2:fen--;
if(fen<0) fen=59;
writetime(0x07,fen);
temp=(fen)/10*16+(fen)%10;
write_1302(0x8e,0x00);
write_1302(0x82,temp);
write_1302(0x8e,0x80);
writecom(h2+8);
break;
case 3:shi--;
if(shi<0) shi=23;
writetime(0x04,shi);
temp=(shi)/10*16+(shi)%10;
write_1302(0x8e,0x00);
write_1302(0x84,temp);
write_1302(0x8e,0x80);
writecom(h2+0x05);
break;
case 4:ri--;
if(ri<1) ri=31;
writeday(0x0b,ri);
temp=(ri)/10*16+(ri)%10;
write_1302(0x8e,0x00);
write_1302(0x86,temp);
write_1302(0x8e,0x80);
writecom(h1+0x0c);
break;
case 5:yue--;
if(yue<1) yue=12;
writeday(0x08,yue);
temp=(yue)/10*16+(yue)%10;
write_1302(0x8e,0x00);
write_1302(0x88,temp);
write_1302(0x8e,0x80);
writecom(h1+0x09);
break;
case 6:nian--;
if(nian<0) nian=99;
writeday(0x05,nian);
temp=(int)((nian)/10*16+(nian)%10);
write_1302(0x8e,0x00);
write_1302(0x8c,temp);
write_1302(0x8e,0x80);
writecom(h1+0x06);
break;
}
}
}
}
}
void init()
{
TMOD=0x01;
TH0=(65536-60000)/256;//10毫秒
TL0=(65536-60000)%256;
EA=1;
ET0=1;//允许T0中断
TR0=1;//启动中断
}
void main()
{
lcdinit();
ds1302_init();
init();//定时器初始化函数
while(1)
{
keyscan();
}
}
void timer0() interrupt 1
{
TH0=(65536-60000)/256;
TL0=(65536-60000)%256;
// TR0=0;
//读取数据
miao=turnBCD(read_1302(0x81));
fen=turnBCD(read_1302(0x83));
shi=turnBCD(read_1302(0x85));
ri=turnBCD(read_1302(0x87));
yue=turnBCD(read_1302(0x89));
nian=turnBCD(read_1302(0x8d));
//显示数据
print(0x80+3,"20");
print(0x80+7,"/");
print(0x80+10,"/");
writeday(0x0b,ri);//显示日
writeday(0x08,yue);//显示月
writeday(0x05,nian);//显示年
print(0x40+6,":");
print(0x40+9,":");
writetime(0x0a,miao);//显示出秒
writetime(0x07,fen);//显示出分
writetime(0x04,shi);//显示出时,第二行第一个开始
}
4、 运行结果或者测试结果
①次电路主要是检测格其引脚电压是否正常,晶振和电源是否接好,检测硬件电路是否有短路、断路、虚焊等,以确保设计的可靠性和电器元件的性能。而电路中的电源电路、晶体振荡电路、按键接口电路及复位电路、闹钟电路等都是采用基础的电路设计,除了基础电路硬件调试外我们还可以通过软件来测试硬件,如通过下载口写入其它一个比较简单的程序,以便测试。
②首先由USB电源插口接入5V的直流电压供给系统使用。在这里接上一个发光二级管作为指示,单输入电压正常时,二极管亮,LCD同时显示正常。系统在正常工作时,LCD液晶上第一行显示时分秒和温度,第二行显示年月日和星期,如果想要对时间进行调整,可以通过调整设置模块来实现。当按下设置键P3.0键时可调节主页面的时分秒、年月日的调节,P3.1为调整加按键,P3.2为调整减按键,P3.3按下时可进入另一种模式。第二种模式可显示闰年,第三种模式可设置闹钟时间。如果想要退出该模式就在按一下P3.3即可。
③在硬件调试过程中,当接通电源的时候,我们发现液晶显示器没有工作,背光灯有亮但没有数据出来。但电源指示灯已亮,说明电源输入正常,待我们用万用表电路中各电压时发现,单片机各引脚电压也正常,显示器的各引脚也正常。经过同学与老师的帮助,发现程序出错,改后再接电源,电路一切正常。
5、 遇到的问题及解决的方法
出现电子数码万年历死机的现象:
此故障多为电压不稳和其他干扰造成的,首先更换记忆电池,可排除多数情况下的故障;仍不工作,拔下电源与主板连线,再次插上,部分死机故障可以恢复正常;若还不工作,可以在不通电的情况下,用镊子短路主板上的所有滤波电容,通电看是否正常工作,最后检查晶振。
出现走时不准现象:
5V电压低,记忆电池欠压,晶振性能不良,修理电源使其恢复正常,更换电池,更换晶振。
再次通电时间和日期出错:
这是很长见得故障,很可能是线路板自带的圆形电子没电了,长时间不用很可能会耗尽其自带的电量,那样的话就失去了其记忆功能。
6、总 结
在整个设计过程中,硬件方面主要设计了AT89S52单片机的最小系统、DS1302接口电路、DS18B20接口电路、闹钟及LCD显示;软件方面借助各个渠道的资料,主要设计了阳历数据读取程序、阳历转阴历程序、温度采集程序、闹铃程序以及LCD显示程序;系统的调试主要是通过一块AT89S52开发板,再借助于Keil、STC以及少许自己搭建的外围电路实现的;再此过程中,分步调试时显示出了阳历的日期及时间,还有实时温度,集中调试时没有达到预期效果。此万年历具有读显示直观、功能多样、电路简洁、成本低廉等诸多优点,符合电子仪器仪表的发展趋势,具有广阔的市场前景。
在整个设计过程中学到了许多没学到的知识,对电路的设计、布局要先有一个好的构思,才显得电路板美观、大方。程序编写中,由于思路不清晰,开始时遇到了很多的问题,经过静下心来思考,理清了思路,反而得心应手。在此次设计中,知道了做事要有一颗平常的心,不要想着走捷径,一步一脚印。也练就了我们的耐心,做什么事都要有耐心。在本次设计中学到了很多很多东西,这是最重要的
7、源码获取
万年历