奋斗者实时时钟例程_第15章 实时时钟DS1302

本文详细介绍了单片机中实时时钟芯片DS1302的工作原理、SPI通信协议、时序操作以及如何通过C语言的结构体来封装日期时间数据。内容涵盖DS1302的BCD码、SPI变异通信模式、寄存器配置和读写操作,以及结构体在时间数据管理中的应用。此外,还探讨了复合数据类型中的结构体和共用体,并提供了一个带按键功能的万年历程序实例,以加深对结构体和DS1302的理解和应用。
摘要由CSDN通过智能技术生成
第15章 实时时钟DS1302

在前面的课程中我们已经了解到了不少关于时钟的概念,比如我们用的单片机的主时钟是11.0592M、I2C总线有一条时钟信号线SCL等,这些时钟本质上都是一个某一频率的方波信号。那么除了这些在前面新学到的时钟概念外,还有一个我们早已熟悉的不能再熟悉的时钟概念——“年-月-日 时:分:秒”,就是我们的钟表和日历给出的时间,它的重要程度我想就不需要多说了吧,在单片机系统里我们把它称作实时时钟,以区别于前面提到的几种方波时钟信号。实时时钟,有时也被称作墙上时钟,很形象的一个名词,对吧,大家知道它们讲的一回事就行了。本章,我们将学习实时时钟的应用,有了它,你的单片机系统就能在漫漫历史长河中找到自己的时间定位啦,可以在指定时间干某件事,或者记录下某事发生的具体时间,等等。除此之外,本章还会学习到C语言的结构体,它也是C语言的精华部分,我们通过本章先来了解它的基础,后面再逐渐达到熟练、灵活运用它,你的编程水平会提高一个档次哦。

15.1 BCD码的概念

在日常生产生活中用的最多的数字是十进制数字,而单片机系统的所有数据本质上都是二进制的,所以聪明的前辈们就给我们创造了BCD码。

BCD码(Binary-Coded Decimal)亦称二进码十进制数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数字。是一种二进制的数字编码形式,用二进制编码的十进制代码。BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。我们前边讲过十六进制和二进制本质上是一回事,十六进制仅仅是二进制的一种缩写形式而已。而十进制的一位数字,从0到9,最大的数字就是9,再加1就要进位,所以用4位二进制表示十进制,就是从0b0000到0b1001,不存在0b1010、0b1011、0b1100、0b1101、0b1110、0b1111这6个数字。BCD码如果到了0b1001,再加1的话,数字就变成0b00010000这样了,相当于用了8位的二进制数字表示了2位的十进制数字。

BCD码的应用还是非常广泛的,比如我们这节课要学的实时时钟,日期时间在时钟芯片中的存储格式就是BCD码,当我们需要把它记录的时间转换成可以直观显示的ASCII码时(比如在液晶上显示),就可以省去一步由二进制的整型数到ASCII的转换过程,而直接取出表示十进制1位数字的4个二进制位然后再加上0x30就可组成一个ASCII码字节了,这样就会方便的多,在后面的实际例程中将看到这个简单的转换。

15.2 SPI时序初步认识

UART、I2C和SPI是单片机系统中最常用的三种通信协议。前边我们已经学了UART和I2C通信协议,这节课我们来学习剩下的SPI通信协议。

SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片机和EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。SPI通信原理比I2C要简单,它主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,标准的SPI是4根线,分别是SSEL(片选,也写作SCS)、SCLK(时钟,也写作SCK)、MOSI(主机输出从机输入Master Output/Slave Input)和MISO(主机输入从机输出Master Input/Slave Output)。

SSEL:从设备片选使能信号。如果从设备是低电平使能的话,当拉低这个引脚后,从设备就会被选中,主机和这个被选中的从机进行通信。

SCLK:时钟信号,由主机产生,和I2C通信的SCL有点类似。

MOSI:主机给从机发送指令或者数据的通道。

MISO:主机读取从机的状态或者数据的通道。

在某些情况下,我们也可以用3根线的SPI或者2根线的SPI进行通信。比如主机只给从机发送命令,从机不需要回复数据的时候,那么MISO就可以不要;而在主机只读取从机的数据,不需要给从机发送指令的时候,那MOSI就可以不要;当一个主机一个从机的时候,从机的片选有时可以固定为有效电平而一直处于使能状态,那么SSEL就可以不要;此时如果再加上主机只给从机发送数据,那么SSEL和MISO都可以不要;如果主机只读取从机送来的数据,SSEL和MOSI都可以不要。

3线和2线的SPI大家要知道怎么回事,实际使用也是有应用的,但是当我们提及SPI的时候,一般都是指标准SPI,都是指4根线的这种形式。

SPI通信的主机也是我们的单片机,在读写数据时序的过程中,有四种模式,要了解这四种模式,首先我们得学习以下两个名词。

CPOL: Clock Polarity,就是时钟的极性。时钟的极性是什么概念呢?通信的整个过程分为空闲时刻和通信时刻,如果SCLK在数据发送之前和之后的空闲状态是高电平,那么就是CPOL=1,如果空闲状态SCLK是低电平,那么就是CPOL=0。

CPHA: Clock Phase,就是时钟的相位。

主机和从机要交换数据,就牵涉到一个问题,即主机在什么时刻输出数据到MOSI上而从机在什么时刻采样这个数据,或者从机在什么时刻输出数据到MISO上而主机什么时刻采样这个数据。同步通信的一个特点就是所有数据的变化和采样都是伴随着时钟沿进行的,也就是说数据总是在时钟的边沿附近变化或被采样。而一个时钟周期必定包含了一个上升沿和一个下降沿,这是周期的定义所决定的,只是这两个沿的先后并无规定。又因为数据从产生的时刻到它的稳定是需要一定时间的,那么,如果主机在上升沿输出数据到MOSI上,从机就只能在下降沿去采样这个数据了。反之如果一方在下降沿输出数据,那么另一方就必须在上升沿采样这个数据。

CPHA=1,就表示数据的输出是在一个时钟周期的第一个沿上,至于这个沿是上升沿还是下降沿,这要视CPOL的值而定,CPOL=1那就是下降沿,反之就是上升沿。那么数据的采样自然就是在第二个沿上了。

CPHA=0,就表示数据的采样是在一个时钟周期的第一个沿上,同样它是什么沿由CPOL决定。那么数据的输出自然就在第二个沿上了。仔细想一下,这里会有一个问题:就是当一帧数据开始传输第一个bit时,在第一个时钟沿上就采样该数据了,那么它是在什么时候输出来的呢?有两种情况:一是SSEL使能的边沿,二是上一帧数据的最后一个时钟沿,有时两种情况还会同时生效。

我们以CPOL=1/CPHA=1为例,把时序图画出来给大家看一下,如图15-1所示。

308dd275171119e2c2ac2a302d93fde4.png

15-1  SPI通信时序图(一)

大家看图15-1所示,当数据未发送时以及发送完毕后,SCK都是高电平,因此CPOL=1。可以看出,在SCK第一个沿的时候,MOSI和MISO会发生变化,同时SCK第二个沿的时候,数据是稳定的,此刻采样数据是合适的,也就是上升沿即一个时钟周期的后沿锁存读取数据,即CPHA=1。注意最后最隐蔽的SSEL片选,这个引脚通常用来决定是哪个从机和主机进行通信。剩余的三种模式,我们把图画出来,简化起见把MOSI和MISO合在一起了,大家仔细对照看看研究一下,把所有的理论过程都弄清楚,有利于你对SPI通信的深刻理解,如图15-2所示。

7661f949625fb568447e6e688427ce60.png

15-2  SPI通信时序图(二)

在时序上,SPI是不是比I2C要简单的多?没有了起始、停止和应答,UART和SPI在通信的时候,只负责通信,不管是否通信成功,而I2C却要通过应答信息来获取通信成功失败的信息,所以相对来说,UART和SPI的时序都要比I2C简单一些。

15.3 实时时钟芯片DS1302

DS1302是个实时时钟芯片,我们可以用单片机写入时间或者读取当前的时间数据,下面带着大家通过阅读这个芯片的数据手册来学习和掌握这个器件。

由于IT技术国际化比较强,因此数据手册绝大多数都是英文的,导致很多英语基础不好的同学看到英文手册头就大了。这里我要告诉大家的是,只要精神不退缩,方法总比困难多,很多英语水平不高的,看数据手册照样完全没问题,因为我们用到的专业词汇也就那么几个,多看几次就认识了。我们现在不是考试,因此大家可以充分利用一些英文翻译软件,翻译过来的中文意思有时候可能不是那么准确,那你就把翻译的内容和英文手册里的一些图表比较参考学习。此外数据手册除了介绍性的说明外,一般还会配相关的图形或者表格,结合起来看也有利于理解手册所表达的意思。这节课我会把DS1302的英文资料尽可能的用比较便于理解的方式给大家表达出来,同学们可以把我的表达和英文手册多做一下对比,尽可能快的开始学会了解英文手册。

15.3.1 DS1302的特点

DS1302是DALLAS(达拉斯)公司推出的一款涓流充电时钟芯片,2001年DALLAS被MAXIM(美信)收购,因此我们看到的DS1302的数据手册既有DALLAS的标志,又有MAXIM的标志,大家了解即可。

DS1302实时时钟芯片广泛应用于电话、传真、便携式仪器等产品领域,它的主要性能指标如下:

1、DS1302是一个实时时钟芯片,可以提供秒、分、小时、日期、月、年等信息,并且还有软件自动调整的能力,可以通过配置AM/PM来决定采用24小时格式还是12小时格式。

2、拥有31字节数据存储RAM。

3、串行I/O通信方式,相对并行来说比较节省IO口的使用。

4、DS1302的工作电压比较宽,在2.0~5.5V的范围内都可以正常工作。

5、DS1302这种时钟芯片功耗一般都很低,它在工作电压2.0V的时候,工作电流小于300nA。

6、DS1302共有8个引脚,有两种封装形式,一种是DIP-8封装,芯片宽度(不含引脚)是300mil,一种是SOP-8封装,有两种宽度,一种是150mil,一种是208mil。我们看一下DS1302的引脚封装图,如图15-3所示。

b1f33248866a890cd72f82eae08f4f76.png

图15-3  DS1302封装图

所谓的DIP(Dual In-line Package)封装,就是双列直插式封装技术,就如同我们开发板上的STC89C52单片机,就是个典型的DIP封装,当然这个STC89C52还有其它的封装样式,为了方便学习使用,我们采用的是DIP封装。而74HC245、74HC138、24C02、DS1302我们用的都是SOP(Small Out-Line Package)封装,是一种芯片两侧引出L形引脚的封装技术,大家可以看看开发板上的芯片,了解一下这些常识性知识。

7、当供电电压是5V的时候,兼容标准的TTL电平标准,这里的意思是,可以完美的和单片机进行通信。

8、由于DS1302是DS1202的升级版本,所以所有的功能都兼容DS1202。此外DS1302有两个电源输入,一个是主电源,另外一个是备用电源,比如可以用电池或者大电容,这样做是为了在系统掉电的情况下,我们的时钟还会继续走。如果使用的是充电电池,还可以在正常工作时,设置充电功能,给我们的备用电池进行充电。

DS1302的特点第二条“拥有31字节数据存储RAM”,这是DS1302额外存在的资源。这31字节的RAM相当于一个存储器一样,我们编写单片机程序的时候,可以把我们想存储的数据存储在DS1302里边,需要的时候读出来,这块功能和EEPROM有点类似,相当于一个掉电丢失数据的“EEPROM”,如果我们的时钟电路加上备用电池,那么这31个字节的RAM就可以替代EEPROM的功能了。这31字节的RAM功能使用很少,所以在这里我们就不讲了,大家了解即可。

15.3.2 DS1302的硬件信息

我们平时所用的不管是单片机,还是其它一些电子器件,根据使用条件的约束,可以分为商业级和工业级,主要是工作温度范围的不同,DS1302的购买信息如下图15-4所示。

de30b54cffbf4037e21f727c084336ae.png

图15-4  DS1302订购信息

我们在订购DS1302的时候,就可以根据图15-4所标识的来跟销售厂家沟通,商业级的工作温度范围略窄,是0~70摄氏度,而工业级可以工作在零下40~85摄氏度。TOP MARK就是指在芯片上印的字。

DS1302一共有8个引脚,下边要根据引脚分布图和典型电路图来介绍一下每个引脚的功能,如图15-5和图15-6所示。

   63c30972441234705b84d51e4856ce5d.png

图15-5  DS1302引脚图             

dbe817c3fc86a3a237a970d51c3072f9.png 

图15-6  DS1302典型电路

1脚VCC2是主电源正极的引脚,2脚X1和3脚X2是晶振输入和输出引脚,4脚GND是负极,5脚CE是使能引脚,接单片机的IO口,6脚I/O是数据传输引脚,接单片机的IO口,7脚SCLK是通信时钟引脚,接单片机的IO口,8脚VCC1是备用电源引脚。考虑到KST-51开发板是一套以学习为目的的板子,加上备用电池对航空运输和携带不方便,所以8脚没有接备用电池,而是接了一个10uF的电容,这个电容就相当于一个电量很小的电池,经过试验测量得出其可以在系统掉电后仍维持DS1302运行1分钟左右,如果大家想运行时间再长,可以加大电容的容量或者换成备用电池,如果掉电后不需要它再维持运行,也可以干脆悬空,如图15-7和图15-8所示。

21da9b0ebd8ebd4758e19c5eaecc8219.png

图15-7  DS1302电容作备用电源        

001242850e4c048bf4720b6817dbc87e.png

 图15-8  DS1302无备用电源

涓流充电功能,基本也用不到,因为实际应用中很少会选择可充电电池作为备用电源,成本太高,本课程也不讲了,大家作为选学即可。我们使用的时候直接用5V电源接一个二极管,在主电源上电的情况下给电容充电,在主电源掉电的情况下,二极管可以防止电容向主电路放电,而仅用来维持DS1302的供电,这种电路的最大用处是在电池供电系统中更换主电池的时候保持实时时钟的运行不中断,1分钟的时间对于更换电池足够了。此外,通过我们的使用经验,在DS1302的主电源引脚串联一个1K电阻可以有效的防止电源对DS1302的冲击,R6就是这个电阻,而R9、R26、R32都是上拉电阻。

我们把8个引脚功能分别介绍,如表15-1所示。

表15-1  DS1302引脚功能图

993817c02f710966aa9d3776cc4a9984.png

DS1302电路的一个重点就是晶振电路,它所使用的晶振是一个32.768k的晶振,晶振外部也不需要额外添加其它的电容或者电阻了。时钟的精度,首先取决于晶振的精度以及晶振的引脚负载电容。如果晶振不准或者负载电容过大或过小,都会导致时钟误差过大。在这一切都搞定后,最终一个考虑因素是晶振的温漂。随着温度的变化,晶振的精度也会发生变化,因此,在实际的系统中,其中一种方法就是经常校对。比如我们所用的电脑的时钟,通常我们会设置一个选项“将计算机设置与internet时间同步”。选中这个选项后,一般过一段时间,我们的计算机就会和internet时间校准同步一次。

15.3.3 DS1302寄存器介绍

DS1302的一条指令一个字节共8位,其中第7位(即最高位)固定为1,这一位如果是0的话,那写进去也是无效的。第6位是选择RAM还是CLOCK的,我前边说过,我们这里主要讲CLOCK时钟的使用,它的RAM功能我们不用,所以如果选择CLOCK功能,第6位是0,如果要用RAM,那第6位就是1。从第5到第1位,决定了寄存器的5位地址,而第0位是读写位,如果要写,这一位就是0,如果要读,这一位就是1。指令字节直观位分配如图15-9所示。

2bd7d1156a68e60313b0950250011d44.png

图15-9  DS1302命令字节

DS1302时钟的寄存器,其中8个和时钟有关的,5位地址分别是0b00000~0b00111,还有一个寄存器的地址是01000,这是涓流充电所用的寄存器,我们这里不讲。在DS1302的数据手册里的地址,直接把第7位、第6位和第0位值给出来了,所以指令就成了0x80、0x81那些了,最低位是1,那么表示读,最低位是0表示写,如图15-10所示。

0b58b7a5ee2de6c57912428c5d7a4cf7.png

 图15-10  DS1302的时钟寄存器

寄存器0:最高位CH是一个时钟停止标志位。如果时钟电路有备用电源,上电后,我们要先检测一下这一位,如果这一位是0,那说明时钟芯片在系统掉电后,由于备用电源的供给,时钟是持续正常运行的;如果这一位是1,那么说明时钟芯片在系统掉电后,时钟部分不工作了。如果Vcc1悬空或者是电池没电了,当我们下次重新上电时,读取这一位,那这一位就是1,我们可以通过这一位判断时钟在单片机系统掉电后是否还正常运行。剩下的7位高3位是秒的十位,低4位是秒的个位,这里再提请注意一次,DS1302内部是BCD码,而秒的十位最大是5,所以3个二进制位就够了。

寄存器1:最高位未使用,剩下的7位中高3位是分钟的十位,低4位是分钟的个位。

寄存器2:bit7是1的话代表是12小时制,0代表是24小时制;bit6固定是0,bit5在12小时制下0代表的是上午,1代表的是下午,在24小时制下和bit4一起代表了小时的十位,低4位代表的是小时的个位。

寄存器3:高2位固定是0,bit5和bit4是日期的十位,低4位是日期的个位。

寄存器4:高3位固定是0,bit4是月的十位,低4位是月的个位。

寄存器5:高5位固定是0,低3位代表了星期。

寄存器6:高4位代表了年的十位,低4位代表了年的个位。请特别注意,这里的00~99指的是2000年~2099年。

寄存器7:最高位一个写保护位,如果这一位是1,那么是禁止给任何其它寄存器或者那31个字节的RAM写数据的。因此在写数据之前,这一位必须先写成0。

15.3.4 DS1302通信时序介绍

DS1302我们前边也有提起过,是三根线,分别是CE、I/O和SCLK,其中CE是使能线,SCLK是时钟线,I/O是数据线。前边我们介绍过了SPI通信,同学们发现没发现,这个DS1302的通信线定义和SPI怎么这么像呢?

事实上,DS1302的通信是SPI的变异种类,它用了SPI的通信时序,但是通信的时候没有完全按照SPI的规则来,下面我们一点点解剖DS1302的变异SPI通信方式。

先看一下单字节写入操作,如图15-11所示。

5da17ea0c74bfc24b27350d256f2415e.png

图15-11  DS1302单字节写操作

然后我们再对比一下CPOL=0/CPHA=0情况下的SPI的操作时序,如图15-12所示。

55d87d4126395552dee6c1b67b93477b.png

图15-12  CPOL=0/CPHA=0通信时序

图15-11和图15-12的通信时序,其中CE和SSEL的使能控制是反的,对于通信写数据,都是在SCK的上升沿,从机进行采样,下降沿的时候,主机发送数据。DS1302的时序里,单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据。

对于单字节读操作,我就不做对比了,把DS1302的时序图贴出来,大家自己看一下即可,如图15-13所示。

7a0615fb3428bacff948f5149a1e8d79.png

图15-13  DS1302单字节读操作

读操作有两处需要特别注意的地方。第一,DS1302的时序图上的箭头都是针对DS1302来说的,因此读操作的时候,先写第一个字节指令,上升沿的时候DS1302来锁存数据,下降沿我们用单片机发送数据。到了第二个字数据,由于我们这个时序过程相当于CPOL=0/CPHA=0,前沿发送数据,后沿读取数据,第二个字节是DS1302下降沿输出数据,我们的单片机上升沿来读取,因此箭头从DS1302角度来说,出现在了下降沿。

第二个需要注意的地方就是,我们的单片机没有标准的SPI接口,和I2C一样需要用IO口来模拟通信过程。在读DS1302的时候,理论上SPI是上升沿读取,但是程序是用IO口模拟的,所以数据的读取和时钟沿的变化不可能同时了,必然就有一个先后顺序。通过实验发现,如果先读取IO线上的数据,再拉高SCLK产生上升沿,那么读到的数据一定是正确的,而颠倒顺序后数据就有可能出错。这个问题产生的原因还是在于DS1302的通信协议与标准SPI协议存在的差异造成的,如果是标准SPI的数据线,数据会一直保持到下一个周期的下降沿才会变化,所以读取数据和上升沿的先后顺序就无所谓了;但DS1302的IO线会在时钟上升沿后被DS1302释放,也就是撤销强推挽输出变为弱下拉状态,而此时在51单片机引脚内部上拉的作用下,IO线上的实际电平会慢慢上升,从而导致在上升沿产生后再读取IO数据的话就可能会出错。因此这里的程序我们按照先读取IO数据,再拉高SCLK产生上升沿的顺序。

下面我们就写一个程序,先将2013年10月8号星期二12点30分00秒这个时间写到DS1302内部,让DS1302正常运行,然后再不停的读取DS1302的当前时间,并显示在我们的液晶屏上。

b0793cbc874e94d8c82773162e9191f7.png

75e1b9b1feb62cf34e0cf926786aee15.png

14e233b8ae51c9ab9dea7d276e377f77.png

74bbf4f23ff01d03911a72eaf99d5cc0.png

前边学习了I2C和EEPROM的底层读写时序,那么DS1302的底层读写时序程序的实现方法是与之类似的,这里就不过多解释了,大家自己认真揣摩一下。

15.3.5 DS1302的BURST模式

进行产品开发的时候,逻辑的严谨性非常重要,如果一个产品或者程序逻辑上不严谨,就有可能出现功能上的错误。比如我们15.3.4节里的这个程序,我们再回顾一下,当单片机定时器时间到了200ms后,我们连续把DS1302的时间参数的7个字节读了出来。但是不管怎么读,都会有一个时间差,在极端的情况下就会出现这样一种情况:假如我们当前的时间是00:00:59,我们先读秒,读到的秒是59,然后再去读分钟,而就在读完秒到还未开始读分钟的这段时间内,刚好时间进位了,变成了00:01:00这个时间,我们读到的分钟就是01,显示在液晶上就会出现一个00:01:59,这个时间很明显是错误的。出现这个问题的概率极小,但却是实实在在可能存在的。

为了解决这个问题,芯片厂家肯定要给我们提供一种解决方案,这就是DS1302的突发模式。突发模式也分为RAM突发模式和时钟突发模式,RAM部分我们不讲,我们只看和时钟相关的clock burst mode。

当我们写指令到DS1302的时候,只要我们将要写的5位地址全部写1,即读操作用0xBF,写操作用0xBE,这样的指令送给DS1302之后,它就会自动识别出来是burst模式,马上把所有的8个字节同时锁存到另外的8个字节的寄存器缓冲区内,这样时钟继续走,而我们读数据是从另外一个缓冲区内读取的。同样的道理,如果我们用burst模式写数据,那么我们也是先写到这个缓冲区内,最终DS1302会把这个缓冲区内的数据一次性送到它的时钟寄存器内。

要注意的是,不管是读还是写,只要使用时钟的burst模式,则必须一次性读写8个寄存器,要把时钟的寄存器完全读出来或者完全写进去。

下边就提供一个burst模式的例程给大家学习一下,程序的功能还是与上一节一样的。

998940f956054fcc06de0cc649486702.png

abbe5c7cdb2a35b90fc8555f9f11cecb.png

05577b68bb34647eb979f1820f46be13.png

f23cc9d95b7b1562da4e250b6aaa86aa.png

15.4 复合数据类型

我们在前边学数据类型的时候,主要是字符型、整型、浮点型等基本类型,而学数组的时候,数组的定义要求数组元素必须是相同的数据类型。在实际应用中,有时候还需要把不同类型的数据组成一个有机的整体来处理,这些组合在一个整体中的数据之间还有一定的联系,比如一个学生的姓名、性别、年龄、考试成绩等,这就引入了复合数据类型。复合数据类型主要包含结构体数据类型、共用体数据类型和枚举体数据类型。

15.4.1 结构体数据类型

首先我们回顾一下上面的例程,我们把DS1302的7个字节的时间放到一个缓冲数组中,然后把数组中的值稍作转换显示到液晶上,这里就存在一个小问题,DS1302时间寄存器的定义并不是我们常用的“年月日时分秒”的顺序,而是在中间加了一个字节的“星期几”,而且每当我要用这个时间的时候都要清楚的记得数组的第几个元素表示的是什么,这样一来,一是很容易出错,二是程序的可读性不强。当然你可以把每一个元素都定一个明确的变量名字,这样就不容易出错也易读了,但结构上却显得很零散了。于是,我们就可以用结构体来将这一组彼此相关的数据做一个封装,它们既组成了一个整体,易读不易错,而且可以单独定义其中每一个成员的数据类型,比如说把年份用unsigned int类型,即4个十进制位来表示显然比2位更符合日常习惯,而其它的类型还是可以用2位来表示。结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者是一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用之前必须先定义它。

声明结构体变量的一般格式如下:

    struct  结构体名

    {

        类型1  变量名1;

        类型2  变量名2;

        ……

        类型n  变量名n;

    } 结构体变量名1, 结构体变量名2, ... 结构体变量名n;

这种声明方式是在声明结构体类型的同时又用它定义了结构体变量,此时的结构体名是可以省略的,但如果省略后,就不能在别处再次定义这样的结构体变量了。这种方式把类型定义和变量定义混在了一起,降低了程序的灵活性和可读性,因此我们并不建议采用这种方式,而是推荐用以下这种方式:

    struct  结构体名

    {

        类型1  变量名1;

        类型2  变量名2;

        ……

        类型n  变量名n;

    };

    struct 结构体名 结构体变量名1, 结构体变量名2, ... 结构体变量名n;

为了方便大家理解,我们来构造一个实际的表示日期时间的结构体。

    struct sTime {  //日期时间结构体定义

        unsigned int  year;   //年

        unsigned char mon;   //月

        unsigned char day;    //日

        unsigned char hour;   //时

        unsigned char min;    //分

        unsigned char sec;    //秒

        unsigned char week;  //星期

    };

    struct sTime bufTime;

struct是结构体类型的关键字,sTime是这个结构体的名字,bufTime就是定义了一个具体的结构体变量。那如果要给结构体变量的成员赋值的话,写法是

    bufTime.year = 0x2013;

    bufTime.mon = 0x10;

数组的元素也可以是结构体类型,因此可以构成结构体数组,结构体数组的每一个元素都是具有相同结构类型的结构体变量。例如我们前边构造的这个结构类型,直接定义成struct sTime bufTime[3];就表示定义了一个结构体数组,这个数组的3个元素,每一个都是一个结构体变量。同样的道理,结构体数组中的元素的成员如果需要赋值,就可以写成

    bufTime[0].year = 0x2013;

    bufTime[0].mon = 0x10;

一个指针变量如果指向了一个结构体变量的时候,称之为结构指针变量。结构指针变量是指向的结构体变量的首地址,通过结构体指针也可以访问到这个结构变量。

结构指针变量声明的一般形式如下:

    struct sTime *pbufTime;

这里要特别注意的是,使用结构体指针对结构体成员的访问,和使用结构体变量名对结构体成员的访问,其表达式有所不同。结构体指针对结构体成员的访问表达式为

    pbufTime->year = 0x2013;  或者是

    (*pbufTime).year = 0x2013;

很明显前者更简洁,所以推荐大家使用前者。

15.4.2 共用体数据类型

共用体也称之为联合体,共用体定义和结构体十分类似,我们同样是推荐以下形式:

    union  共用体名

    {

        数据类型1  成员名1;

        数据类型2  成员名2;

        ……

        数据类型n  成员名n;

    };

    union 共用体名 共用体变量;

共用体表示的是几个变量共用一个内存位置,也就是成员1、成员2……成员n都用一个内存位置。共用体成员的访问方式和结构体是一样的,成员访问的方式是:共用体名.成员名,使用指针来访问的方式是:共用体名->成员名。

共用体可以出现在结构体内,结构体也可以出现在共用体内,在我们编程的日常应用中,最多应用是结构体出现在共用体内,例如:

    union

    {

        unsigned int  value;

        struct

        {

            unsigned char  first;

            unsigned char  second;

        } half;

    } number;

这样将一个结构体定义到一个共用体内部,我们如果采用无符号整型赋值的时候,直接调用value这个变量,同时,我们也可以通过访问或赋值给first和 second这两个变量来访问或修改value的高字节和低字节。

这样看起来似乎是可以高效率的在int型变量和它的高低字节之间切换访问,但请回想一下,我们在介绍数据指针的时候就曾提到过,多字节变量的字节序取决于单片机架构和编译器,并非是固定不变的,所以这种方式写好的程序代码在换到另一种单片机和编译环境后,就有可能是错的,从安全和可移植的角度来讲,这样的代码是存在隐患的,所以现在诸多以安全为首要诉求的C语言编程规范里干脆直接禁止使用共用体。我们虽然不禁止,但也不推荐你用,除非你清楚的了解你所使用的开发环境的实现细节。

共用体和结构体的主要区别如下:

1、结构体和共用体都是由多个不同的数据类型成员组成,但在任何一个时刻,共用体只能存放一个被选中的成员,而结构体所有的成员都存在。

2、对于共同体的不同成员的赋值,将会改变其它成员的值,而对于结构体不同成员的赋值是相互之间不影响的。

15.4.3 枚举数据类型

在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期从周一到周日有7天,一年从1月到12月有12个月,蜂鸣器有响和不响两种状态等等。如果把这些变量定义成整型或者字符型不是很合适,因为这些变量都有自己的范围。C语言提供了一种称为“枚举”的类型,在枚举类型的定义中列举出所有可能的值,并可以为每一个值取一个形象化的名字,它的这一特性可以提高程序代码的可读性。

枚举的说明形式如下:

    enum  枚举名

    {

        标识符1[=整型常数],

        标识符2[=整型常数],

        ……

        标识符n[=整型常数]

    };

    enum 枚举名 枚举变量;

枚举的说明形式中,如果没有被初始化,那么“=整型常数”是可以被省略的,如果是默认值的话,从第一个标识符顺序赋值0、1、2……,但是当枚举中任何一个成员被赋值后,它后边的成员按照依次加1的规则确定数值。

枚举的使用,有几点要注意:

1、枚举中每个成员结束符是逗号,而不是分号,最后一个成员可以省略逗号。

2、枚举成员的初始化值可以是负数,但是后边的成员依然依次加1。

3、枚举变量只能取枚举结构中的某个标识符常量,不可以在范围之外。

15.5 电子钟实例

共用体除非必要,否则我们不推荐使用,枚举的用法比较简单,在本书19章的项目实践中有很好的示例,这节课我们先来练习一下结构体的使用。下边这个程序的功能是一个带日期的电子钟,相当于一个简易万年历了,并且加入了按键调时功能。学有余力的同学看到这里,不妨先不看我们提供的代码,自己写写试试。如果能够独立写一个按键可调的万年历程序,单片机可以说基本入门了。如果自己还不能够独立完成这个程序,那么还是老规矩,先抄并且理解,而后自己独立默写出来,并且要边默写边理解。

本例直接忽略了星期这项内容,通过上、下、左、右、回车、ESC这6个按键可以调整时间。这也是一个具有综合练习性质的实例,虽然在功能实现上没有多少难度,但要进行的操作却比较多而且烦琐,同学们可以从中体会到把繁杂的功能实现分解为一步步函数操作的必要性以及方便灵活性。简单说一下这个程序的几个要点,方便大家阅读理解程序。

1、把DS1302的底层操作封装为一个DS1302.c文件,对上层应用提供基本的实时时间的操作接口,这个文件也是我们的又一个功能模块了,我们的积累也越来越多了。

2、定义一个结构体类型sTime用来封装日期时间的各个元素,又用该结构体定义了一个时间缓冲区变量bufTime来暂存从DS1302读出的时间和设置时间时的设定值。需要注意的是在其它文件中要使用这个结构体变量时,必须首先再声明一次sTime类型;

3、定义一个变量setIndex来控制当前是否处于设置时间的状态,以及设置时间的哪一位,该值为0就表示正常运行,1~12分别代表可以修改日期时间的12个位;

4、由于这节课的程序功能要进行时间调整,用到了1602液晶的光标功能,添加了设置光标的函数,我们要改变哪一位的数字,就在1602对应位置上进行光标闪烁,所以Lcd1602.c在之前文件的基础上添加了两个控制光标的函数;

5、时间的显示、增减、设置移位等上层功能函数都放在main.c中来实现,当按键需要这些函数时则在按键文件中做外部声明,这样做是为了避免一组功能函数分散在不同的文件内而使程序显得凌乱。

1d22109fcd01878663a576e7fc94cd57.png

7803716c38137094545ad9c86ccdb46e.png

DS1302.c最终向外提供出与具体时钟芯片寄存器位置无关的、由时间结构类型sTime作为接口的实时时间的读取和设置函数,如此处理体现了我们前面提到过的层次化编程的思想。应用层可以不关心底层实现细节,底层实现的改变也不会对应用层造成影响,比如说日后你可能需要换一款时钟芯片,而它与DS1302的操作和时间寄存器顺序是不同的,那么你需要做的也仅是针对这款新的时钟芯片设计出底层操作函数,最终提供出同样的以sTime为接口的操作函数即可,应用层无需做任何的改动。

e2af12349d77235bb5c3868de6931321.png

ecf58d9084ded3fc40a240b35ad48295.png

为了本例的具体需求,在之前文件的基础上添加两个控制光标效果打开和关闭的函数,虽然函数都很简单,但为了保持程序整体上良好的模块化和层次化,还是应该在液晶驱动文件内以函数的形式提供,而不是由应用层代码直接来调用具体的液晶写命令操作。

150d36df2ed3f697f05034c796785534.png

febb65270632e32ab8b4b3b9d5d0d2d1.png

13c776569b7482a75c296823b4c0a20c.png

3ec6ddaa0ceeb1039307412038b2e2af.png

0656691fa4847c8ea860beddb968a7ad.png

9335e06c42cd610842a9eefd4def87b9.png

main.c主文件,负责所有应用层的功能实现,文件比较长,还是那句话“不难但比较烦琐”,希望对具体问题分析细化能力还不太强的同学们把这个文件多练习几遍,学习一下其中把具体问题逐步细化并一步步实现出来的编程思想,多进行此类练习,锻炼程序思维能力,将来遇到具体项目设计需求的时候,你很快就可以找到方法并实现它们了。

15.6 练习题

1、理解BCD码的原理。

2、理解SPI的通信原理,SPI通信过程的四种模式配置。

3、能够结合教程阅读DS1302的英文数据手册,学会DS1302的读写操作。

4、理解复合数据类型的结构和用法。

5、能够独立完成带按键功能的万年历程序。

f637d1244b77919081b0f4eb91563517.gif

金沙滩工作室

长按识别关注

62f71166e3ff2d24a80aca2fbe992bef.png

知识共享|助力梦想

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值