前言
现在流行的串行时钟电路很多,如DS1302、 DS1307、PCF8485等。这些电路的接口简单、价格低廉、使用方便,被广泛地采用。
- 本文介绍的实时时钟电路DS1302是DALLAS公司的一种具有涓细电流充电能力的电路
- 主要特点是采用串行数据传输,可为掉电保护电源提供可编程的充电功能,并且可以关闭充电功能。采用普通32.768kHz晶振。
一、DS18B20芯片介绍
DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。
RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片。
特征
DS1302模块:
二、芯片引脚定义与应用电路
晶振给芯片提供一个时钟源
典型工作电路
51单片机DS1302电路图
蓝桥杯15单片机开发板实际电路
注意51单片机与15单片机使用的I/O口是不同的
该开发板中没有接备用电池,因此掉电再上电之后是初始化设置的时间。
带电池的DS1302模块可以存储时间
引脚名 | 作用 |
VCC2 | 主电源 |
VCC1 | 备用电池 |
GND | 电源接地 |
X1,X2 | 32.768kHz晶振 |
CE | 芯片使能 |
IO | 数据输入/输出 |
SCLK | 串行时钟 |
三、寄存器定义
DS1302相关寄存器如下:
- 第一行的 CH(BIT7)表示时钟暂停控制位,这一位置1代表时钟暂停,置0时钟正常运行
- 倒数第二行的 WP(BIT7),这一位置1代表芯片写保护,写入操作无效,置0即可解除芯片写保护,就可以写入数据
- 12/24 第四行 BIT7 =0,是24小时模式
- 如果BIT7 = 1 是12小时模式 BIT5 = 0是AM, BIT5 = 1是PM
DS1302有12个寄存器,其中有7个寄存器与日历、时钟相关,存放的数据位为BCD码形式,其日历、时间寄存器如上图所示。
此外,DS1302 还有年份寄存器、控制寄存器、充电寄存器、时钟突发寄存器及与RAM相关的寄存器等。时钟突发寄存器可一次性顺序读写除充电寄存器外的所有寄存器内容。 DS1302与RAM相关的寄存器分为两类:一类是单个RAM单元,共31个,每个单元组态为一个8位的字节,其命令控制字为C0H~FDH,其中奇数为读操作,偶数为写操作;另一类为突发方式下的RAM寄存器,此方式下可一次性读写所有的RAM的31个字节,命令控制字为FEH(写)、FFH(读)。
四、命令字
- 第0位表示读还是写(置1表示读,置0表示写);
- 第1-5位表示地址(秒地址为0,分钟地址为10,…);
- 第6位表示操作RAM还是时钟CK(置1表示操作RAM,置0表示操作CK);
- 第7位:固定为1;
举个例子:
对时钟操作、秒地址操作、读:
10000001(0x81)
对时钟操作、秒地址操作、写:
10000000(0x80)
这与上面寄存器中的读地址和写地址是对应的。
五、数据流
在控制指令字输入后的下一个SCLK时钟的上升沿时,数据被写入DS1302,数据输入从低位即位0开始。同样,在紧跟8位的控制指令字后的下一个SCLK脉冲的下降沿读出DS1302的数据,读出数据时从低位0位到高位7。
六、时序图与数据读写
读写时序图:
从时序图中也能看出DS1302使用了三个引脚:CE(使能端)、SCLK(时钟)、I/O(数据)
规定:在时钟的上升沿,I/O口的数据将会被写入,在时钟的下降沿,时钟芯片的数据将会被读出。
初始化
1. //上电默认1,先初始化为0 2. void DS1302_Init(void) 3. { 4. DS1302_CE = 0; // 5. DS1302_SCLK = 0; 6. }
6.1 单字节写(Write)
从时序图中可以看到,要发两个字节:第一个字节是命令字(先发最低位R/W),第二个字节是数据;【一位一位依次发送】,一共发送16位数据,产生16个脉冲
单字节写数据步骤
将CE置1;
将命令字最低位放在I/O线上,等待SCLK的上升沿写入单片机
SCLK给上升沿,发送最低位
SCLK给下降沿
将命令字次低位放在I/O线上
SCLK给上升沿,发送次低位
SCLK给下降沿
…(依次循环8次,将命令字发送出去)
将要写入的数据最低位放在I/O线上,等待SCLK的上升沿写入单片机
SCLK给上升沿,发送最低位
SCLK给下降沿
将数据次低位放在I/O线上
SCLK给上升沿,发送次低位
SCLK给下降沿
…(依次循环8次,将数据发送出去)
将CE置0;
示例代码:
1. //单字节写(Command命令字(地址),Data数据) 2. void DS1302_WriteByte(unsigned char Command, unsigned char Data) 3. { 4. unsigned char i; 5. DS1302_CE = 1; 6. for (i = 0; i < 8; i ++) //写命令字 7. { 8. DS1302_IO = Command & (0x01<<i);//将命令字放在I/O线上,等待写入 9. DS1302_SCLK = 1; //上升沿写入 10. DS1302_SCLK = 0; // (准备下一次写入) 11. } 12. for (i = 0; i < 8; i ++) //写数据 13. { 14. DS1302_IO = Data & (0x01<<i);//将数据放在I/O线上,等待写入 15. DS1302_SCLK = 1; //上升沿写入 16. DS1302_SCLK = 0; 17. } 18. DS1302_CE = 0; 19. }
从低位向高位写数据,嵌套一个for循环,用Command & (0x01<
6.2 单字节读(Read)
注意:从上述时序图中可以看出,单字节写(Write)有16个脉冲,而单字节读(Read)只有15个脉冲,因为当最后一个命令字的上升沿之后的下降沿数据马上就读出来了,如下图所示。
读时序的SCLK有15个脉冲,相比单字节写少了一个脉冲,写代码要注意
如果写命令字和之前一样,错误示例代码如下
1. for (i = 0; i < 8; i ++) //写命令字 2. { 3. DS1302_IO = Command & (0x01<<i); 4. DS1302_SCLK = 1; 5. DS1302_SCLK = 0; 6. 7. }
写完8位数据,要读取的第一位数据已经出来了,这样写是有问题的
我们需要的时序是这样的:先给0再给1(蓝线),这样写完命令字就到了图示位置(红线)
后面读取的时候,从红线位置开始,先给1再给0(蓝线),循环8次就能全部读取出来
单字节读取数据步骤
将CE置1;
将命令字最低位放在I/O线上,等待SCLK的上升沿写入单片机
先SCLK给下降沿
SCLK给上升沿,发送最低位
将命令字次低位放在I/O线上
SCLK给下降沿
SCLK给上升沿,发送次低位
…(依次循环8次,将命令字发送出去)
SCLK给上升沿,保持高电平,保证时序正确
SCLK给下降沿
此时下降沿读取I/O口数据
SCLK给上升沿,发送次低位
SCLK给下降沿
下降沿读取I/O口数据
…(依次循环8次,将数据读取出来)
将CE置0;
最后要讲I/O也置0
读取数据的操作:如果检测到I/O口是1,就用或运算,取出1,如果读出0,就不操作(默认当前位是0,进入下一次for循环)
示例代码:
1. //单字节读,用上面的宏定义即可 2. unsigned char DS1302_ReadByte(unsigned char Command) 3. { 4. unsigned char Data = 0x00; 5. unsigned char i; 6. Command |= 0x01;//变成读的地址 7. 8. DS1302_CE = 1; 9. for (i = 0; i < 8; i ++) //写命令字 10. { 11. DS1302_IO = Command & (0x01<<i); 12. DS1302_SCLK = 0; //先给0后给1,因为时序图上少一个脉冲 13. DS1302_SCLK = 1 14. } 15. for (i = 0; i < 8; i ++) 16. { 17. DS1302_SCLK = 1; //先给1后给0,模拟时序图 18. DS1302_SCLK = 0; 19. if(DS1302_IO == 1) //读取端口是1 20. { Data |= (0x01<<i); } //读数据操作 21. } 22. DS1302_CE = 0; 23. DS1302_IO = 0; //修改的地方 24. return Data; 25. }
Command |= 0x01;//变成读的地址
这句代码是啥意思,别急,看宏定义
宏定义
读数据比写数据的命令码大1
0x80 | 0x01 = 0x81
0x82 | 0x01 = 0x83
将写数据的命令码与0x01进行或运算,即得到读数据的命令码
所以我们将写数据的命令码用宏定义封装一下
1. #define DS1302_SECOND 0x80 //秒 2. #define DS1302_MINTUE 0x82 //分钟 3. #define DS1302_HOUR 0x84 //小时 4. #define DS1302_DATE 0x86 //日 5. #define DS1302_MONTH 0x88 //月 6. #define DS1302_DAY 0x8A //星期 7. #define DS1302_YEAR 0x8C //年 8. #define DS1302_WP 0x8E //写保护的
Command(写) | 0x01 = Command(读)
让写的地址 |= 0x01,就是读数据的地址,减少一半的宏定义!!!
实际测试一下
给 秒寄存器写个3,然后读取看结果
1. #include <STC15F2K60S2.H> 2. #include "smg.h" 3. #include "DS1302.h" 4. 5. unsigned char Sec; 6. 7. void main() 8. { 9. All_Init(); 10. DS1302_Init(); 11. 12. DS1302_WriteByte(0x80,0x03); 13. 14. while(1) 15. { 16. Sec = DS1302_ReadByte(0x81); 17. Nixie(1, Sec / 10); 18. Nixie(2, Sec % 10); 19. } 20. }
效果
3 4 5 6 7 8 9之后变成16了
原因?
上面提到过BCD码,放在这提一嘴
BCD码
十进制 | 十六进制 | BCD码 |
9 | 0000 1001 | 09 |
16 | 0001 0000 | 10 |
介绍BCD码与十进制互相转换的文章:
明白了BCD码,就可以对DS1302模块读写时间进行封装了
年 月 日 时 分 秒 星期
数据比较多,用数组存放各个数据
1. unsigned char DS1302_Time[9]={23,2,11,23,59,55,4};//年 月 日 时 分 秒 星期 2. 3. void DS1302_SetTime(void) 4. { 5. DS1302_WriteByte(DS1302_WP, 0x00);//解除芯片写保护 6. DS1302_WriteByte(DS1302_YEAR, DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码 7. DS1302_WriteByte(DS1302_MONTH, DS1302_Time[1]/10*16+DS1302_Time[1]%10); 8. DS1302_WriteByte(DS1302_DATE, DS1302_Time[2]/10*16+DS1302_Time[2]%10); 9. DS1302_WriteByte(DS1302_HOUR, DS1302_Time[3]/10*16+DS1302_Time[3]%10); 10. DS1302_WriteByte(DS1302_MINTUE, DS1302_Time[4]/10*16+DS1302_Time[4]%10); 11. DS1302_WriteByte(DS1302_SECOND, DS1302_Time[5]/10*16+DS1302_Time[5]%10); 12. DS1302_WriteByte(DS1302_DAY, DS1302_Time[6]/10*16+DS1302_Time[6]%10); //星期 13. DS1302_WriteByte(DS1302_WP, 0x80);//此句WP给0x80,打开芯片写保护 14. } 15. 16. void DS1302_ReadTime(void) //此函数读取时钟芯片的数据,写到数组DS1302_Time里 17. { 18. unsigned char Temp; 19. Temp = DS1302_ReadByte(DS1302_YEAR);//进去就被 或运算 置1,变成读,读BCD码 20. DS1302_Time[0] = Temp/16*10+Temp%16; 21. Temp = DS1302_ReadByte(DS1302_MONTH);//月 22. DS1302_Time[1] = Temp/16*10+Temp%16; 23. Temp = DS1302_ReadByte(DS1302_DATE); 24. DS1302_Time[2] = Temp/16*10+Temp%16; 25. Temp = DS1302_ReadByte(DS1302_HOUR); 26. DS1302_Time[3] = Temp/16*10+Temp%16; 27. Temp = DS1302_ReadByte(DS1302_MINTUE); 28. DS1302_Time[4] = Temp/16*10+Temp%16; 29. Temp = DS1302_ReadByte(DS1302_SECOND); 30. DS1302_Time[5] = Temp/16*10+Temp%16; 31. Temp = DS1302_ReadByte(DS1302_DAY); 32. DS1302_Time[6] = Temp/16*10+Temp%16; 33. }
6.3 DS1302时钟实现部分
给DS1302模块也封装起来
DS1302.c
1. #include "DS1302.h" 2. 3. //对端口重新定义,位变量,只进行逻辑判断,非零即真 4. sbit DS1302_CE = P1^3; 5. sbit DS1302_IO = P2^3; 6. sbit DS1302_SCLK = P1^7; 7. 8. #define DS1302_SECOND 0x80 //秒 9. #define DS1302_MINTUE 0x82 //分钟 10. #define DS1302_HOUR 0x84 //小时 11. #define DS1302_DATE 0x86 //日 12. #define DS1302_MONTH 0x88 //月 13. #define DS1302_DAY 0x8A //星期 14. #define DS1302_YEAR 0x8C //年 15. #define DS1302_WP 0x8E //写保护的 16. 17. unsigned char DS1302_Time[9]={23,2,9,23,59,55,4};//年 月 日 时 分 秒 星期 18. 19. //上电默认1,先初始化为0 20. void DS1302_Init(void) 21. { 22. DS1302_CE = 0; // 23. DS1302_SCLK = 0; 24. } 25. 26. //单字节写(命令字,数据) 27. void DS1302_WriteByte(unsigned char Command, unsigned char Data) 28. { 29. unsigned char i; 30. DS1302_CE = 1; 31. for (i = 0; i < 8; i ++) //写命令字 32. { 33. DS1302_IO = Command & (0x01<<i); 34. DS1302_SCLK = 1; 35. DS1302_SCLK = 0; 36. } 37. for (i = 0; i < 8; i ++) //写数据 38. { 39. DS1302_IO = Data & (0x01<<i); 40. DS1302_SCLK = 1; 41. DS1302_SCLK = 0; 42. } 43. DS1302_CE = 0; 44. } 45. 46. //单字节读,用上面的宏定义即可 47. unsigned char DS1302_ReadByte(unsigned char Command) 48. { 49. unsigned char Data = 0x00; 50. unsigned char i; 51. Command |= 0x01;//变成读的地址 52. 53. DS1302_CE = 1; 54. for (i = 0; i < 8; i ++) //写命令字 55. { 56. DS1302_IO = Command & (0x01<<i); 57. DS1302_SCLK = 0; //先给0后给1,因为时序图上少一个脉冲 58. DS1302_SCLK = 1; 59. } 60. for (i = 0; i < 8; i ++) 61. { 62. DS1302_SCLK = 1; //先给1后给0,模拟时序图 63. DS1302_SCLK = 0; 64. if(DS1302_IO == 1) //读取端口是1 65. { Data |= (0x01<<i); } //读数据操作 66. } 67. DS1302_CE = 0; 68. DS1302_IO = 0; //修改的地方 69. return Data; 70. } 71. 72. void DS1302_SetTime(void) 73. { 74. DS1302_WriteByte(DS1302_WP, 0x00);//解除芯片写保护 75. DS1302_WriteByte(DS1302_YEAR, DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码 76. DS1302_WriteByte(DS1302_MONTH, DS1302_Time[1]/10*16+DS1302_Time[1]%10); 77. DS1302_WriteByte(DS1302_DATE, DS1302_Time[2]/10*16+DS1302_Time[2]%10); 78. DS1302_WriteByte(DS1302_HOUR, DS1302_Time[3]/10*16+DS1302_Time[3]%10); 79. DS1302_WriteByte(DS1302_MINTUE, DS1302_Time[4]/10*16+DS1302_Time[4]%10); 80. DS1302_WriteByte(DS1302_SECOND, DS1302_Time[5]/10*16+DS1302_Time[5]%10); 81. DS1302_WriteByte(DS1302_DAY, DS1302_Time[6]/10*16+DS1302_Time[6]%10); //星期 82. DS1302_WriteByte(DS1302_WP, 0x80);//此句WP给0x80,打开芯片写保护 83. } 84. 85. void DS1302_ReadTime(void) //此函数读取时钟芯片的数据,写到数组DS1302_Time里 86. { 87. unsigned char Temp; 88. Temp = DS1302_ReadByte(DS1302_YEAR);//进去就被 或运算 置1,变成读,读BCD码 89. DS1302_Time[0] = Temp/16*10+Temp%16; 90. Temp = DS1302_ReadByte(DS1302_MONTH);//月 91. DS1302_Time[1] = Temp/16*10+Temp%16; 92. Temp = DS1302_ReadByte(DS1302_DATE); 93. DS1302_Time[2] = Temp/16*10+Temp%16; 94. Temp = DS1302_ReadByte(DS1302_HOUR); 95. DS1302_Time[3] = Temp/16*10+Temp%16; 96. Temp = DS1302_ReadByte(DS1302_MINTUE); 97. DS1302_Time[4] = Temp/16*10+Temp%16; 98. Temp = DS1302_ReadByte(DS1302_SECOND); 99. DS1302_Time[5] = Temp/16*10+Temp%16; 100. Temp = DS1302_ReadByte(DS1302_DAY); 101. DS1302_Time[6] = Temp/16*10+Temp%16; 102. }
DS1302.h
1. #ifndef __DS1302_H__ 2. #define __DS1302_H__ 3. 4. #include <STC15F2K60S2.H> 5. 6. extern unsigned char DS1302_Time[9];//年月日时分秒星期 7. //外部变量必须加extern,外部数组或函数可以不加extern 8. 9. void DS1302_Init(void); 10. void DS1302_WriteByte(unsigned char Command, unsigned char Data); 11. unsigned char DS1302_ReadByte(unsigned char Command); 12. 13. void DS1302_SetTime(void); 14. void DS1302_ReadTime(void); 15. 16. #endif
main.c
用数码管和独立按键做个界面显示,默认显示时间界面,按键7也显示时间界面,按键6显示年月日界面,按键5显示星期界面
1. #include <STC15F2K60S2.H> 2. #include "smg.h" 3. #include "Key.h" 4. #include "Delay.h" 5. #include "DS1302.h" 6. 7. unsigned char Keynum; 8. unsigned char Show_Flag = 0; 9. void main() 10. { 11. All_Init(); 12. DS1302_Init(); 13. 14. DS1302_SetTime(); 15. 16. while(1) 17. { 18. Keynum = Duli_Anjian(); 19. DS1302_ReadTime(); 20. if (Keynum == 7) Show_Flag = 0; //按键7 21. else if (Keynum == 6) Show_Flag = 1; //按键6 22. else if (Keynum == 5) Show_Flag = 2; 23. 24. if (0 == Show_Flag)//按键7 或默认状态 25. { 26. Nixie(1, DS1302_Time[3]/10); //时 27. Nixie(2, DS1302_Time[3]%10); 28. Nixie(3, 16); //显示 - 29. Nixie(4, DS1302_Time[4]/10); //分 30. Nixie(5, DS1302_Time[4]%10); 31. Nixie(6, 16); //显示 - 32. Nixie(7, DS1302_Time[5]/10); //秒 33. Nixie(8, DS1302_Time[5]%10); 34. } 35. else if (1 == Show_Flag)//按键6 36. { 37. Nixie(1, DS1302_Time[0]/10); //年 38. Nixie(2, DS1302_Time[0]%10); 39. Nixie(3, 16); //显示 - 40. Nixie(4, DS1302_Time[1]/10); //月 41. Nixie(5, DS1302_Time[1]%10); 42. Nixie(6, 16); //显示 - 43. Nixie(7, DS1302_Time[2]/10); //日 44. Nixie(8, DS1302_Time[2]%10); 45. } 46. else if (2 == Show_Flag) 47. { 48. Nixie(1, DS1302_Time[6]); 49. Nixie(2,17); //熄灭码 50. Nixie(3,17); 51. Nixie(4,17); 52. Nixie(5,17); 53. Nixie(6,17); 54. Nixie(7,17); 55. Nixie(8,17); 56. } 57. 58. } 59. }
界面显示的亮度差强人意,还是用定时器刷新数码管比较好
参考资料:
B站视频(链接打不开)名称如下:
江科大自化协-51单片机入门教程-2020版 程序全程纯手打 从零开始入门【10-1】【10-2】