目录
前言
DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以通过简单的串行接口与微处理器通讯。内部包含的实时时钟/日历和31字节静态RAM可以提供年月日,时分秒信息。对于少于 31 天的月份月末会自动调整,还有闰年校正功能。
基于51单片机来对简单芯片的学习可以极大的增强初学者的信心,使广大学者有继续坚持下去的动力。
一、初学者需要了解DS1302的一些基本特性
DS1302基本特性详解(学习篇):
1、实时时钟计算能力,与精确度不高的定时器不同,它能精确的计算秒、分、时、月、年,还具有平闰年调节功能。但它也不是永久计时,最终寿命将在2100年结束。
2、操作范围在2.0V至2.5V之间,在2.0V时工作电流小于300nA,使用的是31*8通用暂存RAM。拥有简单的3线接口,与TTL兼容(VCC=5V)
3、读写时钟RAM数据时有单字节或者多字节(脉冲串模式)数据传送方式!!!重点。
4、典型工作电路:串行计时器的主要元素:移位寄存器,控制逻辑,实时时钟,RAM
这里只介绍实现本节时钟实现最重要的几点:
①:CE在信号读写时必须保持高电平!CE输入实现两个功能。第一, CE 开启允许对地址/命令序 列的移位寄存器进行读写的控制逻辑.。第二 CE提供终止数据传输的方法。
②:IO让寄存器知道,接下来是写入还是读出以及数据的存入
③:SCLK用来同步串行接口上的数据动作,上文提到的脉冲传输模式就是通过SCLK的上升沿以及下降沿控制完成,如何进行实现下文将详细阐述。
5、总而言之,操作DS1302的基本步骤就是:我要在哪? 做什么事?做的这个事是什么?
简单理解:在哪 写入or读出 数据
二、脉冲串模式数据传输
1.单字节写入
图2-1
如图2-1,CE在数据写入中一定要保持高电平,直至脉冲串结束。
①:首先IO口的数据读取规则按照的是低位优先!以左半边为例
图2-2
如图2-2:正常的逻辑应该是,从右往左依次是0位—7位。最低为则是二级制中的0000 0001,也就是图2-1中的R/W,这就能解释为什么图一中的R/W是在最前但十六进制用的则是0x01了。理解这个对接下来的学习很有帮助。
值得注意的是:原图中(R/W—1)左半边为命令,(D0—D7)右半边为数据,SCLK进入到指定的脉冲后就由命令行转入数据行。何为命令何为数据如图所示:
图2-3
(R/W—1)左半边为命令:例如:如果你要进行读取操作,那么左半边的命令行字节为0x81对时钟的秒钟进行读取,也就是该图的第二行。同理如果你要进行写入秒钟,命令字节就为0x80。分,时,月,年皆同理。
②:写入中的SCLK脉冲如何操作?SCLK口处于低电平时也就是下降沿,此时R/W字节还没有正式被写入,一旦SCLK进入上升沿,字节立马被推进存储,随后进入下降沿等待下一次数据的推入。
图2-4
2.单字节读取
了解了单字节的写入,那么对单字节的读取那便是手到擒来:
图2-5
读取与单字节的写入基本类似,只是读取是把写入的字节拿出来而已。不过仔细的同学可能会发现单字节读取中的SCLK脉冲比写入少一个,这里也很有意思,可谓是Live to old ,Study to old。
①:上文提到写入字节是需要上升沿来完成,而读取字节恰恰不同,它靠的是SLCK的下降沿。当SLCK=0处于下降沿时,字节数据就已经被读取出来了,而不用等待上升沿。图中红圈的地方就是命令字节与数据字节交会的地方,为什么是这样的,因为目的就是为了在命令字节脉冲结束后保持高电平也就是上升沿来防止后面的字节数据过早的被读取出去,保持上升沿就凸显了作用。这一点在后面的代码中可以深刻的体会到。
图2-6
3.BCD码的转换
根据图2-3我们可以发现,寄存器是通过我们写下的十六进制命令字节转换为十进制最终被读取出来显示的就是十进制数,这一系列操作就是BCD码的转换公式
首先BCD转十进制:BCD/16 将二进制的0001 0011 高四位(每位4字节所以除16)提取出来 + BCD%16 低四位 = 二位BCD码 DEC
例如:2356 要提取高两位:2356/100=23 提取低两位:2356%100=56
三、理论存在实践开始(代码模块)
1.DS1302时钟模块原理图
图3-1
根据了解了DS1302的基本特性以及寄存器的控制方式、脉冲传输模式,已经可以动手做一个简单时钟了。
2.时钟的初始化
根据原理图可以用特殊位寄存器来分别代表SCLK,IO,CE的三引脚:
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
然后对时钟进行初始化:
void DS1302_Init() //时钟配置初始化
{
DS1302_SCLK = 0;
DS1302_CE = 0;
}
3.字节写入操作
根据上一章对字节写入的分析,在写入模块中分为两大部分:一是命令字节的写入,二是数据字节的写入。所以在这模块中,我们需要两个参数:Command(命令字节),Data(数据字节)
void DS1302_Write(unsigned char Command,Data) //向寄存器写入数据
{
unsigned char i=0;
DS1302_CE = 1; //写入时保持CE的高电平
for(i=0;i<8;i++) //此时脉冲在写入命令阶段
{
DS1302_IO = Command&(0x01<<i); //0x01 从低位开始依次写入IO口
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置为低电平
}
4.字节读取操作
根据上一章对字节读取操作的分析,在读取模块中只需要读取的命令从IO口取出已经存入的数据,所以在此模块中只需要一个参数Command(命令字节)
unsigned char DS1302_Read(unsigned char Command) //从寄存器读取数据
{
/*写在前面*
读是下降沿,也就是SCLK低电平
写是上升沿,也就是SCLK高电平
所以写比读多一个脉冲,读取数据需要过滤一个脉冲才不会造成数据提前被读取
文中有详细解释
*/
unsigned char i=0,Data=0x00; //内部变量Data用来存取读出的数据
DS1302_CE = 1; //保持高电平
for(i=0;i<8;i++) //脉冲读取命令阶段
{
DS1302_IO = Command&(0x01<<i);
DS1302_SCLK = 0; //下降沿读取数据
DS1302_SCLK = 1;
}
for(i=0;i<8;i++) //脉冲读取数据阶段
{
DS1302_SCLK = 1; //第一次重复上升沿,过滤多余脉冲
DS1302_SCLK = 0;
if(DS1302_IO)
{
Data|= (0x01<<i); //依次从写入的数据中读出二进制数
} //IO为0则不动,为一时则移动到相应位置给1
//以此类推取出二进制数
}
DS1302_CE = 0;
DS1302_IO = 0; //多次读取时IO需要重置以进行读取
return Data; //返回从寄存器读取的数据
}
四、最终效果
1.模块整合
以上是对DS1302数据传输控制的基本模块,在主函数中分别用年月日、时分秒作为Command命令参数和数据字节参数,配合LCD屏幕模块就能达到时间可视化。
这里只做简单演示,还能继续优化。比如可以通过结构体、数组、链表来对时分秒的存入及读出达到效率更高的目的。
main.c
#include <REGX52.H>
#include "DS1302_Clock.h"
#include "LCD1602.h"
void main()
{
LCD_Init(); //LCD初始化
LCD_ShowString(1,1," / / ");
LCD_ShowString(2,1," : : ");
DS1302_Write(0x8E,0x00);//解除芯片写保护
DS1302_Init(); //时钟芯片初始化
Writ_Time();
while(1)
{
int flag=1,i,j;
Read_Time();
for(i=3,j=0;i<6,j<3;i++,j++) //显示时分秒
{
LCD_ShowNum(2,flag,DS1302_Time[i]/16*10+DS1302_Time[i]%16,2); //BCD码转十进制
flag+=3; //控制LCD列
}
flag=1;
for(j=0;j<3;j++) //显示年月日
{
LCD_ShowNum(1,flag,DS1302_Time[j]/16*10+DS1302_Time[j]%16,2);
flag+=3;
}
}
}
其中DS1302_Time用来初始化时间并且存储从芯片中读取的时间。
DS1302_Clock.c
#include <REGX52.H>
#include "delay.h"
sbit DS1302_SCLK =P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
unsigned char DS1302_Time[]={22,9,8,14,29,0}; //初始化时间:年月日时分秒
unsigned char DS1302_CommandWrite[]={0x8C,0x88,0x86,0x84,0x82,0x80};
unsigned char DS1302_CommandRead[]={0x8D,0x89,0x87,0x85,0x83,0x81};
void DS1302_Init() //时钟配置初始化
{
DS1302_SCLK = 0;
DS1302_CE = 0;
}
//在哪? 写入还是读出? 写什么读什么? 三大步骤
void DS1302_Write(unsigned char Command,Data) //向寄存器写入数据
{
unsigned char i=0;
DS1302_CE = 1;
for(i=0;i<8;i++)
{
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;
}
unsigned char DS1302_Read(unsigned char Command) //从寄存器读取数据
{
//读是下降沿,也就是SCLK低电平
//写是上升沿,也就是SCLK高电平
//所以写比读多一个脉冲,所以读取数据需要过滤一个脉冲
unsigned char i=0,Data=0x00;
DS1302_CE = 1;
for(i=0;i<8;i++)
{
DS1302_IO = Command&(0x01<<i);
DS1302_SCLK = 0; //下降沿读取数据
DS1302_SCLK = 1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK = 1; //第一次重复上升沿,过滤多余脉冲
DS1302_SCLK = 0;
if(DS1302_IO)
{
Data|= (0x01<<i); //依次从写入的数据中读出二进制数
} //IO为0则不动,为一时则移动到相应位置给1
//以此类推取出二进制数
}
DS1302_CE = 0;
DS1302_IO = 0;
return Data; //返回从寄存器读取的数据
}
void Writ_Time()
{
int i;
for(i=0;i<6;i++)
DS1302_Write(DS1302_CommandWrite[i],DS1302_Time[i]/10*16+DS1302_Time[i]%10);
}
void Read_Time()
{
int i;
for(i=0;i<6;i++)
DS1302_Time[i] = DS1302_Read(DS1302_CommandRead[i]);
}
DS1302_Clock.h
#ifndef __DS1302_CLOCK_H__
#define __DS1302_CLOCK_H__
unsigned char DS1302_Read(unsigned char Command) ;
unsigned char DS1302_Time[]; //初始化时间:年月日时分秒
unsigned char DS1302_CommandWrite[];
//初始化写入命令:年月日时分秒
unsigned char DS1302_CommandRead[];
void DS1302_Init();
void DS1302_Write(unsigned char Command,Data);
void Writ_Time();
void Read_Time();
#endif
2.LCD代码模块
这里贴上LCD模块,配合时钟芯片的学习。LCD1602.c
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
LCD1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
3.效果图