外设驱动库开发笔记29:DS17887实时时钟驱动
一些时候,在我们的嵌入式产品中需要记录时间,所以我们就需要获取时钟,当然实现的方式多种多样,有的MCU本身就有这一功能,不过精度较低。当我们的应用要求较高时就需要使用专门的实时时钟芯片,如DS17887。在本篇中,我们来实现DS17887的驱动。
1、功能概述
DS17287、DS17487和DS17887(以下简称DS17x87)将石英晶体和锂电源集成到一个24针的DIP封装中。
1.1、硬件设备
DS17x87电源控制电路允许系统由外部激励供电,如键盘或时间和日期(唤醒)报警。PWR输出引脚是由上述事件之一或其中之一触发,并用于打开外部电源。PWR引脚由软件控制,因此当一项工作完成时,便可关掉系统电源。
对于所有设备,月底的日期将自动调整为31天以下的月份,包括闰年的修正日期。它也运行在24小时或12小时的格式与AM/PM指标。一个精确的温度补偿电路监控VCC的状态。如果检测到主电源故障,设备将自动切换到备用电源。DS17x87包括一个VBAUX输入,用于驱动辅助功能。
1.2、寄存器结构
时间和日历信息是通过读取适当的寄存器字节获得的。通过编写适当的寄存器字节来设置或初始化时间、日历和警报。
1.2.1、寄存器地址分配
DS17887包括有12个时间、日期和报警寄存器、控制寄存器A到D,以及仅驻留在bank 1中的两个扩展寄存器。12个时间、日历和警报字节的内容可以是二进制或二进制编码的十进制(BCD)格式。
在控制寄存器B的DM位为0时,12个时间、日历和警报字节的内容以BCD码的格式输出。各寄存器具体排布如下:
在控制寄存器B的DM位为1时,12个时间、日历和警报字节的内容以二进制码的格式输出。各寄存器具体排布如下:
1.2.2、控制寄存器
DS17887有四个控制寄存器(A、B、C和D)同时位于bank 0和bank 1中。这些寄存器在任何时候都是可访问的,甚至在更新周期期间也是如此。
控制寄存器A地址为0x0A,第7位为只读,其它位可读可写。控制寄存器A的结构如下:
UIP是一个可以监视的状态标志。状态为1则表示最新发生了更新。DV2、DV1用于设置时钟,DV0用于决定存储区是bank0还是bank1,为0则是bank0,为1则是bank1。RS3到RS0几位则用于设置速率。
控制寄存器B地址为0x0B,控制寄存器B可读可写。控制寄存器B的结构如下:
SET位为0允许更新,为0禁止更新。PIE为周期型中断控制。AIE为报警中断控制。UIE为更新结束中断使能。SQWE为方波输出使能。DM控制选择存储区(bank0或bank1)。
控制寄存器C地址为0x0C,控制寄存器C为只读。控制寄存器C的结构如下:
控制寄存器C实际上是对控制寄存器B各中断配置为的状态指示。
控制寄存器D地址为0x0D,控制寄存器D为只读。控制寄存器D的结构如下:
控制寄存器D只有VRT位有效,这个位表示连接到VBAT和VBAUX引脚的电池的状态。如果任何一个电源高于内部电压阈值,VRT位将是高的。这个位是不可写的,读的时候应该总是1。如果存在0,则表示内部锂电源耗尽,RTC数据和RAM数据的内容都有问题。
2、驱动设计与实现
在上述介绍中,我们已经清楚了DS17887实时时钟芯片的基本功能及寄存器布置。接下来我们将据此实现器驱动。
2.1、对象定义
我们依然是采用基于对象的操作。所以我们首先需要获得对象,并为这个对象按我们的需要设计驱动。所以在这里我们首先要设计DS17887这个操作对象。
2.1.1、抽象对象类型
我们首先来分析DS17887的特点。DS17887有4个控制寄存器用以配置操作和展示状态,我们将其作为属性标识DS17887当前的状态。DS17887最主要的返回值就是时间数据,我们将其作为属性返回时间数据。我们将控制引脚的控制,寄存器的读写、总线方向设定等作为DS17887对象的操作。由此我们抽象的DS17887对象类型如下:
1 /* 定义DS17887对象类型 */ 2 typedef struct Ds17887Object{ 3 uint8_t ctlReg[4]; //控制寄存器 4 uint16_t dateTime[6]; //读取的系统时间 5 void (*SetCtlPin[6])(DS17887PinValue value); //控制引脚操作 6 void (*WriteByte)(uint16_t data); //写一个字节 7 uint16_t (*ReadByte)(void); //读一个字节 8 void (*SetBusDirection)(DS17887BusDirection direction);//设置总线方向 9 void (*Delayus)(volatile uint32_t nTime); //延时ms操作指针 10 }Ds17887ObjectType;
2.1.2、对象初始化函数
对象必须先初始化才可使用,所以我们还需要设计对象的初始化函数。初始化函数除了为对象属性赋初始值和给操作指定函数指针外,还需要检测参数的合法性以及对硬件设备做必要的配置。基于此我们设计DS17887的初始化函数如下:
1 /*对DS17887进行初始化配置*/ 2 void Ds17887Initialization(Ds17887ObjectType *ds17887, 3 DS17887CtlPinOperation *SetCtlPin, 4 WriteByteToDs17887 WriteByte, 5 ReadByteFromDs17887 ReadByte, 6 Ds17887SetBusDirection SetBusDirection, 7 Ds17887Delayus Delayus) 8 { 9 if((ds17887==NULL)||(SetCtlPin==NULL)||(WriteByte==NULL)||(ReadByte==NULL)||(SetBusDirection==NULL)||(Delayus==NULL)) 10 { 11 return; 12 } 13 14 for(int i=0;i<6;i++) 15 { 16 ds17887->dateTime[0]=0; 17 ds17887->SetCtlPin[i]=SetCtlPin[i]; 18 } 19 ds17887->WriteByte=WriteByte; 20 ds17887->ReadByte=ReadByte; 21 ds17887->SetBusDirection=SetBusDirection; 22 ds17887->Delayus=Delayus; 23 24 /*将ALE、RD与WR复位*/ 25 SetCtlPin[DS17887_ALE](Reset); 26 SetCtlPin[DS17887_WR](Reset); 27 SetCtlPin[DS17887_RD](Reset); 28 29 /*设置寄存器B和A的值,启动DS17887*/ 30 WriteDataToDS17887(ds17887,DS17887_Reg_B,0x06); 31 WriteDataToDS17887(ds17887,DS17887_Reg_A,0x20); 32 33 //读取DS17887的时间 34 GetDateTimeFromDs17887(ds17887); 35 }
2.2、对象操作
我们定义一个对象的目的最终是为了操作这个对象获取我们需要的数据。DS17887实时时钟最基本的操作就是对个寄存器的读写,进而引申为对事实时间的获取及校准。这一节我们就据此来实现DS17887对象的操作函数。
2.2.1、读数据操作
DS17887的读操作时序如下图所示:
我们根据以上时序图来开发DS17887对象的读操作函数如下:
1 /*从DS17887读数据*/ 2 static uint16_t ReadDataFromDS17887(Ds17887ObjectType *ds17887,uint16_t address) 3 { 4 /*将片选信号置位,失能片选*/ 5 ds17887->SetCtlPin[DS17887_CS](Set); 6 /*将RD与WR置位*/ 7 ds17887->SetCtlPin[DS17887_WR](Set); 8 ds17887->SetCtlPin[DS17887_RD](Set); 9 ds17887->Delayus(2); 10 /*置位ALE*/ 11 ds17887->SetCtlPin[DS17887_ALE](Set); 12 13 /*将地址数据总线的模式改为输出*/ 14 ds17887->SetBusDirection(Out); 15 /*写寄存器地址*/ 16 ds17887->WriteByte(address); 17 /*将片选信号置位,使能片选*/ 18 ds17887->SetCtlPin[DS17887_CS](Reset); 19 ds17887->Delayus(2); 20 /*复位ALE*/ 21 ds17887->SetCtlPin[DS17887_ALE](Reset); 22 ds17887->Delayus(2); 23 /*复位RD*/ 24 ds17887->SetCtlPin[DS17887_RD](Reset); 25 ds17887->Delayus(10); 26 27 /*将地址数据总线的模式改为输入*/ 28 ds17887->SetBusDirection(In); 29 ds17887->Delayus(40); 30 /*读取数据*/ 31 uint16_t readData=0; 32 readData=ds17887->ReadByte(); 33 ds17887->Delayus(4); 34 /* 将RD置位,并将CS信号置位,失能芯片 */ 35 ds17887->SetCtlPin[DS17887_RD](Set); 36 ds17887->SetCtlPin[DS17887_CS](Set); 37 ds17887->Delayus(4); 38 /*将ALE置位*/ 39 ds17887->SetCtlPin[DS17887_ALE](Set); 40 ds17887->Delayus(20); 41 return readData; 42 }
2.2.2、写数据操作
DS17887实时时钟的写操作时序如下图所示:
我们根据以上时序图来开发DS17887对象的写操作函数如下:
1 /*向DS17887写数据*/ 2 static void WriteDataToDS17887(Ds17887ObjectType *ds17887,uint16_t address,uint16_t data) 3 { 4 /*将DS17887的片选信号失能*/ 5 ds17887->SetCtlPin[DS17887_CS](Set); 6 /*将RD与WR置位*/ 7 ds17887->SetCtlPin[DS17887_WR](Set); 8 ds17887->SetCtlPin[DS17887_RD](Set); 9 ds17887->Delayus(2); 10 /*将ALE信号置高*/ 11 ds17887->SetCtlPin[DS17887_ALE](Set); 12 13 /*将地址数据总线的模式改为输出*/ 14 ds17887->SetBusDirection(Out); 15 /*写寄存器地址*/ 16 ds17887->WriteByte(address); 17 /*将片选信号置位,使能片选*/ 18 ds17887->SetCtlPin[DS17887_CS](Reset); 19 ds17887->Delayus(4); 20 /*复位ALE信号*/ 21 ds17887->SetCtlPin[DS17887_ALE](Reset); 22 ds17887->Delayus(4); 23 /*复位WR*/ 24 ds17887->SetCtlPin[DS17887_WR](Reset); 25 /*写数据*/ 26 ds17887->WriteByte(data); 27 ds17887->Delayus(4); 28 29 /* 将WR置位,并将CS信号置位,失能芯片 */ 30 ds17887->SetCtlPin[DS17887_WR](Set); 31 ds17887->SetCtlPin[DS17887_CS](Set); 32 ds17887->Delayus(4); 33 /*将ALE置位*/ 34 ds17887->SetCtlPin[DS17887_ALE](Set); 35 ds17887->Delayus(10); 36 }
2.2.3、时间数据获取
我们操作DS17887的根本目的就是获取系统时钟,在我们实现了对DS17887寄存器的读写操作后,我们可以据此得到时钟数据。
1 /*从实时时钟模块读取时间*/ 2 void GetDateTimeFromDs17887(Ds17887ObjectType *ds17887) 3 { 4 /*读取系统时间值*/ 5 ds17887->dateTime[0]=ReadDataFromDS17887(ds17887,DS17887_Year);//系统时间年 6 ds17887->Delayus(5); 7 ds17887->dateTime[1]=ReadDataFromDS17887(ds17887,DS17887_Month);//系统时间月 8 ds17887->Delayus(5); 9 ds17887->dateTime[2]=ReadDataFromDS17887(ds17887,DS17887_Date);//系统时间日 10 ds17887->Delayus(5); 11 ds17887->dateTime[3]=ReadDataFromDS17887(ds17887,DS17887_Hour);//系统时间时 12 ds17887->Delayus(5); 13 ds17887->dateTime[4]=ReadDataFromDS17887(ds17887,DS17887_Minute);//系统时间分 14 ds17887->Delayus(5); 15 ds17887->dateTime[5]=ReadDataFromDS17887(ds17887,DS17887_Second);//系统时间秒 16 ds17887->Delayus(5); 17 }
2.2.4、时间校准
获取的时钟数据也许会存在偏差,这时就需要对系统的时钟进行校准。停止时间更新后,修改时间寄存器的数据,然后再开启计时就完成了时间的校准。
1 /*校准DS17887的时间*/ 2 void CalibrationDs17887DateTime(Ds17887ObjectType *ds17887,uint16_t * dateTime) 3 { 4 /*将ALE、RD与WR复位*/ 5 ds17887->SetCtlPin[DS17887_ALE](Reset); 6 ds17887->SetCtlPin[DS17887_WR](Reset); 7 ds17887->SetCtlPin[DS17887_RD](Reset); 8 9 /*初始化控制寄存器,以便校准时间*/ 10 WriteDataToDS17887(ds17887,DS17887_Reg_A,0x20); 11 WriteDataToDS17887(ds17887,DS17887_Reg_B,0x06); 12 WriteDataToDS17887(ds17887,DS17887_Reg_B,0x80); 13 14 /*设置系统时间值*/ 15 WriteDataToDS17887(ds17887,DS17887_Year,dateTime[0]);//系统时间年 16 WriteDataToDS17887(ds17887,DS17887_Month,dateTime[1]);//系统时间月 17 WriteDataToDS17887(ds17887,DS17887_Date,dateTime[2]);//系统时间日 18 WriteDataToDS17887(ds17887,DS17887_Hour,dateTime[3]);//系统时间时 19 WriteDataToDS17887(ds17887,DS17887_Minute,dateTime[4]);//系统时间分 20 WriteDataToDS17887(ds17887,DS17887_Second,dateTime[5]);//系统时间秒 21 22 /*设置寄存器B和A的值,启动DS17887*/ 23 WriteDataToDS17887(ds17887,DS17887_Reg_B,0x06); 24 WriteDataToDS17887(ds17887,DS17887_Reg_A,0x20); 25 26 //读取DS17887的时间 27 GetDateTimeFromDs17887(ds17887); 28 }
3、驱动的使用
我们实现了DS17887的驱动,那么如何使用这一驱动呢?其实与我们在第二节中开发驱动的流程是一致的,先定义对象,再操作对象。
3.1、声明并初始化对象
我们前面已经定义了DS17887的对象类型。我们要得到一个DS17887对象,首先要使用Ds17887ObjectType声明一个DS17887对象变量,如下: Ds17887ObjectType ds17887。
有了这个变量后并不能马上使用它进行操作,还需要先对它使用Ds17887Initialization初始化函数进行初始化。这个函数有很多参数,其中有5个参数是函数指针。
1 /* 定义DS17887控制引脚操作函数指针 */ 2 typedef void (*DS17887CtlPinOperation)(Ds17887PinValueType value); 3 4 /* 定义DS17887写数据操作函数指针 */ 5 typedef void (*WriteByteToDs17887)(uint16_t data); 6 7 /* 定义DS17887读数据操作函数指针 */ 8 typedef uint16_t (*ReadByteFromDs17887)(void); 9 10 /* 定义设置数据地址总线方向函数指针 */ 11 typedef void (*Ds17887SetBusDirection)(Ds17887BusDirectionType direction); 12 13 /* 定义延时操作函数指针类型 */ 14 typedef void (*Ds17887Delayus)(volatile uint32_t nTime);
所以我们要译者5个函数指针类型定义相应的函数,并将这些函数作为参数传递给初始化函数实现对DS17887对象的使用。调用初始化函数如下:
void Ds17887Initialization(&ds17887,SetCtlPin,WriteByte,ReadByte,SetBusDirection,Delayus)
需要说明一下的是第二个参数实际是一个函数指针数组,也就是说每一个控制引脚都需要定义一个DS17887CtlPinOperationType类型的操作函数,并组成数组传递进来。这个数据必须按照enum Ds17887CtlPins枚举定义的顺序,即:
1 /* 定义DS17887控制引脚的种类 */ 2 typedef enum Ds17887CtlPins{ 3 DS17887_CS, 4 DS17887_WR, 5 DS17887_RD, 6 DS17887_ALE, 7 DS17887_KS, 8 DS17887_RCLR 9 }Ds17887CtlPinsType;
3.2、基于对象进行操作
完成了对对象的初始化就可以实现对对象的操作了。其实对DS17887的操作比较简单无非就是获取时间数据和校准时间数据。
获取时间数据就是调用GetDateTimeFromDs17887函数来实现就可以了。前面初始化完成的DS17887对象就是其参数。调用如下:
GetDateTimeFromDs17887(&ds17887);
而校准时间数据就是调用CalibrationDs17887DateTime函数来校准时间。前面初始化完成的DS17887对象就是其参数,此外需要输入标准时间数据作为第二个参数。
uint16_t dateTime[6]={year,month,day,hour,minute,second};
然后调用函数校准时间:
CalibrationDs17887DateTime(&ds17887,dateTime);
4、应用总结
在一个项目我们需要在每十秒的时间间隔记录一些列数据。所以我们需要以最小为1秒的精度读取系统实时时钟。我们将采用DS17877作为系统的实时时钟,完全能够符合我们的要求。
在我们这个驱动程序中,我们默认将DS17887对象二进制数据格式、24小时制、并使用存储区域bank0。如果需要不同的配置,则可以修改初始化函数中的配置。
还有对控制引脚的控制是一个6个元素的函数指针数组。这6个函数的顺序必须按照枚举Ds17887CtlPinsType的排布顺序。
欢迎关注:
如果阅读这篇文章让您略有所得,还请点击下方的【好文要顶】按钮。
当然,如果您想及时了解我的博客更新,不妨点击下方的【关注我】按钮。
如果您希望更方便且及时的阅读相关文章,也可以扫描上方二维码关注我的微信公众号【木南创智】