我也谈谈DS1302实时时钟芯片的操作时序

陪她去流浪 桃子 2013年11月10日 编辑 阅读次数:4896

前言

说起DS1302时钟芯片,多少有人曾被它弄伤心过。至少我是弄了好几次才成功读出数据的,因为它的操作时序问题,听说不是标准的SPI协议。这个只是听说,我一向使用模拟 SPI3 线协议和 DS1302 通信。注意:官方的数据手册上也没有提到其协议是否是 SPI。

今天下午在群里面和一网友讨论了很久关于 DS1302 的操作时序问题,还好,最终我们有了一致的观点。我还是写点文章来纪念一下这个曾经多次伤我心的“芯”吧。

下面的内容来自对官方文档的翻译与重点整理。重要的东西我会用红色标注。

特点

  • 实时时钟,对年月日,时分秒,星期计数;闰年自己补偿(调整),有效计数到2100年;
  • 31*8 位 RAM 高速数据存储器;
  • 串行数据输入/输出,3线接口;
  • 2.0-5.5V 全操作供电范围;
  • 2.0V 时电流低于 300nA;
  • 单字节/突发模式的 时钟/RAM 数据读写;

封装

引脚描述

X1,X2接标准的 32.768kHz 石英晶振
GND接地
/RST复位引脚,由于在读写操作期间要求为高电平,所以可以作片选来使用
I/O3线通信接口的数据输入/输出(双向)
SCLK串行同步时钟输入
Vcc1提供电池备份电源
Vcc2 主电源供应,可以用于给 Vcc1 充电
当 Vcc2 - Vcc1 > 0.2V 时由 Vcc2 供电
当 Vcc2 < Vcc1 时, 由Vcc1供电

芯片操作命令字

BIT7必须为1, 如果是0的话, 将不能写入到DS1302内部
BIT6表示接下来要操作的是时钟/日历数据(如果为0), 还是是RAM(如果为1)
BIT5~BIT1指定待读写的寄存器或RAM地址(0~30)
BIT0指定是读操作还是写操作, 如果是0则为写, 是1则为读

注意:命令字总是从最低位(LSB)开始输入DS1302。

复位和时钟控制

所有的数据传输操作都是是 /RST 为高的时候进行的。/RST 引脚服务于两种功能:

  1. /RST 打开芯片移位寄存器访问控制逻辑,就是片选;
  2. /RST 信号用于终止单字节或多字节的数据传输;

A clock cycle is a sequence of a falling edge followed by a rising edge. - 这句话是啥意思,不怎么明白,似懂非懂。

对于数据输入:数据必须在SCLK的上升沿保持有效。先写数据,然后一个SCLK从低到高的跳变。

对于数据输出:数据在SCLK的下降沿从DS1302输出。先SCLK从高到低的跳变,然后取数据。

如果 /RST 是低的话,所有的数据传输被终止,并且此时IO口变为高阻状态。

在上电的时候,/RST 在 Vcc > 2.0V 之前必须保证为0 - 试问:怎么来设计这样的电路?我不怎么懂硬件。

SCLK 在 /RST 由0变为1的时候必须为0。

数据输入到 DS1302

在通过8个SCLK输入一个写命令字后, 在下8个上升沿数据被输入到DS1302 , 数据位从LSB开始 注:先在IO上放置数据, 然后产生一个上升沿. 多余的SCLK将被忽略~

单字节读写操作时序:

数据从 DS1302 输出

(图见上)

在通过8个SCLK输入一个写命令字后, 在下8个上升沿数据从DS1302输出 , 数据位从LSB开始

注意:第1个被输出的数据位是在写完命令字的最后一位的第1个下降沿被输出

多余的SCLK的效果是重复发送数据, 不产生其它影响~

突发模式(多字节操作)

多字节突发读取/写入可用于时钟, 同样可用于RAM的读取, 在命令控制中的位6中指示. 位5到位1==逻辑1.

时钟的9到31号寄存器没有数据存储能力, RAM的31号也没有(0-30).

突发模式从地址0的第0位开始数据传输.

在写时钟寄存器时(共8个), 必须按照寄存器的顺序依次写入.

在写RAM数据时,不需要同时写所有31个寄存器.

时钟/日历

如下图所示. 这些寄存器中的相关数据都是以BCD码的形式保存的.

寄存器说明:

秒寄存器第7位为时钟停摆标志位; 10SEC和SEC分别代表秒的十位和个位.
分寄存器10MIN和MIN分别代表分的十位和个位.
时寄存器第7位为12/24小时时制选择位,见下面的描述.
日寄存器10DATE和DATE分别用来表示日的十位和个位.
月寄存器10M和MONTH分别用来表示月份的十位和个位.
星期寄存器DAY用来表示星期
年寄存器10YEAR表示年的十位,最大为99, 表示2099年; YEAR表示年份的个位.
控制寄存器见写保护位描述.
涓流充电器寄存器见涓流充电器描述.

时钟停摆标志

秒寄存器的第7位被定义为时钟停摆标志.

当该位被置为1的时候, 时钟停摆, DS1302被置于低功耗模式, 当前电源损耗低于100nA.

当该位被写清除的时候, 时钟就开始摆动. 开始上电时该位未定义.

上午-下午/12-24小时制

时寄存器的第7位被定义为12小时/24小时选择位. 当该位为高, 选择12小时制.

在12小时制模式中, 第5位为AM/PM标志, 逻辑高表示PM.

在24小时制模式中, 第5位为第2个'10'小时位(仅该位时表示20-23小时, 懂了吧?).

写保护位

控制寄存器的第7位为写保护标志位. 第6到第0位被强制为0, 读的时候也是0. 上电初始化状态未定义.写之前记得去除写保护位.

涓流充电寄存器

TCS:涓流寄存器选择位, 必须为1010才能选通. 上电时被禁用.

DS:二极管选择位,第2,3位, 选择在Vcc2和Vcc1之间连接1个还是2个二极管. 1就是1个,2就是两个. 00或11无效.

RS:电阻选择位,第1,0位, 选择连接到Vcc1和Vcc2之间的电阻的大小. 如下表所示:

RS BitsResistorTypical Value
00NoneNone
01R12kΩ
10R24kΩ
11R38kΩ

二极管和电阻的选择取决于用户当前需要的最大充电电流.

最大充电电流可以按照如下公式来计算, 假定Vcc2=5.0V,并且有一个super cap(超级电容)连接到Vcc1.,

涓流充电器被初始化为:1个二极管和R1选中:

Imax = (5.0V-diode drop) / R1
    ~= (5.0 - 0.7v) / 2kΩ
    ~= 2.2mA

时钟/日历 突发模式

该模式下, 时钟/日历寄存器的前8个可以被连续地读或写, 从地址0的第0位开始.

涓流寄存器无法在突发模式下访问.

在开始读之前,当前时间值被传送到第2套内部寄存器中.因此读到的数据被锁定了, 此时真正的时间寄存器仍然在走,以免在读完之前对新的时间造成干扰

RAM

静态RAM大小为31*8位, 可以连续寻址于RAM地址空间.

RAM突发模式

RAM命令字指定RAM突发模式.

在突发模式下, 31个字节的RAM寄存器可以被连续地读或写, 从0地址的第0位开始.

晶振选择

(略)

读写操作详细时序图

示例代码

代码是在 STM32f103C8T6 上面测试的, 运行效果很好~

#ifndef __DS1302_H__
#define __DS1302_H__

#ifdef __cplusplus
extern "C" {
#endif

#define DS1302_BURST_MODE 1

#pragma pack(push,1)
typedef struct{
    uint8_t second;
    uint8_t minute;
    uint8_t hour;
    uint8_t day;
    uint8_t month;
    uint8_t week;
    uint8_t year;
}DS1302;
#pragma pack(pop)

void    ds1302_init(void);
void    ds1302_time(DS1302* pds,uint8_t dowhat);
uint8_t ds1302_ram(uint8_t addr,uint8_t dowhat, uint8_t dat);
void    ds1302_charger(uint8_t tcs,uint8_t ds, uint8_t rs);

#ifdef __cplusplus
}
#endif


#endif//!__DS1302_H__
#include "stm32f10x.h"
#include "ds1302.h"

/*************************************************************************
文件名称:ds1302.c/ds1302.h
文件功能:DS1302时钟芯片模块操作代码
文件作者:女孩不哭
文件版本:
	2013-04-06,18:08:00 for MSP430F149
	2013-11-10,06:03:47 for STM32F103X

文件说明:
*. 概要:
	*. 获取时间, 不支持单个寄存器的读取, 必须全部读走
	*. 读写RAM, 0-30, 共31个字节, 只支持单字节读取

*. 模块的配置
	*. 根据是否需要突发模式决定是否定义DS1302_BURST_MODE宏,在头文件中定义为好
	*. 配置文件下面提到的 '端口配置'
	*. 按需配置ds1302_init初始化函数
	*. IO端口的配置:
		很多的单片机的IO口在输入与输出时需要进行一些配置,请按需要配置spi3_io_switch函数
		如果你的单片机是51, 则不需要配置; 是MSP430, STM32等就需要配置了,参见该函数的说明

*. 模块文件使用方法:
	*. 按需调用 ds1302_init 初始化模块相关总线
	*. 如需配置DS1302的时间,调用ds1302_time并将dowhat=1,结构体需完全初始化
	*. 获取当前时间:调用ds1302_time,并将dowhat=0,结构体无需初始化

*. 关于DS1302结构体:
	*. 该结构体按照芯片顺序分布成员变量,不能对该结构体作不相关的任何修改
	*. 成员的值已经进行了相应的数据转换, 成员的值即为实际值, 即:
		比如2013年, 则year成员的值为13D, 而不是0x13
		比如25号, 则day的值为25D, 而不是0x25

*. 其它:
	*. 不需要管写保护位, 程序已处理
*************************************************************************/

/*************************内部函数声明************************************/
uint8_t		ds1302_read(uint8_t cmd);
void		ds1302_write(uint8_t cmd,uint8_t dat);
uint8_t		ds1302_base_conv(uint8_t dowhat,uint8_t dat);

void		spi3_io_switch(int8_t in_out);
uint8_t		spi3_recv_byte(void);
void		spi3_send_byte(uint8_t dat);
/************************************************************************/

/***********************端口配置*********************************************/
#define DS1302_RST_H	GPIO_SetBits(GPIOD,GPIO_Pin_0)
#define DS1302_RST_L	GPIO_ResetBits(GPIOD,GPIO_Pin_0)

#define DS1302_SCK_H	GPIO_SetBits(GPIOD,GPIO_Pin_2)
#define DS1302_SCK_L	GPIO_ResetBits(GPIOD,GPIO_Pin_2)

#define DS1302_IOP_H	GPIO_SetBits(GPIOD,GPIO_Pin_1)
#define DS1302_IOP_L	GPIO_ResetBits(GPIOD,GPIO_Pin_1)
#define DS1302_IOP		GPIO_ReadInputDataBit(GPIOD,GPIO_Pin_1)
/************************************************************************/





/**************************************************
函  数:ds1302_init
功  能:初始化DS1302的总线
参  数:
返  回:
说  明:
**************************************************/
void ds1302_init(void)
{
	GPIO_InitTypeDef gpio;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);

	gpio.GPIO_Mode = GPIO_Mode_Out_PP;
	gpio.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2;
	gpio.GPIO_Speed = GPIO_Speed_50MHz;

	GPIO_Init(GPIOD,&gpio);

}

//////////////////////////////////////////////////////////////////////////

#ifdef DS1302_BURST_MODE
/**************************************************
函  数:ds1302_time
功  能:突发方式多字节时间读取,快速
参  数:
	pds-DS1302结构体
	dowhat:0-读,1-写
返  回:
说  明:只支持24小时进制
**************************************************/
void ds1302_time(DS1302* pds,uint8_t dowhat)
{
	uint8_t cx=7;
	uint8_t* p = (uint8_t*)pds;
	if(dowhat==0){//--------------------突发方式多字节读取
		DS1302_RST_L;
		DS1302_SCK_L;
		DS1302_RST_H;
		spi3_send_byte(0xBF);	//突发读命令字
		while(cx--){
			*p++ = ds1302_base_conv(1,spi3_recv_byte()&0x7F);
		}
		DS1302_RST_L;
	}else{//----------------------------突发方式多字节写入
		ds1302_write(0x8E,0x00);	//去写保护
		DS1302_RST_L;
		DS1302_SCK_L;
		DS1302_SCK_H;
		spi3_send_byte(0xBE);	//突发方式多字节写入
		while(cx--){
			spi3_send_byte(ds1302_base_conv(0,*p++));
		}
		DS1302_RST_L;
		ds1302_write(0x8E,0x80);
	}
}

#else // ---- 非突发方式

/**************************************************
函  数:ds1302_time
功  能:非突发方式读取所有时间数据
参  数:
	pds-时间结构体
	dowhat-读还是写:0-读,1-写
返  回:
说  明:只针对24小时制作处理,未处理12小时制
**************************************************/
void ds1302_time(DS1302* pds,uint8_t dowhat)
{
	uint8_t* p=(uint8_t*)pds;
	uint8_t wp=0x8E;
	uint8_t start_addr;
	uint8_t cx=7;

	if(dowhat == 0){//--------------------读时间
		start_addr = 0x81;
		while(cx--){
			//由于&0x7F, 所以年份不能超过2079年
			*p++ = ds1302_base_conv(1,ds1302_read(start_addr)&0x7F);
			start_addr += 2;
		}
	}else{//-----------------------------写时间
		start_addr=0x80;
		ds1302_write(wp,0x00);					//去写保护
		while(cx--){
			ds1302_write(start_addr,ds1302_base_conv(0,*p++));
			start_addr += 2;
		}
		ds1302_write(wp,0x80);
	}
}
#endif//DS1302_BURST_MODE

//////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////
/**************************************************
函  数:ds1302_ram
功  能:读取/写入RAM的值
参  数:
	addr:0-30的RAM地址
	dowhat:0-读取,1-写入
	dat:若是写入, 则为待写入的数据; 读取时无效
返  回:若为读取,返回读取到的数据; 写操作总是返回0xFF
说  明:
**************************************************/
uint8_t ds1302_ram(uint8_t addr,uint8_t dowhat, uint8_t dat)
{
	uint8_t cmd = 0x80 + 0x40;
	uint8_t wp = 0x8E;
	cmd += (addr&0x1F)<<1;

	if(dowhat == 0){//--------------读取RAM
		cmd += 1;
		return ds1302_read(cmd);
	}else{//------------------------写入RAM
		cmd += 0;
		ds1302_write(wp,0x00);
		ds1302_write(cmd,dat);
		ds1302_write(wp,0x80);
		return 0xFF;
	}
}

//////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////

/**************************************************
函  数:ds1302_charger
功  能:设置涓流充电器寄存器
参  数:
	tcs - 固定为 0x0A
	ds - 二极管的个数,0-2
	rs - 电阻的个数,0-3
返  回:
说  明:
**************************************************/
void ds1302_charger(uint8_t tcs,uint8_t ds, uint8_t rs)
{
	uint8_t wp = 0x8E;
	uint8_t tc = 0x90;
	uint8_t cmd = 0;

	cmd = tcs<<4 | (ds&0x03)<<2 | (rs&0x03);
	ds1302_write(wp,0x00);
	ds1302_write(tc,cmd);
	ds1302_write(wp,0x80);
}

//////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////

/**************************************************
函  数:ds1302_read
功  能:读DS1302寄存器或RAM,通过spi3总线
参  数:cmd - 命令字
返  回:寄存器值或RAM数据
说  明:单字节读函数
**************************************************/
uint8_t ds1302_read(uint8_t cmd)
{
	uint8_t dat;
	DS1302_RST_L;
	DS1302_SCK_L;
	DS1302_RST_H;
	spi3_send_byte(cmd);
	dat=spi3_recv_byte();
	DS1302_RST_L;
	return dat;
}

/**************************************************
函  数:ds1302_write
功  能:写DS1302寄存器或RAM,通过spi3总线
参  数:
	cmd - 命令字
	dat - 数据
返  回:
说  明:
**************************************************/
void ds1302_write(uint8_t cmd,uint8_t dat)
{
	DS1302_RST_L;
	DS1302_SCK_L;
	DS1302_RST_H;
	spi3_send_byte(cmd);
	spi3_send_byte(dat);
	//DS1302_SCK_H;
	DS1302_RST_L;
}

/**************************************************
函  数:ds1302_base_conv
功  能:用于BCD与DEC之间的相互转换
参  数:dowhat:0-DEC->BCD,1-BCD->DEC
返  回:
说  明:
**************************************************/
uint8_t ds1302_base_conv(uint8_t dowhat,uint8_t dat)
{
	if(dowhat==1){//----------BCD->DEC
		return ((dat>>4)*10)+(dat&0x0f);
	}else{//----------------DEC->BCD
		if(dat==0) return 0x00;
		else return ((dat/10)<<4)+(dat%10);
	}
}

//////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////

/**************************************************
函  数:spi3_io_switch
功  能:切换端口的输入输出状态
参  数:in_out:0-输入,1-输出
返  回:
说  明:该函数按需配置,可能为空
**************************************************/
void spi3_io_switch(int8_t in_out)
{
	GPIO_InitTypeDef gpio;
	gpio.GPIO_Pin = GPIO_Pin_1;
	gpio.GPIO_Speed = GPIO_Speed_50MHz;
	if(in_out==0) gpio.GPIO_Mode = GPIO_Mode_IPU;
	else gpio.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOD,&gpio);
}

/**************************************************
函  数:spi3_recv_byte
功  能:从3线spi总线读取1字节数据
参  数:
返  回:读取到的1字节数据
说  明:
**************************************************/
uint8_t spi3_recv_byte(void)
{
	uint8_t cx=8;
	uint8_t dat=0;
	spi3_io_switch(0);
	while(cx--){
		DS1302_SCK_H;
		DS1302_SCK_L;
		dat>>=1;
		if(DS1302_IOP) dat |= 0x80;
	}
	return dat;
}

/**************************************************
函  数:spi3_send_byte
功  能:向3线spi总线写入1字节数据
参  数:dat - 待写入的数据
返  回:
说  明:
**************************************************/
void spi3_send_byte(uint8_t dat)
{
	uint8_t cx=8;
	spi3_io_switch(1);
	while(cx--){
		if(dat&0x01) DS1302_IOP_H;
		else DS1302_IOP_L;
		DS1302_SCK_L;
		DS1302_SCK_H;
		dat >>= 1;
	}
}

//////////////////////////////////////////////////////////////////////////
#include "stm32f10x.h" 
#include "ds1302.h"

#include <stdio.h>

int main(void) 
{  
    DS1302 ds;
    char buf[128];
    ds1302_init();

    ds.year = 13;
    ds.month = 11;
    ds.day = 10;
    ds.hour = 4;
    ds.minute = 46;
    ds.second = 0;
    ds.week = 7;
    //ds1302_time(&ds,1);//是否需要初始化    
    
    while(1){
        ds1302_time(&ds,0);
        sprintf(buf,"%02d-%02d-%02d %02d:%02d:%02d %d",
            ds.year,ds.month,ds.day,
            ds.hour,ds.minute,ds.second,
            ds.week);
        //buf里面包含时间字符串, 输出即可~
        //some delay
    }
    
}