实时时钟_实时时钟设计 - CSDN
  • 关于实时时钟模块DS1302使用心得

    万次阅读 多人点赞 2017-10-22 23:45:16
    最近在做万年历,用到实时时钟DS1302模块,花了两天时间看资料和写驱动,想记录一下我的学习经过,顺便做一下总结。 首先就是在图书馆查各种资料,于是查到的大多是这些,主要时硬件方面的资料:   ...

    最近在做万年历,用到实时时钟DS1302模块,花了两天时间看资料和写驱动,想记录一下我的学习经过,顺便做一下总结。

    首先就是在图书馆查各种资料,于是查到的大多是这些,主要时硬件方面的资料:

     

     

    其实能查到很多资料,但是能为我们所用的不是很多。在使用一个芯片时,我一般时按照一下步骤去学习:

    1、芯片介绍;

    2、查看引脚定义;

    3、外围电路

    4、分析时序图;

    5、模仿着编写驱动程序,然后自己动手写驱动。

    6、实现功能。

    下面我就按照这个顺序去学习这款芯片;

    一、芯片介绍

    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.0V~5.5V都可以正常工作。采用双电源供电,当主电源比备用电源高0.2V时,由主电源供电,否则采用备用电源,一般是一个纽扣电池。
    5、DS1302这种时钟芯片功耗一般都很低,它在工作电压2.0V的时候,工作电流小于300nA。
    6、DS1302共有8个引脚,有两种封装形式,一种是DIP-8封装,芯片宽度(不含引脚)是300mil,一种是SOP-8封装,有两种宽度,一种是150mil,一种是208mil。

    二、引脚定义

    三、外围电路

    一般与单片机IO口相连时要加上拉电阻,提高 IO 口的驱动能力,这样信号比较稳定,计时也比较准确。

    四、分析时序图

    这是单字节写入的时序图,可见,先拉高使能端,进行使能选择,然后在时钟上升沿写入一个字节。

    DS1302在进行读写操作时最少读写两个字节,第一个是控制字节,就是一个命令,说明是读还是写操作,第二个时需要读写的数据。

    对于单字节写,只有在SCLK为低电平时才能将 CE 置高电平,所以刚开始将SCLK 置低,CE置高,然后把需要写入的字节送入 IO口,然后跳变SCLK,在SCLK下降沿时,写入数据

     

    五、编写驱动程序

    有了 上面的分析,我们就可以学着编写驱动程序了,可以把驱动程序分为几个模块来写,由底层慢慢往上累加,比如,我们先编写单个字节的读写操作,在编写整个数据的读写,

    复制代码
    #include "DS1302.h"
    
    //*******************
    void ds1302_writebyte(uchar byte){
        uint i;
        uint t = 0x01;
        for(i=0;i<8;i++){
            SCIO = byte & t;        
            t<<=1;
            DOWN();       //下降沿完成一个位的操作
        }
        SCIO = 1;//确保释放io引脚
    }
    //********************
    void ds1302_writedata(uchar addr,uchar data_){
        
        CE = 0;        nop();    
        SCLK = 0;    nop();    
        CE = 1;        nop();    //使能片选信号
        ds1302_writebyte((addr<<1)|0x80);    //方便后面写入
        ds1302_writebyte(data_);
        CE = 0;        nop();//传送数据结束
    
    }
    //*************************
    uchar ds1302_readbyte(){
        uint i;
        uchar data_ = 0;
        uint t = 0x01;     
        for(i=0;i<7;i++){     //c51好像不支持直接在for循环里面直接定义变量
    
            if(SCIO){
    
                data_ = data_ | t;    //低位在前,逐位读取,刚开始不对,估计是这个的问题
            }                
            t<<=1;
            DOWN();
        }
         return data_;
    }
    
    
    //************************
    uchar ds1302_readdata(uchar addr){
    
        uchar data_ = 0;
    
        CE = 0;     nop();
        SCLK = 0;  nop();
        CE = 1;      nop();
        ds1302_writebyte((addr<<1)|0x81);
        data_ = ds1302_readbyte();
        CE = 0;       nop();
        SCLK = 1;  nop();
        SCIO = 0;  nop();
        SCIO = 1;  nop();
    
        return data_;
    }
    
    //*********************
    void init_ds1302(){
    
         uchar i;
         CE = 0;   //初始化引脚
         SCLK = 0; 
         i  = ds1302_readdata(0x00);  //读取秒寄存器,秒在最低位
         if((i & 0x80 != 0)){
    
             ds1302_writedata(7,0x00); //撤销写保护,允许写入数据
            for(i = 0;i<7;i++){
    
                ds1302_writedata(i,init_time[i]);
            }
         }    
    }
    
    //**************
    void ds1302_readtime(){       //读取时间
          uint i;
          for(i = 0;i<7;i++){
    
                 init_time[i] = ds1302_readdata(i);
          }
    }
    复制代码

    其中头文件为:

    复制代码
    #ifndef __DS1302_H
    #define __DS1302_H
    
    #include "reg52.h"
    #include "intrins.h"
    
    
    #define uint unsigned int
    #define uchar unsigned char
    #define nop() _nop_()
    
    #define UP() {SCLK = 0;nop();SCLK = 1;nop();} //上升沿  ,使用宏定义函数时最后一定家分号
    #define DOWN() {SCLK = 1;nop();SCLK = 0;nop();} //下降沿
    
     //这个模块内没有集成上拉电阻,使用时最好接上2
    sbit CE = P2^5;//RET,使能输入引脚,当读写时,置高位
    sbit SCIO = P2^6;//IO     ,双向通信引脚,读写数据都是通过这个完成
    sbit SCLK = P2^7;//SCLK,时钟信号
    
    
    //为什么有时候好好的,也会出错显示少了分号呢?还气人啊!!!!
    
    void ds1302_writebyte(uchar byte);//写一个字节; 
    void ds1302_writedata(uchar addr,uchar data_);//给某地址写数据,data是c51内部的关键字,表示将变量定义在数据存储区,故此处用data_;
    uchar ds1302_readbyte();//读一个字节
    uchar ds1302_readdata(uchar addr);//读取某寄存器数据     ;
    void init_ds1302();
    void ds1302_readtime();
    
    
    extern uchar init_time[];
    
    #endif
    复制代码

     

    六、功能实现

     功能实现就简单了,就是加上主函数嘛,然后加上我们可以亲眼看见并感知的模块,比如用数码管显示时间:

    复制代码
    #include "DS1302.h"
    
    #define DIG P0
    sbit LSA = P2^2;
    sbit LSB = P2^3;
    sbit LSC = P2^4;
    
    
    
    uchar init_time[] = {0x50,0x15,0x14,0x22,0x10,0x06,0x17};//初始化的时间    //秒 分 时 日 月 周 年 
    uchar code DIG_CODE[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; //数码管数字表
    uint disp[8]={0x3f,0x3f,0x3f,0x3f,0x3f,0x3f,0x3f,0x3f};//把要显示的数字传给他    
    uchar Num=0;
    uint count_flag = 0;     //中断溢出次数
    
    void time0_init();//定时器0初始化
    void display();//数码管显示时间
    
    void main(void){
        
         init_ds1302();     //初始化时写入起始时间
         time0_init();
    
         while(1){
                display();
         }
    }
    
    //***************
    void display(){
                
         ds1302_readtime();    //读取时间
        disp[7] = DIG_CODE[init_time[0]&0x0f];
        disp[6] = DIG_CODE[init_time[0]>>4];
        disp[5] = 0X40;        //显示一个横线
        disp[4] = DIG_CODE[init_time[1]&0x0f];
        disp[3] = DIG_CODE[init_time[1]>>4];
        disp[2] = 0X40;
        disp[1] = DIG_CODE[init_time[2]&0x0f];
        disp[0] = DIG_CODE[init_time[2]>>4];
    }
    //******************
    void time0_init(){
    
        TMOD=0X02;//选择为定时器模式,工作方式2,8位自动重装模式,仅用TRX打开启动。
        TH0=0X9C;    //给定时器赋初值,定时100us,0x9c就是156,就是还需计数100次产生溢出,就是0.1ms
        TL0=0X9C;    
        ET0=1;//打开定时器0中断允许
        EA=1;//打开总中断
        TR0=1;//打开定时器
    }
    
    void DigDisplay() interrupt 1    //中断入口函数,扫描以实现动态显示
    {
    //定时器在工作方式二会自动重装初,所以不用在赋值。
    //    TH0=0X9c;//给定时器赋初值,定时0.1ms
    //    TL0=0X00;
    
        count_flag++;    
        if(count_flag==1)
        {
            count_flag = 0;
            DIG=0; 
            switch(Num)     //位选,选择点亮的数码管,
            {
                case(7):
                    LSA=0;LSB=0;LSC=0; break;
                case(6):
                    LSA=1;LSB=0;LSC=0; break;
                case(5):
                    LSA=0;LSB=1;LSC=0; break;
                case(4):
                    LSA=1;LSB=1;LSC=0; break;
                case(3):
                    LSA=0;LSB=0;LSC=1; break;
                case(2):
                    LSA=1;LSB=0;LSC=1; break;
                case(1):
                    LSA=0;LSB=1;LSC=1; break;
                case(0):
                    LSA=1;LSB=1;LSC=1; break;    
            }
    
            DIG=disp[Num]; //段选,选择显示的数字。
            Num++;
            if(Num>7)
                Num=0;
        }    
    }
    复制代码

     

     总结一下:

    这个芯片基本上不是很难,但是想要用的灵活,用的上手,还是得多练的,最好是先把上面的驱动程序对着时序图自己分析一遍,然后自己亲手编写一下。

    还有就是看数据手册,一个芯片所能用到的数据,在数据手册上基本都能查到。资料谁都能查到,就看怎么用了。

    展开全文
  • 实时时钟RTC

    2020-04-01 18:13:47
    外围设备控制器芯片(ICH)内部,集成了实时时钟电路(RTC),以及两个CMOS组成的静态存储器(CMOS RAM),通常为128B;RTC负责计时,由1个32.768kHz的石英晶体振荡器驱动,经分频后用于CMOS RAM进行每秒一次的时间刷新; ...

    1.简述

    外围设备控制器芯片(ICH)内部,集成了实时时钟电路(RTC),以及两个CMOS组成的静态存储器(CMOS RAM),通常为128B;
    RTC负责计时,由1个32.768kHz的石英晶体振荡器驱动,经分频后用于CMOS RAM进行每秒一次的时间刷新;
    CMOS RAM中时间信息如下表所示,前10字节为常规时间信息:

    偏移地址 内容 偏移地址 内容
    0x00 0x07
    0x01 闹钟秒 0x08
    0x02 0x09
    0x03 闹钟分 0x0A 寄存器A
    0x04 0x0B 寄存器B
    0x05 闹钟时 0x0C 寄存器C
    0x06 星期 0x0D 寄存器D

    2.访问

    CMOS RAM访问须通过两个端口访问:索引端口0x70/0x74,数据端口0x71/0x75;
    端口0x70的位7用于允许或禁止NMI,寄存器ABCD为8位寄存器,ABD可读可写,C只读;
    RTC芯片中断信号通向8259从片IR0,在计算机启动期间,BIOS初始化中断控制器:主片中断号设为从0x08开始,从片中断号从0x70开始
    ==>启动后,RTC中断号默认0x70

    展开全文
  • 实时时钟DS1302详细介绍

    千次阅读 2018-05-28 22:19:32
    来源于:http://www.51hei.com/bbs/dpj-22465-1.html 在前面的课程中我们已经...那么除了这些在前面新学到的时钟概念外,还有一个我们早已熟悉的不能再熟悉的时钟概念——年-月-日 时:分:秒,就是我们的钟表和日...

    来源于:http://www.51hei.com/bbs/dpj-22465-1.html

         在前面的课程中我们已经了解到了不少关于时钟的概念,比如我们用的单片机的主时钟是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位二进制表示十进制,就是从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时序初步认识        

            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通信的主机也是我们的单片机,在读写数据时序的过程中,有四种模式,要了解这四种模式,首先我们得学习一下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所示。
        大家看图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的英文资料尽可能的用比较便于理解的方式给大家表达出来,同学们可以把我的表达和英文手册多做一下对比,尽可能快的慢慢开始学会了解英文手册。
    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.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的购买信息如下图15-4所示。
      我们在订购DS1302的时候,就可以根据图15-4所标识的来跟销售厂家沟通,商业级的工作电压略窄,是0到70度,而工业级可以工作在零下40度到85度。TOP MARK就是指在芯片上印的字。
           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都是上拉电阻。
    我们把8个引脚功能分别介绍,如表15-1所示。
            DS1302的电路一个重点就是时钟电路,它所使用的晶振是一个32.768k的晶振,晶振外部也不需要额外添加其他的电容或者电阻电路了。时钟的精度,首先取决于晶振的精度以及晶振的引脚负载电容。如果晶振不准或者负载电容过大过小,都会导致时钟误差过大。在这一切都搞定后,最终一个考虑因素是晶振的温漂。随着温度的变化,晶振往往精度会发生变化,因此,在实际的系统中,其中一种方法就是经常校对。比如我们所用的电脑的时钟,通常我们会设置一个选项“将计算机设置于internet时间同步”。选中这个选项后,一般可以过一段时间,我们的计算机就会和internet时间校准同步一次。


    15.3.3 DS1302寄存器介绍 

             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个二进制位就够了。
            寄存器二: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我们前边也有提起过,是三根线,分别是CE、I/O和SCLK,其中CE是使能线,SCLK是时钟线,I/O是数据线。前边我们学过SPI通信,同学们发现没发现,这个DS1302的通信线定义和SPI怎么这么像呢?
    事实上,DS1302的通信是SPI的变异种类,它用了SPI的通信时序,但是通信的时候没有完全按照SPI的规则来,下面我们一点点解剖一下DS1302的变异SPI通信方式。
    先看一下单字节写入操作,如图15-11所示。
                                                    图15-11 DS1302单字节写操作
    然后我们在对比一下再对比一下CPOL=0并且CPHA=0的情况下的SPI的操作时序,如图15-12所示。
                                                    图15-12 CPOL=0/CPHA=0通信时序
            图15-11和图15-12的通信时序,其中CE和SSEL的使能控制是反的,对于通信写数据,都是在SCK的上升沿,从机进行采样,下降沿的时候,主机发送数据。DS1302的时序里,单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据。
            对于单字节读操作,我就不做对比了,把DS1302的时序图贴出来给大家看一下,如图15-13所示。
            读操作有两处特别注意的地方。第一,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产生上升沿的顺序。
            前边学习了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个寄存器,要把时钟的寄存器完全读出来或者完全写进去。

    15.4 结构体数据类型

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


    展开全文
  • STM32之RTC实时时钟

    万次阅读 多人点赞 2018-08-31 16:41:25
    STM32之RTC实时时钟 ...

    STM32之RTC实时时钟

    RTC实时时钟简介:
    STM32的RTC外设,实质是一个掉电后还继续运行的定时器,从定时器的角度来看,相对于通用定时器TIM外设,它的功能十分简单,只有计时功能(也可以触发中断).但是从掉电还能继续运行来看,它是STM32中唯一一个具有这个功能功能的外设.(RTC外设的复杂之处不在于它的定时,而在于它掉电还可以继续运行的特性)
    所谓掉电,是指电源Vpp断开的情况下,为了RTC外设掉电可以继续运行,必须给STM32芯片通过VBAT引脚街上锂电池.当主电源VDD有效时,由VDD给RTC外设供电.当VDD掉电后,由VBAT给RTC外设供电.无论由什么电源供电,RTC中的数据始终都保存在属于RTC的备份域中,如果主电源和VBA都掉电,那么备份域中保存的所有数据都将丢失.(备份域除了RTC模块的寄存器,还有42个16位的寄存器可以在VDD掉电的情况下保存用户程序的数序,系统复位或电源复位时,这些数据也不会被复位).
    从RTC的定时器特性来说,它是一个32位的计数器,只能向上计数.他使用的时钟源有三种,分别为:
    1,高速外部时钟的128分频:HSE/128;
    2,低速内部时钟LSI;
    3,低速外部时钟LSE;
    使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作.所以RTC一般都时钟低速外部时钟LSE,频率为实时时钟模块中常用的32.768KHz,因为32768 = 2^15,分频容易实现,所以被广泛应用到RTC模块.(在主电源VDD有效的情况下(待机),RTC还可以配置闹钟事件使STM32退出待机模式).

    RTC工作过程:
    这里写图片描述

    RTC架构:
    图中浅灰色的部分都是属于备份域的,在VDD掉电时可在VBAT的驱动下继续运行.这部分仅包括RTC的分频器,计数器,和闹钟控制器.若VDD电源有效,RTC可以触发RTC_Second(秒中断)、RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断).从结构图可以看到到,其中的定时器溢出事件无法被配置为中断.如果STM32原本处于待机状态,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)使它退出待机模式.闹钟事件是在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发的.
    因为RTC的寄存器是属于备份域,所以它的所有寄存器都是16位的.它的计数RTC_CNT的32位由RTC_CNTL和RTC_CNTH两个寄存器组成,分别保存计数值的低16位和高16位.在配置RTC模块的时钟时,把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟TR_CLK = RTCCLK/37768 = 1Hz,计时周期为1秒,计时器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值加1(常用)

    由于备份域的存在,使得RTC核具有了完全独立于APB1接口的特性,也因此对RTC寄存器的访问要遵守一定的规则.
    系统复位后,禁止访问后备寄存器和RCT,防止对后卫区域(BKP)的意外写操作.(执行以下操作使能对后备寄存器好RTC的访问):
    1,设置RCC_APB1ENR寄存器的PWREN和BKPEN位来使能电源和后备接口时钟.
    2,设置PWR_CR寄存器的DBP位使能对后备寄存器和RTC的访问.
    设置为可访问后,在第一次通过APB1接口访问RTC时,必须等待APB1与RTC外设同步,确保被读取出来的RTC寄存器值是正确的,如果在同步之后,一直没有关闭APB1的RTC外设接口,就不需要再次同步了.
    如果内核要对RTC寄存器进行任何的写操作,在内核发出写指令后,RTC模块在3个RTCCLK时钟之后,才开始正式的写RTC寄存器操作.我们知道RTCCLK的频率比内核主频低得多,所以必须要检查RTC关闭操作标志位RTOFF当这个标志被置1时,写操作才正式完成.
    (以上操作在STM32库里面都有库函数,不需要具体的查阅寄存器~~~~)

    UNIX时间戳:
    假如从现在起,把计数器RTC_CNT的计数值置0,然后每秒加1,RTC_CNT什么时候会溢出? RTC_CNT是一个32位寄存器,可存储的最大值为(2^32-1),这样的话就是在2^32秒之后溢出,大概换算为:
    Time = 2^32/365/24/60/60大约等于136年
    假如某个时刻读取到计数器的数值为X = 60*60*24*2(2天),又知道计数器是在2016年1月1日的0时0分0秒置0的,那么根据计数器的这个相对时间数值,可以计算得到这个时刻是2016年1月3日的0时0分0秒了,而计数器会在(2016+136)年左右溢出.(如果我们穿越回到2016年1月1日,如果还在使用这个计数器提供事件的话就会出问题啦.).
    定时器被置0的这个事件被称为计时元年,相对计时元年经过的秒数称为时间戳.

    PS:
    大多数操作系统都是利用时间戳和计时元年来计算当前时间的,而这个时间戳和计时元年大家都取了同一个标准——UNIX时间戳和UNIX计时元年.UNIX 计时元年被设置为格林威治时间1970年1月1日0时0分0秒,大概是为了纪念UNIX的诞生吧.而UNIX时间戳即为当前时间相对于UNIX计时元年经过的秒数.在这个计时系统中,使用的是有符号的32位整型变量来保存UNIX时间戳的,即实际可用计数位数比我们上面例子中的少了一位,少了这一位,UNIX 计时元年也相对提前了,这个计时方法在2038年1月19日03时14分07秒将会发生溢出.这个时间离我们并不远,UNIX时间戳被广泛应用到各种系统中,溢出可能会导致系统发生严重错误,差不多到这个时候,记得注意这个问题呀.

    实例分析:
    利用RTC提供北京时间:
    RTC外设这个连续计数的计数器,在相应软件配置下,可提供时钟日历的功能,修改计数器的值则可以重新设置系统当前的时间和日期.而 由于它的时钟配置系统(RCC_BDCR 寄存器)是在备份域,在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,利用它,可以实现实时时钟的功能.

    main函数:
    struct rtc_time systmtime;
    int main(void)
    {
    /串口配置/
    USART1_Config();
    /配置RTC秒中断优先级/
    NVIC_Configuration();
    //RTC检测及配置
    RTC_CheckAndConfig(&systmtime);
    //刷新时间
    Time_Show(&systmtime);
    }
    main函数流程:
    1,用到了串口,配置好串口(代码和之前的例程一样);
    2,配置RTC秒中断优先级,这里设置主优先级为1,次优先级为0(只用到一个RTC,中断随便写都可以).(代码和之前的中断例程相似,只不过中断通道不一样,这里使用的中断通道是RTC_IRQn);
    3,查看RTC外设是否在本次VDD上电前被配置过,如果没有被配置过,则需要输入当前时间,重新初始化RTC和配置时间;
    4,配置好RTC后,根据秒中断设置的标志位,每隔1秒向终端更新一次;

    事件管理结构体 rtc_time
    struct rtc_time
    {
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    }
    这个类型的结构体有时,分,秒,日,月,年及星期7个成员.当需要给RTC的计时器重新配置时间时(更改时间戳),肯定不会询问用户现在距离UNIX计时元年过了多少秒,而是向用户询问现在的公元纪年,以及所在时区的事件.根据RTC计时器向用户输出时间.
    这就是 rtc_time 这个结构体的作用,配置RTC时,保存用户输入的时间,其它函数通过它求出UNIX时间戳,写入RTC,RTC正常运行后,需要输出时间时,其它函数通过RTC获取UNIX时间戳,转化成用友好的时间表示方式保存在这个结构体上.

    PS:
    起始在C语言标准库ANSI C中,也有类似的结构体所以 struct tm,位于标准的time.h文件中,转化函数是mktime()和localtime(),分别把tm结构体成员转化成时间戳和用时间戳转化成结构体成员.

    检查RTC RTC_CheckAndConfig()

    void RTC_CheckAndConfig(struct rtc_time *tm)
    {
    /检查备份寄存器BKP_DR1,内容不为0xA5A5,则需要重新配置时间并且询问用户调整时间/
    if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
    {
    printf(“\r\n\r\n RTC not yet configured….”);
    /* RTC 配置 */
    RTC_Configuration();
    printf(“\r\n\r\n RTC configured….”);
    /* 用户输入时间*/
    Time_Adjust(tm);
    /再往备份寄存器BKP_DR1写入0xA5A5/
    BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
    }
    /启动无需设置新时钟/
    else
    {
    /检查是否掉电重启/
    if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
    {
    printf(“\r\n\r\n Power On Reset occurred….”);
    }
    /检查是否Reset复位/
    else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
    {
    printf(“\r\n\r\n External Reset occurred….”);
    }
    printf(“\r\n No need to configure RTC….”);
    /等待寄存器同步/
    RTC_WaitForSynchro();
    /允许RTC秒中断/
    RTC_ITConfig(RTC_IT_SEC, ENABLE);
    /等待上次RTC寄存器写操作完成/
    RTC_WaitForLastTask();
    }
    /定义了时钟输出宏,则配置校正时钟输出到 PC13,用于RTC时钟频率的校准或调整时间补偿/
    #ifdef RTCClockOutput_Enable
    /使能PWR和BKP的时钟/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    /允许访问BKP备份域/
    PWR_BackupAccessCmd(ENABLE);
    /输出64分频时钟/
    BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
    #endif
    RCC_ClearFlag();
    }
    if语句调用BKP_ReadBackupRegister()读取RTC备份域寄存器里面的值,判断备份寄存器里面的是否正确,根据后面代码,如果配置成功,会向备份域寄存器写入数值0xA5A5.
    (这个数值在VDD掉电后仍然会保存,如果VBAT也掉电,那么备份域,RTC所有寄存器将被复位,这时这个寄存器的值就不会等于0xA5A5了,RTC的计数器的值也是无效的.
    简单的说,就是写入的这个数值用作标志RTC是否从未被配置或配置是否已经失效,然后写入任何数值到任何一个备份域寄存器,只要检查的时候与写入值匹配就行了)

    RTC未被配置或者配置已经失效的情况:
    1,如果RTC从未被配置或者配置已经失效(备份域寄存器写入值等于0xA5A5)这两种情况其中一种为真的话,则调用RTC_Configuration()来初始化RTC,配置RTC外设的控制参数,时钟分频等,并往电脑的超级终端打印出相应的调试信息;
    2,初始化好RTC之后,调用函数 Time_Adjust() 让用户键入(通过超级终端输入)时间值;
    3,输入时间值后,Time_Adjust() 函数把用户输入的北京时间转化为UNIX时间戳,并把这个UNIX时间戳写入到RTC外设的计数寄存器RTC_CNT.接着RTC外设在这个时间戳的基础上,每秒对RTC_CNT加1,RTC时钟就运行起来了,并且在VDD掉电还运行,以后需要知道时间就直接读取RTC的计时值,就可以计算出时间了;
    4,设置好时间后,调用BKP_WriteBackupRegister()把0xA5A5这个值写入备份域寄存器,作为配置成功的标志;

    确认RTC曾经被配置过的情况:
    1,调用RCC_GetFlagStatus检测是上电复位还是按键复位,根据不同的复位情况在超级终端中打印出不同的调试信息(两种复位都不需要重新设置RTC里面的时间值);
    2,调用RTC_WaitForSynchro等待APB1接口与RTC外设同步,上电后第一次通过APB1接口访问RTC时必须要等待同步;
    3,同步完成后调用RTC_ITConfig()使能RTC外设的秒中断(使能RTC的秒中断是一个对RTC外设寄存器的写操作);
    4,进行写操作以后,必须调用RTC_WaitForLastTask()来等待,确保写操作完成;

    在下面有一个条件编译选项询问是否需要output RTCCLK/64 on Tamper pin,这是RTC的时钟输出配置,在rtc的头文件定义 RTCClockOutput_Enable这个宏,PC13引脚会输出RTCCLK的64分频时钟,主要是用于RTC时钟频率的校准或调整时间补偿.
    (如果需要用到这个时钟信号的话,只需要在头文件定义RTCClockOutput_Enable这个宏就行了,不要定义为0值就行了~~~~)

    初始化RTC RTC_Configuration():
    void RTC_Configuration(void)
    {
    /使能PWR和BKP时钟/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    /对备份域进行软件复位/
    PWR_BackupAccessCmd(ENABLE);
    /对备份域进行软件复位/
    BKP_DeInit();
    /* 使能低速外部时钟 LSE */
    RCC_LSEConfig(RCC_LSE_ON);
    /* 等待LSE起振稳定 */
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
    {}
    /* 选择LSE作为 RTC 外设的时钟*/
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    /* 使能RTC时钟 */
    RCC_RTCCLKCmd(ENABLE);
    /* 等待RTC寄存器与APB1同步*/
    RTC_WaitForSynchro();
    /* 等待对RTC的写操作完成*/
    RTC_WaitForLastTask();
    /* 使能RTC秒中断 */
    RTC_ITConfig(RTC_IT_SEC, ENABLE);
    /* 等待对RTC的写操作完成 */
    RTC_WaitForLastTask();
    /* 设置RT 时钟分频: 使RTC定时周期为1秒 */
    RTC_SetPrescaler(32767);
    /* RTC 周期 = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */
    /等待对RTC的写操作完成 /
    RTC_WaitForLastTask();
    }
    在这个初始化函数里,没有见到熟悉的初始化结构体,对RTC的每一个初始化参数都是使用相应的库函数来配置的.RTC作为备份域的一份子,在访问前首先要使能备份域、电源管理外设的时钟,设置备份域访问权限,作为定时器,初始化时必须要选择好时钟来源,时钟分频.

    时间调节Time_Adjust():
    void Time_Adjust(struct rtc_time *tm)
    {
    /* 等待前面可能的 RTC 写操作完成 */
    RTC_WaitForLastTask();
    /* 利用串口,在终端向用户询问当前北京时间(年月日时分秒),
    写入到 rtc_time 型结构体 */
    Time_Regulate(tm);
    /* 计算输入的日期是星期几,把rtc_time型结构体填充完整 */
    GregorianDay(tm);
    /* 根据输入日期,计算出 UNIX 时间戳,修改当前 RTC 计数寄存器内容*/
    RTC_SetCounter(mktimev(tm));
    /* 等待 RTC 写操作完成 */
    RTC_WaitForLastTask();
    }
    这里流程就是使用Time_Regulate()从终端获取当前北京时间,然后根据用户的输入,调用函数mktimev()根据用户输入的年,月,日,时,.分,秒数据,计算出相应的UNIX时间戳,最后调用库函数RTC_SetCounter()把这个UNIX时间戳写入到计数器RTC_CNT,RTC就正式运行了.

    获取时间Time_Regulate():
    void Time_Reglate(struct rtc_time *tm)
    {
    u32 Tmp_YY = 0xFF, Tmp_MM = 0xFF, Tmp_DD = 0xFF, Tmp_HH =0xFF, Tmp_MI = 0xFF, Tmp_SS = 0xFF;

    printf("\r\n==========Time Settings==================");
    
    printf("\r\n 请输入年份(Please Set Years): 20");
    while (Tmp_YY == 0xFF)
    {
    Tmp_YY = USART_Scanf(99);
    }
    printf("\n\r 年份被设置为: 20%0.2d\n\r", Tmp_YY);
    tm->tm_year = Tmp_YY+2000;
    
    Tmp_MM = 0xFF;
    printf("\r\n 请输入月份(Please Set Months): ");
    while (Tmp_MM == 0xFF)
    {
    Tmp_MM = USART_Scanf(12);
    }
    printf("\n\r 月份被设置为: %d\n\r", Tmp_MM);
    tm->tm_mon= Tmp_MM;
    
    Tmp_DD = 0xFF;
    printf("\r\n 请输入日期(Please Set Dates): ");
    while (Tmp_DD == 0xFF)
    {
    Tmp_DD = USART_Scanf(31);
    }
    printf("\n\r 日期被设置为: %d\n\r", Tmp_DD);
    tm->tm_mday= Tmp_DD;
    
    Tmp_HH = 0xFF;
    printf("\r\n 请输入时钟(Please Set Hours): ");
    while (Tmp_HH == 0xFF)
    {
    Tmp_HH = USART_Scanf(23);
    }
    printf("\n\r 时钟被设置为: %d\n\r", Tmp_HH );
    tm->tm_hour= Tmp_HH;
    
    Tmp_MI = 0xFF;
    printf("\r\n 请输入分钟(Please Set Minutes): ");
    while (Tmp_MI == 0xFF)
    {
    Tmp_MI = USART_Scanf(59);
    }
    printf("\n\r 分钟被设置为: %d\n\r", Tmp_MI);
    tm->tm_min= Tmp_MI;
    
    Tmp_SS = 0xFF;
    printf("\r\n 请输入秒钟(Please Set Seconds): ");
    while (Tmp_SS == 0xFF)
    {
    Tmp_SS = USART_Scanf(59);
    }
    printf("\n\r 秒钟被设置为: %d\n\r", Tmp_SS);
    tm->tm_sec= Tmp_SS; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    }
    这里就是在里面从终端获取用户输入的时间,要留意的是,从终端输入的ASCII码,而不是实际数值(在USART_Scanf里面做处理)

    PS:这里补上USART_Scanf()的代码,之前串口篇的时候好像没有附上
    static uint8_t USART_Scanf(uint32_t value)
    {
    uint32_t index = 0;
    uint32_t tmp[2] = {0, 0};
    while (index < 2)
    {
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ==RESET)
    {}

        tmp[index++] = (USART_ReceiveData(USART1));
        /*数字0到9的ASCII码为0x30至0x39*/
        if((tmp[index - 1] < 0x30) || (tmp[index -1] > 0x39))
        {
            printf("\n\rPlease enter valid number between 0 and 9 -->: ")
            index--;
        }
    }
    /* 计算输入字符的 ASCII 码转换为数字*/
    index = (tmp[1] - 0x30) + ((tmp[0] - 0x30) * 10);
    
    if (index > value)
    {
        printf("\n\rPlease enter valid number between 0 and %d", value);
        return 0xFF;
    }
    return index;   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    }

    计算UNIX时间戳mktimev():
    从用户端获取了北京时间后,就可以用它换成 UNIX 时间戳了,但不能忽略一个重要的问题——时差.UNIX时间戳的计时元年是以标准时间(GMT 时区)为准的,而北京时间为 GMT+8,即时差为+8小时.为了保证我们写入到RTC_CNT的是标准的UNIX时间戳(主要是为了兼容),以北京时间转化出的秒数要减去8*60*60才是标准的UNIX时间戳.
    u32 mktimev(struct rtc_time *tm)
    {
    if (0 >= (int) (tm->tm_mon -= 2))
    {
    tm->tm_mon += 12;
    tm->tm_year -= 1;
    }
    /计算出输入的北京时间的一共的秒数/
    return((( (u32)(tm->tm_year/4 - tm->tm_year/100 + tm->tm_year/400 + 367*tm->tm_mon/12 + tm->tm_mday)
    + tm->tm_year*365 - 719499)*24 + tm->tm_hour)*60 + tm->tm_min)*60 + tm->tm_sec-8*60*60;
    /8*60*60把输入的北京时间转换为标准时间在写入计时器中,确保计时器的数据为标准UNIX时间戳/
    }
    8*60*60把输入的北京事件转换为标准事件在写入计时器中,确保计时器的数据为标准UNIX时间戳,如果向使用其他时区,则根据不同哟的时区修改这个值.
    返回值最终被写入到RTC_CNT计数器中RTC_SetCounter(mktimev(tm));

    输出时间到终端Time_Show():
    void Time_Show(struct rtc_time *tm)
    {
    while (1)
    {
    /每个1s/
    if(TimeDisplay == 1)
    {
    /显示时间/
    Time_Display(RTC_GetCounter(),tm);
    TimeDisplay = 0;
    }
    }
    }
    TimeDisplay是RTC秒中断标志,RTC的秒中断被触发后,进入中断服务函数,把这个变量 TimeDisplay置1.这个函数是死循环检查这个标志,变为1时,调用Time_Display()显示最新时间,实现每隔1秒向终端更新一次时间,更新完后再把 TimeDisplay置0,等待下次秒中断.

    RTC秒中断服务函数:
    void RTC_IRQHandler(void)
    {
    if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
    {
    /* 清除秒中断标志 */
    RTC_ClearITPendingBit(RTC_IT_SEC);
    /* 把标志位置 1 */
    TimeDisplay = 1;
    /* 等待写操作完成 */
    RTC_WaitForLastTask();
    }
    }
    在这个函数中并没有任何对RTC_CNT的操作,如果VDD掉电,RTC是无法触发秒中断的,所以想利用秒中断的方案实现实时时钟是不现实的,秒中断最适合用在类似本例程的触发显示的时间更新场合,而不是用于计数.

    显示时间Time_Display():
    void Time_Display(uint32_t TimeVar,struct rct_time *tm)
    {
    static uint32_t FirstDisplay = 1;
    uint32_t BJ_TimeVar;
    uint8_t str[15]; // 字符串暂存

    /* 把标准时间转换为北京时间*/
    BJ_TimeVar =TimeVar + 8*60*60;
    /*利用时间戳转换为北京时间*/
    to_tm(BJ_TimeVar, tm);
    
    if((!tm->tm_hour && !tm->tm_min && !tm->tm_sec) || (FirstDisplay))
    {
        GetChinaCalendar((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str);
    
        printf("\r\n\r\n 今天农历:%0.2d%0.2d,%0.2d,%0.2d", str[0], str[1], str[2], str[3]);
    
        GetChinaCalendarStr((u16)tm->tm_year,(u8)tm->tm_mon,(u8)tm->tm_mday,str);
        printf(" %s", str);
    
        if(GetJieQiStr((u16)tm->tm_year, (u8)tm->tm_mon, (u8)tm->tm_mday, str))
        {
            printf(" %s\n\r", str);
        }
        FirstDisplay = 0;
    }
    printf("\r UNIX 时间戳 = %d ,当前时间为: %d 年(%s 年) %d%d日 (星期%s) %0.2d:%0.2d:%0.2d",TimeVar,tm->tm_year, zodiac_sign[(tm->tm_year-3)%12], tm->tm_mon, tm->tm_mday,WEEK_STR[tm->tm_wday], tm->tm_hour,tm->tm_min, tm->tm_sec);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    }
    这里的第一个输入参数为UNIX时间戳,在Time_Show()调用的时候,利用库函数RTC_GetCounter()读取了RTC_CNT的当前数值,并把这个计数值作为Time_Dispaly()的输入参数.
    根据配置,RTC_CNT的计数值是标准时间GMT的UNIX时间戳,为了计算北京时间,在使用RTC_CNT计数值转换北京时间时,要加上时差(BJ_TimeVar =TimeVar + 8*60*60;).之后,把这个变量 BJ_TimeVar作为函数 to_tm()的输入参数,把时间戳转换成年,月,日,时,分,秒的格式,并保存到时间结构体中.
    (to_tm()(纯算法)和GetChinaCalendar()这里就不展开了,需要的话可以留言我会发送给你)

    PS:
    如果要使用普通的51芯片实现实时时钟,需要借助时钟芯片,DS1302或DS12C887,在STM32里面只要用到一个定时器就搞掂了!!!

    原文章地址:https://blog.csdn.net/linzhihan7410/article/details/52195857

    展开全文
  • RTC实时时钟特征与原理

    千次阅读 2019-09-01 20:10:54
    RTC模块拥有一个连续计数的计数器,在相应的软件配置下,可以提供时钟日历的功能。修改计数器的值可以重新设置当前时间和日期。 RTC模块和时钟配置系统(RCC_BDCR寄存器)是在后备区域,即在系统复位或从待机模式唤醒...
  • 【STM32】RTC实时时钟,步骤超细详解,一文看懂RTC

    千次阅读 多人点赞 2020-04-24 23:01:42
    RTC (Real Time Clock):实时时钟 RTC是个独立的定时器。RTC模块拥有一个连续计数的计数器,在相应的软件配置下,可以提供时钟日历的功能。修改计数器的值可以重新设置当前时间和日期 RTC还包含用于管理低功耗模式的...
  • RTC实时时钟(LCD显示)

    千次阅读 2018-12-09 15:41:50
    实时时钟(RTC)是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。 RTC模块和时钟配置系统(RCC_BDCR寄存器)处...
  • RTC(Real-TimeClock)实时时钟为操作系统提供了一个可靠的时间,并且在断电的情况下,RTC实时时钟也可以通过电池供电,一直运行下去。本文以飞凌嵌入式的OK4418-C为例,介绍在Android实时时钟框架。 整体流程介绍 ...
  • 《STM32中文参考手册V10》-第16章 实时时钟(RTC)   RTC实时时钟 RTC实时时钟简介 实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值...
  • RTC(Real-Time Clock) 实时时钟。RTC是集成电路,通常称为时钟芯片。在一个嵌入式系统中,通常采用RTC来提供可靠的系统时间,包括时分秒和年月日等,而且要求在系统处于关机状态下它也能正常工作(通常采用后备电池...
  • 实时时钟:RTC时钟,用于提供年、月、日、时、分、秒和星期等的实时时间信息,由后备电池供电,当你晚上关闭系统和早上开启系统时,RTC仍然会保持正确的时间和日期。 系统时钟:是一个存储于系统内存中的逻辑时钟...
  • 系统时钟,硬件时钟(后备时钟,实时时钟),网络时钟 辨析 1. 系统时钟 系统时钟即为我们看到的操作系统上显示的时间。 系统时钟在电脑开机的时候进行初始化,通过对硬件时钟的“拷贝”完成初始化 注意:这里...
  • STM32-(27):RTC实时时钟

    2019-05-30 20:24:16
    实时时钟的缩写是RTC(Real_Time Clock) 实时时钟是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期 RTC由两个...
  • SOC RTC时钟——为什么实时时钟的晶振都是32.768KHZ呢? 0. 实时时钟(RTC,Real Time Clock) 实时时钟的缩写是RTC(Real_Time Clock)。RTC 是集成电路,通常称为时钟芯片 1. 晶振 晶振一般叫做晶体谐振器,是一种...
  • 1、大多数单片机都只有系统时钟一个。就是CPU的各节拍工作时序的驱动源了。这个频率一般为几MHz。速度比较快,其目的无非是让单片机快点干活。...2、实时时钟,是单片机计时的时钟或独立的可被单片机访问的时钟。它
  • 关于RTC(实时时钟

    2020-05-13 11:06:45
    它为人们提供精确的实时时间或为电子系统提供精确的时间基准,目前实时时钟大多采用精度较高的晶体振荡器作为时钟源。 RTC的晶振:(硬件结构) 任何实时时钟的核心都是晶振,晶振频率为32768 Hz 。它为分频计数器...
  • STM32 RTC实时时钟

    万次阅读 2016-10-06 10:09:05
    今天是周天,哈尔滨阴天转阵雨。。博主原本准备今天去逛街的。原因你懂的→_→ 接下来进入今天的正题(博主用的是战舰STM32库函数版):博主今天将会和大家讨论两个知识点: ... 1、STM32的实时时钟(RTC)是
  • C语言经典程序设计(源代码)之实时时钟1111
  • JSP做的实时时钟 简单的 JSP做的实时时钟 简单的 希望对大家有帮助
  • STM32学习笔记一一RTC实时时钟

    千次阅读 2020-07-04 11:56:59
    STM32 的实时时钟(RTC)是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。 RTC 模块和时钟配置系统 ...
1 2 3 4 5 ... 20
收藏数 47,626
精华内容 19,050
关键字:

实时时钟