51单片机之DS1302实时时钟

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

        还有别的实现吗?还在慢慢发掘中...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/624133.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis消息队列-基于Stream的消息队列-消费者组

7.5 Redis消息队列-基于Stream的消息队列-消费者组 消费者组&#xff08;Consumer Group&#xff09;&#xff1a;将多个消费者划分到一个组中&#xff0c;监听同一个队列。具备下列特点&#xff1a; 创建消费者组&#xff1a; key&#xff1a;队列名称 groupName&#xff1a…

python 绘制六种激活函数(sigmoid、tanh、relu、softmax、relu、elu)

1、效果 2、实现代码&#xff08;带注释&#xff09; import numpy as np # 导入 numpy 库, 用于数学运算 import matplotlib.pyplot as plt # 导入 matplotlib.pyplot, 用于绘图 import matplotlib as mpl # 导入 matplotlib 库, 用于图形配置plt.rcParams[font.sans-se…

福布斯发布2024年人工智能初创企业50强

随着人工智能热潮的持续&#xff0c;一种新的技术经济正在帮助企业开发和部署人工智能驱动的应用程序。在《福布斯》第六届年度“人工智能50强”榜单上&#xff0c;这类新锐企业正大行其道。该榜单遴选了AI领域最有前途的初创公司&#xff0c;由《福布斯》在领先行业专家的帮助…

I2C总线接上拉电阻的原因

I2C为什么要接上拉电阻&#xff1f;因为它是开漏输出。 为什么是开漏输出&#xff1f; I2C协议支持多个主设备与多个从设备在一条总线上&#xff0c;如果不用开漏输出&#xff0c;而用推挽输出&#xff0c;会出现主设备之间短路的情况。所以总线一般会使用开漏输出&#xff0c;…

河北专升本(微机原理编程题)

目录 第一类、循环结构 1.求内存变量 NUM 中 10 个有符号数的最大值并放入 MAX 中。 2.将 0 至 100 中的奇数求和&#xff0c;结果送 SUM 字单元。 3.编程统计 NUM 字节单元中奇数的个数&#xff0c;将个数存入 RESULT 单元。 4.编程统计 BUF 内存区若干个有符号数中正数、…

全球顶级的低代码开发平台,你知道几个?

什么是低代码开发平台? 低码开发平台是一个应用程序,提供图形用户界面编程,从而以非常快的速度开发代码,减少了传统的编程工作。 这些工具有助于快速开发代码,最大限度地减少手工编码的努力。这些平台不仅有助于编码,而且还能快速安装和部署。 低码开发工具的好处 低代码平…

【教学类-52-05】20240417动物数独(4宫格)黏贴卡片需要至少几张?难度1-9 打印版

作品展示&#xff1a; 背景需求&#xff1a; 实际打印的是以下代码生成的动物数独&#xff08;2*2&#xff09;学具 【教学类-52-03】20240412动物数独&#xff08;4宫格&#xff09;难度1-9 打印版-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞30次&#xff0c;收藏17次。【教…

中国科学院大学学位论文LaTeX模版

Word排版太麻烦了&#xff0c;公式也不好敲&#xff0c;推荐用LaTeX模版&#xff0c;全自动 官方模版下载位置&#xff1a;国科大sep系统 → \rightarrow → 培养指导 → \rightarrow → 论文 → \rightarrow → 论文格式检测 → \rightarrow → 撰写模板下载百度云&#…

day02|最小花费爬梯子

最小花费爬梯子 比如 有一个数组 【2 5 20】我们直接选择从1号梯子&#xff08;从零编号&#xff09;跳两格就出去了。 算法原理 我们可以得出楼顶其实是数组的最后一个元素的下一个位置。对于最值问题我们可以尝试使用dpdp我们首先应该定义状态方差的含义&#xff0c;一般以…

bestvike --bvframe学习

ref title fetch后台api 分页属性&#xff0c;pagination 要差几条&#xff1f;pagelimit 在api中写一个饭方法&#xff0c;vue中用用他 vue.cinfig.js中配置别名 nacos微服务 实体类要继承basedata&#xff08;封装了公共数据&#xff09; 控制器autowired&#xff0c;getm…

力扣101. 对称二叉树(java)

思路&#xff1a; 一、验证 左右子树是否可翻转对称的&#xff1f; 二、分析左右子树情况&#xff1a; 1&#xff09;左右都也空 对称 2&#xff09;左右有一个为空 不对称 3&#xff09;左右都不为空&#xff0c;但数字不同 不对称 4&#xff09;左右都不为空&#xff0c;且数…

【C语言】带你完全理解指针(六)指针笔试题

目录 1. 2. 3. 4. 5. 6. 7. 8. 1. int main() {int a[5] { 1, 2, 3, 4, 5 };int* ptr (int*)(&a 1);printf("%d,%d", *(a 1), *(ptr - 1));return 0; } 【答案】 2&#xff0c;5 【解析】 定义了一个指向整数的指针ptr&#xff0c;并将其初始化为&…