1.DS1302时钟芯片介绍
- DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能
- RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片
这个时钟芯片的应用十分广泛,而且使用它作为时钟计时也是很常见的操作。
我们51单片机上采用的也是这个芯片,有人认为,我们的定时器也可以做一个时钟计时,但其实,我们的定时器毕竟不是专业的,在长时间的积累下还是会产生一定的误差,而且我们对定时器的采样和各种操作都是会占用一定的的CPU时间的,因为CPU要去处理这个定时器的信息。并且,很重要的一点就是:我们的单片机断电就会让定时器暂停,再次重启单片机的时候,计时才会继续,所以这需要我们一直供电,并且单片机的高频率调用定时器还是挺耗电的,一点都不节能。
让专业的人做专业的事——我们使用DS1302芯片就是为了解决这个问题的:高精度计时,无惧误差;自带内置电池,不怕断电,断电计时还在继续计时,开启继续显示。
这里是这个芯片的引脚图:
这里要注意的是芯片有两个VCC供电,其中VCC2是主电源,就是我们单片机的直接供电,VCC1就是芯片内部的备用电池,让芯片在断电的情况下还可以继续运作。
这个就是芯片的内部结构图:
在芯片手册里面还有这样的一张图:
这个就是时序图,什么是时序图?你可以理解为单片机内部一个周期内发生的事情用一个电波来表示,这里有三个芯片上的引脚:CE(芯片使能),SCLK(时钟沿),I/O(输入输出)。CE比较简单,置为1就开始工作,置为0就停止工作。SCLK就是时钟周期,图上的箭头表示上升沿有效和下降沿有效。IO就是经典的寄存器,R/W到A0到1这个方向就是从低位到高位的数据。
R/W表示Read和Write(读和写)。R是高电平有效,W是低电平有效,这个位置决定了调用哪个模式。后面的D0到D7就是芯片内部对应的区域了,代入你想要写入哪个地址,这个就是最后的写入的数据。
芯片手册里还有这个图片:
这个图最左边就是读和写对应的地址,中间是对应地址的对应位表示的数据,最后就是数据的范围,第一行是秒,第二行是分钟,第三行是小时(对应有12和24小时制),第四行是日,第五行是月份,第六行是星期,第七行就是年,第八行主要看到WP(即Write Protect),写入保护(WP为1的时候生效,此时所有写入的操作无效),最后一行是电池充电,这个不需要我们配置,保持默认就好。
然后这个表格中的读写地址是根据这个图的出来的:
这里就不多解释了,看上面的详细的表格更好一点。它对应的运作模式就是上面的时序图。
2.代码实现时钟
这里主要用到LCD1602显示屏和这个时钟芯片相互配合实现,有人可能会疑惑为什么不用数码管,其实数码管也是可以的,这里主要是为了显示更多的数字和信息,数码管可以显示的位太少了,所以不使用数码管。
我们配置寄存器前还要看一下原理图:
可以看到,SCLK,IO,CE三个引脚都有定义,我们要在代码里使用sbit把它们重新定义一下,以便我们以后调用程序的时候一眼看出写的是什么。
sbit DS1302_SCLK = P3^6;
sbit DS1306_IO = P3^4;
sbit DS1306_CE = P3^5;
这里我们要配置初始的时间数值,我们按照时序图先写一个写入函数:
这里我们看到每次开始前SCLK和CE都是低电平,所以我们在写写入函数前,还要再写一个初始化函数,先把SCLK和CE先初始化为低电平
void DS1602_Init()
{
DS1306_CE = 0;
DS1302_SCLK = 0;
}
看时序图我们可以知道:一个上升沿表示一个数据的写入,和之前我们LED点阵屏的寄存器一样,使用SCLK控制读写数据,先把准备好的数据0/1放在IO口,当时钟上升沿生效之后,这个数据就被读入。并且,有一点要注意的是:数据是从低位(R/W)到高位(1),这个顺序,也就是上面图中从左到右对应输入数据的从低到高,这样我们就可以写出一个IO从低到高输入我们传入指令的代码了(按照时钟周期,先读入前面那8个控制位,后读入后面那八个数据位):
unsigned char i = 0;
for(i = 0;i<8;i++)
{
DS1306_IO = command&(0x01<<i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
这样就可以读入指令集,然后我们可以看到,后面的部分和前面的部分都是一样的,所以我们仿照前面的循环代码,直接把数据输入到IO口:
for(i = 0;i<8;i++)
{
DS1306_IO = Data&(0x01<<i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
把两段代码合并,再加上把使能开启和关闭,就有了:
void DS1602_Write(unsigned char command,unsigned char Data)
{
unsigned char i = 0;
DS1306_CE = 1;
for(i = 0;i<8;i++)
{
DS1306_IO = command&(0x01<<i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
for(i = 0;i<8;i++)
{
DS1306_IO = Data&(0x01<<i);
DS1302_SCLK = 1;
DS1302_SCLK = 0;
}
DS1306_CE = 0;
}
然后我们就要按照时序图配置读数据的函数了:
这里我们如果复用上面的读取指令的代码会出现一点问题:
按照上面代码的写法,我们的函数会停留在时钟周期的这个红线的位置,这个时候我们发现:我们由触发了一次下降沿,也就是我们把后面数据又多读入了一位,这样其实不利于我们再写读数据部分的思路,我们就要把这两部分分离开来。怎么做?很简单,把SCLK置为0的步骤放在前面就好了:
for(i = 0;i<8;i++)
{
DS1306_IO = command&(0x01<<i);
DS1302_SCLK = 0;
DS1302_SCLK = 1;
}
这样我们会发现,我们的停止位置红线到了这个地方:
意满离,我们可以开始配置后面读取数据的代码了:
这里我们先给SCLK一个下降沿,这个时候数据就到了IO口上了,我们就可以拿一个变量把它存起来,然后等下一个周期,直到全部读取完成。
这里数一下,一共有8个下降沿,但是上升沿却只有7个,所以我们的代码还是要要使用循环,保证上升沿和下降沿的个数相同,我们就可以进入循环时(此时为高电平)先置为1,再置为0,再读取数据,就解决了前面的痛点了:
for(i = 0;i<8;i++)
{
DS1302_SCLK = 1;
DS1302_SCLK = 0;
if(DS1306_IO)
{
Data |= 0x01 << i;
}
}
然后就有下面的代码:
unsigned char DS1602_Read(unsigned char command)
{
unsigned char i = 0;
unsigned char Data = 0x00;
DS1306_CE = 1;
for(i = 0;i<8;i++)
{
DS1306_IO = command&(0x01<<i);
DS1302_SCLK = 0;
DS1302_SCLK = 1;
}
for(i = 0;i<8;i++)
{
DS1302_SCLK = 1;
DS1302_SCLK = 0;
if(DS1306_IO)
{
Data |= 0x01 << i;
}
}
DS1306_CE = 0;
return Data;
}
至此,我们主要的函数就实现了,现在只要使用LCD1602把数据显示一下就好了。
这里我们先演示一下把数据写入和读出:
unsigned char Second = 0x00;
void main()
{
LCD_Init();
DS1602_Init();
DS1602_Write(0x8E,0x00);
DS1602_Write(0x80,0x03);
Second = DS1602_Read(0x81);
LCD_ShowNum(1,1,Second,3);
while(1)
{
}
}
这里有一句写入0x8e时比较重要的,不知道什么情况,这个只要使用一次下次这个就不需要加了,这句的作用就是取消写保护,在前面表格里我们介绍过的,如果没有这个,很可能你的显示出来就是128或者255这种没有初始化的值。
那么,我们把显示数字和读取数字调用放在while循环前面的时候,我们可以显示出来一个数字,那么我们把它们放在while循环里面,他是不是就可以显示秒的变化了呢?没错!但是你直接这样做可能显示出来的数字时一个有点花的数字,这里猜测可能是循环进行太快,导致IO口上的数字串位,总之我们只要做一件事——在read函数返回前加上一个DS1306_IO = 0;
然后我们就可以看到我们的显示出来的数字正常运作,但是仔细一看又有一点不对:9之后数字变成了16,这是为什么?
我们的DS1302芯片使用的是BCD码,BCD码是什么?就是一个使用类似于十六进制格式十进制的一种格式这里举个例子:
十六进制的0x18中,8占的是8的0次方的权重,1占的是8的1次方的权重,而在BCD码的表示里,4占的权重是10的0次方,2占的权重是10的1次方,使用2进制就还是正常表示但是又数据范围限制,换句话来说,其实BCD码可以用二进制表示,也可以用十六进制表示,但是它在二进制转化成16进制之后,它的每一位权重要改变一下,而且无法表示十六进制中A B C D E F这些位。
因此,我们前面使用这样用BCD码表示的时候,它使用0000 1001/0x09,即数字9,但是它加1的时候,它就变成了0001 0000/0x10,即数字10,而在16进制中意思是16,我们使用LCD1602的时候调用的函数是普通的十进制使用函数,所以它显示就从9变成了16,我们这里有两种办法:1.把函数使用16进制转化,这个时候即便它从0x09变成0x10,在显示屏幕上显示的还是9和10;2.使用公式转换:
这里为了更加清晰了解我们的逻辑,更多使用这个公式法,当然,我们可不能把这个参数直接就改了,这样会出大问题的,要借助一个临时变量来做这个动作,当然,我使用函数:
unsigned char BCDchange(unsigned char BCDNum)
{
return BCDNum/16*10+BCDNum%16;
}
void main()
{
LCD_Init();
DS1602_Init();
DS1602_Write(0x8E,0x00);
DS1602_Write(0x80,0x03);
while(1)
{
Second = DS1602_Read(0x81);
LCD_ShowNum(1,1,BCDchange(Second),3);
}
}
这样就很好,即不改变两种函数内部原本的运行逻辑,又把它们完美的串联在了一起。
接下来就比较简单了,完成了上面的操作之后,依葫芦画瓢,做出一个时钟就跟喝水一样简单,但是完美这里想要把它们都集成为简单的函数,只要调用函数就可以实现写和读
既然要追求简单,那就贯彻到底了,我们发现我们经常使用0x80,0x81...这样的东西,真的是很麻烦,难道我们每次都要查表看这个地址吗?
读写模式之间只差了一个位,例如秒的写是0x80,读就是0x81,分的写是0x82,读就变成0x83了,其实就是最后一个位的区别,写为0,读为1
知道这样的规律之后,我们就可以做一点不一样的了:
从秒到年到周,这个都是有顺序的,从0x80到0x8C,我们为什么不按照这样的规律写我们的函数呢?
这里我们再定义一个读写的缓存区,就是定义一个数组,我们把读取和写入的放在这个数组里,这样就OK了,所以我们就可以定义一个全局变量数组:充当缓存区
//0.second 1.minute 2.hour 3.data 4.month 5.week 6.year
unsigned char DS_Buffer[7];
这样我们就不用频繁传参返回函数了,十分的简单
然后,我们这里就使用上面的规律和这个数组,实现十分简单的读数函数:
void DS1302_ReadTime()
{
unsigned char PreAdress = 0x80 + 1;
unsigned char i = 0;
for(i = 0;i < 7;i++)
{
DS_Buffer[i] = DS1602_Read(PreAdress + i*2);
}
}
这里前面加一是因为所有读数都要加一,这里后面i*2是因为每两个形式之间(比如秒和分)相隔2。
void DS1302_WriteTime()
{
unsigned char PreAdress = 0x80;
unsigned char i = 0;
DS1602_Write(0x8E,0x00);
for(i = 0;i < 7;i++)
{
DS1602_Write(PreAdress + i*2,DS_Buffer[i]);
}
DS1602_Write(0x8E,0x80);
}
这里我们和读不同的是,我们需要在写入之前关闭写保护,然后出函数时再开启写保护,然后就是这里的缓存数组变成了我们读取信息的数组,我们就要把它先使用别的函数初始化这个信息,然后再调用该函数读取缓存数组里面的数。当然,别忘了初始地址,读写的地址是不一样的。
然后我们就可以实现显示时间的函数,这里做了一个格式,直接使用这个函数就可以显示年月日,时分秒:
void Formate()
{
LCD_ShowString(1,1," : : ");
LCD_ShowString(2,1," - - ");
DS1302_ReadTime();
LCD_ShowNum(1,1,BCDchange(DS_Buffer[2]),2);
LCD_ShowNum(1,4,BCDchange(DS_Buffer[1]),2);
LCD_ShowNum(1,7,BCDchange(DS_Buffer[0]),2);
LCD_ShowNum(2,1,20,2);
LCD_ShowNum(2,3,BCDchange(DS_Buffer[6]),2);
LCD_ShowNum(2,6,BCDchange(DS_Buffer[4]),2);
LCD_ShowNum(2,9,BCDchange(DS_Buffer[3]),2);
}
放在函数while循环内:
void main()
{
LCD_Init();
DS1602_Init();
DS1302_WriteTime();
DS1302_ReadTime();
while(1)
{
Formate();
}
}
这里显示的格式是这样的:
21:11:50
2024-04-16
但是这里各位会发现一个问题:函数频闪的问题有点严重,这是因为我们有些不需要变化的数字也还是重新写入了,这里我们的解决方案:
使用Delay函数,大概需要0.5s以上的Delay才可以比较有效解决频闪,但是会导致有一点的误差(每个1s之间的读取都是靠芯片的,这里的误差只是刷新的误差,比如已经从5s到了6s,但是我们还在Delay中导致没有及时显示出来)
还有别的实现吗?还在慢慢发掘中...