历史上的今天
返回首页

历史上的今天

今天是:2025年06月25日(星期三)

正在发生

2018年06月25日 | 关于STM32驱动DS1302实时时钟的一点思考

2018-06-25 来源:eefocus

之前用51驱动过DS1302,没用多久就输出了正确的时间。当时以为这块芯片其实没啥,很简单。但是现在用STM32做项目,用到同样的芯片,以为这有何难,只要把那个程序拿过来复制黏贴改一下IO设置不就行了?但是事情远没有想想的那么简单。               


经过3天的挣扎,现在才知道当时自己是多么天真。


关于DS1302的基本操作可以看这里:http://www.cnblogs.com/qsyll0916/p/7712695.html


好了,废话少说了,进入正题。


首先DS1302读写方式属于3线SPI。CE、SCK、IO。其中IO口属于双向IO口,我们读写都要经过这个IO口。在用51开发的时候,因外他是准双向IO,不需要我们额外关心他的输入输出设置。需要输出的时候直接写            P0^1 = 1;   


需要检测外部输入的时候直接写       if(P0^1 == 1)   ,


都很方便,但是方便的同时带来的是读写速度上的限制。那么在STM32中,每个IO口都有8种输出模式。复杂的同时也意味着每一种模式都是专门定制的,带来了速度上的优势。所以在移植这个程序的时候,就需要注意这个双向IO的设置问题。一开始我也不是很懂,各种百度查资料,各种问人。最后才知道有两种方式可以实现双向的IO读写设置。


第一:


#define DS1302_DATIN        PBin(6)  

#define DS1302_DATOUT       PBout(6)


#define DS1302_DAT_INPUT()     {GPIOB->CRL&= 0XF0FFFFFF;GPIOB->CRL|= 8<<24;}  //设置成上拉或者下拉输入模式,需要外接上拉电阻

#define DS1302_DAT_OUTPUT()    {GPIOB->CRL&= 0XF0FFFFFF;GPIOB->CRL|= 3<<24;}  //设置成最大50M的通用推挽输出

通过简单的寄存器操作,可以实现输入输出的快速切换。需要在端口处接上拉电阻。


第二:



    GPIO_InitTypeDef GPIO_InitStruct;  

    

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  


    GPIO_InitStruct.GPIO_Pin = DS1302_IO_PIN;         //这里最好设成开漏,当然也可以普通推挽,但是需要后面一直切换输入输出模式

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;     //开漏输出,需要接上拉,不需要切换输入输出了。

    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    //输出速度50MHz  

    GPIO_Init(DS1302_PORT, &GPIO_InitStruct);


把IO配置成开漏输出模式。必须外接上拉电阻,不然读出的全是低电平。这样就不用一直切换输入输出模式,可以直接像51那样,直接使用。


双向IO的配置基本上就是上面所说的两种情况,但是可能还有其他方式,但是我目前就只知道这个方式,后面学习了在补充。


配置完IO之后就开始对IO进行操作,从而读写DS1302,获取实时时间。读写的时候特别需要注意。网上很多教程都没有提到这一点,估计是太基础了吧。但是这个小小的问题却困扰了我很长时间。就是在进行对DS1302的写命令和数据的操作的时候,我们需要按照从低位到高位依次发送数据。发送数据的时候:


在51里面我们经常会这样写:{    SCIO = byte_1 & 0x01;  byte_1 >> = 1;    }


那么在32中我们可以这样写吗?:   {    PBout(6) =  byte_1 & 0x01;  byte_1 >> = 1;    }


绝对不行。虽然看上去没有什么问题,也不会报错,但是却就是不同正确通信。很气人。


原因是PBout(6)这样的操作是属于STM32的位带操作。但是在CM3中不允许位带操作赋值除0和1以外的数。


也就是说上面那种操作方式是给  PBout(6) 赋值2,3,4,之类的数,但是stm32却不能理解这是什么意思。因为它只认识0和1!!!


所以我们可以简单的这样处理:



        if((byte_1 & t) != 0)     //之前的问题出在这里,32的位带操作不能赋值0和1之外的值。

        {

            DS1302_DATOUT = 1;

        }

        else

        {

            DS1302_DATOUT = 0;

        }


这样就可以正常通信了。但是如果不能像上面那样处理的话,现象是一直读出85这个数。关于读出85这个数,除了上面我提到的这种特殊情况,网上还是有很多人有这方面的经验的,我总结了一下,大致是下面这几种情况:


1、读取完时间后没有把DATA_IO引脚拉低。导致显示问号和85等一些乱七八糟的东西。但是我加了。

2、电压不够,小于4.6V。但是这个网上有争议,我接的是5V,实测4.8V,应该没问题。

3、没有接上拉电阻。我只在需要双向IO的地方加了上拉电阻,利用的是板子上预留的IIC的SDA,上面有一个4.7K的上拉,我把IO接在了这里。应该也没问题

4、仿真时序不对。但是我之前用这个时序在51上面实现过一样的功能,现在移植到32上应该也没什么问题啊,延时时间也仿真了,严格按照1us的延时仿真的。


但是在我的实验过程中还是发现了很多其他的现象,再次也记录下来,防止有人遇到相同的问题,就能不浪费那么多时间。


1、确实要注意DS1302的电压,最好不要用STM32开发板上面的3.3V,反正我是没做出来。如果用外部电源给DS1302供电的话,需要将外部电源和开发板共地,不然读出全是85.


2、DS1302如果需要修改时间。需要把初始化函数里面的上电保护去掉,再次下载重置的时间,然后再把上电保护那段给添加上去,防止复位后时间被重置。


目前我的问题就是这么多。在一一解决了上述问题之后,就能准确的读取时间了,实测一天24H误差不超过2s,还是很准的了。


好了,下面就是源程序了。


首先是DS1302的头文件,主要是一些位带操作和预定义



#ifndef __DS1302_H

#define __DS1302_H


#include

#include "hardware.h"


//相对应的IO口配置

#define DS1302_PORT         GPIOB


#define DS1302_SCK_PIN         GPIO_Pin_7        //时钟

#define DS1302_IO_PIN         GPIO_Pin_6    //双向IO口,

#define DS1302_CE_PIN         GPIO_Pin_5   //片选使能,当需要读写的时候,置高位


#define DS1302_SCK          PBout(7)  //位带操作,可直接给高低电平,但是切记不能给0.1之外的数。切记

#define DS1302_CE           PBout(5)

#define DS1302_DATIN        PBin(6)  

#define DS1302_DATOUT       PBout(6)

//存放时间

typedef struct _time{ 


    u8 second;

    u8 minute;

    u8 hour;

    u8 date;

    u8 month;

    u8 week;

    u8 year;


}my_time;

void DS1302_Init(void);

void ds1302_readtime(void);

void display_real_time(void);  //显示实时时间


#endif


DS1302操作源文件:



#include "ds1302.h"

#include "spi.h"


//READ_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};//读取时间的命令地址,已经通过读写操作来直接实现这些地址

//WRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};//写时间的命令地址


my_time TIME = {0};  //显示时间的结构体

u8 init_time[] = {0x00,0x28,0x21,0x27,0x12,0x06,0x17}; //初始化时间:秒 分 时 日 月 周 年



static void ds1302_gpio_init(void);

static void ds1302_writebyte(u8 byte_1);//写一个字节; byte是保留字,不能作为变量

static void ds1302_writedata(u8 addr,u8 data_);//给某地址写数据,data是c51内部的关键字,表示将变量定义在数据存储区,故此处用data_;

static u8 ds1302_readbyte(void);//读一个字节

static u8 ds1302_readdata(u8 addr);//读取某寄存器数据;

static void DS1302_delay_us(u16 time);  //简单延时1us



//基本IO设置

static void ds1302_gpio_init(void)

{

    GPIO_InitTypeDef GPIO_InitStruct;  

    

    //开启GPIOD的时钟  

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  

    

    //设置GPIO的基本参数  

    GPIO_InitStruct.GPIO_Pin = DS1302_SCK_PIN | DS1302_CE_PIN ;  

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;    //这两个普通端口设为推挽输出  

    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    //输出速度50MHz  

    GPIO_Init(DS1302_PORT, &GPIO_InitStruct);  


    GPIO_InitStruct.GPIO_Pin = DS1302_IO_PIN;         //这里最好设成开漏,当然也可以普通推挽,但是需要后面一直切换输入输出模式

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;     //开漏输出,需要接上拉,不需要切换输入输出了。

    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    //输出速度50MHz  

    GPIO_Init(DS1302_PORT, &GPIO_InitStruct);


}


//写一个字节

//数据和地址都是从最低位开始传输的

static void ds1302_writebyte(u8 byte_1)

{

    u8 i = 0;

    u8 t = 0x01;

    

    for(i = 0;i<8;i++)

    {

        if((byte_1 & t) != 0)     //之前的问题出在这里,32的位带操作不能赋值0和1之外的值。

        {

            DS1302_DATOUT = 1;

        }

        else

        {

            DS1302_DATOUT = 0;

        }

        

        DS1302_delay_us(2);

        DS1302_SCK = 1;  //上升沿写入

        DS1302_delay_us(2);

        DS1302_SCK = 0; 

        DS1302_delay_us(2);

        

        t<<= 1;

    }

    DS1302_DATOUT = 1;      //释放IO,后面读取的话会准确很多

    DS1302_delay_us(2);     //因为如果写完之后IO被置了低电平,开漏输出模式下读取的时候会有影响,最好先拉高,再读取

}


//地址写数据

static void ds1302_writedata(u8 addr,u8 data_)

{    

    DS1302_CE = 0;        DS1302_delay_us(2);    

    DS1302_SCK = 0;        DS1302_delay_us(2);    

    DS1302_CE = 1;        DS1302_delay_us(2);    //使能片选信号

    

    ds1302_writebyte((addr<<1)|0x80);    //方便后面写入,转化之后是地址寄存器的值,

    ds1302_writebyte(data_);

    DS1302_CE = 0;        DS1302_delay_us(2);//传送数据结束,失能片选

    DS1302_SCK = 0;     DS1302_delay_us(2);//拉低,准备下一次写数据

}


//读取一个字节,上升沿读取

static u8 ds1302_readbyte(void)

{

    u8 i = 0;

    u8 data_ = 0;

    

//因为上面已经把端口设置为开漏,电路外部接了山拉电阻,可以不切换输入输出模式,直接使用。

    //    DS1302_DAT_INPUT();  

    

    DS1302_SCK = 0;

    DS1302_delay_us(3);

    for(i=0;i<7;i++)   //这里发现设为8的话输出数据不对,很乱

    {

        if((DS1302_DATIN) == 1) 

        {

            data_ = data_ | 0x80;    //低位在前,逐位读取,刚开始不对,估计是这个的问题

        }

        data_>>= 1;

        DS1302_delay_us(3);

        

        DS1302_SCK = 1;

        DS1302_delay_us(3);

        DS1302_SCK = 0;

        DS1302_delay_us(3);

    }

     return (data_);

}


//读取寄存器的值

static u8 ds1302_readdata(u8 addr)

{

    u8 data_ = 0;


    DS1302_CE = 0;        DS1302_delay_us(2);

    DS1302_SCK = 0;        DS1302_delay_us(2);

    DS1302_CE = 1;        DS1302_delay_us(2);   //读写操作时CE必须为高,切在SCK为低时改变

    

    ds1302_writebyte((addr<<1)|0x81);   //写入读时间的命令

    data_ = ds1302_readbyte(); 

    

    DS1302_SCK = 1;      DS1302_delay_us(2);

    DS1302_CE = 0;        DS1302_delay_us(2);

    DS1302_DATOUT = 0;  DS1302_delay_us(3);  //这里很多人说需要拉低,但是我发现去掉这个也可以显示啊,不过为了保险,还是加上。

    DS1302_DATOUT = 1;  DS1302_delay_us(2);


    return data_;

}


void DS1302_Init(void)

{

     u8 i = 0;

    

    ds1302_gpio_init();  //端口初始化

    

    DS1302_CE = 0;  DS1302_delay_us(2);

    DS1302_SCK = 0; DS1302_delay_us(2);  

    

    i  = ds1302_readdata(0x00);  //读取秒寄存器,


     if((i & 0x80) != 0)//通过判断秒寄存器是否还有数据来决定下次上电的时候是否初始化时间,就是掉电保护

    {

         ds1302_writedata(7,0x00); //撤销写保护,允许写入数据,0x8e,0x00


        for(i = 0;i<7;i++)

        {

            ds1302_writedata(i,init_time[i]);

        }

    }

         ds1302_writedata(7,0x80);//打开写保护功能,防止干扰造成的数据写入。

}


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

void ds1302_readtime(void)   //读取时间

{

      u8 i;

      for(i = 0;i<7;i++)

      {

         init_time[i] = ds1302_readdata(i);

      }

}


static void DS1302_delay_us(u16 time)

{    

   u16 i = 0;  

   while(time--)

   {

      i = 5;  //自己定义

      while(i--);

   }

}


//显示实时时间

void display_real_time(void)

{

    

    ds1302_readtime();   //先获取时间到缓冲区

    

    //BCD码转换ASCII码

    TIME.year =  ((init_time[6]&0x70)>>4)*10 + (init_time[6]&0x0f); //高三位加低四位

    TIME.month = ((init_time[4]&0x70)>>4)*10 + (init_time[4]&0x0f);

    TIME.date =  ((init_time[3]&0x70)>>4)*10 + (init_time[3]&0x0f);

    TIME.week =  ((init_time[5]&0x70)>>4)*10 + (init_time[5]&0x0f);

    TIME.hour =  ((init_time[2]&0x70)>>4)*10 + (init_time[2]&0x0f);

    TIME.minute = ((init_time[1]&0x70)>>4)*10 + (init_time[1]&0x0f);

    TIME.second = ((init_time[0]&0x70)>>4)*10 + (init_time[0]&0x0f);

       

    OLED_ShowNum(48,0,TIME.hour,2,16);

    

    OLED_ShowChar(64,0,':');


    OLED_ShowNum(72,0,TIME.minute,2,16);

    

    OLED_ShowChar(88,0,':');

    

    OLED_ShowNum(96,0,TIME.second,2,16);

    

}


推荐阅读

史海拾趣

富士康(FOXCONN)公司的发展小趣事

面对日新月异的半导体技术,FMS始终保持对创新的执着追求。公司不断加大研发投入,致力于新产品的研发与旧产品的升级换代。同时,FMS还积极关注行业动态,紧跟市场趋势,不断调整和优化产品结构,以满足客户日益多样化的需求。展望未来,FMS将继续秉承“新速勤俭、创造利润、热诚信义、前瞻未来”的经营理念,致力于成为全球半导体供应链中的佼佼者,为客户提供更加优质的产品和服务。

ATO SOLUTION公司的发展小趣事

面对日益激烈的市场竞争,ATO SOLUTION公司不断创新营销策略,提升品牌知名度和影响力。公司利用互联网和社交媒体等新媒体平台,开展线上宣传和推广活动;同时,还积极参加各类行业展会和论坛,与业界同行和潜在客户进行深入交流和合作。这些创新的营销策略有效地提升了公司的市场影响力,为公司的业务拓展提供了有力支持。

FEI Microwave Inc公司的发展小趣事

随着5G技术的商用化,微波通信市场迎来了新的发展机遇。为了满足市场需求,FEI Microwave Inc公司迅速调整研发策略,加大投入力度,成功研发出了一系列针对5G应用的微波通信产品。这些产品以其高性能、高可靠性和高性价比迅速在市场上获得了认可,为FEI Microwave Inc公司赢得了更多的市场份额。

台湾富致(FUZETEC)公司的发展小趣事

自1983年Futaba的VFD产品被福特汽车公司采用后,其在美国三大汽车厂的市场占有率迅速达到100%。这一成就不仅证明了Futaba VFD产品在汽车行业的卓越性能和广泛应用,还推动了其在全球汽车市场上的快速扩张。VFD的抗震性能、良好的温差适应性以及高可靠性,使其在汽车仪表盘和抬头显示器等关键部件中扮演了重要角色。随着新能源汽车的兴起,Futaba的VFD技术也进一步适应市场需求,为新能源汽车提供了更加先进的显示解决方案。

EVER-WAY公司的发展小趣事

EVER-WAY公司成立于XX世纪初,当时电子行业正处于蓬勃发展的阶段。创始人李明看准了市场机遇,决定创立一家专注于电子元器件制造的公司。他带领一支由几名工程师组成的小团队,开始了艰苦的创业之路。初期,公司面临着资金短缺、技术落后和市场竞争激烈等多重挑战。然而,李明和他的团队凭借坚定的信念和不懈的努力,逐渐在市场中站稳脚跟。他们不断优化产品性能,提高生产效率,同时积极拓展国内外市场,逐步扩大了公司的知名度和影响力。

DALLAS公司的发展小趣事

随着移动设备市场的蓬勃发展,Dallas公司敏锐地捕捉到了电池电量管理技术的市场需求。在深入研究和技术攻关后,Dallas成功推出了可编程锂离子电池电量计DS2790。这款产品集成了先进的微控制器、大容量程序和数据存储器以及精密的电池电流、电压和温度测量系统,为定制单节电池电量计的算法提供了优秀的平台。DS2790的推出不仅满足了市场需求,也为Dallas公司在电池管理技术方面赢得了声誉。

问答坊 | AI 解惑

ZigBee谁笑到最后(1)

ZIGBEE主要竞争对手分析 吴线    一ZigBee无线技术一鸣惊人   ZigBee是一种崭新的,专注于低功耗、低成本、低复杂度、低速率的近程无线网络通信技术。也是目前嵌入式应用的一个大热点。   ZigBee的特点主要有以 ...…

查看全部问答>

反激式开关电源RCD吸收电路的设计

反激式开关电源RCD吸收电路的设计…

查看全部问答>

合成的电流矢量怎么回事

三个电流的和不是等于零的吗  ,怎么还有个合成的电流矢量啊,不是等于零吗…

查看全部问答>

帮我看看这个简单的程序

#include #include #define uchar unsigned char #define uint unsigned int void delay_nms(uint n); void main() {   NOP();   PORTD=PORTD|BIT(2);   NOP();   DDRD=DDRD|0x04;   ...…

查看全部问答>

如何实现printf????

各位大侠:    我想在单片机下实现串口打印功能的一个函数PrintfEx 但是要可以想printf一样格式化变量 如: int a = 100; PrintfEx(\"a is %d\\r\\n\",a); 请问如何实现这一功能呢?谢谢!…

查看全部问答>

软件工程专业应届生找不到工作

我是上海一大学软件工程专业的09届应届生,本科,不是很会说话。 在学校里学的是单片机开发之类的(偏软件 C语言),接触过ARM+Wince之类的嵌入式软件的开发。(其实学校里嵌入式软件方面的课基本就没有,都是我凭着兴趣自学的)单片机的水平达到自 ...…

查看全部问答>

用户dll在一个进程中的描述?

请问一个用户的dll在一个进程中是怎么描述? 比如系统dll:ntdll.dll和kenerl32.dll是在peb中ldr里的一个链表ininitializationorderlinks中。 那么用户自己的dll呢? 我想找到一个进程所有的dll的虚拟地址,应该怎么找?…

查看全部问答>

单片机DA芯片的问题

本人工作的第一个工程中的第二个麻烦的问题.....涉及到10位DAC,4路输出(0~10V以及4~20mA)的DA芯片,因为要用高速光耦与MCU隔离,为节省成本用的是串行SPI这样的线少的控制方式,请问有没有正好符合要求且价不高的DA芯片呢?如果没有的话有没有适 ...…

查看全部问答>

学好单片机四大步骤

       要怎么学习单片机呢,如何学好单片机,学好单片机的方法是什么呢,很多刚来到我们卓跃教育的学员都会这么问我们的讲师,现在让我们来简单的总结一下要如何学习好单片机,以及学习单片机的过程,我们一般把单 ...…

查看全部问答>