目标:
使用Arduino Uno开发板,在没有调用DS1302.h的头文件下对DS1302时钟模块进行时分秒的读写。
一、DS1302介绍
DS1302是美国DALLAS推出的一款高性能、低功耗的日历时钟芯片。DS1302是一种串行接口的实时时钟,芯片内部具有可编程的日历时钟和31个字节的静态RAM,日历时钟可以自动进行闰年补偿,计时准确,接口简单,使用方便,工作电压范围宽(2.5~5.5V),芯片自身还具有对备用电池进行涓流充电功能,可有效延长备用电池的使用寿命。
DS1302用于数据记录,能实现数据与该数据出现的时间同时记录,因此广泛应用于测量系统中。
这里使用现成的DS1302模块,该模块带有一个纽扣电池,没有纽扣电池的不能断电计时:
二、 DS1302芯片的引脚
- VCC2(VCC):接Arduino的5V引脚,电源正极;
- VCC1:接纽扣电池的正极;一般来说VCC2引脚的电压小于VCC1引脚的电压,则模块自动选择VCC1纽扣电池进行供电;
- GND:接Arduino的GND引脚,Arduino没有接的情况下,则内部与纽扣电池的负极导通;
- X1、X2:接32768Hz的晶振,X1流入DS1302,X2流出DS1302;
- CE(RST):使能端,CE为高时允许读写DS1302数据,为低时禁止读写。Arduino对这个引脚输出高电平1,则可以对DS1302寄存器进行读写;反之不能进行读写操作;
- IO(DAT):双向输入引脚,这个引脚可以输入输出电平,对DS1302进行读写寄存器操作的引脚;
- SCLK(CLK):串行时钟输入端,控制DS1302中寄存器的数据输入与输出;
三、接线表
Arduino Uno | DS1302模块 |
5V | VCC |
GND | GND |
2(可随机定义) | RST |
3(可随机定义) | DAT |
4(可随机定义) | CLK |
四、DS1302寄存器
DS1302芯片中有31个静态RAM寄存器,以下是我们常用的“年月日时分秒”以及写保护寄存器
读寄存器 指令 | 写寄存器 指令 | Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 | 范围 |
0x81 | 0x80 | CH 暂停 | 10秒 | 秒 | 00-59 | |||||
0x83 | 0x82 | 10分 | 分 | 00-59 | ||||||
0x85 | 0x84 | 12/24 | 0 | 10 AM/PM | 时 | 时 | 1-12/0-24 | |||
0x87 | 0x86 | 0 | 0 | 10日 | 日 | 1-31 | ||||
0x89 | 0x88 | 0 | 0 | 0 | 10月 | 月 | 1-12 | |||
0x8B | 0x8A | 0 | 0 | 0 | 0 | 0 | 周(周日为1) | 1-7 | ||
0x8D | 0x8C | 10年 | 1年 | 00-99 | ||||||
0x8F | 0x8E | WP 写保护 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | —— |
这里我们用到:
- 秒寄存器:写0x80、读0x81;Bit7定义为时钟暂停标志(CH)。当该位置为1时,时钟振荡器停止,DS1302处于低功耗状态;当该位置为0时,时钟开始运行。
- 分寄存器:写0x82、读0x83;
- 时寄存器:写0x84、读0x85;Bit7用于定义DS1302是运行于12小时模式还是24小时模式,当为1时,选择12小时模式,此时BIT5为AM/PM位,在24小时模式时此位为小时数据位。
- 写保护寄存器:写0x8E、读0x8F;Bit7是写保护位(WP),其它7位均为0。在任何对时钟或RAM读写操作之前,WP位必须为0。当WP位为1时,不能对任何时钟日历寄存器或RAM进行写操作。
五、DS1302读写数据时序图
5.1 读数据
在读数据之前,我们要让使能CE引脚置为高电平,也就是要上使能,才能进行读写操作,在Arduino中可以使用digitalWrite(RST,1),RST就是CE,就相当于把CE引脚拉高,可以开始读写数据。
在读数据的操作中,首先我们要明确我们要读的是什么数据(时分秒),这个数据存储在哪个寄存器里面,而读写寄存器的16进制指令是什么,这个16进制读写寄存器的指令就是我们所需要带入的参数。
我们要创建一个uchar DS1302_Read_Data(uchar cmd)函数,只要代入对应的读写寄存器的指令,就能返回指定寄存器中的值。
重点在于数据的发送和接收,在Arduino上,如果想要获取秒寄存器上的值,我们需要将0x81写入到DS1302,这样DS1302才知道我们要读秒寄存器,首先用pinMode定义DAT引脚为输出模式。DAT引脚只能输出高电平1或低电平0,所以要将16进制的地址发送给DS1302,我们要把16进制的地址转化成二进制(0和1)的形式:
0x81 = 0b10000001
但实际在程序中,我们不需要刻意去转化成二进制的形式,写成二进制为了更好理解一些。
从上面的转化关系能够看出,一位16进制数等于4位二进制数,那么2位16进制数就等于8位二进制数,也就是一个字节的数据大小。
转化成二进制数后,数据只有0和1组成,这时候就可以通过引脚的高电平和低电平来传输数据了,因为一次只能发送一个位(读数据也是一样),并且读和写数据,都是从最低位开始的,0b10000001,从最右边开始一位一位发。一个字节大小的指令,要发8次才能完成。
我们可以写一个for循环,来完成8个位的数据发送,在发送之前需要对数据进行位分解,也就是把一大串的二进制数分解成一个个独立的位,然后赋值给一个bool变量,为什么要选择bool变量呢?是因为分解出来的位数值要直接代入digitlaWrite函数中,该函数的第二个参数要求的就是bool类型的参数。如果写成digitalWrtie(DAT,100);这样是会报错的。
将数值和0x01进行按位与运算就可以得到独立的第一位数值,这时候再把数值右移一位,再次与0x01按位与运算,就可以得到第二位的数值。依次类推就可以将一个字节的数据都发送出去了。
这时候把读指令发送出去后,我们就要接收DS1302返回给我们的数据了,这时候要重新定义DAT引脚模式,pinMode(DAT,INPUT);,变成开始读数据。原理也是类似,从最低位开始读,读过来之后我们要放在一个字节中的最高位,然后第二次读的时候右移一位,再把读到的第二位数填入到最高位中。这里用的按位或运算,自定义了data变量,一个字节大小,然后这个变量与0x80按位或运算,可以将读取到的每个位拼凑成完整的数值。这里定义了data_bit用来存储读到的每一个位数据。for循环读取8次,然后拼凑而成data就可以被函数返回了。详细说明可见程序内代码注释。
//DS1302读数据
//cmd 读寄存器指令
uchar DS1302_Read_Data(uchar cmd)
{
//确保时序一开始都是从低电平开始的
digitalWrite(RST,0);
digitalWrite(CLK,0);
//开始写时序操作(上使能)
digitalWrite(RST,1);
//写cmd数据
//定义DAT引脚位输出模式,用于写数据
pinMode(DAT,OUTPUT);
for(uchar i=0; i<8; i++)
{
bool cmd_bit = 0; //bool类型的数据只有0和1,用于带入到digitalWrite中
digitalWrite(CLK,0); //再一次确保CLK引脚是低电平的
cmd_bit = cmd & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,cmd_bit); //将DAT引脚置高,相当于发了1,置低发0;
digitalWrite(CLK,1); //CLK引脚置高,DAT的位数据才能传入到DS1302
cmd >>= 1; //cmd数据值右移一位
}
//读data数据
//定义DAT引脚位输入模式,用于读数据
pinMode(DAT,INPUT);
uchar data = 0; //定义data变量,用来存储读到的数据
for(uchar i=0; i<8; i++)
{
digitalWrite(CLK,0); //CLK置低,产生下降沿才能读数据,这一句执行了,下面就可以把DAT读进来
data >>= 1; //data数据值右移一位,注意!!这个右移一定不能放在下面if块中,或者if语句之后。因为最后一次循环结束后,还会再右移一位,这时候数据就是错误的。还要注意的是这里接收的值是bcd码,在主程序中再从bcd码转成普通数据,如果这时候这条指令放在if后面,输出的数据也就会奇怪了;就好比分钟设了25分,结果输出是12分,这就是data>>=1位置放错而导致的一系列错误
bool data_bit = digitalRead(DAT);
if(data_bit) //判断DAT有没有获取到高电平,也就是数据1
data = data | 0x80; //数据是从最低位开始读的,最低位最后是在最右边。所以如果最低位为高电平,那么会被按位或运算0x80=0b10000000,把最高位置1,后面再右移1位,最后8个位读完,最低位也就到了最右边了;如果这个判断不执行,DAT就是0,那么直接跳过判断右移,相当于把最高位置0
digitalWrite(CLK,1); //CLK置1,回到高电平,以便下一次循环产生下降沿而读取数据
}
return data; //返回读到的数据
}
5.2 写数据
原理跟读数据是一样的,这里不再赘述。
//DS1302写数据
//cmd 写寄存器指令
//data 要写入的数据
void DS1302_Write_Data(uchar cmd, uchar data)
{
//确保时序一开始都是从低电平开始的
digitalWrite(RST,0);
digitalWrite(CLK,0);
//开始执行写时序操作
digitalWrite(RST,1);
//定义DAT引脚为输出模式,用于写数据
pinMode(DAT,OUTPUT);
//写cmd数据
for(uchar i=0; i<8; i++) //每次写一位,循环8次,写一个字节的数据
{
bool cmd_bit = 0; //定义一个bool变量用来存储分解后的位数据
digitalWrite(CLK,0); //CLK引脚设置电平位0,用以后面置1时能够产生上升沿
cmd_bit = cmd & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,cmd_bit); //从cmd数据的最低位开始发送
digitalWrite(CLK,1); //CLK置高电平,DAT上的数据0或1就发送出去了
cmd >>= 1; //cmd数据值右移一位
}
//写data数据
for(uchar i=0; i<8; i++) //每次写一位,循环8次,写一个字节的数据
{
bool data_bit = 0; //定义一个bool变量用来存储分解后的位数据
digitalWrite(CLK,0); //CLK引脚设置电平位0,用以后面置1时能够产生上升沿
data_bit = data & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,data_bit); //从cmd数据的最低位开始发送
digitalWrite(CLK,1); //CLK置高电平,DAT上的数据0或1就发送出去了
data >>= 1; //data数据值右移一位
}
}
六、BCD数据转换
这时候我们已经定义好了读写数据的函数了,还有需要注意的是,DS1302除了指令外,读写的数据都是BCD的数据。
假设我要写入一个15的数据,那么这个数据要先被转换成BCD码的形式:
先把这个十进制15,拆成两位1 5,然后拆开的每一位十进制数都用4位二进制表示
0001 0101
这时候得到了一个8位二进制数,然后再转化成十进制就是:21
可以知道这普通的十进制和BCD码还是不一样的,这就有点类似于16进制转10进制。所以我们还需要定义BCD码的转化函数的。
6.1 数据转BCD
计算方法:
data = 15;
data1 = 15/10 = 1
data2 = 15%10 = 5
bcd = data1*16 + data2 = 1*16 + 5 = 21
//数据转BCD码
//十进制转BCD码,就相当于把这个十进制数看成十六进制,然后再转换成二进制数,十六进制的10相当于十进制的16;所以后面求出来的商要乘16
uchar data_bcd(uchar data)
{
uchar data1 = data/10;
uchar data2 = data%10;
uchar bcd = data1*16 + data2;
return bcd;
}
6.2 BCD转数据
计算方法:
bcd = 21;
bcd1 = 21/16 = 1
bcd2 = 21%16 = 5
data = bcd1*16 + bcd2 = 1*10 + 5 = 15
//BCD码转数据
//十进制转BCD码,就相当于把这个十进制数看成十六进制,然后再转换成二进制数,十六进制的10相当于十进制的16;所以后面求出来的商要乘16
uchar bcd_data(uchar bcd)
{
uchar bcd1 = bcd/16;
uchar bcd2 = bcd%16;
uchar data = bcd1*10 + bcd2;
return data;
}
七、设置时分秒,读取时分秒
时分秒寄存器上一旦给了数值,只要没有给暂停指令,那么时间就会自己走起来。
不管在设置时间还是读取时间,都需要先进行关闭写保护,设置好时间或者读取完时间后,在恢复写保护的状态。
/*=============================================*/
typedef unsigned char uchar; //将unsigned char数据类型简写成uchar
/*=============================================*/
int RST = 2; //引脚名称定义
int DAT = 3; //引脚名称定义
int CLK = 4; //引脚名称定义
/*=============================================*/
//DS1302读数据
//cmd 读寄存器指令
uchar DS1302_Read_Data(uchar cmd)
{
//确保时序一开始都是从低电平开始的
digitalWrite(RST,0);
digitalWrite(CLK,0);
//开始写时序操作(上使能)
digitalWrite(RST,1);
//写cmd数据
//定义DAT引脚位输出模式,用于写数据
pinMode(DAT,OUTPUT);
for(uchar i=0; i<8; i++)
{
bool cmd_bit = 0; //bool类型的数据只有0和1,用于带入到digitalWrite中
digitalWrite(CLK,0); //再一次确保CLK引脚是低电平的
cmd_bit = cmd & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,cmd_bit); //将DAT引脚置高,相当于发了1,置低发0;
digitalWrite(CLK,1); //CLK引脚置高,DAT的位数据才能传入到DS1302
cmd >>= 1; //cmd数据值右移一位
}
//读data数据
//定义DAT引脚位输入模式,用于读数据
pinMode(DAT,INPUT);
uchar data = 0; //定义data变量,用来存储读到的数据
for(uchar i=0; i<8; i++)
{
digitalWrite(CLK,0); //CLK置低,产生下降沿才能读数据,这一句执行了,下面就可以把DAT读进来
data >>= 1; //data数据值右移一位,注意!!这个右移一定不能放在下面if块中,或者if语句之后。因为最后一次循环结束后,还会再右移一位,这时候数据就是错误的。还要注意的是这里接收的值是bcd码,在主程序中再从bcd码转成普通数据,如果这时候这条指令放在if后面,输出的数据也就会奇怪了;就好比分钟设了25分,结果输出是12分,这就是data>>=1位置放错而导致的一系列错误
bool data_bit = digitalRead(DAT);
if(data_bit) //判断DAT有没有获取到高电平,也就是数据1
data = data | 0x80; //数据是从最低位开始读的,最低位最后是在最右边。所以如果最低位为高电平,那么会被按位或运算0x80=0b10000000,把最高位置1,后面再右移1位,最后8个位读完,最低位也就到了最右边了;如果这个判断不执行,DAT就是0,那么直接跳过判断右移,相当于把最高位置0
digitalWrite(CLK,1); //CLK置1,回到高电平,以便下一次循环产生下降沿而读取数据
}
return data; //返回读到的数据
}
//DS1302写数据
//cmd 写寄存器指令
//data 要写入的数据
void DS1302_Write_Data(uchar cmd, uchar data)
{
//确保时序一开始都是从低电平开始的
digitalWrite(RST,0);
digitalWrite(CLK,0);
//开始执行写时序操作
digitalWrite(RST,1);
//定义DAT引脚为输出模式,用于写数据
pinMode(DAT,OUTPUT);
//写cmd数据
for(uchar i=0; i<8; i++) //每次写一位,循环8次,写一个字节的数据
{
bool cmd_bit = 0; //定义一个bool变量用来存储分解后的位数据
digitalWrite(CLK,0); //CLK引脚设置电平位0,用以后面置1时能够产生上升沿
cmd_bit = cmd & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,cmd_bit); //从cmd数据的最低位开始发送
digitalWrite(CLK,1); //CLK置高电平,DAT上的数据0或1就发送出去了
cmd >>= 1; //cmd数据值右移一位
}
//写data数据
for(uchar i=0; i<8; i++) //每次写一位,循环8次,写一个字节的数据
{
bool data_bit = 0; //定义一个bool变量用来存储分解后的位数据
digitalWrite(CLK,0); //CLK引脚设置电平位0,用以后面置1时能够产生上升沿
data_bit = data & 0x01; //cmd数据位分解,用按位与的方式,提取出每一位二进制数
digitalWrite(DAT,data_bit); //从cmd数据的最低位开始发送
digitalWrite(CLK,1); //CLK置高电平,DAT上的数据0或1就发送出去了
data >>= 1; //data数据值右移一位
}
}
//数据转BCD码
//十进制转BCD码,就相当于把这个十进制数看成十六进制,然后再转换成二进制数,十六进制的10相当于十进制的16;所以后面求出来的商要乘16
uchar data_bcd(uchar data)
{
uchar data1 = data/10;
uchar data2 = data%10;
uchar bcd = data1*16 + data2;
return bcd;
}
//BCD码转数据
//十进制转BCD码,就相当于把这个十进制数看成十六进制,然后再转换成二进制数,十六进制的10相当于十进制的16;所以后面求出来的商要乘16
uchar bcd_data(uchar bcd)
{
uchar bcd1 = bcd/16;
uchar bcd2 = bcd%16;
uchar data = bcd1*10 + bcd2;
return data;
}
/*=============================================*/
void setup()
{
Serial.begin(9600); //串口初始化,波特率为9600
pinMode(RST,OUTPUT); //引脚初始化,RST为输出模式
pinMode(DAT,OUTPUT); //引脚初始化,DAT为输出模式
pinMode(CLK,OUTPUT); //引脚初始化,CLK为输出模式
//设置DS1302时钟日历寄存器
//关闭写保护,0x8e是DS1302中寄存器的写保护寄存器地址,置0,则关闭写保护;读的是0x8f
DS1302_Write_Data(0x8e,0);
//写秒,秒的写寄存器是0x80,秒的读寄存器是0x81;由于DS1302的写入的数据都是BCD码,需要先用data_bcd()函数对普通的十进制数进行处理,返回后的值再代入 DS1302_Write_Data 函数中
DS1302_Write_Data(0x80,data_bcd(30)); //30秒
//写分钟,分钟的写寄存器是0x82,分钟的读寄存器是0x83;由于DS1302的写入的数据都是BCD码,需要先用data_bcd()函数对普通的十进制数进行处理,返回后的值再代入 DS1302_Write_Data 函数中
DS1302_Write_Data(0x82,data_bcd(15)); //15分
//写小时,小时的写寄存器是0x84,小时的读寄存器是0x85;由于DS1302的写入的数据都是BCD码,需要先用data_bcd()函数对普通的十进制数进行处理,返回后的值再代入 DS1302_Write_Data 函数中
DS1302_Write_Data(0x84,data_bcd(19)); //19时
//开启写保护,0x8e是DS1302中寄存器的写保护寄存器地址,最高位置1,也就是0x80,则开启写保护;读的是0x8f
DS1302_Write_Data(0x8e,0x80);
}
void loop()
{
//定义 秒、分、时 三个变量
uchar second, minute, hour;
while(1) //无限循环
{
DS1302_Write_Data(0x8e,0); //关闭写保护
second = bcd_data(DS1302_Read_Data(0x81)); //读取当前秒,DS1302_Read_Data(0x81)返回的值是BCD码,要转化成普通的十进制数才看得懂,外面再套上bcd_data函数
minute = bcd_data(DS1302_Read_Data(0x83)); //读取当前分,DS1302_Read_Data(0x83)返回的值是BCD码,要转化成普通的十进制数才看得懂,外面再套上bcd_data函数
hour = bcd_data(DS1302_Read_Data(0x85)); //读取当前时,DS1302_Read_Data(0x85)返回的值是BCD码,要转化成普通的十进制数才看得懂,外面再套上bcd_data函数
DS1302_Write_Data(0x8e,0x80); //打开写保护
Serial.print(hour); //打印当前秒
Serial.print(":"); //打印分隔符
Serial.print(minute); //打印当前分
Serial.print(":"); //打印分隔符
Serial.println(second); //打印当前时,并且println比print多了'\n'换行符,
delay(1000); //延时1秒
}
}
在Arduino IDE上的串口监视器上就能看见设置的时间,并且时间在变化;
需要注意的是,串口监视器关闭后重新打开,就相当于是把整个程序又重新执行了一遍,如果代码里有设置时间的这一部分,那么就相当于时间不会没有被断电保持。