51单片机内部含有晶振,可以实现定时/计数功能。但是其缺点有:精度往往不高、不能掉电使用等。 我们可以通过DS1302时钟芯片来解决以上的缺点。
DS1302时钟芯片
功能:DS1302是一种低功耗实时时钟芯片,内部有自动的计时功能,其范围包括:年、月、日、时、分、秒、星期。(且每个月的天数和闰年的天数可以内部自动调整)。
结构以及引脚定义:
常见的DS1302时钟芯片有DIP(直插式)和SO(表贴式)两种封装 。
|-------|---------------------------|
| 引脚名称 | 功能 |
| X1、X2 | DS1302外部晶振引脚,通常接32.768K晶振 |
| VCC1 | 备用电源,在主电源使用时可被充电 |
| VCC2 | 主电源供应管脚(与单片机共用一个电源) |
| GND | 电源地 |
| CE | 使能端,也是复位引脚,在进行读写时CE要保持高电平 |
| I/O | 串行数据输入或输出 |
| SCLK | 串行时钟 |
[引脚定义]
通常通过单片机直接与CE、I/O、SCLK这3个引脚相连,控制其高低电平,进而控制DS1302时钟芯片。
DS1302电路图
其中,C2和C3为旁路电容,目的是消除晶振起振时产生的电感干扰。对于此电路,没有接入备用电池,可自行将外部备用电源接入VCC1。
DS1302内部寄存器
控制寄存器
功能:通过给控制寄存器数据,来决定:进入年月日等具体哪一个寄存器、进行读/写操作。
上图为控制寄存器样式。
最高位一直为1;
第6位:1表示RAM,寻址内部存储器;0为CK,寻址内部寄存器。
第5位~第1位:为年月日等日历寄存器的地址。
最低位:1表示下一步操作为"读";0表示下一步操作为"写"。
日历/时钟寄存器
上图为年月日等相关寄存器的地址(绿框内)、功能等。以下对其进行详细说明:
|--------|---------------|--------------|----|-------|------|----|----|----|----|
| 寄存器名称 | 取值范围 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
| 秒寄存器 | 00~59 | CH | 秒的十位 ||| 秒的个位 ||||
| 分寄存器 | 00~59 | 0 | 分的十位 ||| 分的个位 ||||
| 小时寄存器 | 1~12或 00~23 | 12小时制/ 24小时制 | 0 | AM/PM | Hour | 小时的个位 ||||
| 日寄存器 | 01~31 | 0 | 0 | 日的十位 || 日的个位 ||||
| 月寄存器 | 01~12 | 0 | 0 | 0 | 1或0 | 月的个位 ||||
| 星期寄存器 | 01~07 | 0 | 0 | 0 | 0 | 星期几 ||||
| 年寄存器 | 01~99 | 20XX年的十位 |||| 年的个位 ||||
| 写保护寄存器 | | WP | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
[相关寄存器的位说明]
需特别注意以下部分:
秒寄存器:CH为DS1302的运行标志,当CH=0时,DS1302内部才能工作;当CH=1时,DS1302停止工作。
写保护寄存器:当WP为1时,DS1302只读不写。所以进行写操作时要确保WP为0。
DS1302初始时间设置
举个例子,如果想将2024.05.04---周7---23:59:49这个时间写入DS1302内部,具体操作如下:
1、对0x8E地址操作,关闭写保护;
2、对秒寄存器0x80操作,写入0x49;
3、对分寄存器0x82操作,写入0x59;
4、对时寄存器0x84操作,写入0x23;
......以此类推,最后对0x8E地址操作,打开写保护。
BCD码
BCD码:用4位二进制数来表示1位十进制数。
例如:0001 0011表示13、1000 0101表示85、0001 1010不合法。
在日历/时钟寄存器内部 都是以BCD码来存放数据。因此要设置DS1302的时间,要写入BCD码格式。同时读取数据时,读到的也是BCD码,使用时需转换为对应十进制。
|---|------|---|------|
| 0 | 0000 | 5 | 0101 |
| 1 | 0001 | 6 | 0110 |
| 2 | 0010 | 7 | 0111 |
| 3 | 0011 | 8 | 1000 |
| 4 | 0100 | 9 | 1001 |
转换公式:
读写时序
前面提到:单片机与DS1302相连的线有3根,分别为:CE、SCLK和I/O引脚;如何通过这3根线进行数据写入和读出呢?
CE:初始化使其为低电平,在整个读写过程中,要保持高电平;一次读写操作完毕后,再回到低电平。
cpp
// 功能:DS1302初始化
void DS1302_Init(){
DS1302_CE=0; //使能端置0
DS1302_SCLK=0; //时钟脉冲置0
}
单字节写入 :(先关闭写保护)当CE为高电平时,通过单片机控制SCLK产生脉冲,每一个上升沿,I/O线的数据就进入控制寄存器;当控制寄存器配置完成后,紧接着的I/O线数据会在上升沿时进入对应地址的寄存器。
cpp
/**
*@breaf DS1302单字节写入函数
*@param command:写入命令字,包含要写入寄存器的地址;
*@param Data:将要写入的数据内容;
*@retval 无
*/
void DS1302_WriteBety(unsigned char command,Data)
{
unsigned char i;
DS1302_CE=1;//使能位置高电平;
//设置控制寄存器
for(i=0;i<8;i++)//控制寄存器数据需要通过IO线一个一个写入控制寄存器;低位先写入
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=1; //给一个上升沿
DS1302_SCLK=0;
}
//设置相关年月日寄存器
for(i=0;i<8;i++)//数据写入
{
DS1302_IO=Data&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
单字节读出 :当CE为高电平时,通过单片机控制SCLK产生脉冲,每一个上升沿,I/O线的数据就进入控制寄存器;当控制寄存器配置完成后,紧接着对应地址的寄存器数据会在下降沿时进入I/O线。
cpp
/**
*@breaf DS1302单字节读出函数
*@param command:写入控制指令的指令,包含要读出寄存器的地址;
*@retval Data:读出的数据;
*/
unsigned char DS1302_ReadBety(unsigned char command)
{
unsigned i,Data=0X00;
command|=0X01;//写入指令与读出指令只在最后一位相差1,故在此利用或运算消除;
DS1302_CE=1;//使能位置高电平;
for(i=0;i<8;i++)//控制寄存器数据需要通过IO线一个一个写入控制寄存器;低位先写入
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
DS1302_IO=0;
for(i=0;i<8;i++)//数据读出
{
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO)
{Data |=(0X01<<i);}
}
DS1302_CE=0;
return Data;
}
注意数据输入从低位开始。
代码设计
main.c
cpp
#include <REGX52.H>
#include "lcd1602.h"//包含LCD1602头文件
#include "DS1302.h"//包含DS1302头文件
void main ()//定义主函数
{
LCD_Init(); //LCD1602初始化
DS1302_Init();//DS1302初始化
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);//显示秒
LCD_ShowNum(2,13,DS1302_Time[6], 1);//显示星期
}
}
DS1302.c
cpp
#include <REGX52.H>
//引脚定义;
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
//对应寄存器位置定义
#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
//定义数组,用于存放设定的时间,年月日时分秒星期
unsigned char DS1302_Time[]={23,10, 28,19,00,59,6};//顺序:年月日时分秒星期
/**
*@breaf DS1302初始化
*@param无
*@retval无
*/
void DS1302_Init()
{
DS1302_CE=0;//将使能位置0,低电平;
DS1302_SCLK=0;//将时钟位置0,低电平;
}
/**
*@breaf DS1302单字节写入函数
*@param command:写入控制指令的指令,包含要写入寄存器的地址;
*@param Data:将要写入的数据内容;
*@retval 无
*/
void DS1302_WriteBety(unsigned char command,Data)
{
unsigned char i;
DS1302_CE=1;//使能位置高电平;
for(i=0;i<8;i++)//控制寄存器数据需要通过IO线一个一个写入控制寄存器;低位先写入
{
DS1302_IO=command&(0x01<<i);//相当于把第1--7位置0,只留第0位,如果第0位是0,则为0;反之则为1;
DS1302_SCLK=1;
DS1302_SCLK=0;
}
for(i=0;i<8;i++)//数据写入
{
DS1302_IO=Data&(0x01<<i);//相当于把第1--7位置0,只留第0位,如果第0位是0,则为0;反之则为1;
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
/**
*@breaf DS1302单字节读出函数
*@param command:写入控制指令的指令,包含要读出寄存器的地址;
*@retval Data:读出的数据;
*/
unsigned char DS1302_ReadBety(unsigned char command)
{
unsigned i,Data=0X00;
command|=0X01;//写入指令与读出指令只在最后一位相差1,故在此利用或运算消除;
DS1302_CE=1;//使能位置高电平;
for(i=0;i<8;i++)//控制寄存器数据需要通过IO线一个一个写入控制寄存器;低位先写入
{
DS1302_IO=command&(0x01<<i);//相当于把第1--7位置0,只留第0位,如果第0位是0,则为0;反之则为1;
DS1302_SCLK=0;
DS1302_SCLK=1;
}
DS1302_IO=0;
for(i=0;i<8;i++)//数据读出
{
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO)
{Data |=(0X01<<i);}
}
DS1302_CE=0;
return Data;
}
/**
*@breaf 向DS1302内设定时间
*@param无
*@retval无
*/
void DS1302_SetTime()
{
DS1302_WriteBety(DS1302_WP,0x00);//操作 DS1302 之前,关闭写保护,不然指令无法进入控制寄存器;
DS1302_WriteBety(DS1302_YEAR, DS1302_Time[0]/10*16+DS1302_Time[0]%10);//写入年,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_MONTH, DS1302_Time[1]/10*16+DS1302_Time[1]%10);//写入月,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_DATE, DS1302_Time[2]/10*16+DS1302_Time[2]%10);//写入日,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_HOUR, DS1302_Time[3]/10*16+DS1302_Time[3]%10);//写入时,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_MINUTE, DS1302_Time[4]/10*16+DS1302_Time[4]%10);//写入分,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_SECOND, DS1302_Time[5]/10*16+DS1302_Time[5]%10);//写入秒,并将10进制转化BCD码;
DS1302_WriteBety(DS1302_DAY, DS1302_Time[6]/10*16+DS1302_Time[6]%10);//写入星期,并将10进制转化BCD码;
DS1302_WriteBety( DS1302_WP,0x80);//写入结束,开启写保护;
}
/**
*@breaf 读取DS1302内时间
*@param无
*@retval无
*/
void DS1302_ReadTime()
{
unsigned char Temp;//定义变量,用于暂时存储BCD码
Temp=DS1302_ReadBety(DS1302_YEAR);//读取年BCD码;
DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_MONTH);//读取月BCD码;
DS1302_Time[1]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_DATE);//读取日BCD码;
DS1302_Time[2]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_HOUR);//读取小时BCD码;
DS1302_Time[3]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_MINUTE);//读取分钟BCD码;
DS1302_Time[4]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_SECOND);//读取秒BCD码;
DS1302_Time[5]=Temp/16*10+Temp%16;//BCD码转十进制;
Temp=DS1302_ReadBety(DS1302_DAY);//读取星期BCD码;
DS1302_Time[6]=Temp/16*10+Temp%16;//BCD码转十进制;
}
DS1302.h
cpp
#ifndef __DS1302_H__
#define __DS1302_H__
extern unsigned char DS1302_Time[];//声明设置时间的数组
void DS1302_Init();//声明初始化函数
void DS1302_WriteBety(unsigned char command,Data);//声明时间写入函数
unsigned char DS1302_ReadBety(unsigned char command);//声明时间读出函数
void DS1302_SetTime();//声明设置内部时间函数
void DS1302_ReadTime();//声明读取内部时间函数
#endif