一、DS1302介绍
DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能
RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片
二、工作原理
典型工作电路如图所示

管脚名称 | 功能 |
---|---|
VCC2 | 连接主电源VCC(VCC有电时,对DS1302供电,同时对备用电池进行充电) |
VCC1 | 连接备用电源(VCC没有电时,备用电源对其供电) |
X1与X2 | 接32.768KHz的晶振 |
CE | 使能信号,CE信号在读写时必须保持高电平 |
I/O | 数据输入输出 |
SCLK | 串行时钟 |
DS1302的内部结构框图如图所示:

X1、X2接外部32.768KHz的晶振,经过相应的逻辑电路处理后,产生1Hz的时钟
31*8RAM是存储时间信息
工作原理类似于74HC595
开发板中DS1302的原理图如图所示

其中VCC1并没有接备用电源,也就是说无法使用掉点继续运行走时的功能
三、与RTC有关的寄存器

注:其中“Day”表示星期
WP:写保护,当WP为1时,不能对上述寄存器进行写操作
91h和90h命令对应的是与涓流充电相关的寄存器,本开发板没有使用备用电源,故这个寄存器不用
命令字:

上图显示的是命令字,命令字启动每一次数据传输。MSB (位7)必须是逻辑 1,如果是 0,则禁止对 DS1302写入。位 6 在逻辑 0时规定为时钟/日历数据,逻辑 1时为 RAM数据。位 1 至 位 5 表示了输入输出的指定寄存器。LSB (位 0) 在逻辑0时为写操作(输出),逻辑1时为读操作(输入)。命令字以 LSB (位 0)开始总是输入。
例如,要进行读秒,对应的寄存器地址为00000,位0位1,位6位0(读时钟数据),则命令字为0b10000001,即为0x81,与上图的命令字相对应。
四、实验1
(实验前记得把LED点阵屏的那个黄帽改回原处)
现象:在LCD屏幕上显示时钟,包括年、月、日、时、分、秒
1、模拟时序
DS1302.c
#include <REGX52.H>
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
sbit DS1302_SCLK = P3^6;
void DS1302_Init()//系统上电时,IO口为高电平
{
DS1302_CE = 0;
DS1302_SCLK = 0;
}
void DS1302_WriteByte(unsigned char command, myData)
{
unsigned char i;//局部变量要定义在函数开头
DS1302_CE = 1;
for(i = 0;i < 8;i++)
{
DS1302_IO = command & (0x01 << i);
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
for(i = 0;i < 8;i++)
{
DS1302_IO = myData & (0x01 << i);
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1302_CE = 0;
}
unsigned char DS1302_ReadByte(unsigned char command)
{
unsigned char i;
unsigned char Data = 0x00;
DS1302_CE = 1;
for(i = 0;i < 8;i++)
{
DS1302_IO = command & (0x01 << i);
DS1302_SCLK = 0;
DS1302_SCLK = 1;
}
for(i = 0;i < 8;i++)
{
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
if(DS1302_IO)
{
Data |= (0x01 << i);
}
}
DS1302_CE = 0;
return Data;
}
2、测试
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
unsigned char second;
void main()
{
LCD_Init();
DS1302_Init();
DS1302_WriteByte(0x8e,0x00);//关闭写保护,没有这一行则显示255
LCD_ShowString(1,1,"RTC");
DS1302_WriteByte(0x80,0x03);
second = DS1302_ReadByte(0x81);
LCD_ShowNum(2,1,second,3);
while(1)
{
}
}
当循环显示秒数的时候,即
while(1)
{
second = DS1302_ReadByte(0x81);
LCD_ShowNum(2,1,second,3);
}
会出现显示数字从9直接跳到16的情况,而不是显示10。这是因为DS1302采用的是BCD(Binary Coded Decimal)码,即用4位二进制数来表示1位十进制数。例如0001 0011表示13(前4位表示1,后四位表示3),1000 0101表示85,0001 1010不合法。
而9对应的BCD码为0000 1001,10对应的BCD码为0001 0000,而这个二进制数被理解为16,故LCD显示16。
所以需要进行BCD码和十进制的转换
•BCD码转十进制:DEC=BCD/16*10+BCD%16; (2位BCD)
•十进制转BCD码:BCD=DEC/10*16+DEC%10; (2位BCD)
LCD_ShowNum(2,1,second/16*10+second%16,3);
3、定义时间数组,索引0~6分别为年、月、日、时、分、秒、星期
unsigned char DS1302_Time = {23,7,13,23,59,10,4};
4、根据上图写命令进行宏定义,目的是为了增加可读性和可维护性
//寄存器写入地址/指令定义
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR 0x84
#define DS1302_DATE 0x86
#define DS1302_MONTH 0x88
#define DS1302_DAY 0x8A
#define DS1302_YEAR 0x8C
#define DS1302_WP 0x8E
由于读命令与写命令只是最后一位不同,将command最后一位或上一个1,就可以将命令的最后一位变为1,即将写命令转换成读命令,这样就可以用写命令来当做DS1302_ReadByte
函数的形参了,也就是不需要单独定义读命令了
command |= 0x01;
5、定义DS1302_SetTime
函数,设置时间
void DS1302_SetTime()
{
DS1302_WriteByte(DS1302_WP,0x00);//取消写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码后写入
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
DS1302_WriteByte(DS1302_WP,0x00);//打开写保护
}
6、定义DS1302_ReadTime
函数,读出时间
void DS1302_ReadTime()
{
unsigned char Temp;//用于临时存储读出结果
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制后读取
Temp=DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[3]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[4]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[5]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DAY);
DS1302_Time[6]=Temp/16*10+Temp%16;
}
7、DS1302_Time
数组的外部声明
因为DS1302_Time
数组需要在主函数中调用,所以该数组需要声明为extern(在DS1302.h
中)
extern unsigned char DS1302_Time[];
8、主函数
现象:LCD对应位置能正常显示时间
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
void main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - -");
LCD_ShowString(2,1," : :");
DS1302_SetTime();
while(1)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
}
五、实验2
在实验1的基础上通过按键可以对时间进行调节
1、添加按键模块代码和定时器模块代码
Key.c
和Key.h
Timer.c
和Timer.h
2、定义全局变量keyNum,用于接收按键按下的键值(通过GetKeyNum
获取)
unsigned char keyNum;
keyNum = GetKeyNum();
3、创建TimeShow
函数,将显示时间的代码放入
void TimeShow()
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
4、创建TimeSet
函数,用于按键设置时间
创建变量timeSet_select用于选择设置哪个时间量
timeSet_select=0设置年份,timeSet_select=1设置月份,timeSet_select=2设置天数
timeSet_select=3设置小时,timeSet_select=4设置分钟,timeSet_select=5设置秒数
unsigned char timeSet_select;
与江科大视频中的日期越界的判断方式不同,我使用了一个数组month_day
来保存每个月的天数,进而再进行判断
unsigned char month_day[]={31,28,31,30,31,30,31,31,30,31,30,31};
void TimeSet()
{
if(keyNum == 2)//按键2按下
{
timeSet_select++;
timeSet_select %= 6;//越界清零
}
if(keyNum == 3)//按键3按下
{
DS1302_Time[timeSet_select]++;//时间设置位数值加1
//越界判断
//年份越界
DS1302_Time[0] %= 100;//或 if(DS1302_Time[0]>99){DS1302_Time[0]=0;}
//判断是否是闰年,若是,则2月为29天
month_day[1] = DS1302_Time[0] % 4 == 0 ? 29 : 28;
//月份越界
DS1302_Time[1] = DS1302_Time[1] > 12 ? 1 : DS1302_Time[1];//或 if(DS1302_Time[1]>12){DS1302_Time[1]=1;}
//日期越界
if(DS1302_Time[2] > month_day[DS1302_Time[1] - 1]){DS1302_Time[2] = 1;}
//时
if(DS1302_Time[3] > 23){DS1302_Time[3] = 0;}
//分
if(DS1302_Time[4] > 59){DS1302_Time[4] = 0;}
//秒
if(DS1302_Time[5] > 59){DS1302_Time[5] = 0;}
}
if(keyNum == 4)
{
DS1302_Time[timeSet_select]--;
//年
if(DS1302_Time[0] < 0){DS1302_Time[0] = 99;}
month_day[1] = DS1302_Time[0] % 4 == 0 ? 29 : 28;
//月
if(DS1302_Time[1] < 0){DS1302_Time[1] = 12;}
//日
if(DS1302_Time[2] < 1){DS1302_Time[2] = month_day[DS1302_Time[1] - 1];}
//时
if(DS1302_Time[3] < 0){DS1302_Time[3] = 23;}
//分
if(DS1302_Time[4] < 0){DS1302_Time[4] = 59;}
//秒
if(DS1302_Time[5] < 0){DS1302_Time[5] = 59;}
}
}
**注意:**由于之前定义的DS1302_Time数组是unsigned char类型的,而数组元素从0再减1时变为255,而不是-1,所以这里需要将DS1302_Time数组改为char类型的
char DS1302_Time[] = {23,7,13,23,59,10,4};
5、模式
创建一个变量mode用于表明是哪个模式,mode=0是显示时间模式,mode=1是设置时间模式
unsigned char mode;
按键1用于改变模式。主函数如下:
void main()
{
LCD_Init();
DS1302_Init();
Timer0Init();
LCD_ShowString(1,1," - -");
LCD_ShowString(2,1," : :");
DS1302_SetTime();
while(1)
{
keyNum = GetKeyNum();
if(keyNum == 1)//按键1按下
{
if(mode==0)
{
mode=1;
timeSet_select=0;//让每次设置时间都从年份开始
}
else
{
mode=0;
DS1302_SetTime();//将设置的时间保存
}
}
mode == 1?TimeSet():TimeShow();
}
}
6、设置闪烁功能
通过定时器0实现,每隔一秒对应的时间位置显示为空白,这样就实现了每隔一秒闪烁一次的功能
创建一个变量用于表明是否闪烁的标志
unsigned char timeSet_flashFlag;
定时器0的中断处理函数如下:
//定时器0的中断处理函数
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;
//重新赋值计数器,因为计数器溢出后会变为0,要1ms产生一次中断,需要重新给计数器赋值为64535
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
//1ms产生一次中断,每次中断count+1,1000次后再进行处理,此时处理的时间间隔就为1s
count++;
if(count >= 1000)
{
count = 0;
timeSet_flashFlag = !timeSet_flashFlag;
}
}
在TimeSet函数添加如下代码:
如果闪烁标志为1,则显示空格,否则显示原时间
//更新显示,根据timeSet_select和timeSet_flashFlag判断可完成闪烁功能
if(timeSet_select==0 && timeSet_flashFlag==1){LCD_ShowString(1,1," ");}
else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
if(timeSet_select==1 && timeSet_flashFlag==1){LCD_ShowString(1,4," ");}
else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
if(timeSet_select==2 && timeSet_flashFlag==1){LCD_ShowString(1,7," ");}
else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
if(timeSet_select==3 && timeSet_flashFlag==1){LCD_ShowString(2,1," ");}
else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
if(timeSet_select==4 && timeSet_flashFlag==1){LCD_ShowString(2,4," ");}
else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
if(timeSet_select==5 && timeSet_flashFlag==1){LCD_ShowString(2,7," ");}
else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
7、总结
main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Key.h"
#include "Timer.h"
unsigned char keyNum, timeSet_select, mode, timeSet_flashFlag;
unsigned char month_day[]={31,28,31,30,31,30,31,31,30,31,30,31};
void TimeShow()
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
void TimeSet()
{
if(keyNum == 2)//按键2按下
{
timeSet_select++;
timeSet_select %= 6;//越界清零
}
if(keyNum == 3)//按键3按下
{
DS1302_Time[timeSet_select]++;//时间设置位数值加1
//越界判断
//年份越界
DS1302_Time[0] %= 100;//或 if(DS1302_Time[0]>99){DS1302_Time[0]=0;}
//判断是否是闰年,若是,则2月为29天
month_day[1] = DS1302_Time[0] % 4 == 0 ? 29 : 28;
//月份越界
DS1302_Time[1] = DS1302_Time[1] > 12 ? 1 : DS1302_Time[1];//或 if(DS1302_Time[1]>12){DS1302_Time[1]=1;}
//日期越界
if(DS1302_Time[2] > month_day[DS1302_Time[1] - 1]){DS1302_Time[2] = 1;}
//时
if(DS1302_Time[3] > 23){DS1302_Time[3] = 0;}
//分
if(DS1302_Time[4] > 59){DS1302_Time[4] = 0;}
//秒
if(DS1302_Time[5] > 59){DS1302_Time[5] = 0;}
}
if(keyNum == 4)
{
DS1302_Time[timeSet_select]--;
//年
if(DS1302_Time[0] < 0){DS1302_Time[0] = 99;}
month_day[1] = DS1302_Time[0] % 4 == 0 ? 29 : 28;
//月
if(DS1302_Time[1] < 0){DS1302_Time[1] = 12;}
//日
if(DS1302_Time[2] < 1){DS1302_Time[2] = month_day[DS1302_Time[1] - 1];}
//时
if(DS1302_Time[3] < 0){DS1302_Time[3] = 23;}
//分
if(DS1302_Time[4] < 0){DS1302_Time[4] = 59;}
//秒
if(DS1302_Time[5] < 0){DS1302_Time[5] = 59;}
}
//更新显示,根据TimeSetSelect和TimeSetFlashFlag判断可完成闪烁功能
if(timeSet_select==0 && timeSet_flashFlag==1){LCD_ShowString(1,1," ");}
else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
if(timeSet_select==1 && timeSet_flashFlag==1){LCD_ShowString(1,4," ");}
else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
if(timeSet_select==2 && timeSet_flashFlag==1){LCD_ShowString(1,7," ");}
else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
if(timeSet_select==3 && timeSet_flashFlag==1){LCD_ShowString(2,1," ");}
else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
if(timeSet_select==4 && timeSet_flashFlag==1){LCD_ShowString(2,4," ");}
else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
if(timeSet_select==5 && timeSet_flashFlag==1){LCD_ShowString(2,7," ");}
else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}
void main()
{
LCD_Init();
DS1302_Init();
Timer0Init();
LCD_ShowString(1,1," - -");
LCD_ShowString(2,1," : :");
DS1302_SetTime();
while(1)
{
keyNum = GetKeyNum();
if(keyNum == 1)//按键1按下
{
if(mode==0)
{
mode=1;
timeSet_select=0;//让每次设置时间都从年份开始
}
else
{
mode=0;
DS1302_SetTime();//将设置的时间保存
}
}
mode == 1?TimeSet():TimeShow();
}
}
//定时器0的中断处理函数
void Timer0_Routine() interrupt 1
{
static unsigned int count = 0;
//重新赋值计数器,因为计数器溢出后会变为0,要1ms产生一次中断,需要重新给计数器赋值为64535
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
//1ms产生一次中断,每次中断count+1,1000次后再进行处理,此时处理的时间间隔就为1s
count++;
if(count >= 1000)
{
count = 0;
timeSet_flashFlag = !timeSet_flashFlag;
}
}
DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__
extern char DS1302_Time[];
void DS1302_Init();
void DS1302_WriteByte(unsigned char command, myData);
unsigned char DS1302_ReadByte(unsigned char command);
void DS1302_SetTime();
void DS1302_ReadTime();
#endif
DS1302.c
#include <REGX52.H>
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
sbit DS1302_SCLK = P3^6;
//寄存器写入地址/指令定义
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR 0x84
#define DS1302_DATE 0x86
#define DS1302_MONTH 0x88
#define DS1302_DAY 0x8A
#define DS1302_YEAR 0x8C
#define DS1302_WP 0x8E
char DS1302_Time[] = {23,7,13,23,59,10,4};
void DS1302_Init()//系统上电时,IO口为高电平
{
DS1302_CE = 0;
DS1302_SCLK = 0;
}
void DS1302_WriteByte(unsigned char command, myData)
{
unsigned char i;//局部变量要定义在函数开头
DS1302_CE = 1;
for(i = 0;i < 8;i++)
{
DS1302_IO = command & (0x01 << i);
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
for(i = 0;i < 8;i++)
{
DS1302_IO = myData & (0x01 << i);
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1302_CE = 0;
}
unsigned char DS1302_ReadByte(unsigned char command)
{
unsigned char i;
unsigned char Data = 0x00;
DS1302_CE = 1;
//读命令与写命令只是最后一位不同,将command最后一位或上一个1,就可以将写命令转换成读命令
//这样就不需要单独定义读命令了
command |= 0x01;
for(i = 0;i < 8;i++)
{
DS1302_IO = command & (0x01 << i);
DS1302_SCLK = 0;
DS1302_SCLK = 1;
}
for(i = 0;i < 8;i++)
{
//产生下降沿
DS1302_SCLK = 1;
DS1302_SCLK = 0;
if(DS1302_IO)
{
Data |= (0x01 << i);
}
}
DS1302_CE = 0;
DS1302_IO=0; //读取后将IO设置为0,否则读出的数据会出错
return Data;
}
void DS1302_SetTime()
{
DS1302_WriteByte(DS1302_WP,0x00);//取消写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码后写入
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
DS1302_WriteByte(DS1302_WP,0x00);//打开写保护
}
void DS1302_ReadTime()
{
unsigned char Temp;//用于临时存储读出结果
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制后读取
Temp=DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[3]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[4]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[5]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DAY);
DS1302_Time[6]=Temp/16*10+Temp%16;
}
更多51单片机笔记见主页