1. 基于proteus的51单片机开发实例32-使用DS1302实现数字电子钟1.1. 实验目的
我们在学习51单片机定时器的相关内容时,了解到,在对时间要求不是很准确的情况下,可以使用51单片机的定时器中断实现电子钟。如果要求很精准的时钟,那就需要使用专门的时钟芯片。DS1302就是一款应用非常广泛、时间非常准确的时钟芯片。本实例的目的就是通过设计一幅基于实时时钟芯片DS1302的应用电路,通过51单片机控制DS1302的读写来获取时钟信息,并通过LCD1602液晶显示出年、月、日、时、分、秒。
图1 DS1302数字电子钟
1.2. 设计思路本实例通过设计实时时钟芯片DS1302的读写控制电路,实现51单片机对DS1302的时间设置、时钟读取功能,并在电路中加入LCD1602液晶显示电路,51单片机通过读取DS1302的时钟数据,并把这些数据送到LCD1602液晶显示。
1.3. 基础知识关于LCD1602的工作原理、电路实现、程序实现,我们在之前的例子中已经作过介绍,这里不再赘述。
本例主要讲解DS1302的原理、电路及编程控制。
DS1302能够计算2100年之前的年、月、日、时、分、秒、星期等信息。并且DS1302有闰年的调整功能。
1.3.1. DS1302引脚功能DS1302与单片机的接口很简单:只需三根I/O口线即可。下图是DS1302的引脚图。
VCC1:主电源
VCC2:备用电源输入,当VCC2>(VCC1 0.2V)时,由VCC2供电,当VCC2<VCC1时,由VCC1供电。
SCLK:串行时钟
RST:复位引脚,高有效。
I/O:数据输入/输出
X1,X2:外部晶振引脚。
1.3.2. DS1302的操作方法DS1302的操作有两种:从DS1302读取数据;向DS1302写入数据。执行这两种操作时,都需要通过特定的寄存器进行。下图所示是DS1302的寄存器。
例如:想要设置秒的初始值,需要先写入命令字H80,然后才能向秒寄存器写入初始值;再例如要读出当前时间的分钟数值,则需要先写入命令字83H,然后才能从分钟寄存器中读取数据。
1、将命令字写入DS1302
单片机与DS1302的通信时,必须是单片机首先发起。无论是对DS1302进行读数据或者写数据的操作,单片机都必须首先向DS1302发送一个命令帧,其格式如下。
Bit7为1时,允许写入,为0时,禁止写入;
Bit6为1时,对RAM操作,为0时,对时钟寄存器操作;
Bit5~Bit1是RAM或时钟寄存器的内部地址;
Bit0位1时,读操作,为0时,写操作。
DS1302还有两个状态寄存器命令字“0x8e”--允许将数据写入DS1302;“0x8f”--禁止将数据写入DS1302。
如果向状态寄存器写入数据“0x00”,表示不需要对DS1302写保护;
如果向状态寄存器写入数据“0x80”,表示对DS1302写保护,此时不能将数据写入其它时钟寄存器和数据寄存器;
1.3.3. DS1302数据读/写的方法DS1302的通信协议规定,在没有数据传递时,SCLK应保持低电平。如果RST由低电平变为高电平,则表示开始进行数据操作。RST为低电平时,禁止进行数据操作。
在SCLK的上升沿,数据被写入DS1302,在时钟脉冲的下降沿,从DS1302读出数据。
数据发送时,低位数据在前,高位数据在后。
1.4. 电路设计
本实例电路如图1所示。使用单片机的P3.5,P3.6,P3.7分别连接DS1302的RST,SCLK,I/O。注意这三个端口要连接上拉电阻。单片机的P0口连接LCD1602液晶的8根数据线,单片机P2.1,P2.2,P2.3分别连接LCD1602液晶的RS,RW,E。
1.5. 程序设计本程序采用模块化设计方法。包括4个模块程序:主程序模块,时钟芯片DS1302程序模块,延时程序模块,LCD1602液晶程序模块。
各模块程序代码如下:
主程序模块
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
#include"LCD1602.h" //包含1602液晶显示函数的头文件
#include"DS1302.h" //包含1302时钟读写的头文件
#include"Delay.h" //包含延时函数的头文件
unsigned char code DispDigit[10]={"0123456789"}; //定义字符数组显示数字
/*****************************************************
函数功能:主函数
***************************************************/
void main(void)
{
unsigned char Second,Minute,Hour,Day,Month,Year; //分别储存苗、分、小时,日,月,年
unsigned char ReadValue; //储存从1302读取的数据
LcdInitiate(); //液晶初始化
WriteAddress(0x00); //在第1行第1列开始显示
WriteData('D'); //显示:Date:
WriteData('a');
WriteData('t');
WriteData('e');
WriteData(':');
WriteAddress(0x08); //在第1行第9列显示年月之间的分隔符-
WriteData('-');
WriteAddress(0x0b); //在第1行第12列显示月日之间的分隔符-
WriteData('-'); //将字符常量写入LCD
WriteAddress(0x40); //在第2行第1列开始显示:Time:
WriteData('T');
WriteData('i');
WriteData('m');
WriteData('e');
WriteData(':');
WriteAddress(0x48); //显示小时与分钟之间的分隔符
WriteData(':');
WriteAddress(0x4b); //显示分钟与秒之间的分隔符
WriteData(':');
Init_DS1302(); //将DS1302初始化
while(1)
{
ReadValue = ReadSetDS1302(0x81); //从秒寄存器读数据
Second=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F);//将读出数据转化
DisplaySecond(Second); //显示秒
ReadValue = ReadSetDS1302(0x83); //从分寄存器读
Minute=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F); //将读出数据转化
DisplayMinute(Minute); //显示分
ReadValue = ReadSetDS1302(0x85); //从分寄存器读
Hour=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F); //将读出数据转化
DisplayHour(Hour); //显示小时
ReadValue = ReadSetDS1302(0x87); //从分寄存器读
Day=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F); //将读出数据转化
DisplayDay(Day); //显示日
ReadValue = ReadSetDS1302(0x89); //从分寄存器读
Month=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F); //将读出数据转化
DisplayMonth(Month); //显示月
ReadValue = ReadSetDS1302(0x8d); //从分寄存器读
Year=((ReadValue&0x70)>>4)*10 (ReadValue&0x0F); //将读出数据转化
DisplayYear(Year); //显示年
}
}
DS1302.h代码
sbit DATA=P3^7; //位定义1302芯片的接口,数据输出端定义在P1.1引脚
sbit RST=P3^5; //位定义1302芯片的接口,复位端口定义在P1.1引脚
sbit SCLK=P3^6; //位定义1302芯片的接口,时钟输出端口定义在P1.1引脚
//向DS1302写一个字节数据
void WriteDS1302(unsigned char DS1302data);
//根据命令字,向DS1302写一个字节数据
void WriteSetDS1302(unsigned char DS1302Cmd,unsigned char DS1302data);
//从1302读一个字节数据
unsigned char ReadDS1302(void);
//根据命令字,从DS1302读取一个字节数据
unsigned char ReadSetDS1302(unsigned char DS1302Cmd);
// DS1302初始化设置
void Init_DS1302(void);
DS1302.c代码
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
#include"LCD1602.h" //包含1602液晶显示函数的头文件
#include"DS1302.h" //包含1302时钟读写的头文件
#include"Delay.h" //包含延时函数的头文件
extern unsigned char code DispDigit[10]; //定义字符数组显示数字
/*****************************************************
函数功能:向1302写一个字节数据
入口参数:DS1302data
***************************************************/
void WriteDS1302(unsigned char DS1302data)
{
unsigned char i;
SCLK=0; //拉低SCLK,为脉冲上升沿写入数据做好准备
DelayXus(2); //稍微等待,使硬件做好准备
for(i=0;i<8;i ) //连续写8个二进制位数据
{
DATA=DS1302data&0x01; //取出待写数据的第0位数据写入1302
DelayXus(2); //稍微等待,使硬件做好准备
SCLK=1; //上升沿写入数据
DelayXus(2); //稍微等待,使硬件做好准备
SCLK=0; //重新拉低SCLK,形成脉冲
DS1302data>>=1; //将数据位右移1位,准备写入下一个数据位
}
}
/*****************************************************
函数功能:根据命令字,向1302写一个字节数据
入口参数:DS1302Cmd,储存命令字;DS1302data,储存待写的数据
***************************************************/
void WriteSetDS1302(unsigned char DS1302Cmd,unsigned char DS1302data)
{
RST=0; //禁止数据传递
SCLK=0; //确保写数居前SCLK被拉低
RST=1; //启动数据传输
DelayXus(2); //稍微等待,使硬件做好准备
WriteDS1302(DS1302Cmd); //写入命令字
WriteDS1302(DS1302data); //写数据
SCLK=1; //将时钟电平置于已知状态
RST=0; //禁止数据传递
}
/*****************************************************
函数功能:从1302读一个字节数据
入口参数:x
***************************************************/
unsigned char ReadDS1302(void)
{
unsigned char i,ucdata;
DelayXus(2); //稍微等待,使硬件做好准备
for(i=0;i<8;i ) //连续读8个二进制位数据
{
ucdata>>=1; //将数据位右移1位,因为先读出的是字节的最低位
if(DATA==1) //如果读出的数据是1
ucdata|=0x80; //将1取出,写在dat的最高位
SCLK=1; //将SCLK置于高电平,为下降沿读出
DelayXus(2); //稍微等待
SCLK=0; //拉低SCLK,形成脉冲下降沿
DelayXus(2); //稍微等待
}
return ucdata; //将读出的数据返回
}
/*****************************************************
函数功能:根据命令字,从1302读取一个字节数据
入口参数:DS1302Cmd
***************************************************/
unsigned char ReadSetDS1302(unsigned char DS1302Cmd)
{
unsigned char ucdata;
RST=0; //拉低RST
SCLK=0; //确保写数居前SCLK被拉低
RST=1; //启动数据传输
WriteDS1302(DS1302Cmd); //写入命令字
ucdata=ReadDS1302(); //读出数据
SCLK=1; //将时钟电平置于已知状态
RST=0; //禁止数据传递
return ucdata; //将读出的数据返回
}
/*****************************************************
函数功能: 1302进行初始化设置
***************************************************/
void Init_DS1302(void)
{
WriteSetDS1302(0x8E,0x00); //根据写状态寄存器命令字,写入不保护指令
WriteSetDS1302(0x80,((30/10)<<4|(30))); //根据写秒寄存器命令字,写入秒的初始值
WriteSetDS1302(0x82,((11/10)<<4|(11))); //根据写分寄存器命令字,写入分的初始值
WriteSetDS1302(0x84,((8/10)<<4|(8))); //根据写小时寄存器命令字,写入小时的初始值
WriteSetDS1302(0x86,((18/10)<<4|(18))); //根据写日寄存器命令字,写入日的初始值
WriteSetDS1302(0x88,((8/10)<<4|(8))); //根据写月寄存器命令字,写入月的初始值
WriteSetDS1302(0x8c,((20/10)<<4|(20))); //根据写年寄存器命令字,写入小时的初始值
}
LCD1602.h代码
sbit RS=P2^1; //寄存器选择位,将RS位定义为P2.0引脚
sbit RW=P2^2; //读写选择位,将RW位定义为P2.1引脚
sbit E=P2^3; //使能信号位,将E位定义为P2.2引脚
sbit BF=P0^7; //忙碌标志位,,将BF位定义为P0.7引脚
//判断液晶模块的忙碌状态
bit BusyTest(void);
//将模式设置指令或显示地址写入液晶模块
void WriteInstruction (unsigned char dictate);
//指定字符显示的实际地址
void WriteAddress(unsigned char x);
//将数据(字符的标准ASCII码)写入液晶模块
void WriteData(unsigned char y);
//对LCD的显示模式进行初始化设置
void LcdInitiate(void);
/**************************************************************
以下是1302数据的显示程序
**************************************************************/
//显示秒
void DisplaySecond(unsigned char x);
//显示分钟
void DisplayMinute(unsigned char x);
//显示小时
void DisplayHour(unsigned char x);
//显示日
void DisplayDay(unsigned char x);
//显示月
void DisplayMonth(unsigned char x);
//显示年
void DisplayYear(unsigned char x);
LCD1602.c
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
#include"LCD1602.h" //包含1602液晶显示函数的头文件
#include"DS1302.h" //包含1302时钟读写的头文件
#include"Delay.h" //包含延时函数的头文件
extern unsigned char code DispDigit[10]; //定义字符数组显示数字
/*******************************************************************************
以下是对液晶模块的操作程序
*******************************************************************************/
/*****************************************************
函数功能:判断液晶模块的忙碌状态
返回值:result。result=1,忙碌;result=0,不忙
***************************************************/
bit BusyTest(void)
{
bit result;
RS=0; //根据规定,RS为低电平,RW为高电平时,可以读状态
RW=1;
E=1; //E=1,才允许读写
_nop_(); //空操作
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
result=BF; //将忙碌标志电平赋给result
E=0; //将E恢复低电平
return result;
}
/*****************************************************
函数功能:将模式设置指令或显示地址写入液晶模块
入口参数:dictate
***************************************************/
void WriteInstruction (unsigned char dictate)
{
while(BusyTest()==1); //如果忙就等待
RS=0; //根据规定,RS和R/W同时为低电平时,可以写入指令
RW=0;
E=0; //E置低电平(根据表8-6,写指令时,E为高脉冲,
// 就是让E从0到1发生正跳变,所以应先置"0"
_nop_();
_nop_(); //空操作两个机器周期,给硬件反应时间
P0=dictate; //将数据送入P0口,即写入指令或地址
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
}
/*****************************************************
函数功能:指定字符显示的实际地址
入口参数:x
***************************************************/
void WriteAddress(unsigned char x)
{
WriteInstruction(x|0x80); //显示位置的确定方法规定为"80H 地址码x"
}
/*****************************************************
函数功能:将数据(字符的标准ASCII码)写入液晶模块
入口参数:y(为字符常量)
***************************************************/
void WriteData(unsigned char y)
{
while(BusyTest()==1);
RS=1; //RS为高电平,RW为低电平时,可以写入数据
RW=0;
E=0; //E置低电平(根据表8-6,写指令时,E为高脉冲,
// 就是让E从0到1发生正跳变,所以应先置"0"
P0=y; //将数据送入P0口,即将数据写入液晶模块
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
}
/*****************************************************
函数功能:对LCD的显示模式进行初始化设置
***************************************************/
void LcdInitiate(void)
{
DelayXms(15); //延时15ms,首次写指令时应给LCD一段较长的反应时间
WriteInstruction(0x38); //显示模式设置:16×2显示,5×7点阵,8位数据接口
DelayXms(5); //延时5ms ,给硬件一点反应时间
WriteInstruction(0x38);
DelayXms(5); //延时5ms ,给硬件一点反应时间
WriteInstruction(0x38); //连续三次,确保初始化成功
DelayXms(5); //延时5ms ,给硬件一点反应时间
WriteInstruction(0x0c); //显示模式设置:显示开,无光标,光标不闪烁
DelayXms(5); //延时5ms ,给硬件一点反应时间
WriteInstruction(0x06); //显示模式设置:光标右移,字符不移
DelayXms(5); //延时5ms ,给硬件一点反应时间
WriteInstruction(0x01); //清屏幕指令,将以前的显示内容清除
DelayXms(5); //延时5ms ,给硬件一点反应时间
}
/**************************************************************
以下是1302数据的显示程序
**************************************************************/
/*****************************************************
函数功能:显示秒
入口参数:x
***************************************************/
void DisplaySecond(unsigned char x)
{
unsigned char i,j; //i,j分别表示秒的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x4c); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
/*****************************************************
函数功能:显示分钟
入口参数:x
***************************************************/
void DisplayMinute(unsigned char x)
{
unsigned char i,j; //i,j分别表示分钟的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x49); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
/*****************************************************
函数功能:显示小时
入口参数:x
***************************************************/
void DisplayHour(unsigned char x)
{
unsigned char i,j; //i,j分别表示小时的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x46); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
/*****************************************************
函数功能:显示日
入口参数:x
***************************************************/
void DisplayDay(unsigned char x)
{
unsigned char i,j; //i,j分别表示日的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x0c); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
/*****************************************************
函数功能:显示月
入口参数:x
***************************************************/
void DisplayMonth(unsigned char x)
{
unsigned char i,j; //i,j分别表示月的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x09); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
/*****************************************************
函数功能:显示年
入口参数:x
***************************************************/
void DisplayYear(unsigned char x)
{
unsigned char i,j; //i,j分别表示年的十位、个位
i=x/10;//取十位
j=x;//取个位
WriteAddress(0x06); //写显示地址,将在第2行第7列开始显示
WriteData(DispDigit[i]); //将百位数字的字符常量写入LCD
WriteData(DispDigit[j]); //将十位数字的字符常量写入LCD
DelayXms(50); //延时1ms给硬件一点反应时间
}
Delay.h代码
//延时1ms
void Delay1ms();
//延时若干毫秒
void DelayXms(unsigned int DelayCounter);
//延时若干微秒
void DelayXus(unsigned char n);
Delay.c代码
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
#include"Delay.h" //包含延时函数的头文件
/*****************************************************
函数功能:延时1ms
(3j 2)*i=(3×33 2)×10=1010(微秒),可以认为是1毫秒
***************************************************/
void Delay1ms()
{
unsigned char i,j;
for(i=0;i<10;i )
for(j=0;j<33;j )
;
}
/*****************************************************
函数功能:延时若干毫秒
入口参数:n
***************************************************/
void DelayXms(unsigned int DelayCounter)
{
unsigned int i;
for(i=0;i<DelayCounter;i )
Delay1ms();
}
/*****************************************************
函数功能:延时若干微秒
入口参数:n
***************************************************/
void DelayXus(unsigned char n)
{
unsigned char i;
for(i=0;i<n;i )
;
}
在Proteus环境下建立图1所示的电路,将编译完成的hex文件装载到单片机中,开始仿真,观察LCD1602液晶的显示变化情况。液晶第一行显示日期:年、月、日。第二行显示时间:时、分、秒
1.7. 总结
本实例我们学习了时钟芯片DS1302的工作原理,电路设计、编程实现。
,