RYMCU

第 17 章 实时时钟 DS1302 实验

ronger 4 月前
# Nebula Pi # DS1302

前言

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

17.1 DS1302 功能简介

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

image181.png

图 17-1 DS1302 封装示意图

DS1302 典型通信电路如下图所示,

image182.png

图 17-2 DS1302 典型通信图

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

表 17-1 DS1302 管脚定义

管脚号 管脚名称 功能介绍
1 VCC2 为芯片的主供电端, VCC1 为芯片的备用供电端。当 VCC2 端电压值比 VCC1 电压值大 0.2V 以上时,由 VCC2 供电,否则由 VCC1 供电。通常情况下在 VCC1 处接入备用电池,当系统主电源掉电后,芯片切换到 VCC1 供电,保证芯片时钟继续运行。
2 X1 两端直接与 32.768KHz 的石英晶振相连接,或者 X1 连接外部 32.768KHz 时钟, X2 悬空。
3 X2 两端直接与 32.768KHz 的石英晶振相连接,或者 X1 连接外部 32.768KHz 时钟, X2 悬空。
4 GND 地引脚
5 CE 读或者写数据期间 CE 必须被拉高,称为片选信号
6 I/O 双相数据输入/输出口
7 SCLK 数据传输的同步时钟,由外部处理器提供
8 VCC1 备用电池接口

17.2 单字节操作模式

与 DS1302 进行数据通信时,首先得向 DS1302 传输一个字节的控制指令,控制指令位定义如下所示。

image183.png

图 17-3 控制指令字节

字节的最高位 bit7 必须为 1,否则无法向 DS1302 写入数据。 Bit6 为 1 时,表示后续对 31 字节的 RAM 进行读写操作。为 0 时,表示后续将对时钟寄存器进行读写操作。 Bit5~bit1 为后续操作的时钟寄存器或 RAM 的地址。最低位 bit0 为 1 时,表示读取 DS1302 的数据,为 0 时,表示向 DS1302 写入数据。对 DS1302 进行单字节模式的读、写操作时序如下图所示。

image184.png

图 17-4 单字节读写时序

单字节写操作(Single-Byte Write)时序为首先向 DS1302 写入控制指令,紧接着写入一个字节的数据。写入的顺序为低位在前,高位在后的传输方式,要求在时钟上升沿准备好数据。

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

#define uchar unsigned char  
 #define  uint unsigned int  
   
 sbit    CE_1302 = P0^5;  //DS1302 通信引脚 CE, I/O, SCLK 定义
sbit   IO_1302 = P0^4;
sbit SCLK_1302 = P0^3;
  
// 写字节
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;
}

图 17-5 字节读、写函数

如上图所示,写字节函数 WrByte_1302() 中,要求单片机在时钟 SCLK_1302 上升沿前将数据放到数据总线 IO_1302 上,然后产生一个 SCLK_1302 上升,完成 1bit 数据的写入,同时要求 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;
}

图 17-6 单字节读、写模式函数

我们这里重点讲述 DS1302 的时钟功能,因此与涓流充电有关的 31 字节 RAM 操作这里不做详细的介绍。与实时时钟有关的寄存器如图所示。

image185.png

图 17-7 实时时钟寄存器定义

与时钟有关的寄存器总共有 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 个寄存器不影响时钟功能,暂不做介绍。

因此,在 DS1302 应用时要对它进行初始化,首先解除写保护,然后将与时间有关的 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个时钟数据
   }
   // Wr Burst _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++;
   }
     
   // Rd Burst _1302(CurrentTime); // 当采用 Burst 模式时,使用此语句替代上面 for 循环语句
}

17-8 初始化与时间值获取函数

按照惯例将上述代码打包放入 DS1302 驱动文件中, Drive_ DS1302 .h, Drive_ DS1302 .c。

到目前为止,我们已经学习了时钟芯片 DS1302 的功能介绍,以及初始化和时钟获取函数的编写, Nebula Pi 单片机开发板上 DS1302 电路原理图如下所示,三根通信线分别与单片机的 I/O 口相连接。

image186.png

图 17-9 DS1302 原理图

我们利用上面编写的函数以及学习的单片机知识,开始编写一个小的时钟显示综合应用程序。程序的功能为:

上电时由单片机对 DS1302 进行初始化,设置时间为 2021 年、星期二、6 月 1 日、23 时 58 分 56 秒。初始化完成后,每隔 500ms 获取 DS1302 的时间,并将时间显示到 1602 液晶显示器上,主函数程序如下图所示:

/*******************************************************************
*             实时时钟 DS1302 显示测试
* ******************************************************************
* 【主芯片】:STC89SC52/STC12C5A60S2
* 【主频率】: 11.0592MHz
*
* 【版  本】: V1.0
* 【作  者】: stephenhugh
* 【网  站】:https://rymcu.taobao.com/
* 【邮  箱】:
*
* 【版  权】All Rights Reserved
* 【声  明】此程序仅用于学习与参考,引用请注明版权和作者信息!
*
*******************************************************************/
#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 FM = P0^0;// 蜂鸣器
sbit DU = P0^6;// 数码管段选、位选引脚定义
sbit WE = P0^7;
     
uchar T_flag  = 0;// 定时 500ms 标志位
uchar str[23]=0;  // 字符临时存储变量
 
unsigned char code SetTime[7]={//2021 年,星期二, 06 月 01 日,23 时 58 分 56 秒,时间初始值
                           //0x56,0x58,0x23,0x31,0x12,0x07,0x17};
                           0x56,0x58,0x23,0x01,0x06,0x02,0x21};
uchar CurrentTime[7]={0};// 存储时间变量
 
void main()
{
   Init_1602();//1602 初始
 
   P2 = 0xff;// 关闭所有数码管
   WE = 1;
   WE = 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;
   }
}

图 17-10 主函数

显示结果如下图所示:

image187.png

图 17-11 时间显示试验结果

17.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位时钟数据
   }
       WrByte_1302(0);// 写第8个寄存器,不写的话可能无法使用
   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;// 拉低片选
}

图 17-12 突发模式读、写函数

如上图所示,首先向 DS1302 写入突发读或者写指令,然后紧接着读取或写入 8 个时间数据。将函数更新到 DS1302 的驱动文件中,头文件如下图所示。

#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

图 17-13 DS1302 的驱动头文件

突发模式的应用与单字节模式类似,只需用图 17-8 所示的第 15 行替代 11-14 行的 for 循环语句,第 31 行替代 25-29 行的 for 循环语句便可,这里不再赘述。

17.4 本章小结

本章详细讲解了实时时钟芯片 DS1302 的工作原理,并且编写了驱动函数以及实际应用,后续可以直接使用驱动文件,无需重复造轮子了。

后发布评论