参考教程:[10-1] DS1302实时时钟_哔哩哔哩_bilibili
一、DS1302实时时钟模块概述
DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片,它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。
RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片。
二、开发板上的DS1302时钟模块
1、DS1302模块简介
引脚名 | 作用 | 引脚名 | 作用 |
VCC2 | 主电源 | CE | 芯片使能 |
VCC1 | 备用电池 | IO | 数据输入/输出 |
GND | 电源地 | SLCK | 串行时钟 |
X1、X2 | 32.768kHz晶振 (提供计数脉冲) |
注:在单片机断电后,备用电池启动,时钟会继续计时,直到下次重启单片机时备用电池休息(单片机通电时备用电池充电,时钟由VCC2供电;开发板貌似没有提供备用电池,只是提供了这么一个引脚,备用电池需要自己安装)。
2、内部结构框图
X1、X2提供计数脉冲,实时时钟负责记录时间数据,当CE处于高电平时,实时时钟中的时间数据可以和移位寄存器的数据发生交互(读/写),这个过程和74HC595类似,这里不再赘述;命令控制逻辑用来解释命令字,然后执行相应操作,这个命令字在下面马上就有解释。
3、相关寄存器与命令字
(1)下面九个寄存器是RTC相关的寄存器(实时时钟下的寄存器):WP置为1,前7个寄存器打开写保护,里面的数据只可读不可改;最后一个寄存器与充电相关,目前可以不予理会;前7个寄存器负责记录年月日时分秒以及星期几(采用的是BCD码,个位和十位分别记录),CH置为1的话,时钟会暂停计时。
BCD码(Binary Coded Decimal),是用4位二进制数来表示1位十进制数的编码方式。
①举例:0001 0011表示13,1000 0101表示85,0001 1010不合法,对应在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法。
②BCD码转十进制:DEC=BCD/16*10+BCD%16; (2位BCD)
③十进制转BCD码:BCD=DEC/10*16+DEC%10; (2位BCD)
(2)下图所示的是地址/命令字节,第7位固定为1,第6位置为0可操作时钟(操作RAM则置为1),第5位到第1位则是寄存器地址,第0位如果置为0则进行写操作(如果置为1则进行读操作)。这个命令字,其实就是上图中第一列和第二列,比如要对秒寄存器进行读操作,则命令字为0x81。
4、时序
(1)CE为高电平时才能进行读/写操作。
(2)SCLK提供脉冲,每一个上升沿写入一位数据,每一个下降沿读出一位数据。
(3)CE置为高电平后,先将8位命令通过I/O写入单片机,单片机根据命令字的解释进行读or写操作,读/写的数据也是通过I/O进行传输,操作完成后CE置为低电平,SCLK也停止提供脉冲。
三、DS1302时钟应用实验
1、利用DS1302时钟显示当前时间实验
(1)项目包含的文件:其中需要重写的都会在下面给出,未给出的沿用旧例出现过的即可(本例需要液晶屏模块的代码文件)。
(2)补充需要重写或新添加的代码文件,然后进行编译。
①DS1302.h文件:
#ifndef __DS1302_H__
#define __DS1302_H__
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command, Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);
extern unsigned char DS1302_Time[]; //外部可调用该数组
#endif
②DS1302.c文件:
#include <REGX52.H>
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
#define DS1302_SECOND 0x80 //这些都是写操作的命令字,要进行读操作+1即可
#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
unsigned char DS1302_Time[] =
{23,11,23,12,59,55,4}; //记录当前时间的数组(初始值为初始时间)
void DS1302_Init(void)
{
DS1302_CE = 0;
DS1302_SCLK = 0;
}
void DS1302_WriteByte(unsigned char Command, unsigned char Data)
{
unsigned char i = 0; //局部变量放在第一行,否则有些编译器可能会报错
DS1302_CE = 1; //CE置为高电平,开始操作
for(i = 0; i < 8; i++) //使用循环将8位命令通过I/O口一位一位写入移位寄存器
{
DS1302_IO = Command & (0x01 << i); //将8位数据一位一位写入I/O口
DS1302_SCLK = 1; //上升沿(触发上升沿,将I/O的1位数据送到控制逻辑)
DS1302_SCLK = 0; //下降沿
//这里写了一个时钟脉冲,需要查看手册了解单片机能承受的时钟频率
//如果不能承受过高频率,需要在两条语句间加延时函数
}
//控制逻辑接收完整的命令字,进行解释与操作
for(i = 0; i < 8; i++) //使用循环将8位数据通过I/O口一位一位写入寄存器
{
DS1302_IO = Data & (0x01 << i); //将8位数据一位一位写入I/O口
DS1302_SCLK = 1; //上升沿(触发上升沿,将I/O的1位数据写进实时时钟下的寄存器)
DS1302_SCLK = 0; //下降沿
}
DS1302_CE = 0; //操作完成,CE置为低电平
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i = 0;
unsigned char Data = 0x00;
DS1302_CE = 1;
for(i = 0; i < 8; i++) //使用循环将8位命令通过I/O口一位一位写入移位寄存器
{
DS1302_IO = Command & (0x01 << i); //使用循环将8位命令通过I/O口一位一位写入寄存器
DS1302_SCLK = 0; //下降沿
DS1302_SCLK = 1; //上升沿(触发上升沿,将I/O的1位数据送到控制逻辑)
//这里写了一个脉冲,需要查看手册了解单片机能承受的频率
//如果不能承受过高频率,需要在两条语句间加延时函数
}
//控制逻辑接收完整的命令字,进行解释与操作
for(i = 0; i < 8; i++)
{
DS1302_SCLK = 1; //上升沿
DS1302_SCLK = 0; //下降沿(触发下降沿,I/O从寄存器中读出一位数据)
if(DS1302_IO)
{
Data = Data | (0x01 << i); //将I/O读出的一位数据记录在Data变量中
//Data本身8位都是低电平,遇到高电平数据再进行更改即可
}
}
DS1302_CE = 0; //操作完成,CE置为低电平
DS1302_IO = 0; //I/O口清零
return Data;
}
void DS1302_SetTime(void)
{
DS1302_WriteByte(DS1302_WP,0x00); //解除写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10); //设置初始年份
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,0x80); //恢复写保护
}
void DS1302_ReadTime(void)
{
unsigned char Temp;
Temp = DS1302_ReadByte(DS1302_YEAR + 1); //读当前年份
DS1302_Time[0] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_MONTH + 1); //读当前月份
DS1302_Time[1] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_DATE + 1); //读当前日
DS1302_Time[2] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_HOUR + 1); //读当前时
DS1302_Time[3] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_MINUTE + 1); //读当前分
DS1302_Time[4] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_SECOND + 1); //读当前秒
DS1302_Time[5] = Temp/16*10+Temp%16;
Temp = DS1302_ReadByte(DS1302_DAY + 1); //当前时间是星期几
DS1302_Time[6] = Temp/16*10+Temp%16;
}
③main.c文件:
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
void main()
{
LCD_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_Init();
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); //显示秒
}
}
(3)将生成的.hex文件下载到开发板中,可以看到液晶屏上显示当前的时间。
2、利用DS1302时钟显示当前时间,并且可修改当前显示时间实验
(1)项目包含的文件:其中需要重写的都会在下面给出,未给出的沿用旧例出现过的即可(本例需要液晶屏模块以及延时函数的代码文件)。
(2)补充需要重写或新添加的代码文件,然后进行编译。
①DS1302.c文件以及DS1302.h文件中的DS1302_Time[]数组改为char类型。
②main.c文件:
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "key.h"
#include "Timer0.h"
unsigned char KeyNum, MODE, TimeSetSelect, TimeSetFlashFlag;
void TimeShow(void)
{
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(void)
{
if(KeyNum == 2) //按键2用于选择需要修改的数字
{
TimeSetSelect++; //TimeSetSelect作为DS1302_Time数组的下标,用于访问以及修改DS1302_Time数组
TimeSetSelect %= 6; //防止越界(统共有6个选项)
}
if(KeyNum == 3) //按键3用于增加设置值
{
DS1302_Time[TimeSetSelect]++;
if(DS1302_Time[0]>99){DS1302_Time[0] = 0;}
if(DS1302_Time[1]>12){DS1302_Time[1] = 1;}
if(DS1302_Time[1] == 1 || DS1302_Time[1] == 3 || DS1302_Time[1] == 5
|| DS1302_Time[1] == 7 || DS1302_Time[1] == 8 ||DS1302_Time[1] == 10
|| DS1302_Time[1] == 12)
{
if(DS1302_Time[2]>31){DS1302_Time[2] = 1;}
}
else if(DS1302_Time[1] == 4 || DS1302_Time[1] == 6 || DS1302_Time[1] == 9
|| DS1302_Time[1] == 11)
{
if(DS1302_Time[2]>30){DS1302_Time[2] = 1;}
}
else if(DS1302_Time[1] == 2)
{
if((DS1302_Time[0]%4 == 0 && DS1302_Time[0]%100 != 0)
|| (DS1302_Time[0]%400 == 0))
{
if(DS1302_Time[2]>29){DS1302_Time[2] = 1;} //闰年2月有29天
}
else
{
if(DS1302_Time[2]>28){DS1302_Time[2] = 1;} //平年2月有28天
}
}
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) //按键4用于减少设置值
{
DS1302_Time[TimeSetSelect]--;
if(DS1302_Time[0]<0){DS1302_Time[0] = 99;}
if(DS1302_Time[1]<1){DS1302_Time[1] = 12;}
if(DS1302_Time[1] == 1 || DS1302_Time[1] == 3 || DS1302_Time[1] == 5
|| DS1302_Time[1] == 7 || DS1302_Time[1] == 8 ||DS1302_Time[1] == 10
|| DS1302_Time[1] == 12)
{
if(DS1302_Time[2]<1){DS1302_Time[2] = 31;}
if(DS1302_Time[2]>31){DS1302_Time[2] = 1;}
}
else if(DS1302_Time[1] == 4 || DS1302_Time[1] == 6 || DS1302_Time[1] == 9
|| DS1302_Time[1] == 11)
{
if(DS1302_Time[2]<1){DS1302_Time[2] = 30;}
if(DS1302_Time[2]>30){DS1302_Time[2] = 1;}
}
else if(DS1302_Time[1] == 2)
{
if((DS1302_Time[0]%4 == 0 && DS1302_Time[0]%100 != 0)
|| (DS1302_Time[0]%400 == 0))
{
if(DS1302_Time[2]<1){DS1302_Time[2] = 29;} //闰年2月有29天
if(DS1302_Time[2]>29){DS1302_Time[2] = 1;}
}
else
{
if(DS1302_Time[2]<1){DS1302_Time[2] = 28;} //平年2月有28天
if(DS1302_Time[2]>28){DS1302_Time[2] = 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;}
}
//定时器0每隔0.5秒产生一次中断,TimeSetFlashFlag逻辑取反
//那么TimeSetFlashFlag为1时被选中数字的位置显示空格,为0时正常显示数字,就能达到被选中数字闪烁的效果
if(TimeSetFlashFlag == 1 && TimeSetSelect == 0)
{
LCD_ShowString(1,1," ");
}
else
{
LCD_ShowNum(1,1,DS1302_Time[0],2); //显示更改后的年份
}
if(TimeSetFlashFlag == 1 && TimeSetSelect == 1)
{
LCD_ShowString(1,4," ");
}
else
{
LCD_ShowNum(1,4,DS1302_Time[1],2); //显示更改后的月份
}
if(TimeSetFlashFlag == 1 && TimeSetSelect == 2)
{
LCD_ShowString(1,7," ");
}
else
{
LCD_ShowNum(1,7,DS1302_Time[2],2); //显示更改后的日
}
if(TimeSetFlashFlag == 1 && TimeSetSelect == 3)
{
LCD_ShowString(2,1," ");
}
else
{
LCD_ShowNum(2,1,DS1302_Time[3],2); //显示更改后的时
}
if(TimeSetFlashFlag == 1 && TimeSetSelect == 4)
{
LCD_ShowString(2,4," ");
}
else
{
LCD_ShowNum(2,4,DS1302_Time[4],2); //显示更改后的分
}
if(TimeSetFlashFlag == 1 && TimeSetSelect == 5)
{
LCD_ShowString(2,7," ");
}
else
{
LCD_ShowNum(2,7,DS1302_Time[5],2); //显示更改后的秒
}
LCD_ShowNum(2,11,TimeSetSelect,2); //显示当前选中的时间单位
}
void main()
{
LCD_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
Timer0_Init();
DS1302_Init();
DS1302_SetTime(); //设置初始时间
while(1)
{
KeyNum = Key();
if(KeyNum == 1) //按下按键1,修改模式和显示模式来回切换
{
if(MODE == 0)
{
MODE = 1;
}
else if(MODE == 1)
{
MODE = 0;
DS1302_SetTime(); //设置显示修改后的时间
}
}
switch(MODE)
{
case 0:TimeShow();break;
case 1:TimeSet();break;
}
}
}
void Timer0_Routine() interrupt 1 //CPU响应中断后执行的函数
{
static unsigned int T0Count = 0; //定义计数器
T0Count++;
if(T0Count >= 500) //每500个中断信号(0.5秒)执行一次下面的代码段
{
T0Count = 0;
TimeSetFlashFlag = !TimeSetFlashFlag; //是否亮灯变量
}
//每次中断结束都要重置计数单元
TH0 = 0xFC; //定时器0的计数单元高8位
TL0 = 0x66; //定时器0的计数单元低8位
}
(3)将生成的.hex文件下载到开发板中,依据代码中的注释进行调试。