51单片机之DS1302实时时钟

发布于:2024-04-23 ⋅ 阅读:(17) ⋅ 点赞:(0)

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]/10*16+DS_Buffer[i]%10);
	}
	DS1602_Write(0x8E,0x80);
}

        这里我们和读不同的是,我们需要在写入之前关闭写保护,然后出函数时再开启写保护,然后就是这里的缓存数组变成了我们读取信息的数组,我们就要把它先使用别的函数初始化这个信息,然后再调用该函数读取缓存数组里面的数。当然,别忘了初始地址,读写的地址是不一样的。

        而且这里的把数组中十进制的数据写入寄存器,要先把十进制转成BCD码,前面写的函数是用来把BCD转成十进制的,这里使用十进制转BCD函数调用较少,所以就不写函数了。

        然后我们就可以实现显示时间的函数,这里做了一个格式,直接使用这个函数就可以显示年月日,时分秒:

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中导致没有及时显示出来)

        后来又反复检验了好几遍,终于找到了这个频闪问题的原因:我们调用Formate函数的时候,每次进入函数都要使用showstring显示空格加字符,所以我们的数字位是以空格-数字-空格的特征周期性变化的,那么我们要解决的话就可以使用两个方法(当然,前面的Delay是适用于所有频闪最有效的办法)

        1.修改formate函数:把formate函数内部改成:

void Formate()
{
		DS1302_ReadTime();
		LCD_ShowString(1,3,":");
		LCD_ShowString(1,6,":");
		LCD_ShowString(2,5,"-");
		LCD_ShowString(2,8,"-");
		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);
}

        这样就不存在显示空格导致数字频闪了

        2.把显示符号的函数放在while循环外:

void Formate()
{
		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);
}
void main()
{
	LCD_Init();
	DS1602_Init();
	LCD_ShowString(1,1,"  :  :  ");
	LCD_ShowString(2,1,"    -  -  ");
	DS1302_WriteTime();
	while(1)
	{
		Formate();
	}
}

        这样我们的频闪问题就解决了