DS1302 — 摸索式前进

今天有没有好好学习鸭?????!!!!


学到现在,基本上把底层过了一遍,,,DS1302不是很懂,今天又看了一遍,记录一下我的理解吧,,,之前学的底层这几天也打算再写一遍,加深一下理解。


<我的GitHub>:DS1302时钟显示


1. SPI时序

首先明确DS1302的通信时序是SPI的变种,所以我们自然要清楚SPI时序的特点。
DS1302 — 摸索式前进_第1张图片
上面是 CPOL = 1,CPHA = 1 时的SPI通信时序图

  1. CPHA= 1表示数据的输出是在一个时钟周期的第一个沿上,反之表示数据的采样在第一个沿上。
  2. CPOL=1表示数据发送之前和之后SCLK(SCK)也就是时钟信号的空闲状态都是高电平(图上红色字体处),反之为低电平。
    另外注意:SPI只负责通信,不管通信成功与否,这点与I2C不同

那么SPI与DS1302有什么关系呢?
DS1302内部有三根线:CE(使能线),I/O(数据线),SCLK(时钟线)是不是与SPI有点相像呢,下面给出对比图
DS1302 — 摸索式前进_第2张图片
图中可以看出:

  1. CE = 1时才能进行读写操作,即在向DS1302内部寄存器上写(读)数据时必须要先将CE拉高,使其处于使能状态,既然前面使能了,那在程序的最后一定要记得关闭使能(初学者容易漏掉这个,比如我,,,,);
  2. 初始状态 CE=0;CK=0(后面初始化DS1302时需要用到)

2. DS1302的寄存器

寄存器在DS1302内部是非常重要,主要用来存储信息,一定要清楚每个寄存器的特点以及功能,这样才能更好的理解底层代码。
DS1302 — 摸索式前进_第3张图片
图 2 DS1302命令字节

  1. 固定位:第7位为1,如果第0,则写入无效。
  2. 第6位:可选择功能,选择RAM为1,选择CLOCK为0(我们主要利用时钟功能,故第6位默认为0)
  3. 第0-5位:地址位,寄存器的5位地址
  4. 第0位:读写标志位,1表示读,0表示写(从图上也可以很容易看出,因为WR上面有一条横线,表示低电平有效,故0表示写)

DS1302内部有8个与时钟相关的寄存器,5位地址为0b00000-0b00111(这里是用二进制非圧缩型BCD码表示的)

明确了上面那些之后,我们就可以理解下面这段代码了!!!!!!
哦哦!!还要注意的一点是,DS1302在写入一个字节数据时,要先写一个字节指令,表明要写入的寄存器地址以及后续操作为写操作

/*向某一寄存器中写入一个字节,reg表示寄存器地址,dat为待写入字节*/
void DS1302SingleWrite(u8 reg, u8 dat)
{
	DS1302_CE = 1;
	DS1302ByteWrite((reg<<1)|0x80);//指明要写入的寄存器地址以及后续操作为写
	DS1302ByteWrite(dat);
	DS1302_CE = 0;
}

现在再看这段程序就能理解为什么0x80表示操作了吧,哈哈~
可以写出第7位到第0位上的值:1000 0000,然后再用16进制表示一下:0x80
同理,可以知道 0x81 表示操作

当然我们在上面还要再定义一个函数,发送一个字节到DS1302的通信总线上去,也就是上面代码里的DS1302ByteWrite()这个函数。下面是代码:

/*发送一个字节到通信总线上*/
void DS1302ByteWrite(u8 dat)
{
	u8 mask;
	
	DS1302_IO = 1;//注意这里要把IO引脚拉高!!!!
	for(mask = 0x01; mask != 0; mask <<= 1)
	{
		if((dat & mask) == 0)//注意这里!!!优先级的问题
			DS1302_IO = 0;
		else
			DS1302_IO = 1;
		DS1302_CK = 1;//拉高时钟
		DS1302_CK = 0;//拉低时钟,完成一个位的操作
	}
	DS1302_IO = 1;//最后确保释放IO引脚?
}

有几个问题:

  1. 为什么先写入通信总线再写入寄存器呢?(即为什么要定义两个写的函数呢?)
    是为了方便程序的调用,我们写了单独的写函数,就不仅仅可以往寄存器里写了,想写入数据的时候就可以直接调用写函数。
  2. 关于最后拉高由拉低时钟的部分?
    可以理解为数据写入的过程,先拉高再拉低时钟才能写入一位数据。
  3. 最后为什么要确保释放IO引脚呢?
    写入数据的操作最后都要确保释放总线,在DS1302中就是拉高IO引脚(DS18B20 写操作中写入数据后也要释放总线(IO_18B20 = 1)
    一定需要理解的部分!!!!
    利用中间变量mask写入数据的过程,举一个例子即可理解,这个部分在I2C部分也要使用,所以务必理解!!!(这里就不详细说明了,读者举个例子一看便知)

明白了写的过程后,我们再理解 的程序就比较容易啦~
下面贴出代码:

u8 DS1302ByteRead()
{
	u8 mask;
	u8 dat = 0;
	
	for(mask = 0x01; mask != 0; mask <<= 1)
	{
		if(DS1302_IO)
			dat |= mask;
		DS1302_CK = 1;
		DS1302_CK = 0;
	}
	return dat;
}

u8 DS1302SingleRead(u8 reg)
{
	u8 dat;
	DS1302_CE = 1;
	DS1302ByteWrite((reg<<1)|0x81);
	dat = DS1302ByteRead();
	DS1302_CE = 0;
	DS1302_IO = 0;//注意最后一定要将IO口拉低!!
	
	return dat;
}

需要注意!!!

  1. 读数据是需要有返回值的(返回读到的数据,后续调用函数时用到的也是读出的数据),故定义函数时要在函数名前面写u8(有返回值即可)不能写成void了!!!
  2. 读者需要自行理解一下利用mask读数据的过程,方法同上(也很好理解)

为了更好地理解下面的程序,我们需要知道DS1302内部有哪些时钟寄存器!!!
DS1302 — 摸索式前进_第4张图片
从上到下分别是 寄存器0到寄存器7 ,其中 寄存器0到寄存器6 分别用来存储“秒, 分, 时, 日,月, 周,年(这个顺序一定要记清楚!!!)
寄存器7:最高位一个写保护位,如果这一位是 1禁止给任何其它寄存器写数据的。因此在写数据之前,这一位必须先写成 0。
这里先大致了解一下这些寄存器,后面的程序里我会再一一说明。

3. DS1302的BURST模式

在这里我们单独说一下BURST模式,也叫做突发模式,这里也只说明时钟突发模式。
先说明几个问题

  1. BURST模式的作用是什么?
    当DS1302识别到BURST模式时,可以将所有八个字节锁存到八个寄存器中,也就是说在BURST模式下我们可以向八个寄存器里同时写入(读出)数据。(看到这里,我们就可以知道在BURST模式中一定会用到循环
  2. DS1302如何识别BURST模式呢?
    只需在写指令到DS1302中时,将寄存器的5位地址位全部写为1,联系一下我们上面所讲的内容,我们这时候应该写入什么呢?
    写操作:第7位到第0位(10111110)16进制表示:0xBE
    同样的我们也能得到读操作应该写入0xBF
    下面是代码:
/*突发写*/
void DS1302BurstWrite(u8 *dat)
{
	u8 i;
	
	DS1302_CE = 1;
	DS1302ByteWrite(0xBE);
	for(i = 0; i<7; i++)//循环写入
	{
		DS1302ByteWrite(*dat++);
	}
	DS1302_CE = 0;
}

/*突发读*/
void DS1302BurstRead(u8 *dat)//指针相当于是定义了一个数组,突发读是无返回值的
{
	u8 i;
	
	DS1302_CE = 1;
	DS1302ByteWrite(0xBF);
	for(i = 0; i<7; i++)//循环读
	{
		dat[i] = DS1302ByteRead();
	}
	DS1302_CE = 0;
	DS1302_IO = 0;//注意!!!!拉低IO引脚
}

4. 底层剩余部分

  1. 结构体:
    DS1302的程序里还用到了结构体,学过C语言的同学应该很好理解,emmm,可以把它理解成一个数据类型,类似u8,u16那种的,只不过它里面可以定义很多的数据,这些数据也可以是不同的类型,另外它有自己的使用规则,想详细了解的话,可以自己查找一下资料(我的认识比较浅,,,,)
struct sTime{/*年, 月, 日, 时, 分, 秒, 周*/
	u16 year;
	u8 mon;
	u8 day;
	u8 hour;
	u8 min;
	u8 sec;
	u8 week;
};
  1. 最主要的三个底层!!!!! 核心部分!!!!!
/*获取实时时间,即读取DS1302当前时间并转换为时间结构体格式*/
void GetRealTime(struct sTime *time)
{
	u8 buf[8];
	
	DS1302BurstRead(buf);
	time->year = buf[6] + 0x2000;
	time->mon = buf[4];
	time->day = buf[3];
	time->hour = buf[2];
	time->min = buf[1];
	time->sec = buf[0];
	time->week = buf[5];	
}
/*设定实时时间,时间结构体格式的设定时间转换为数组并写入DS1302*/
void SetRealTime(struct sTime *time)
{
	u8 buf[8];
	
	buf[7] = 0;
	buf[6] = time->year;
	buf[4] = time->mon;
	buf[3] = time->day;
	buf[2] = time->hour;
	buf[1] = time->min;
	buf[0] = time->sec;
	buf[5] = time->week;
	DS1302BurstWrite(buf);
}
/*DS1302初始化,设置初始时间*/
void InitDS1302()
{
/*下面的结构体数组是用来存储初始化时间的*/
	struct sTime InitTime[] = {/*年,月,日, 时, 分, 秒,显示初始化时间为
	10时40分0秒*/
	
		0x00, 0x00, 0x00, 0x10, 0x40, 0x00,0x00
	};
	DS1302_CE = 0;
	DS1302_CK = 0;
	DS1302SingleWrite(7, 0X00);//撤销写保护,可以看一下寄存器7的功能
	SetRealTime(&InitTime);
}

几个问题:
(1). 关于初始化时间部分,可能会有疑问,为什么“struct sTime InitTime[] = {0x00, 0x00, 0x00, 0x10, 0x40, 0x00,0x00};”这样定义数组显示10时40分00秒呢?
这是因为我们在定义结构体时的变量顺序是“年,月,日, 时, 分, 秒”。

(2)关于设定以及获取实时时间部分: 还记得上面我强调一定要记清楚每个寄存器里储存什么内容吗?寄存器0-寄存器6 分别存储“秒,分,时,日,月,周,年
那么我们自然就可以理解下面这段程序啦~

    buf[6] = time->year;//寄存器6存储年
	buf[4] = time->mon;//寄存器4存储月
	buf[3] = time->day;
	buf[2] = time->hour;
	buf[1] = time->min;
	buf[0] = time->sec;
	buf[5] = time->week;
  1. 刷新时间并显示
void ShowLedNumber(u8 index, u8 num)
{
	if(num == 0xBF)//第二个及第五个数码管的‘-’处理
		LedBuff[index] = 0xBF;
	else
		LedBuff[index] = LedChar[num];
}

void RefreshTime()
{
	GetRealTime(&buffTime);//主函数里有定义buffTime变量
	
	ShowLedNumber(7, buffTime.hour>>4);//高四位中存储有小时的十位
	ShowLedNumber(6, buffTime.hour&0x0F);//低四位是小时的个位
	ShowLedNumber(5, 0xBF);
	ShowLedNumber(4, buffTime.min>>4);
	ShowLedNumber(3, buffTime.min&0x0F);
	ShowLedNumber(2, 0xBF);
	ShowLedNumber(1, buffTime.sec>>4);
	ShowLedNumber(0, buffTime.sec&0x0F);
}

关于刷新时间部分,可能也会不太理解,这里其实是用到了寄存器0(秒),寄存器1(分),寄存器2(时)的特点;
寄存器0:最高位 CH 是一个时钟停止标志位,剩下的7 位高 3 位是秒的十位,低 4 位是秒的个位;
寄存器1:最高位未使用,剩下的 7 位中高 3 位是分钟的十位,低 4 位是分钟的个位;
寄存器2:低 4 位代表的是小时的个位。
关于显示部分,当然还用到了Led扫描函数,等我再写数码管显示的底层的时候会再细讲哒~

你可能感兴趣的:(蓝桥备赛)