15.1 BCD码的学习
在我们日常生产生活中用的最多的数字是十进制数字,而单片机系统的所有数据本质上都是二进制的,所以聪明的前辈们就给我们创造了BCD码。
BCD码(Binary-Coded Decimal)亦称二进码十进制数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数字。是一种二进制的数字编码形式,用二进制编码的十进制代码。BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。我们前边讲过十六进制和二进制本质上是一回事,十六进制仅仅是二进制的一种缩写形式而已。而十进制的一位数字,从0到9,最大的数字就是9,再加1就要进位,所以用4位二进制表示十进制,就是从0000到1001,不存在1010、1011、1100、1101、1110、1111这6个数字。BCD码如果到了1001,再加1的话,数字就变成了00010000这样的数字了,相当于用了8位的二进制数字表示了2位的十进制数字。关于BCD码更详细的介绍请点击www.51hei.com的基础教程栏目里面有很多相关文章.
BCD码的应用还是非常广泛的,比如我们这节课要学的实时时钟,日期时间在时钟芯片中的存储格式就是BCD码,当我们需要把它记录的时间转换成可以直观显示的ASCII码时(比如在液晶上显示),就可以省去一步由二进制的整型数到ASCII的转换过程,而直接取出表示十进制1位数字的4个二进制位然后再加上0x30就可组成一个ASCII码字节了,这样就会方便的多,在后面的实际例程中将看到这个简单的转换。
15.2 SPI时序初步认识
SSEL:从设备片选使能信号。如果从设备是低电平使能的话,当拉低这个引脚后,从设备就会被选中,主机和这个被选中的从机进行通信。
SCLK:时钟信号,由主机产生,和I2C通信的SCL有点类似。
MOSI:主机给从机发送指令或者数据的通道。
MISO:主机读取从机的状态或者数据的通道。
在某些情况下,我们也可以用3根线的SPI或者2根线的SPI进行通信。比如主机只给从机发送命令,从机不需要回复数据的时候,那MISO就可以不要;而在主机只读取从机的数据,不需要给从机发送指令的时候,那MOSI可以不要;当一个主机一个从机的时候,从机的片选有时可以固定为有效电平而一直处于使能状态,那么SSEL可以不要;此时如果再加上主机只给从机发送数据,那么SSEL和MISO都可以不要;如果主机只读取从机送来的数据,SSEL和MOSI都可以不要。 3线和2线的SPI大家要知道怎么回事,实际使用也是有应用的,但是当我们提及SPI的时候,一般都是指标准SPI,都是指4根线的这种形式。
SPI通信的主机也是我们的单片机,在读写数据时序的过程中,有四种模式,要了解这四种模式,首先我们得学习一下2个名词。
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所示。


在时序上,SPI是不是比I2C要简单的多?没有了起始、停止和应答,UART和SPI在通信的时候,只负责通信,不管是否通信成功,而I2C却要通过应答信息来获取通信成功失败的信息,所以相对来说,UART和SPI的时序都要比I2C简单一些。
15.3 实时时钟芯片DS1302
本节课的DS1302是个实时时钟芯片,我们可以用单片机写入时间或者读取当前的时间数据,我也会带着大家通过阅读这个芯片的数据手册来学习和掌握这个器件。
由于IT技术国际化比较强,因此数据手册绝大多数都是英文的,导致很多英语基础不好的同学看到英文手册头就大了。这里我要告诉大家的是,只要精神不退缩,方法总比困难多,很多英语水平不高的,看数据手册照样完全没问题,因为我们的专业词汇也就那么几个,多看几次就认识了。我们现在不是考试,因此大家可以充分利用一些英文翻译软件,翻译过来的中文意思有时候可能不是那么准确,那你就把翻译的内容和英文手册里的一些图表比较参考学习。此外数据手册除了介绍性的说明外,一般还会配相关的图形或者表格,结合起来看也有利于理解手册所表达的意思。这节课我会把DS1302的英文资料尽可能的用比较便于理解的方式给大家表达出来,同学们可以把我的表达和英文手册多做一下对比,尽可能快的慢慢开始学会了解英文手册。
15.3.1 DS1302的特点
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所示。

所谓的DIP封装Dual In-line Package,也叫做双列直插式封装技术,就如同我们开发板上的STC89C52RC单片机,就是个典型的DIP封装,当然这个STC89C52RC还有其他的封装,为了方便学习使用,我们采用的是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一共有8个引脚,下边要根据引脚分布图和典型电路图来介绍一下每个引脚的功能,如图15-5和图15-6所示。




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

15.3.3 DS1302寄存器介绍


寄存器二:bit7没意义,剩下的7位高3位是分钟的十位,低4位是分钟的个位。
寄存器三:bit7是1的话代表是12小时制,是0的话代表是24小时制,bit6固定是0,bit5在12小时制下0代表的是上午,1代表的是下午,在24小时制下和bit4一起代表了小时的十位,低4位代表的是小时的个位。
寄存器四:高2位固定是0,bit5和bit4是日期的十位,低4位是日期的个位。
寄存器五:高3位固定是0,bit4是月的十位,低4位是月的个位。
寄存器六:高5位固定是0,低3位代表了星期。
寄存器七:高4位代表了年的十位,低4位代表了年的个位。这里特别注意,这里的00到99年指的是2000年到2099年。
寄存器八:bit7是一个保护位,如果这一位是1,那么是禁止给任何其他的寄存器或者那31个字节的RAM写数据的。因此在写数据之前,这一位必须先写成0。
15.3.4 DS1302通信时序介绍
事实上,DS1302的通信是SPI的变异种类,它用了SPI的通信时序,但是通信的时候没有完全按照SPI的规则来,下面我们一点点解剖一下DS1302的变异SPI通信方式。
先看一下单字节写入操作,如图15-11所示。

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

图15-11和图15-12的通信时序,其中CE和SSEL的使能控制是反的,对于通信写数据,都是在SCK的上升沿,从机进行采样,下降沿的时候,主机发送数据。DS1302的时序里,单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据。
对于单字节读操作,我就不做对比了,把DS1302的时序图贴出来给大家看一下,如图15-13所示。

第二个需要注意的地方就是,我们的单片机没有标准的SPI接口,和I2C一样需要用IO口来模拟通信过程。在读DS1302的时候,理论上SPI是上升沿读取,但是我们的程序是用IO口模拟的,所以数据的读取和时钟沿的变化不可能同时了,必然就有一个先后顺序。通过实验发现,如果先读取IO线上的数据,再拉高SCLK产生上升沿,那么读到的数据一定是正确的,而颠倒顺序后数据就有可能出错。这个问题产生的原因还是在于DS1302的通信协议与标准SPI协议存在的差异造成的,如果是标准SPI的数据线,数据会一直保持到下一个周期的下降沿才会变化,所以读取数据和上升沿的先后顺序就无所谓了;但DS1302的IO线会在时钟上升沿后被DS1302释放,也就是撤销强推挽输出变为弱下拉状态,而此时在51单片机引脚内部上拉的作用下,IO线上的实际电平会慢慢上升,从而导致在上升沿产生后再读取IO数据的话就可能出错。因此这里的程序我们按照先读取IO数据,再拉高SCLK产生上升沿的顺序。
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个寄存器,要把时钟的寄存器完全读出来或者完全写进去。
15.4 结构体数据类型
我们在前边学数据类型的时候,主要是字符型、整型、浮点型等基本类型,而学数组的时候,数组的定义要求数组元素必须是想同的数据类型。在实际应用中,有时候还需要把不同类型的数据组成一个有机的整体来处理,这些组合在一个整体中的数据之间还有一定的联系,比如一个学生的姓名、性别、年龄、考试成绩等,这就引入了复合数据类型。复合数据类型主要包含结构体数据类型、共用体数据类型和枚举体数据类型,我们本节主要要学习一下结构体数据类型。
首先我们回顾一下上面的例程,我们把DS1302的7个字节的时间放到一个缓冲数组中,然后把数组中的值稍作转换显示到液晶上,这里就存在一个小问题,DS1302时间寄存器的定义并不是我们常用的“年月日时分秒”的顺序,而是在中间加了一个字节的“星期几”,而且每当我要用这个时间的时候都要清楚的记得数组的第几个元素表示的是什么,这样一来,一是很容易出错,而是程序的可读性不强。当然你可以把每一个元素都定一个明确的变量名字,这样就不容易出错也易读了,但结构上却显得很零散了。于是,我们就可以用结构体来将这一组彼此相关的数据做一个封装,他们既组成了一个整体,易读不易错。而且可以单独定义其中每一个成员的数据类型,比如说把年份用unsigned int类型,即4个十进制位来表示显然比2位更符合日常习惯,而其它的类型还是可以用2位来表示。结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者是一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用之前必须先定义它。