51单片机实时时钟DS1302·

51单片机第15章--实时时钟DS1302实验

Hugh

Hugh

212 2

概述

在许多系统当中都需要精确的时钟功能,因此时钟芯片孕育而生。其中美国达拉斯 DALLAS 公司设计的 DS1302 是一款非常流行的数字时钟芯片。DS1302 是一款具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、星期、时、分、秒进行计时,并且具有闰年功能。年计数可达到 2100 年。

15.1 DS1302 功能简介

DS1302 内部包含 31 字节的通用 RAM,实现设置备用电池功能。采用 3 线制的串行数据通信接口,并且适用于大多数的微处理器。工作电压范围达到 2~5V,与 5VTTL 电平完全兼容。支持单字节或多字节时钟、RAM 数据读、写操作。当工作电压为 2V 时,工作电流低至 320nA。工业级 DS1302 正常工作温度范围为:-40℃ ~85℃,芯片包括直插和贴片两种封装模式,封装示意图如下所示:

DS1302 典型通信电路如下图所示: 如上图所示,只需 3 根线 CE、I/O、SCLK 便可实现处理器与 DS1302 之间通信,上图中 X1,X2 之间为外接时钟晶振,VCC2 为电源供电端,VCC1 为备用电池端。各管脚定义及功能如下所示:

15.2 单字节操作模式

与 DS1302 进行数据通信时,首先得向 DS1302 传输一个字节的控制指令,控制指令位定义如下所示。 字节的最高位 bit7 必须为 1,否则无法向 DS1302 写入数据。Bit6 为 1 时,表示后续对 31 字节的 RAM 进行读写操作,为 0 时,表示后续将对时钟寄存器进行读写操作。Bit5~bit1 为后续操作的时钟寄存器或 RAM 的地址。最低位 bit0 为 1 时,表示读取 DS1302 的数据,为 0 时,表示向 DS1302 写入数据。对 DS1302 进行单字节模式的读、写操作时序如下图所示。

单字节写操作(Single-Byte Write)时序为首先向 DS1302 写入控制指令,紧接着写入一个字节的数据。写入的顺序为低位在前,高位在后的传输方式,要求在时钟上升沿准备好数据。单字节读操作(Single-Byte Read)时序为首先向 DS1302 写入控制指令,紧接着 SCLK 的的下降沿 DS1302 有数据 D0 输出,因此在接下来的上升沿前读取稳定的 D0 值,依次类推至 D7。根据上图时序要求,往 DS1302 写入一个字节和读取一个字节的函数如下所示:

//写字节
void WrByte_1302(uchar dat)
{
    uchar j;
    bit flag;

    for(j=1;j<=8;j++)
    {   //从低到高依次将1Byte数据写入DS1302
        flag = dat&0x01;

        IO_1302 = flag;//将要写的位放到总线
        SCLK_1302 = 0;
        SCLK_1302 = 1;//产生一个上升沿,完成1位数据写入

        dat=dat>>1;//将数据移到下一位
    }   
}
//读字节
uchar RdByte_1302(void)
{
    uchar dat,flag,j;
    for(j=1;j<=8;j++)
    {       
        SCLK_1302 = 1;//产生一个下降沿
        SCLK_1302 = 0;

        flag = IO_1302;//读取DS1302发出的一位数据
        dat=(dat>>1)|(flag<<7);//读出的值最低位在前面
    }
    return dat; 
}

如上所示,写字节函数 WrByte_1302()中,要求单片机在时钟 SCLK_1302 上升沿前将数据放到数据总线 IO_1302 上,然后产生一个 SCLK_1302 上升,完成一 bit 数据的写入,同时要求 1Byte 的数据低位在前,高位在后依次发送。读字节函数 RdByte_1302()中,要求在时钟 SCLK_1302 下降沿之后,将总线 IO_1302 数据读出,同时要求 1Byte 的数据低位在前,高位在后依次读取。

在上述函数的基础上,单字节操作模式的读、写函数如下所示:

//单字节写模式
void WrSingle_1302(uchar addr,uchar dat)
{
    CE_1302 = 1;//拉高片选
    WrByte_1302(addr);//写入地址及控制指令
    WrByte_1302(dat);//写入数据
    CE_1302 = 0;//拉低片选
    SCLK_1302 = 0;//释放始终总线,满足下次操作时序要求(非常重要)

}
//单字节读模式
uchar RdSingle_1302(uchar addr)
{
    uchar dat;

    CE_1302 = 1;//拉高片选
    WrByte_1302(addr);//写入地址及控制指令
    dat = RdByte_1302();//读取一个字节数据
    CE_1302 = 0;//拉低片选

    return dat;
}

我们这里重点讲述 DS1302 的时钟功能,因此与涓流充电有关的 31 字节 RAM 操作这里不做详细的介绍。与实时时钟有关的寄存器如图所示。 与时钟有关的寄存器总共有 9 各如上图所示,前 7 个分别为:秒、分、时、日、月、星期、年,均为 8 位寄存器。以秒寄存器为例介绍时间的表示法,其中 bit6-bit4 为秒的十位,bit3-bit0 为秒的个位。59 秒时,bit6-bit4=“101”,bit3-bit0=“1001”,其它依此类推。另外,设置“时”寄存器的 bit7 可以设置为 12 小时或 24 小时制。上述寄存器的读写控制指令字节分别如图左侧两列所示。

秒寄存器的 CH(bit7)定义为时钟运行标志位,当该位被设置成 1 时,时钟计时停止,并且 DS1302 进入低功耗模式。当设置为 0 时,启动计时。上电初始状态时,该位状态不定,因此在时钟初始化时确保该为被清 0,保证后续时钟芯片正常运行。

第 8 个寄存器为控制寄存器,WP(bit7)为写保护位,当设置为 1 时,无法向 DS1302 写入数据,上电是该位状态不定,因此,需要对 DS1302 其它寄存器进行写操作之前,务必先将 WP 设置为 0。第 9 个寄存器不影响实现时钟功能,暂不做介绍。

因此,在 DS 1302 应用时要对它进行初始化,首先解除写保护,然后将与时间有关的 7 个寄存器赋初值,将初始化的内容放到函数 Init_1302(Uchar *SetTime)中。另外,我们将从 DS1302 读取 7 个时间值的操作放到函数 GetTime(*CurrentTime)中,如下代码所示。

//1302初始化
void Init_1302(uchar *SetTime)
{ 
      uchar j;

      CE_1302 = 0;//初始化通信引脚
    SCLK_1302 = 0;

    WrSingle_1302(0x8E,0x00);//解除写保护(WP=0)
    
    for(j=0;j<=6;j++)
    {
        WrSingle_1302(0x80+2*j,SetTime[j]);//写入7个时钟数据
    }
    //WrBurst_1302(SetTime);//当采用Burst模式时,使用此语句替代上面for循环语句
}
//获取当前时间值
void GetTime(uchar *CurrentTime)
{
      uchar j;

      CE_1302 = 0;//初始化通信引脚
    SCLK_1302 = 0;

    for(j=0;j<=6;j++)
    {
         *CurrentTime = RdSingle_1302(0x81+2*j);//读取7个时钟数据
         CurrentTime++;
    }
    
    //RdBurst_1302(CurrentTime); //当采用Burst模式时,使用此语句替代上面for循环语句
}

到目前为止,我们已经学习了时钟芯片 DS1302 的共能介绍,以及初始化和时钟获取函数的编写,RY-51 单片机开发板上 DS1302 电路原理图如下所示,三根通信线分别接 4.7K 上拉电阻,分别与单片机的 I/O 口相连接。 按照惯例我们将和 DS1302 有关的函数打包放入"Drive_DS1302.h","Drive_DS1302.c"中,方便后续调用及移植。"Drive_DS1302.h"代码如下:

#ifndef __DS1302_H__
#define __DS1302_H__

//1302初始化
extern void Init_1302(unsigned char *SetTime);
//获取时间
extern void GetTime(unsigned char *CurrentTime); 
//单字节模式写
void WrSingle_1302(unsigned char addr,unsigned char dat);
//单字节模式读          
unsigned char RdSingle_1302(unsigned char addr);
//突发模式写
void WrBurst_1302(unsigned char *SetTime);
//突发模式读
void RdBurst_1302(unsigned char *CurrentTime);

#endif

"Drive_DS1302.c"代码如下:

#include<reg52.h>
#include"Drive_DS1302.h"

#define uchar unsigned char
#define  uint unsigned int

sbit   CE_1302 = P1^2;  //DS1302通信引脚CE,I/O,SCLK定义
sbit   IO_1302 = P1^1;
sbit SCLK_1302 = P1^0;  

//写字节
void WrByte_1302(uchar dat)
{
    uchar j;
    bit flag;

    for(j=1;j<=8;j++)
    {   //从低到高依次将1Byte数据写入DS1302
        flag = dat&0x01;

        IO_1302 = flag;//将要写的位放到总线
        SCLK_1302 = 0;
        SCLK_1302 = 1;//产生一个上升沿,完成1位数据写入

        dat=dat>>1;//将数据移到下一位
    }   
}
//读字节
uchar RdByte_1302(void)
{
    uchar dat,flag,j;
    for(j=1;j<=8;j++)
    {       
        SCLK_1302 = 1;//产生一个下降沿
        SCLK_1302 = 0;

        flag = IO_1302;//读取DS1302发出的一位数据
        dat=(dat>>1)|(flag<<7);//读出的值最低位在前面
    }
    return dat; 
}
//单字节写模式
void WrSingle_1302(uchar addr,uchar dat)
{
    CE_1302 = 1;//拉高片选
    WrByte_1302(addr);//写入地址及控制指令
    WrByte_1302(dat);//写入数据
    CE_1302 = 0;//拉低片选
    SCLK_1302 = 0;//释放始终总线,满足下次操作时序要求(非常重要)

}
//单字节读模式
uchar RdSingle_1302(uchar addr)
{
    uchar dat;

    CE_1302 = 1;//拉高片选
    WrByte_1302(addr);//写入地址及控制指令
    dat = RdByte_1302();//读取一个字节数据
    CE_1302 = 0;//拉低片选

    return dat;
}
//突发写模式
void WrBurst_1302(uchar *SetTime)
{
    uchar j;

    CE_1302 = 1;//拉高片选
    WrByte_1302(0xBE);//Burst模式写专用指令
    for(j=0;j<=6;j++)
    {
        WrByte_1302(SetTime[j]);//写入7位时钟数据
    }
    CE_1302 = 0;//拉低片选  
}
//突发读模式
void RdBurst_1302(uchar *CurrentTime)
{
    uchar j;

    CE_1302 = 1;//拉高片选
    WrByte_1302(0xBF);//Burst模式读专用指令
    for(j=0;j<=6;j++)
    {
        *CurrentTime = RdByte_1302();//读取一个字节数据;
        CurrentTime++;
    }
    CE_1302 = 0;//拉低片选  
}
//1302初始化
void Init_1302(uchar *SetTime)
{ 
      uchar j;

      CE_1302 = 0;//初始化通信引脚
    SCLK_1302 = 0;

    WrSingle_1302(0x8E,0x00);//解除写保护(WP=0)
    
    for(j=0;j<=6;j++)
    {
        WrSingle_1302(0x80+2*j,SetTime[j]);//写入7个时钟数据
    }
    //WrBurst_1302(SetTime);//当采用Burst模式时,使用此语句替代上面for循环语句
}
//获取当前时间值
void GetTime(uchar *CurrentTime)
{
      uchar j;

      CE_1302 = 0;//初始化通信引脚
    SCLK_1302 = 0;

    for(j=0;j<=6;j++)
    {
         *CurrentTime = RdSingle_1302(0x81+2*j);//读取7个时钟数据
         CurrentTime++;
    }
    
    //RdBurst_1302(CurrentTime); //当采用Burst模式时,使用此语句替代上面for循环语句
}

下面我们利用上面编写的函数以及学习的单片机的知识,开始编写一个小的时钟显示综合应用程序。程序的功能为:上电时由单片机对 DS1302 进行初始化,设置时间为 2017 年、星期日、12 月 31 日、23 时 58 分 56 秒,初始化完成后,每隔 500ms 获取 DS1302 的时间,并将时间显示到 1602 液晶显示器上,主函数程序如下所示:

#include<reg52.h>
#include"Drive_1602.h"
#include"Drive_DS1302.h"

#define uchar unsigned char
#define  uint unsigned int

#define FOSC 11059200 //单片机晶振频率
#define T_1ms (65536 - FOSC/12/1000)  //定时器初始值计算

sbit DU = P2^7;//数码管段选、位选引脚定义
sbit WE = P2^6;
sbit DU_L = P2^3;
    
uchar T_flag  = 0;//定时500ms标志位
uchar str[23]=0;  //字符临时存储变量
unsigned char code SetTime[7]={//2017年,星期日,12月31日,23时58分56秒,时间初始值
                0x56,0x58,0x23,0x31,0x12,0x07,0x17};
uchar CurrentTime[7]={0};//存储时间变量

void main()
{
    Init_1602();//1602初始

    P0 = 0xff;//关闭所有数码管
    WE = 1;
    WE = 0; 
    DU_L = 0;

    TMOD = 0x01;     //定时器工作模式配置
    TL0  = T_1ms;   //装载初始值
    TH0  = T_1ms>>8;
    TR0  = 1;        //启动定时器
    ET0  = 1;        //允许定时器中断
    EA   = 1;        //开总中断


    Init_1302(SetTime);//1302初始化

    while(1)
    {
        if(T_flag)//500ms定时
        {
            T_flag = 0;
            GetTime(CurrentTime);//获取时间

            str[0] = '2';    
            str[1] = '0';    
            str[2] = (CurrentTime[6]>>4)+'0';    //年
            str[3] = (CurrentTime[6]& 0x0F)+'0'; 
            str[4] = '-';
            str[5] = (CurrentTime[4]>>4)+'0';    //月
            str[6] = (CurrentTime[4]& 0x0F)+'0';
            str[7] = '-';
            str[8] = (CurrentTime[3]>>4)+'0';    //日
            str[9] = (CurrentTime[3]& 0x0F)+'0'; 
           str[10] = '\0';
           str[11] = (CurrentTime[2]>>4)+'0';    //时
           str[12] = (CurrentTime[2]& 0x0F)+'0';
           str[13] = ':';
           str[14] = (CurrentTime[1]>>4)+'0';    //分
           str[15] = (CurrentTime[1]& 0x0F)+'0'; 
           str[16] = ':';      
           str[17] = (CurrentTime[0]>>4)+'0';    //秒
           str[18] = (CurrentTime[0]& 0x0F)+'0';
           str[19] = ' ';
           str[20] = (CurrentTime[5]>>4)+'0';    //星期
           str[21] = (CurrentTime[5]& 0x0F)+'0';
           str[22] = '\0';
            
            Disp_1602_str(1,4,str); //将获得的时间分别显示到1602的第一二行
            Disp_1602_str(2,3,str+11);  
        }
    }
}

//定时器0中断子程序,定时1ms
void timer0() interrupt 1
{
    static uint T_500ms = 0;

    TL0 = T_1ms;//重装初始值
    TH0 = T_1ms>>8; 

    T_500ms++;
    if(T_500ms>=500)//500ms,置位T_flag
    {
        T_500ms = 0;
        T_flag = 1; 
    }
}

将程序下查看结果是否与预想的一致吧。

15.3 突发操作模式

上面我们讲解的是以单字节的模式,从 1302 中连续读取时间数据。仔细的同学可能会发现一个问题,就是我们连续读 7 个时间寄存器是有先后顺序的,会有读错数据的风险。例如我们要读的时间为 23 时 59 分 59 秒,最开始时我们把 59 秒读出来了,如果刚好在你读完的时候 59 秒变成了 00 秒,59 分变成了 00 分,23 时变成了 00 时,接下来把分、时依次读出来,因此我们读出来的时间为 00 时 00 分 59 秒,很显然这个时间是不对的。下面我们讲解的突发操作模式有效的解决了这个问题。

在突发操作读模式下,当 DS1302 收到突发读数据指令,DS1302 首先会把 8 个时间寄存器的数据同时读出存放在 8 个二级时间寄存器中,然后依次把 8 个二级时间寄存器的数据输出给单片机,突发读专用指令为 0xBF。同样,当我们需要写 DS1302 时,当收到突发写指令后,DS1302 将接收到的 8 个连续数据存储到 8 个二级时间寄存器中,然后同时将 8 各数据写到时间寄存器中,突发写专用指令为 0xBE。根据上述原理,编写突发写模式和突发读模式函数如下代码所示。

//突发写模式
void WrBurst_1302(uchar *SetTime)
{
    uchar j;

    CE_1302 = 1;//拉高片选
    WrByte_1302(0xBE);//Burst模式写专用指令
    for(j=0;j<=6;j++)
    {
        WrByte_1302(SetTime[j]);//写入7位时钟数据
    }
    CE_1302 = 0;//拉低片选  
}
//突发读模式
void RdBurst_1302(uchar *CurrentTime)
{
    uchar j;

    CE_1302 = 1;//拉高片选
    WrByte_1302(0xBF);//Burst模式读专用指令
    for(j=0;j<=6;j++)
    {
        *CurrentTime = RdByte_1302();//读取一个字节数据;
        CurrentTime++;
    }
    CE_1302 = 0;//拉低片选  
}

如上图所示,首先为向 DS1302 写入突发读或者写指令,然后紧接着是读取或写入 7 个时间数据。前面讲解的都是 8 个连续的数据,我们这里写 7 个的原因是,时间显示这 7 个就足够了。上述完整代码详见完整代码"Drive_DS1302.h"、"Drive_DS1302.c"中。

突发模式的应用与单字节模式类似,只需将如下代码中的for循环语句替换成“RdBurst_1302(CurrentTime)”即可。

//获取当前时间值
void GetTime(uchar *CurrentTime)
{
      uchar j;

      CE_1302 = 0;//初始化通信引脚
    SCLK_1302 = 0;

    for(j=0;j<=6;j++)
    {
         *CurrentTime = RdSingle_1302(0x81+2*j);//读取7个时钟数据
         CurrentTime++;
    }
    
    //RdBurst_1302(CurrentTime); //当采用Burst模式时,使用此语句替代上面for循环语句
}

15.4 本章小结

本章详细介绍了实时时钟芯片DS1302的工作原理,驱动程序的编写,以及DS1302的简单应用。

所属系列

从当前文章继续阅读它所在合集中的前后内容。

51单片机 第 15 / 19 篇
查看合集

本专栏C语言开发为基础,旨在深入解析51单片机技术开发,嵌入式C语言编程。 订阅了本专栏您将可以收获的技能: 1. 系统学习 51单片机开发 2. 掌握嵌入式C语言开发流程 3. 深度剖析51单片机工作原理 4. 了解更多嵌入式开发原理、流程 适宜人群: 1. 所有想了解 51单片机开发的人 2. 从事嵌入式开发的工程师 3. 想要了解C语言、单片机等技术之外的内容的人 作为订阅福利,我们建立了微信技术交流群,大家在这里可以认识更多做朋友。需要的朋友加微信:「RYMCU交流1群」。欢迎关注微信公众号「rymcucom」,订阅最新内容。

相关文章

优先推荐同专题、同标签和同作者内容,补足热门文章。

评论 2

登录 后参与评论

评论 2

ly824625321
ly82462532111月29日 16:51

这是把教材整上来了么

ronger
ronger 回复 @ly82462532111月30日 12:45

? 是的,相较于 PDF 版,在网页上更方便阅读,校准版 51 教程 关于我和昊楠君学嵌入式开发这件事 教程 github 地址 欢迎提出建议和反馈问题