本教材现以连载的方式由网络发布,并将于2014年由清华大学出版社出版最终完整版,版权归作者和清华大学出版社所有。本着开源、分享的理念,本教材可以自由传播及学习使用,但是务必请注明出处来自金沙滩工作室
在前面的课程中我们已经了解到了不少关于时钟的概念,比如我们用的单片机的主时钟是11.0592M、I2C总线有一条时钟信号线SCL等,这些时钟本质上都是一个某一频率的方波信号。那么除了这些在前面新学到的时钟概念外,还有一个我们早已熟悉的不能再熟悉的时钟概念——年-月-日
时:分:秒,就是我们的钟表和日历给出的时间,它的重要程度我想就不需要多说了吧,在单片机系统里我们把它称作实时时钟,以区别于前面提到的几种方波时钟信号。实时时钟,有时也被称作墙上时钟,很形象的一个名词,对吧,大家知道他们讲的一回事就行了。本章,我们将学习实时时钟的应用,有了它,你的单片机系统就能在漫漫历史长河中找到自己的时间定位啦,可以在指定时间干某件事,或者记录下某事发生的具体时间,等等。除此之外,本章还会学习到C语言的结构体,它也是C语言的精华部分,我们通过本章先来了解它的基础,后面再逐渐达到熟练、灵活运用它,你的编程水平会提高一个档次哦。15.1
Decimal)亦称二进码十进制数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数字。是一种二进制的数字编码形式,用二进制编码的十进制代码。BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。我们前边讲过十六进制和二进制本质上是一回事,十六进制仅仅是二进制的一种缩写形式而已。而十进制的一位数字,从0到9,最大的数字就是9,再加1就要进位,所以用4位二进制表示十进制,就是从0000到1001,不存在1010、1011、1100、1101、1110、1111这6个数字。BCD码如果到了1001,再加1的话,数字就变成了0001
0000这样的数字了,相当于用了8位的二进制数字表示了2位的十进制数字。关于BCD码更详细的介绍请点击的基础教程栏目里面有很多相关文章.
BCD码的应用还是非常广泛的,比如我们这节课要学的实时时钟,日期时间在时钟芯片中的存储格式就是BCD码,当我们需要把它记录的时间转换成可以直观显示的ASCII码时(比如在液晶上显示),就可以省去一步由二进制的整型数到ASCII的转换过程,而直接取出表示十进制1位数字的4个二进制位然后再加上0x30就可组成一个ASCII码字节了,这样就会方便的多,在后面的实际例程中将看到这个简单的转换。
Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片机和EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。SPI通信原理比I2C要简单,它主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,标准的SPI是4根线,分别是SSEL(片选,也写作SCS)、SCLK(时钟,也写作SCK)、MOSI(主机输出从机输入Master
在某些情况下,我们也可以用3根线的SPI或者2根线的SPI进行通信。比如主机只给从机发送命令,从机不需要回复数据的时候,那MISO就可以不要;而在主机只读取从机的数据,不需要给从机发送指令的时候,那MOSI可以不要;当一个主机一个从机的时候,从机的片选有时可以固定为有效电平而一直处于使能状态,那么SSEL可以不要;此时如果再加上主机只给从机发送数据,那么SSEL和MISO都可以不要;如果主机只读取从机送来的数据,SSEL和MOSI都可以不要。
主机和从机要交换数据,就牵涉到一个问题,即主机在什么时刻输出数据到MOSI上而从机在什么时刻采样这个数据,或者从机在什么时刻输出数据到MISO上而主机什么时刻采样这个数据。同步通信的一个特点就是所有数据的变化和采样都是伴随着时钟沿进行的,也就是说数据总是在时钟的边沿附近变化或被采样。而一个时钟周期必定包含了一个上升沿和一个下降沿,这是周期的定义所决定的,只是这两个沿的先后并无规定。又因为数据从产生的时刻到它的稳定是需要一定时间的,那么,如果主机在上升沿输出数据到MOSI上,从机就只能在下降沿去采样这个数据了。反之如果一方在下降沿输出数据,那么另一方就必须在上升沿采样这个数据。 大家看图15-1所示,当数据未发送时以及发送完毕后,SCK都是高电平,因此CPOL=1。可以看出,在SCK第一个沿的时候,MOSI和MISO会发生变化,同时SCK第二个沿的时候,数据是稳定的,此刻采样数据是合适的,也就是上升沿即一个时钟周期的后沿锁存读取数据,即CPHA=1。注意最后最隐蔽的SSEL片选,一般情况下,这个引脚通常用来决定是哪个从机和主机进行通信。剩余的三种模式,我把图画出来,简化起见把MOSI和MISO合在一起了,大家仔细对照看看研究一下,把所有的理论过程都弄清楚,有利于你对SPI通信的深刻理解,如图15-2所示。 在时序上,SPI是不是比I2C要简单的多?没有了起始、停止和应答,UART和SPI在通信的时候,只负责通信,不管是否通信成功,而I2C却要通过应答信息来获取通信成功失败的信息,所以相对来说,UART和SPI的时序都要比I2C简单一些。15.3 实时时钟芯片DS1302 本节课的DS1302是个实时时钟芯片,我们可以用单片机写入时间或者读取当前的时间数据,我也会带着大家通过阅读这个芯片的数据手册来学习和掌握这个器件。 由于IT技术国际化比较强,因此数据手册绝大多数都是英文的,导致很多英语基础不好的同学看到英文手册头就大了。这里我要告诉大家的是,只要精神不退缩,方法总比困难多,很多英语水平不高的,看数据手册照样完全没问题,因为我们的专业词汇也就那么几个,多看几次就认识了。我们现在不是考试,因此大家可以充分利用一些英文翻译软件,翻译过来的中文意思有时候可能不是那么准确,那你就把翻译的内容和英文手册里的一些图表比较参考学习。此外数据手册除了介绍性的说明外,一般还会配相关的图形或者表格,结合起来看也有利于理解手册所表达的意思。这节课我会把DS1302的英文资料尽可能的用比较便于理解的方式给大家表达出来,同学们可以把我的表达和英文手册多做一下对比,尽可能快的慢慢开始学会了解英文手册。 1、DS1302是一个实时时钟芯片,可以提供秒、分、小时、日期、月、年等信息,并且还有软年自动调整的能力,可以通过配置AM/PM来决定采用24小时格式还是12小时格式。 2、拥有31字节数据存储RAM。 3、串行I/O通信方式,相对并行来说比较节省IO口的使用。 4、DS1302的工作电压比较宽,大概是2.0V~5.5V都可以正常工作。 5、DS1302这种时钟芯片功耗一般都很低,它在工作电压2.0V的时候,工作电流小于300nA。 6、DS1302共有8个引脚,有两种封装形式,一种是DIP-8封装,芯片宽度(不含引脚)是300mil,一种是SOP-8封装,有两种宽度,一种是150mil,一种是208mil。我们看一下DS1302的引脚封装图,如图15-3所示。
Package,也叫做双列直插式封装技术,就如同我们开发板上的STC89C52RC单片机,就是个典型的DIP封装,当然这个STC89C52RC还有其他的封装,为了方便学习使用,我们采用的是DIP封装。而74HC245、74HC138、24C02、DS1302我们用的都是SOP封装Small Out-Line
Package,是一种芯片两侧引出L形引脚的封装技术,大家可以看看开发板上的芯片,了解一下这些常识性知识。 DS1302一共有8个引脚,下边要根据引脚分布图和典型电路图来介绍一下每个引脚的功能,如图15-5和图15-6所示。 1脚VCC2是主电源正极的引脚,2脚X1和3脚X2是晶振输入和输出引脚,4脚GND是负极,5脚CE是使能引脚,接单片机的IO口,6脚I/O是数据传输引脚,接单片机的IO口,7脚SCLK是通信时钟引脚,接单片机的IO口,8脚VCC1是备用电源引脚。考虑到KST-51开发板是一套以学习为目的的板子,加上备用电池对航空运输和携带不方便,所以8脚可以直接悬空,断电后不需要DS1302再运行了,或者是在8脚接一个10uF的电容,经过试验可以运行1分钟左右的时间,如果大家想运行时间再长,可以加大电容的容量,如图15-7和图15-8所示。
涓流充电功能,课程也不讲了,大家也作为选学即可,我们使用的时候直接用5V电源接一个二极管,在有主电源的情况下给电容充电,在主电源掉电的情况下,这个电容可以给DS1302大约供电1分钟左右,这种电路的最大用处是在电池供电系统中更换主电池的时候保持实时时钟的运行不中断,1分钟的时间对于更换电池足够了。此外,通过我们的使用经验,在DS1302的主电源引脚串联一个1K电阻可以有效的防止电源对DS1302的冲击,R6就是,而R9,R26,R32都是上拉电阻。 DS1302的一条指令一个字节8位,其中第七位(即最高位)是固定1,这一位如果是0的话,那写进去是无效的。第六位是选择RAM还是CLOCK的,我前边说过,我们这里主要讲CLOCK时钟的使用,它的RAM功能我们不用,所以如果选择CLOCK功能,第六位是0,如果要用RAM,那第六位就是1。从第五到第一位,决定了寄存器的5位地址,而第零位是读写位,如果要写,这一位就是0,如果要读,这一位就是1,如图15-9所示。 DS1302时钟的寄存器,其中8个和时钟有关的,5位地址分别是00000一直到00111这8个地址,还有一个寄存器的地址是01000,这是涓流充电所用的寄存器,我们这里不讲。在DS1302的数据手册里的地址,直接把第七位、第六位和第零位值给出来了,所以指令就成了80H、81H那些了,最低位是1,那么表示读,最低位是0表示写,如图15-10所示。
寄存器一:最高位CH是一个时钟停止标志位。如果我们的时钟电路有备用电源部分,上电后,我们要先检测一下这一位,如果这一位是0,那说明我们的时钟在系统掉电后,由于备用电源的供给,时钟是持续正常运行的;如果这一位是1,那么说明我们的时钟在系统掉电后,时钟部分不工作了。若我们的Vcc1悬空或者是电池没电了,当我们下次重新上电时,读取这一位,那这一位就是1,我们可以通过这一位判断时钟在单片机系统掉电后是否持续运行。剩下的7位高3位是秒的十位,低4位是秒的个位,这里注意再提一次,DS1302内部是BCD码,而秒的十位最大是5,所以3个二进制位就够了。 然后我们在对比一下再对比一下CPOL=0并且CPHA=0的情况下的SPI的操作时序,如图15-12所示。 图15-11和图15-12的通信时序,其中CE和SSEL的使能控制是反的,对于通信写数据,都是在SCK的上升沿,从机进行采样,下降沿的时候,主机发送数据。DS1302的时序里,单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据。 对于单字节读操作,我就不做对比了,把DS1302的时序图贴出来给大家看一下,如图15-13所示。
读操作有两处特别注意的地方。第一,DS1302的时序图上的箭头都是针对DS1302来说的,因此读操作的时候,先写第一个字节指令,上升沿的时候DS1302来锁存数据,下降沿我们用单片机发送数据。到了第二个字数据,由于我们这个时序过程相当于CPOL=0/CPHA=0,前沿发送数据,后沿读取数据,第二个字节是DS1302下降沿输出数据,我们的单片机上升沿来读取,因此箭头从DS1302角度来说,出现在了下降沿。 进行产品开发的时候,逻辑的严谨性非常重要,如果一个产品或者程序逻辑上不严谨,就有可能出现功能上的错误。比如我们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个寄存器,要把时钟的寄存器完全读出来或者完全写进去。 15.4 结构体数据类型我们在前边学数据类型的时候,主要是字符型、整型、浮点型等基本类型,而学数组的时候,数组的定义要求数组元素必须是想同的数据类型。在实际应用中,有时候还需要把不同类型的数据组成一个有机的整体来处理,这些组合在一个整体中的数据之间还有一定的联系,比如一个学生的姓名、性别、年龄、考试成绩等,这就引入了复合数据类型。复合数据类型主要包含结构体数据类型、共用体数据类型和枚举体数据类型,我们本节主要要学习一下结构体数据类型。 首先我们回顾一下上面的例程,我们把DS1302的7个字节的时间放到一个缓冲数组中,然后把数组中的值稍作转换显示到液晶上,这里就存在一个小问题,DS1302时间寄存器的定义并不是我们常用的“年月日时分秒”的顺序,而是在中间加了一个字节的“星期几”,而且每当我要用这个时间的时候都要清楚的记得数组的第几个元素表示的是什么,这样一来,一是很容易出错,而是程序的可读性不强。当然你可以把每一个元素都定一个明确的变量名字,这样就不容易出错也易读了,但结构上却显得很零散了。于是,我们就可以用结构体来将这一组彼此相关的数据做一个封装,他们既组成了一个整体,易读不易错。而且可以单独定义其中每一个成员的数据类型,比如说把年份用unsigned int类型,即4个十进制位来表示显然比2位更符合日常习惯,而其它的类型还是可以用2位来表示。结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者是一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用之前必须先定义它。 这种声明方式仅仅是一个结构体变量的声明方式,但是在实际应用中,可能需要多个具有相同形式的结构体变量,这种定义方式就不是很方便了,因此我们推荐以下方式: Struct是结构体类型的关键字,sTime是这个结构体的名字,bufTime就是定义了一个具体的结构体变量。那如果要给结构体变量的成员赋值的话,写法是 数组的元素也可以是结构体类型,因此可以构成结构体数组,结构体数组的每一个元素都是具有想同结构类型的结构体变量。例如我们前边构造的这个结构类型,直接定义成struct sTime bufTime[3];就表示定义了一个结构体数组,这个数组的3个元素,每一个都是一个结构体变量。同样的道理,结构体数组中的元素的成员如果需要赋值,就可以写成 一个指针变量如果指向了一个结构体变量的时候,称之为结构指针变量。结构指针变量中的值是指向的结构体变量的首地址,通过结构体指针也可以访问到这个结构变量。 这里要特别注意的是,使用结构体指针对结构体成员的访问,和使用结构体变量名对结构体成员的访问,其表达式有所不同。结构体指针对结构体成员的访问表达式为 介绍结构体数据类型要干嘛,毫无疑问要应用在我们的程序中。下边这个程序的功能相当于一个万年历了,并且加入了按键调时功能。学有余力的同学看到这里,不妨先不看我提供的代码,自己写写试试。如果能够独立写一个按键可调的万年历程序,单片机可以说基本入门了。如果自己还不能够独立完成这个程序,那么还是老规矩,先抄并且理解,而后自己独立默写出来,并且要边默写边理解。 本例直接忽略了星期这项内容,通过上、下、左、右、回车、ESC这6个按键可以调整时间。简单说一下这个程序的几个要点,方便大家阅读理解程序。 1、定义一个结构体类型sTime用来封装日期时间的各个元素,又用该结构体定义了一个结构体时间缓冲区变量bufTime来暂存从DS1302读出的时间和设置时间时的设定值。需要注意的是在其它文件中要使用这个结构体变量时,必须首先再声明一次sTime类型; 2、定义一个变量setIndex来控制当前是否处于设置时间的状态,以及设置时间的哪一位,该值为0就表示正常运行,1-12分别代表可以修改日期时间的12个位; 3、由于这节课的程序功能要进行时间调整,用到了1602液晶的光标功能,添加了设置光标的函数,我们要改变哪一位的数字,就在1602对应位置上进行光标闪烁,所以Lcd1602.c程序和之前的有所不同; 4、时间的显示、增减、设置移位等上层功能函数都放在main.c中来实现,当按键需要这些函数时则在按键文件中做外部声明,这样做是为了避免一组功能函数分散在不同的文件内而使程序显得凌乱。 1、理解BCD码的原理。 2、理解SPI的通信原理,SPI通信过程的四种模式配置。 3、能够结合教程阅读DS1302的英文数据手册,学会DS1302的读写操作。 4、能够独立完成带按键功能的万年历程序,并且将课程带的上、下、左、右、回车、ESC这几个按键的调时修改成为数字键、回车、ESC调时的功能。
|
这里将我编写的STC12C5A60S2单片机控制SPI协议时钟芯片ds1302的程序共享一下,是希望前辈们给予斧正 。
(补充:以下代码只需要修改 声明 :经过试验,DS1302内部的时钟部分是没有严格的数据检测功能的,具体是: 在实验室,将秒钟设置为70s,之后,读出并显示,秒钟数为70,并不断变化, 虽然经过一段时间后,ds1302会因为"进位"而硬件"自动调整过来",但是这并 不是真正意义上的"调整",所以,我在程序中增加了时间数据合法性的检测,虽然 1.将设置年星期月日时分秒的相关函数设置为了外部接口函数。 1.添加ds1302RTC_info结构代替之前的数组格式,使得结构更加紧凑。 1.修改对寄存器的配置方法。 例如:对秒寄存器的"秒"数据进行配置时,之前的方法是禁止写保护的前提下, 去将"秒"数据一起写进寄存器中,这里看似没有问题,但是,却有一个 致命的错误(虽然这种错误最后会因为DS1302的硬件自动调整过来(需要一段时间), 但是这样依靠硬件去调整的做法是不严谨的)。 这句话有些难以理解,举例说明: 假如读出的"秒"数据是"0x01秒",而我们恰巧需要设置的"秒"数据,为"0x00秒", 这时候会因为我们的代码中有类似 " 0 | 1"这样的代码,结果为1秒,所以 我们需要设置的秒数就没有被正确的设置进去。 读出的数据temp需要将我们设置的"秒"的有效位即bit0~bit6进行清零,然后再"|", 就可以保证我们需要设置的时间被正确写入。 1.这里的读写方式都是普通字节读写,还有爆发模式读写功能未写,待定。 //以下为DS1302复位的稳定时间,必须的(这句话摘抄于范例代码),不懂 ds1302_io_bit= 0;/*这一块代码中,这一句代码不能少,这一块代码中只需要这一句 Note :读取寄存器操作需要关闭写保护 Note :写寄存器操作需要关闭写保护 Note :注意,这里的转换只限于十进制0~99之间的整数数值向BCD Note :注意,这里转换只限于BCD码向十进制的0~99之间的转换.(1字节) Note :读出的数据位BCD码,需要转化为十进制 Note :设置"月"数据,需要转换为BCD码 Note :读出的"月"数据位BCD码,需要转换为十进制 Note :设置"日"数据,需要转换为BCD码 Note :读取的"日"数据,需要转换为十进制 Note :设置"时"数据格式需要转换为BCD码。而且对小时寄存器操作较为特殊, 因为此寄存器中不仅存有"小时数据",还有小时模式(12/24小时制),以及 上/下午(12小时制时),最安全的做法是先将数据读出来,再将需要设置的 相应位清空,然后再以" | "的方式进行设置相应位。 Note :设置"分"数据,需要转换为BCD码 Note :读出的"分"数据位BCD码,需要转换为十进制 "秒"数值时,为了不会影响到最高位的值,最安全的算法就是先读出值 然后再将需要设置的相应位清空,再以 "|"的方式设置相应位。 /*先读出值,是为了后面写"秒"数值时,不会影响到最高位的"起始/停止"位*/ 这一位并不是真正意义的"秒"数值,所以需要将读出来的 值的最高位清零,然后进行数据码制转换,这样得到的数 值才是真正意义上的"秒"数值。 /*最高位不是有效地"秒"数据*/ Note :经过试验,DS1302内部没有严格的数据检测功能,不论写的数据是否合法,都 会写进去,所以需要软件检测一下时间的合法性。 {//对于所有月份(包含大月) //时钟模式选择:12/24小时制,上/下午选择(12小时模式下才设置) Note :这里的返回值只对12小时模式有有用,是上/下午 标志位,对于24小时模式而言是无用的。 //小时寄存器比较特殊 Note :为了安全,先将小时寄存器里面数据读出来,设置相应位, /*若为12小时制,还需要设置上/下午*/ Note :上电时,时钟起振允许和写保护都是未定义的状态(参照datasheet), //是否时能时钟,初始上电状态未确定,需要设置 //写保护在初始上电时是不确定的,这里打开写保护