DS1302 时钟实验
在前面章节,我们介绍了如何使用单片机 IO 口模拟 IIC 总线、单总线时序。这一章我们来学习 DS1302 时钟芯片,该芯片是 3 线 SPI 接口,所以需要使用 51单片机的 3 个 IO 口模拟 SPI 时序与 DS1302 时钟芯片通信,将时钟日历数据读取出来。开发板上集成了 1 个 DS1302 时钟模块,可使用它设计一个多功能电子时钟。本章要实现的功能是:系统运行时,数码管上显示电子时钟时分秒,格式为“XX-XX-XX”。
DS1302时钟芯片介绍
DS1302简介
DS1302是DALLAS公司推出的涓流充电时钟芯片,内含有一个实时时钟/日历和31字节静态RAM,通过简单的串行接口与单片机进行通信。实时时钟/日历电路提供秒、分、时、日、周、月、年的信息,每月的天数和闰年的天数可自动调整。时钟操作可通过AM/PM指示决定采用24或12小时格式。DS1302与单片机之间能简单地采用同步串行的方式进行通信,仅需用到三根通信线:①RES复位②I/O数据线③SCLK串行时钟。时钟/RAM的读/写数据以一个字节或多达31个字节的字符组方式通信。DS1302工作时功耗很低保持数据和时钟信息时功率小于1mW。
DS1302由DS1202改进而来增加了以下的特性:双电源管脚用于主电源VCC2和备份电源VCC2供应,Vcc1为可编程涓流充电电源,附加七个字节存储器。它广泛应用于电话、传真、便携式仪器以及电池供电的仪器仪表等产品领域下面。
主要的性能指标:
★实时时钟具有能计算2100年之前的秒、分、时、日、星期、月、年的能力,还有闰年调整的能力;
★31个8位暂存数据存储RAM;
★串行I/O口方式使得管脚数量最少;
★宽范围工作电压2.0~5.5V;
★工作在2.0V时,电流小于300nA;
★读/写时钟或RAM数据时有两种传送方式单字节传送和多字节传送字符组
方式;
★8脚DIP封装或可选的8脚SOIC封装根据表面装配;
★简单3线接口;
★与TTL兼容Vcc=5V;
★可选工业级温度范围-40~+85;
下面来看下DS1302芯片的管脚及功能。
1,VCC2:主电源引脚
2,X1、X2:DS1302 外部晶振引脚,通常需外接 32.768K 晶振
3,GND:电源地
4,CE:使能引脚,也是复位引脚(新版本功能变)。
5,I/O:串行数据引脚,数据输出或者输入都从这个引脚
6,SCLK:串行时钟引脚
7,VCC1:备用电源
DS1302使用
操作DS1302的大致过程,就是将各种数据写入DS1302的寄存器,以设置它当前的时间的格式。然后使DS1302开始运作,DS1302时钟会按照设置情况运转,再用单片机将其寄存器内的数据读出。再用液晶显示,就是我们常说的简易电子钟。所以总的来说DS1302的操作分2步,第一步读取时间,第二步将时间显示出来(显示部分属于液晶显示的内容,不属于DS1302本身的内容),但是在讲述操作时序之前,我们要先看看寄存器,
DS1302有一个控制寄存器、12个日历、时钟寄存器和31个RAM。
(1)控制寄存器
控制寄存器用于存放DS1302的控制命令字,DS1302的RST引脚回到高电平后写入的第一个字节就为控制命令。它用于对DS1302读写过程进行控制,格式如下:
上图是DS1302的寄存器样式,我们看到:
1、第7位永远都是1;
2、第6位,1表示RAM,寻址内部存储器地址;0表示CK,寻址内部寄存器;
3、第5到第1位,为RAM或者寄存器的地址;
4、最低位,高电平表示RD,即下一步操作将要“读”;低电平表示W,即下一步操作将要“写”。(与AT24C02寄存器类似,这点要理解好)。
比如要读秒寄存器则命令为 1000 0001,反之写为 1000 0000,要注意其含义。
(2) 日 历/ 时 钟 寄 存 器
DS1302 共有 12 个寄存器,其中有 7 个与日历、时钟相关,存放的数据为 BCD码形式。格式如下:
下面对几个寄存器做下说明:
**秒 寄 存 器 :**低四位为秒的个位,高的次三位为秒的十位。最高位 CH 为DS1302 的运行标志,当 CH=0 时,DS1302 内部时钟运行,反之 CH=1 时停止;
**小 时 寄 存 器 :**时寄存器。最高位为 12/24 小时的格式选择位,该位为 1 时表示 12 小时格式。当设置为 12 小时显示格式时,第 5 位的高电平表示下午(PM);而当设置为 24 小时格式时,第 5 位位具体的时间数据。
**写 保 护 寄 存 器 :**当该寄存器最高位 WP 为 1 时,DS1302 只读不写,所以要在往 DS1302 写数据之前确保 WP 为 0。
**慢 充 电 寄 存 器 ( 涓 细 电 流 充 电 ) 寄 存 器 :**我们知道,当 DS1302 掉电时,可以马上调用外部电源保护时间数据。该寄存器就是配置备用电源的充电选项的。其中高四位(4 个 TCS)只有在 1010 的情况下才能使用充电选项;低四位的情况与 DS1302 内部电路有关。
前面我们提到在日历/时钟寄存器中都是以 BCD 码存放数据,那么 BCD 码是什么呢?BCD 码是通过 4 位二进制码来表示 1 位十进制中的 0~9 这 10 个数码。如下所示:
所以从 DS1302 中读取出来的时钟数据均为 BCD 码格式,需转换为我们习惯的10进制,转换方法在源程序里,后面我们会介绍。
(3)DS1302的读写时序
在控制指令字输入后的下一个SCLK时钟的上升沿时,数据被写入DS1302,数据输入从低位(位0)开始。同样,在紧跟8位的控制指令字后的下一个SCLK脉冲的下降沿读出DS1302的数据,读出数据时从低位0位到高位7。其时序图如下所示:
上图就是DS1302的三个时序:复位时序,单字节写时序,单字节读时序;
CE(RST):复位时序,即在RST引脚产生一个正脉冲,在整个读写器件,RST要保持高电平,一次字节读写完毕之后,要注意把RST返回低电平准备下次读写周期;
单字节读时序:注意读之前还是要先对寄存器写命令,从最低位开始写;可以看到,写数据是在SCLK的上升沿实现,而读数据在SCLK的下降沿实现。所以,在单字节读时序中,写命令的第八个上升沿结束后紧接着的第八个下降沿就将要读寄存器的第一位数据读到数据线上了!这个就是DS1302操作中最特别的地方。当然读出来的数据也是最低位开始。
单字节写时序:两个字节的数据配合16个上升沿将数据写入即可。写命令的第八个上升沿结束后接着的第九个上升沿继续将要读寄存器的第一位数据写入。
程序注意事项:
★要记得在操作DS1302之前关闭写保护(将wp设置为0,下次开启再设置为1);
★注意用延时来降低单片机的速度以配合器件时序;
★DS1302读出来的数据是BCD码形式,要转换成我们习惯的10进制,转换方法在源程序里;
★读取字节之前,将IO设置为输入口,读取完之后,要将其改回输出口;
★在写程序的时候,建议实现开辟数组(内存空间)来集中放置DS1302的一系列数据,方便以后扩展键盘输入。
硬件设计
本实验使用到硬件资源如下:
(1)动态数码管
(2)DS1302
动态数码管电路在前面章节都介绍过,这里就不再重复。下面我们来看下开发板上DS1302时钟模块电路,如下图所示:
从上图中可以看出,该电路是独立的,DS1302芯片的控制管脚接至J3端子上,芯片的VCC1脚外接了一个纽扣电池BT1,以保证系统断电后时钟仍然可以运行,在芯片的X1、X2管脚处外接了一个32.768KHZ晶振,为时钟运行提供一个稳定的时钟频率,C2和C3为旁路电容,目的是消除晶振起振时产生的电感干扰。
由于该模块电路是独立的,所以DS1302芯片的控制管脚J3端子可以使用任意单片机管脚连接,为了与我们例程程序配套,这里使用单片机的P3.4管脚与时钟芯片的I/O脚连接,使用单片机的P3.5管脚与时钟芯片的CE脚连接,使用单片机的P3.6管脚与时钟芯片的SCLK脚连接。
软件设计
本章所要实现的功能是:数码管上显示电子时钟时分秒,格式为“XX-XX-XX”。
程序框架如下:
(1)编写数码管显示功能
(2)编写 DS1302 时钟读写功能
(3)编写主函数
ds1302.h:
#ifndef _ds1302_H
#define _ds1302_H
#include"public.h"
sbit DS1302_RST=P3^5;
sbit DS1302_IO=P3^4;
sbit DS1302_CLK=P3^6;
extern u8 gDS1302_TIME[7];
void ds1302_write_byte(u8 addr,u8 dat);
u8 ds1302_read_byte(u8 addr);
void ds1302_init(void);
void ds1302_read_time(void);
#endif
ds1302.c:
#include "ds1302.h"
#include "intrins.h"
//---DS1302 写入和读取时分秒的地址命令---//
//---秒分时日月周年 最低位读写位;-------//
u8 gREAD_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};
u8 gWRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};
//---DS1302 时钟初始化 2021 年 5 月 20 日星期四 13 点 51 分 47 秒。---//
//---存储顺序是秒分时日月周年,存储格式是用 BCD 码---//
u8 gDS1302_TIME[7] = {0x47, 0x51, 0x13, 0x20, 0x04, 0x05, 0x21};
/********************************************************************
***********
* 函 数 名 : ds1302_write_byte
* 函数功能 : DS1302 写单字节
* 输 入 : addr:地址/命令
dat:数据
* 输 出 : 无
*********************************************************************
**********/
void ds1302_write_byte(u8 addr,u8 dat)
{
u8 i=0;
DS1302_RST=0;
_nop_();
DS1302_CLK=0;//CLK 低电平
_nop_();
DS1302_RST=1;//RST 由低到高变化
_nop_();
for(i=0;i<8;i++)//循环 8 次,每次写 1 位,先写低位再写高位
{
DS1302_IO=addr&0x01;
addr>>=1;
DS1302_CLK=1;
_nop_();
DS1302_CLK=0;//CLK 由低到高产生一个上升沿,从而写入数据
_nop_();
}
for(i=0;i<8;i++)//循环 8 次,每次写 1 位,先写低位再写高位
{
DS1302_IO=dat&0x01;
dat>>=1;
DS1302_CLK=1;
_nop_();
DS1302_CLK=0;
_nop_();
}
DS1302_RST=0;//RST 拉低
_nop_();
}
/********************************************************************
***********
* 函 数 名 : ds1302_read_byte
* 函数功能 : DS1302 读单字节
* 输 入 : addr:地址/命令
* 输 出 : 读取的数据
*********************************************************************
**********/
u8 ds1302_read_byte(u8 addr)
{
u8 i=0;
u8 temp=0;
u8 value=0;
DS1302_RST=0;
_nop_();
DS1302_CLK=0;//CLK 低电平
_nop_();
DS1302_RST=1;//RST 由低到高变化
_nop_();
for(i=0;i<8;i++)//循环 8 次,每次写 1 位,先写低位再写高位
{
DS1302_IO=addr&0x01;
addr>>=1;
DS1302_CLK=1;
_nop_();
DS1302_CLK=0;//CLK 由低到高产生一个上升沿,从而写入数据
_nop_();
}
for(i=0;i<8;i++)//循环 8 次,每次读 1 位,先读低位再读高位
{
temp=DS1302_IO;
value=(temp<<7)|(value>>1);//先将 value 右移 1 位,然后 temp 左移 7 位,
最后或运算
DS1302_CLK=1;
_nop_();
DS1302_CLK=0;
_nop_();
}
DS1302_RST=0;//RST 拉低
_nop_();
DS1302_CLK=1;//对于实物中,P3.4 口没有外接上拉电阻的,此处代码需要添加,使数
据口有一个上升沿脉冲。
_nop_();
DS1302_IO = 0;
_nop_();
DS1302_IO = 1;
_nop_();
return value;
}
/********************************************************************
***********
* 函 数 名 : ds1302_init
* 函数功能 : DS1302 初始化时间
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void ds1302_init(void)
{
u8 i=0;
ds1302_write_byte(0x8E,0X00);
for(i=0;i<7;i++)
{
ds1302_write_byte(gWRITE_RTC_ADDR[i],gDS1302_TIME[i]);
}
ds1302_write_byte(0x8E,0X80);
}
/********************************************************************
***********
* 函 数 名 : ds1302_read_time
* 函数功能 : DS1302 读取时间
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void ds1302_read_time(void)
{
u8 i=0;
for(i=0;i<7;i++)
{
gDS1302_TIME[i]=ds1302_read_byte(gREAD_RTC_ADDR[i]);
}
}
各函数功能在代码内有详细注释,大家可以参考注释理解。程序开头定义了3个数组:gREAD_RTC_ADDR、gWRITE_RTC_ADDR和gDS1302_TIME。gREAD_RTC_ADDR和gWRITE_RTC_ADDR数组内存储的是DS1302写入和读取时分秒的地址命令,这个可以对照前面介绍的寄存器看。TIME数组是用来存储初始化时间,每个数据对应含义在代码内已做注释。在使用中主要调用ds1302init()函数和ds1302readtime()函数来初始化DS1302时钟数据以及读取DS1302时钟数据。
在初始化时钟数据时,首先要禁止写保护即关闭写保护功能,然后将所需设置的时钟数据写入到对应的时钟寄存器地址内,最后打开写保护功能,以防止意外修改DS1302内部寄存器。对于读取时钟数据其实很简单,只要从对应的时钟寄存器地址内读取数据即可,然后将读取的数据存储到一个缓存数组中,方便数据的处理与显示,这个操作在后面main.c文件内会有。
main.c:
/********************************************************************
******************
实验名称:DS1302 时钟实验
接线说明:
实验现象:下载程序后,数码管上显示电子时钟时分秒,格式为“XX-XX-XX”
注意事项:
*********************************************************************
******************/
#include "public.h"
#include "smg.h"
#include "ds1302.h"
/********************************************************************
***********
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*********************************************************************
**********/
void main()
{
u8 time_buf[8];
ds1302_init();//初始化 DS1302
while(1)
{
ds1302_read_time();
time_buf[0]=gsmg_code[gDS1302_TIME[2]/16];
time_buf[1]=gsmg_code[gDS1302_TIME[2]&0x0f];
time_buf[2]=0x40;
time_buf[3]=gsmg_code[gDS1302_TIME[1]/16];
time_buf[4]=gsmg_code[gDS1302_TIME[1]&0x0f];
time_buf[5]=0x40;
time_buf[6]=gsmg_code[gDS1302_TIME[0]/16];
time_buf[7]=gsmg_code[gDS1302_TIME[0]&0x0f];
smg_display(time_buf,1);
}
}
主函数代码非常简单,首先调用外设头文件,然后初始化DS1302并设定好初始时间,进入while循环,读取DS1302时钟数据存储至全局变量数组gDS1302_TIME中,最后将读取的数据转换为数码管可显示的段码数据并调用数码管显示函数显示时间。
细心的朋友可能发现,在处理DS1302读取的数据时,取高低位是使用除16和取余16,并非之前的除10和取余10。这是因为写入进DS1302时是BCD码,读取出的数据也是BCD码,而BCD码即是4位表示一个十进制数,类似于一个字节的十六进制数据的高4位和低4位一样,所以这里是除16和取余16。