51单片机内部外设:实时时钟(SPI)

RTC引入

何为实时时钟
real time clock,真实时间,就是所谓的xx年x月x日x时x分x秒星期x
RTC是SoC中一个内部外设,RTC有自己独立的晶振提供RTC时钟源,内部有一些寄存器用来记录时间(年月日时分秒星期)。一般情况下为了在系统关机时时间仍然在走,还会给RTC提供一个电池供电。

RTC在早期的单片机应用中是外置的,后来因为这个功能比较基础比较常用,比较高端的单片机或者SOC就将RTC直接内置了,所以我这里就当RTC是内部外设,但其实在操作上,没有本质的区别。

时序

时序:字面意思,时序就是时间顺序,实际上在通信中时序就是通信线上按照时间顺序发生的电平变化,以及这些变化对通信的意义就叫时序。 

注意,时序的横轴的时间,所以对于字节是先发低位还是先发高位,可以看谁在前谁在后,在前的先发,在后的后发。

DS1302

DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。

它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。

引脚图:

DS1302 与单片机之间能简单地采用同步串行的方式进行通信,仅需用到三个口线:

  • RST 复位
  • I/O 数据线
  • SCLK 串行时钟

Vcc2为主电源,VCC1为后备电源。

X1和X2是振荡源,外接32.768kHz晶振。

RST是复位/片选线,通过把RST输入驱动置高电平来启动所有的数据传送。RST输入有两种功能:首先,RST接通控制逻辑,允许地址/命令序列送入移位寄存器;其次,RST提供终止单字节或多字节数据传送的方法。

当RST为高电平时,所有的数据传送被初始化,允许对DS1302进行操作。如果在传送过程中RST置为低电平,则会终止此次数据传送,I/O引脚变为高阻态。上电运行时,在Vcc>2.0V之前,RST必须保持低电平。只有在SCLK为低电平时,才能将RST置为高电平。

I/O为串行数据输入输出端(双向)。SCLK为时钟输入端。

具体内容详见DS1302数据手册:ds1302中文资料 - 百度文库

以下记录本人容易搞错的地方:

1、时钟操作可通过AM/PM 指示决定采用24 或12 小时格式;

2、DS1302有12个寄存器,其中有7个寄存器与日历、时钟相关;

3、寄存器中存放的数据位为BCD码形式;

操作DS1302的大致过程,就是将各种数据写入DS1302的寄存器,以设置它当前的时间和格式。然后使DS1302开始运作,DS1302时钟会按照设置情况运转,再用单片机将其寄存器内的数据读出。再用液晶显示,就是我们常说的简易电子钟。

DS1302的寄存器样式如下,我们看到:

第7 位永远都是1;

第6 位,1表示RAM,寻址内部存储器地址;0表示CK,寻址内部寄存器;

第5位到第1位,为RAM或者寄存器的地址;

最低位,高电平表示RD:即下一步操作将要“读”;低电平表示W:即下一步操作将要“写”。

下面来看看寄存器地址和对应的值:

SEC:秒寄存器,注意具体右边内容:低四位为SEC,高的次三位为10SEC,最高位CH为DS1302 的运行标志,当CH=0时,DS1302内部时钟运行,反之CH=1时停止;

MIN:分寄存器;

HR:时寄存器,最高位为12/24 小时的格式选择位,该位为1时表示12小时格式。当设置为12小时显示格式时,第5位的高电平表示下午(PM);而当设置为24 小时格式时,第5位为具体的时间数据。

DATE:日寄存器;

MONTH:月寄存器;

DAY:周寄存器,注意一周只有7天,所以该寄存器只有低三位有效,周一到周日;

YEAR:年寄存器;

CONTROL:写保护寄存器,当该寄存器最高位WP为1时,DS1302 只读不写,所以要在往DS1302 写数据之前确保WP为0;

TRICKLE CHARGE REGISTER:涓细电流充电设置寄存器,我们知道,当DS1302掉电时,可以马上调用外部电源保护时间数据。该寄存器就是配置备用电源的充电选项的。其中高四位(4个TCS)只有在1010 的情况下才能使用充电选项;低四位的情况,与DS1302内部电路有关。

CLOCK BURST:批量读写操作设置寄存器,设置该寄存器后,可以对DS1302的各个寄存器进行连续写入。DS1302的另外一种读写方式。笔者还没用过,感兴趣的朋友可以尝试。

寄存器定义表:

单字节读写时序图如下:

可参考时序图写程序

注意:

1、从最低位开始传送数据。(是不是说不管是主到从,还是从到主,都是最低位开始)

2、时序图中有的信号是高低电平的形式,但是有的是以方框的形式表示的,这代表它可能是高电平,也可能是低电平,一般都是数据信号的时序表示,里面放的是要发送或者接收的数据。

浅析SPI总线协议

SPI是一个同步的数据总线,也就是说它是用单独的数据线和一个单独的时钟信号来保证发送端和接收端的完美同步。

时钟是一个振荡信号,它告诉接收端在确切的时机对数据线上的信号进行采样。

产生时钟的一侧称为主机,另一侧称为从机。总是只有一个主机(一般来说可以是微控制器/MCU),但是可以有多个从机(后面详细介绍);

数据的采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),具体要看对SPI的配置。

整体的传输大概可以分为以下几个过程:

主机先将NSS信号拉低,这样保证开始接收数据;

当接收端检测到时钟的边沿信号时,它将立即读取数据线上的信号,这样就得到了一位数据(1bit);由于时钟是随数据一起发送的,因此指定数据的传输速度并不重要,尽管设备将具有可以运行的最高速度(稍后我们将讨论选择合适的时钟边沿和速度)。

主机发送到从机时:

主机产生相应的时钟信号,然后数据一位一位地将从MOSI信号线上进行发送到从机;

主机接收从机数据:

如果从机需要将数据发送回主机,则主机将继续生成预定数量的时钟信号,并且从机会将数据通过MISO信号线发送;

具体如下图所示:

SPI的时序

注意,SPI是“全双工”(具有单独的发送和接收线路),因此可以在同一时间发送和接收数据。

SPI 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口(Serial Peripheral Interface),是一种高速全双工的通信总线。 

SPI接口是一种同步串行总线(Serial Peripheral Interface)多用于实时时钟、Flash存储器(如NOR Flash&Nand Flash),ADC、LCD控制器等外围器件的通讯接口。大大增强了处理器的外设扩展能力。

SPI是一种全双工、高速的、同步的通信总线。

SPI接口至少有4根线,分别是CS、SCLK、MOSI、MISO

SPI接口的一些缩写
SSEL:slave select,常常也被写作CS(chip select)或SS(slave select)
SCK:serial clock,常常也写作SCLK或SCL
MISO:master input slave output,常常被简写为SO(slave output,也有说是serial output)
MOSI:master output slave input,常常被简写为SI(slave input,也有说是serial input)

SCLK是同步信号,一般由主控来控制;

CS代表片选信号,不同的从设备,不同的电平使能,通常是低电平有效信号;

MOSI英文全称是Master Output Slave Input,主输出从输入;

MISO英文全称是Slave Input Master Output,从输入主输出。

对MOSI的重理解:

之前一直以为MOSI,就是主设备输出的时候从设备就是输入;但其实,MOSI的真正含义是当一个设备作为主设备时是输出,而当其作为从设备的时候就是输入。

主设备和从设备的引脚是一一对应的。

片选示意图:

极性和相位(了解):

CPOL :Clock Polarity(时钟极性 )
CPHA :Clock Phase(时钟相位)
时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。

时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设的时钟相位和极性应该一致。

极性和相位一般重点关注从设备,主设备根据从设备的时序要求进行编程即可。

SPI和串口

串口通信中,有起始位,有停止位;但是SPI中没有,发完8个字节后,接着发,继续发。其步调是由时钟信号统一协调的。

串口因为是异步的,没有统一的工作时钟,所以要事先规定好波特率,起始位和停止位。也正因为没有统一时钟,所以容易出错。

另外,SPI和串口都没有应答机制。有没有接收到发送方也不知道。

SPI没有特定的空闲时状态,也没有固定的上升沿下降沿读写,有几种模式搭配。

SPI单线通讯模式
SPI单线模式是将原来的两根数据线改成一根,通讯方式变成了半双工的通讯方式,在接线上,只需要三根线分别是SCLK、I/O、CS。

DS1302采用的就是这种模式(引脚名称可能不同,但原理是一样的)。

SPI通讯的优势
使SPI作为串行通信接口脱颖而出的原因很多;

全双工串行通信;
高速数据传输速率。
简单的软件配置;
极其灵活的数据传输,不限于8位,它可以是任意大小的字;
非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。


SPI的缺点
没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
通常仅支持一个主设备;
需要更多的引脚(与I2C不同);
没有定义硬件级别的错误检查协议;
与RS-232和CAN总线相比,只能支持非常短的距离;

DS1302读写数据

/************************************************************

日期:2022年7月26日

作者:星辰

文件内容:RTC

**************************************************************/

#include<reg51.h>
#include<intrins.h>

typedef unsigned char uchar;

sbit DSIO = P3^4;
sbit CE = P3^5;
sbit SCLK = P3^6;
    
/*************************************************************

函数入口

**************************************************************/
void main()
{ 

    ……
    
}

/*************************************************************

写入时钟

**************************************************************/
void RtcWrite(uchar regAddr, uchar regValue)
{
    uchar i = 0, j = 0;
    SCLK = 0;
    CE = 1;
    
    //写入8位控制字节
    for(i; i < 8; i++)
    {
        DSIO = regAddr & 0x01;
        regValue >>= 1;
        SCLK = 1;
        _nop_();
        SCLK = 0;
        _nop_();
    }
    
    //写入8位数据字节
    for(j; j < 8; j++)
    {
        DSIO = regAddr & 0x01;
        regValue >>= 1;
        SCLK = 1;
        _nop_();
        SCLK = 0;
        _nop_();
    }
    
    SCLK = 0;
    CE = 0;
}

/*************************************************************

读取时钟

返回值:读取到的寄存器的值

**************************************************************/
uchar RtcRead(uchar regAddr)
{
    uchar i = 0, j = 0;
    uchar tempBit = 0;
    uchar rtcResult = 0;
    SCLK = 0;
    CE = 1;
    
    //写入8位控制字节
    for(i; i < 8; i++)
    {
        DSIO = regAddr & 0x01;
        regAddr >>= 1;
        SCLK = 1;
        _nop_();
        SCLK = 0;
        _nop_();
    }
    
    //读取数据,从设备从低位开始传输,所以主设备也是从低位开始读
    for(j; j < 8; j++)
    {
        tempBit = DSIO;
        rtcResult |= (tempBit << j);
        SCLK = 1;
        _nop_(); 
        SCLK = 0;
        _nop_(); 
    }
    
    SCLK = 0;
    CE = 0;
    DSIO = 0;   //防止出现FF现象
    
    return rtcResult;
}

总结来说,对于数据的写入和读取是这样的:

都是从最低位开始读或者写。

写数据时,先把一位二进制数放到IO口,然后构建上升沿,就可以将数据发送出去。

读数据时,先构建下降沿,这时候数据就到了IO口,我们将其读出来即可。

重点注意:

另外,在DS1302的代码编写中,常会遇到一个问题:读出来的时间数据,经常会看到FF,一般是在前一个单元是偶数的时候会出现,比如:

有两种解决方法:

1、硬件上在IO线上设置10K的电阻做弱上拉处理。
2、如果没有做弱上拉,也有办法解决。在代码的读取寄存器时序之后,加一个将IO置为低电平的代码进去,就可以了。

BCD码

DS1302是用BCD码(8421码)来显示时间的。

那么什么是BCD码呢?

BCD码(Binary-Coded Decimal‎),又称为8421码,用4位二进制数来表示1位十进制中的0~9这10个数码(3位二进制最大只能表示到7),是一种二进制的数字编码形式,用二进制编码的十进制代码。和四位自然二进制码不同的是,它只选用了四位二进制码中前10组代码,即用0000~1001分别代表它所对应的十进制数,余下的六组代码不用。

BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。这种编码技巧最常用于会计系统的设计里,因为会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点数计数法,采用BCD码,既可保存数值的精确度,又可免去使计算机作浮点运算时所耗费的时间。此外,对于其他需要高精确度的计算,BCD编码亦很常用。

示例如下:

十进制的123 用BCD表示为:0001 0010 0011

因为:十进制的 1 用二进制表示是 0001

         十进制的 2 用二进制表示是 0010

         十进制的 3 用二进制表示是 0011

BCD码和二进制的区别:123的二进制应该用短除法求得1111011由此可见,BCD码只是机械地用二进制表示十进制的每一位。
 

十进制要以二进制的形式在计算机中存储,十进制直接转换成与之对应的BCD码比十进制通过除法取余再转换的效率来的高。???

给你一个十进制,让你转成机器码的形式,要么转成BCD码,要么转成二进制码,这两种算法,哪种效率更高?

压缩与非压缩的BCD码
区别就是非压缩BCD码是8位的,浪费4位用0填充;压缩的BCD码是4位的,没有空间浪费。

由于1字节有8bit,如果用一字节存储4位BCD码(其余补0)就是非压缩。

如十进制的123:

非压缩码  00000001 00000010 00000011

压缩码  0001 0010 0011

BCD码和十六进制的关系

本来这二者没有什么直接关系,但是,我们在C语言中,常常使用十六进制来表示一串二进制数,比如将二进制000100100011表示成0x123。

再来看看十进制123,如果是用BCD码的形式来存储,那么就是000100100011,很显然,刚好和十六进制的数字是一致的。

所以,如果某芯片存储十进制的形式为BCD码,那么,我们可以直接写成十六进制的形式。

比如,要存的是45,程序里就写成0x45,虽然0x45是十六进制,但表示的就是十进制的45。

这样,我写入0x45就是写入45,读出0x45就是45,而不用再转成二进制后再计算出十进制。

如果不是BCD码存储,而是按照常规二进制来存,那么45对应的二进制就是00101101,而0x45对应的二进制数为01000101,显然二者并不相等。如果想要表示存入的是十进制45,那么我们要写的十六进制是0x2D。这样的转换显然比较麻烦。

因为BCD码和十六进制刚好有0—9的重合部分,所以在程序里可以直接用十六进制来表示对应形式的十进制,前提是,是以BCD码的形式存储。

十进制的数字以BCD码的形式存入,再将该BCD码读出,正好就是对应的十六进制形式。

45——0x45——00100011(BCD码)——0x45——45

45——0x45这一步我们知道,直接写即可;

0x45——00100011(BCD码)这一步芯片会实现;

00100011(BCD码)——0x45,我们是一位一位地读出来的,那么读出来后,以什么样的形式来显示?就是程序需要做的事情了。读出来后可以直接通过算法转成十进制的形式来显示。

进一步理解BCD码

这里讲到BCD,就顺带捋一捋数据表示法的问题。

其实,不管是十进制,还是二进制、八进制、十六进制,或者是这里讲的BCD编码,或者其他类型的编码类型,都是一种数据表示法。

从头理一理,内存中根据高低电平对应一个二进制数据,比如是10011001,编译器考虑到直接将这个数据显示到屏幕上没几个人能看懂,所以,对应自然语言,就要用看得懂的形式来展示,于是,就有很多种展示方式,比如转成十进制153来显示,或者转成十六进制0x99来显示,或者转成对应的八进制0231来表示,其中常用的就是十进制或者十六进制。

另外,要注意一点,我们在屏幕上看到的所有符号都是字符形式,是编译器将对应的数据用字符的形式让我们看到,反过来,我们在屏幕上输入数据并加上相应的符号,编译器会根据符号的形式来识别我们写的是哪种数据,比如有前缀0x就是个十六进制数据,有前缀0就是八进制数据,没有前缀就是十进制数据等等,具体由编译器完成,我们使用时不用过多理会。

我们要知道的是,我们在屏幕上写的153、0x99、0231,编译器都会将其解析成10011001存储在存储介质中,他们表示的都是同一个二进制码,也就是说表示的高低电平是一样的,也就是说他们表示的值是相等的。

再回到BCD码,如果已知153、0x99、0231是一个BCD码,也就是10011001是一个BCD码,那么它想表示的十进制数据就是99(虽然和十六进制在“字符串形式”上很像,但表示的压根就不是同一个值)。 这里可以看到,BCD码其实就是十进制的一种表示方法,有什么好处呢?假如有个十进制数很大,99999,要想算出对应的二进制数比较麻烦,要不断地进行除法计算,可是如果表示成BCD码就相对简单,因为它是将十进制的数据一位一位地表示,1001 1001 1001 1001 1001,解析起来也比较简单99999。虽然也是一串二进制码,但是不能用常规的方法来解析,存的时候也不能用常规的算法来存。

举例说明:99,用常规方法来存得到的是01100011,可用对2取余的算法得到,可是用BCD码的形式来存就是10011001;反过来,对于10011001,如果用常规方法来解析,得到的就是153,显然,并不是一开始要的99,所以只能用BCD码算法来解析。

综上所述可知,十进制数、十六进制或者八进制只是进制的转换而已,归根结底表示的都是同一个值,都可以通过计算互转,而BCD码则是另一种对数据的解读方法,用另一种形式来表示一个数,虽然BCD也存在计算,但只会4位4位地计算。重点要关注底层的数据,而不是自然语言的表示形式。

如果一个变量接收到10011001的数据,也就是说,变量对应的内存中存的是10011001,我们操作变量,就是操作这一串数据。我们在软件中看到的153、0x99都是经过编译器处理后的自然语言表示法。CPU是直接进行二进制的计算的。当然,底层的计算已经被编译器给屏蔽了,我们不用管,我们只用自然语言表示法即可。也就是十进制或者十六进制表示法。

 

那么,一个二进制数按照常规方式解析,和按照BCD码的方式解析,二者的结果有什么关系吗?这是互相转换的关键。以10011001为例,常规解析的153和BCD解析的99之间,有什么关系吗?

一开始,我总想着用字符串的思维来解决,因为我们平时在界面看到的都是字符串形式,以为拼接就可以了,比如我一开始想着,先把0x99(153),也就是10011001每四位拿出来计算,然后按字符串的形式来拼接。不过,这些都是数据,又不是字符串,这样肯定不合理。我还想着0x99去掉0x不就是对应的十进制数99吗?这都是字符串思维。

其实,把每四位拿出来也可以,不过要计算,低四位+高四位*10,以此类推。

比如:

应该还有其他更好的算法,待补充。

二进制的计算

补充

如何拆分十进制的各个位?比如21,则21/10得到十位数,21%10得到个位数;

十六进制如何拆分数据?比如0x21,>>七位可获取高位,&上0x01可获得低位。

对于计算机来说,移位和位运算是最喜欢的,速度很快,而十进制的计算比较麻烦,尤其是除法运算,效率其实很低。

  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是51单片机使用SPI的详细步骤: 1. 定义SPI总线的引脚,51单片机SPI总线引脚如下: - P1.5:SCK时钟 - P1.6:MOSI数据输出 - P1.7:MISO数据输入 - P1.x:CS片选信号 其,P1.x表示可以根据实际需要选择一个GPIO作为片选信号引脚。 2. 配置SPI总线的工作模式,通过SPI控制寄存器SPCTL进行配置,具体配置如下: - 设置时钟极性和时钟相位,通过设置SPCTL寄存器的CPOL和CPHA位来配置SPI总线的时钟极性和时钟相位,一般情况下选择CPOL=0,CPHA=0。 - 设置数据位数,通过设置SPCTL寄存器的DORD位来配置SPI总线的数据位数,一般情况下选择DORD=0,表示数据传输的高位在前。 - 设置主/从模式,通过设置SPCTL寄存器的SSM和SSS位来配置SPI总线的主/从模式,一般情况下选择主模式,即SSM=0,SSS=0。 - 设置时钟速率,通过设置SPCTL寄存器的SPR1和SPR0位来配置SPI总线的时钟速率,具体配置根据外设的时钟频率和SPI总线的最大时钟速率来确定。 3. 初始化SPI总线,并使能SPI总线,通过调用SPI初始化函数和SPI使能函数来完成,具体代码如下: ``` void spi_init() { SPCTL = 0x50; // 配置SPI总线参数 SPSTAT = 0xC0; // 清除SPI状态寄存器 } void spi_enable() { SPCTL |= 0x40; // 使能SPI总线 } ``` 4. 发送数据到外部设备,通过调用SPI发送函数来完成,具体代码如下: ``` void spi_send(uint8_t data) { SPDAT = data; // 写入发送缓冲区 while (!(SPSTAT & 0x80)); // 等待发送完成 } ``` 5. 接收来自外部设备的数据,通过调用SPI接收函数来完成,具体代码如下: ``` uint8_t spi_receive() { SPDAT = 0xFF; // 写入发送缓冲区 while (!(SPSTAT & 0x80)); // 等待接收完成 return SPDAT; // 返回接收数据 } ``` 6. 关闭SPI总线,通过调用SPI关闭函数来完成,具体代码如下: ``` void spi_disable() { SPCTL &= ~0x40; // 关闭SPI总线 } ``` 需要注意的是,以上代码只是SPI总线的基本操作,具体应用需要根据实际情况进行修改和完善。另外,在使用SPI总线时需要仔细参考外部设备的数据手册,了解其时序要求,以保证正常的数据传输。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值