目录
1,DS1302 时钟芯片介绍
2,BCD码介绍
3,涉及到的寄存器
3.1,控制寄存器
3.2,日历/时钟寄存器
3.3,DS1302 的读写时序
4,相关代码
这一章我们来学习DS1302 时钟芯片,该芯片是3 线SPI 接口,所以需要使用51单片机的3 个IO 口模拟SPI 时序与DS1302 时钟芯片通信,将时钟日历数据读取出来。开发板上集成了1 个DS1302 时钟模块,可使用它设计一个多功能电子时钟。本章要实现的功能是:系统运行时,数码管上显示电子时钟时分秒,格式为“XX-XX-XX”。
1,DS1302 时钟芯片介绍
DS1302 是DALLAS 公司推出的涓流充电时钟芯片,内含有一个实时时钟/日历和31 字节静态RAM,通过简单的串行接口与单片机进行通信。实时时钟/日历电路提供秒、分、时、日、周、月、年的信息,每月的天数和闰年的天数可自动调整。时钟操作可通过AM/PM 指示决定采用24 或12 小时格式。DS1302 与单片机之间能简单地采用同步串行的方式进行通信,仅需用到三根通信线:①RES复位②I/O 数据线③SCLK 串行时钟。时钟/RAM 的读/写数据以一个字节或多达31 个字节的字符组方式通信。DS1302 工作时功耗很低保持数据和时钟信息时功率小于1mW。
DS1302 由DS1202 改进而来增加了以下的特性:双电源管脚用于主电源和备份电源供应,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;
1,VCC2:主电源引脚
2,X1、X2:DS1302 外部晶振引脚,通常需外接32.768K 晶振
3,GND:电源地
4,CE:使能引脚,也是复位引脚(新版本功能变)
5,I/O:串行数据引脚,数据输出或者输入都从这个引脚
6,SCLK:串行时钟引脚
7,VCC1:备用电源
51单片机和DS1302芯片之间的通讯是SPI,SPI的主要特点:3线or4线、同步、主从、串行;
3线or4线: CE、I/O、SCLK,3线I/O是一根线,4线I/O是两根线;
同步:与UART的异步通讯相比,需要SCLK同步信息的发送和接收;
主从:谁来发出SCLK同步信号;
串行:在数据线上按位传输数据;
DS1302是变种SPI,通过SCLK和I/O相互配合,发送和接收信息。SCLK在上升沿前,主机通过I/O口发送数据,DS1302在上升沿是接收;SCLK在下降沿后,DS1302通过I/O口发送数据,主机接收。
SPI:SPI = Serial Peripheral Interface,是串行外围设备接口,是一种高速,全双工,同步的通信总线。
DS1302参考:
DS1302介绍
2,BCD码介绍
为什么要介绍BCD码?因为从DS1302芯片读出来的数据是BCD 码形式,当使用串口助手查看数据时看到的是十进制形式,但其本质上是十六进制数据。
BCD码是一种数字编码,这种计数编码有个特点:很像10进制和16进制的结合。看起来很像10进制(29下来是30而不是2A),BCD码实际是用十六进制来表示的(BCD码的21其实在计算机中就是0x21)。BCD码中只有0-9,而没有ABCDEF等字目。
综合来讲:BCD码其实就是看起来很像十进制数的十六进制数。意思就是:BCD码本质上是十六进制数,但是因为他没有ABCDEF,所以看起来很像十进制数。
DS1302使用BCD码来处理时间信息,对单片机来说是16进制方便单片机处理,对人来说可以认为是10进制,同时方便单片机和人来识别信息。
例如:
如果把一个数21分成两个数,对于十进制的21,需要通过21/10得到2,通过21%10得到1;
对于十六进制的21,分成两个数,通过0x21>>4的得到2 ,通过0x21&0x0F得到1;
对于计算机来说,移位和与操作的复杂度优于除和取余。
3,涉及到的寄存器
操作DS1302 的大致过程,就是将各种数据写入DS1302 的寄存器,以设置它当前的时间的格式。然后使DS1302 开始运作,DS1302 时钟会按照设置情况运转,再用单片机将其寄存器内的数据读出。
3.1,控制寄存器
控制寄存器用于存放DS1302 的控制命令字,DS1302 的RST 引脚回到高电平后写入的第一个字节就为控制命令。它用于对DS1302 读写过程进行控制,格式如下:
上图是DS1302 的寄存器样式,我们看到:
1、第7 位永远都是1;
2、第6 位,1 表示RAM,寻址内部存储器地址;0 表示CK,寻址内部寄存器;
3、第5 到第1 位,为RAM 或者寄存器的地址;
4、最低位,高电平表示RD,即下一步操作将要“读”;低电平表示W,即下一步操作将要“写”。
比如要读秒寄存器则命令为1000 0001,反之写为1000 0000,要注意其含义。
3.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 内部电路有关。
思考:日历/时钟寄存器中还存储了除了时间数据之外的其它数据,这些数据在读写时是如何操作的?
3.3,DS1302 的读写时序
在控制指令字输入后的下一个SCLK 时钟的上升沿时,数据被写入DS1302,数据输入从低位(位0)开始。同样,在紧跟8 位的控制指令字后的下一个SCLK脉冲的下降沿读出DS1302 的数据,读出数据时从低位0 位到高位7。其时序图如下所示:
上图就是DS1302 的三个时序:复位时序,单字节写时序,单字节读时序;CE(RST):复位时序,即在RST 引脚产生一个正脉冲,在整个读写器件,RST 要保持高电平,一次字节读写完毕之后,要注意把RST 返回低电平准备下次读写周期;
单字节读时序:注意读之前还是要先对寄存器写命令,从最低位开始写;可以看到,写数据是在SCLK 的上升沿实现,而读数据在SCLK 的下降沿实现。所以,在单字节读时序中,写命令的第八个上升沿结束后紧接着的第八个下降沿就将要读寄存器的第一位数据读到数据线上了!这个就是DS1302 操作中最特别的地方。当然读出来的数据也是最低位开始。
单字节写时序:两个字节的数据配合16 个上升沿将数据写入即可。
程序注意事项:
★要记得在操作DS1302 之前关闭写保护;
★注意用延时来降低单片机的速度以配合器件时序;
★DS1302 读出来的数据是BCD 码形式,要转换成我们习惯的10 进制,转换
方法在源程序里;
★读取字节之前,将IO 设置为输入口,读取完之后,要将其改回输出口;
★在写程序的时候,建议实现开辟数组(内存空间)来集中放置DS1302 的
一系列数据,方便以后扩展键盘输入。
4,相关代码
ds1302底层时序代码,分为ds1320.c和ds1302.h两个文件:
ds1302.h
#ifndef __DS1302_H__
#define __DS1302_H__
//从ds1302的内部寄存器addr写入一个值value
void ds1302_write_reg(unsigned char addr, unsigned char value);
//从ds1302的内部寄存器addr读出一个值,作为返回值
unsigned char ds1302_read_reg(unsigned char addr);
#endif
ds1320.c
#include "ds1302.h"
#include <reg51.h>
sbit DSIO = P3^4;
sbit RST = P3^5;
sbit SCLK = P3^6;
void delay(void)
{
unsigned char i;
for (i=0; i<3; i++);
}
//从ds1302的内部寄存器addr写入一个值value
void ds1302_write_reg(unsigned char addr, unsigned char value)
{
unsigned char i,dat;
//第一部分:时序初始化
RST = 0;
delay();
SCLK = 0;
delay();
RST = 1;
delay();
//第二部分:写入地址
for(i=0; i<8; i++)
{
dat = addr & 0x01; //取地址的低位
DSIO = dat;
addr >>=1;
SCLK = 1; //创造一个上升沿,让ds1302将信号线上的值读走
delay();
SCLK = 0;
}
//第三步:写入数据
for(i=0; i<8;i++)
{
dat = value & 0x01; //取数据的低位
DSIO = dat;
value >>=1;
SCLK = 1; //创造一个上升沿,让ds1302将信号线上的值读走
delay();
SCLK = 0;
}
//第四部分:时序结束
RST = 0;
delay();
SCLK = 0;
delay();
}
// 从ds1302的内部寄存器addr读出一个值,作为返回值
unsigned char ds1302_read_reg(unsigned char addr)
{
unsigned char i = 0;
unsigned char dat = 0; // 用来存储读取到的一字节数据的
unsigned char tmp = 0;
// 第1部分: 时序起始
SCLK = 0;
delay();
RST = 0;
delay();
RST = 1; // SCLK为低时,RST由低变高,意味着一个大的周期的开始
delay();
// 第2部分: 写入要读取的寄存器地址,addr
for (i=0; i<8; i++)
{
dat = addr & 0x01; // SPI是从低位开始传输的
DSIO = dat; // 把要发送的bit数据丢到IO引脚上去准备好
SCLK = 1; // 制造上升沿,让DS1302把IO上的值读走
delay(); // 读走之后,一个小周期就完了
SCLK = 0; // 把SCLK拉低,是为了给下一个小周期做准备
delay();
addr >>= 1; // 把addr右移一位
}
// 第3部分: 读出一字节DS1302返回给我们的值
dat = 0;
for (i=0; i<8; i++)
{
// 在前面向ds1302写入addr的最后一个bit后,ds1302就会将读取到的寄存器值
// 的第一个bit放入到IO引脚上,所以我们应该先读取IO再制造下降沿然后继续
// 读取下一个bit
tmp = DSIO;
dat |= (tmp << i); // 读出来的数值是低位在前的
SCLK = 1; // 由于上面SCLK是低,所以要先拉到高
delay();
SCLK = 0; // 拉低SCLK制造一个下降沿
delay();
}
// 第4部分: 时序结束
SCLK = 0; // SCLK拉低为了后面的周期时初始状态是对的
delay();
RST = 0; // RST拉低意味着一个大周期的结束
delay();
// 第5部分:解决读取时间是ff的问题
DSIO = 0;
return dat;
}
通过串口接收时钟数据,分为uart.c和uart.h两个文件:
uart.h
#ifndef __UART_H__
#define __UART_H__
//串口初始化
void uart_init();
//串口发送数据
void uart_send_byte(unsigned char c);
#endif
uart.c
#include "uart.h"
#include <reg51.h>
void uart_init()
{
TMOD|=0X20; //设置计数器工作方式2
SCON=0X50; //设置为工作方式1
PCON=0X80; //波特率加倍
TH1=0xFA; //计数器初始值设置
TL1=0xFA;
ES=1; //打开接收中断
EA=1; //打开总中断
TR1=1; //打开计数器
}
void uart_send_byte(unsigned char c)
{
// 第1步,发送一个字节
SBUF = c;
// 第2步,先确认串口发送部分没有在忙
while (!TI);
// 第3步,软件复位TI标志位
TI = 0;
}
main函数:
包括ds1302的上层时序,读出时钟数据和写入时钟数据,以及调用uart函数。
#include <reg51.h>
#include <intrins.h>
#include "ds1302.h"
#include "uart.h"
unsigned char code READ_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};
unsigned char code WRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};
unsigned char time[7]={0};
void delay1s(void) //误差 0us
{
unsigned char a,b,c;
for(c=167;c>0;c--)
for(b=171;b>0;b--)
for(a=16;a>0;a--);
_nop_(); //if Keil,require use intrins.h
}
void ds1302_read_time(void)
{
unsigned char i = 0;
for (i=0; i<7; i++)
{
time[i] = ds1302_read_reg(READ_RTC_ADDR[i]);
}
}
void ds1302_write_time(void)
{
unsigned char i = 0;
// 准备好要写入的时间
time[0] = 0x24; // 对应 24s
time[1] = 0x39; // 对应 39m
time[2] = 0x11; // 对应 11h
time[3] = 0x06; // 对应 6日
time[4] = 0x12; // 对应 12月
time[5] = 0x02; // 对应 星期2
time[6] = 0x16; // 对应 2016年
ds1302_write_reg(0x8E, 0x00); // 去掉写保护
for (i=0; i<7; i++)
{
ds1302_write_reg(WRITE_RTC_ADDR[i], time[i]);
}
ds1302_write_reg(0x8E, 0x80); // 打开写保护
}
void main()
{
uart_init();
ds1302_write_time();
while(1)
{
int i = 0;
ds1302_read_time();
for(i=0;i<7;i++)
{
uart_send_byte(time[i]);
}
delay1s();
}
}