第21章 SPI-DS1302时钟实验
第二十一章 DS1302时钟实验
1. 导入
在前面章节, 我们介绍了如何使用单片机 IO 口模拟 IIC 总线、 单总线时序。这一章我们来学习 DS1302 时钟芯片, 该芯片是 3 线 SPI 接口, 所以需要使用 51单片机的 3 个 IO 口模拟 SPI 时序与 DS1302 时钟芯片通信, 将时钟日历数据读取出来。
2. DS1302时钟芯片介绍
DS1302 是 DALLAS 公司推出的涓流充电时钟芯片, 内含有一个实时时钟/日历和 31 字节静态 RAM, 通过简单的串行接口与单片机进行通信。 实时时钟/日历电路提供秒、 分、 时、 日、 周、 月、 年的信息, 每月的天数和闰年的天数可自动调整。 时钟操作可通过 AM/PM 指示决定采用 24 或 12 小时格式。 DS1302 与单片机之间能简单地采用同步串行的方式进行通信, 仅需用到三根通信线: ①RES复位②I/O 数据线③SCLK 串行时钟。 时钟/RAM 的读/写数据以一个字节或多达31 个字节的字符组方式通信。 DS1302 工作时功耗很低保持数据和时钟信息时功率小于 1mW。
下面来看下 DS1302 芯片的管脚及功能:
-
VCC2: 主电源引脚
-
X1、 X2: DS1302 外部晶振引脚, 通常需外接 32.768K 晶振
-
GND: 电源地4,
-
CE: 使能引脚, 也是复位引脚( 新版本功能变) 。
-
I/O: 串行数据引脚, 数据输出或者输入都从这个引脚
-
SCLK: 串行时钟引脚
-
VCC1: 备用电源
操作 DS1302 的大致过程, 就是将各种数据写入 DS1302 的寄存器, 以设置它当前的时间的格式。 然后使 DS1302 开始运作, DS1302 时钟会按照设置情况运转, 再用单片机将其寄存器内的数据读出。 再用液晶显示, 就是我们常说的简易电子钟。 所以总的来说 DS1302 的操作分 2 步( 显示部分属于液晶显示的内容,不属于 DS1302 本身的内容) , 但是在讲述操作时序之前, 我们要先看看寄存器, DS1302 有一个控制寄存器、 12 个日历、 时钟寄存器和 31 个 RAM。
- 控制寄存器
控制寄存器用于存放 DS1302 的控制命令字, DS1302 的 RST 引脚回到高电平后写入的第一个字节就为控制命令。 它用于对 DS1302 读写过程进行控制, 格式如下:
上图是 DS1302 的寄存器样式, 我们看到:
-
第7位永远是1
-
第 6 位, 1 表示 RAM, 寻址内部存储器地址; 0 表示 CK, 寻址内部寄存器;
-
第 5 到第 1 位, 为 RAM 或者寄存器的地址;
-
最低位, 高电平表示 RD, 即下一步操作将要“ 读” ; 低电平表示 W, 即下一步操作将要“ 写” 。
比如要读秒寄存器则命令为 1000 0001, 反之写为 1000 0000
- 日历/时钟寄存器
DS1302 共有 12 个寄存器, 其中有 7 个与日历、 时钟相关, 存放的数据为 BCD码形式。 格式如下:
下面对几个寄存器做下说明:
-
秒寄存器: 低四位为秒的个位, 高的次三位为秒的十位。 最高位 CH 为DS1302 的运行标志, 当 CH=0 时, DS1302 内部时钟运行, 反之 CH=1 时停止;
-
小时寄存器: 时寄存器。 最高位为 12/24 小时的格式选择位, 该位为 1 时表示 12 小时格式。 当设置为 12 小时显示格式时, 第 5 位的高电平表示下午( PM) ; 而当设置为 24 小时格式时, 第 5 位位具体的时间数据。
-
写保护寄存器: 当该寄存器最高位 WP 为 1 时, DS1302 只读不写, 所以要在往 DS1302 写数据之前确保 WP 为 0。
-
慢充电寄存器( 涓细电流充电) 寄存器: 我们知道, 当 DS1302 掉电时, 可以马上调用外部电源保护时间数据。 该寄存器就是配置备用电源的充电选项的。其中高四位( 4 个 TCS) 只有在 1010 的情况下才能使用充电选项; 低四位的情况与 DS1302 内部电路有关
前面我们提到在日历/时钟寄存器中都是以 BCD 码存放数据, 那么 BCD 码是什么呢? BCD 码是通过 4 位二进制码来表示 1 位十进制中的 0~9 这 10 个数码。如下所示:
3. 硬件设计
本实验使用到硬件资源如下:
-
动态数码管
-
DS1302
动态数码管电路在前面章节都介绍过, 这里就不再重复。 下面我们来看下开发板上 DS1302 时钟模块电路, 如下图所示:
从上图中可以看出, 该电路是独立的, DS1302 芯片的控制管脚接至 J3 端子上, 芯片的 VCC1 脚外接了一个纽扣电池 BT1, 以保证系统断电后时钟仍然可以运行, 在芯片的 X1、 X2 管脚处外接了一个 32.768KHZ 晶振, 为时钟运行提供一个稳定的时钟频率, C2 和 C3 为旁路电容, 目的是消除晶振起振时产生的电感干扰。
4. 软件设计
本章所要实现的功能是: 数码管上显示电子时钟时分秒, 格式为“ XX-XX-XX”。程序框架如下:
-
编写数码管显示功能
-
编写 DS1302 时钟读写功能
-
编写主函数
下面我们开始写DS1302时钟读写相关函数:
#ifndef _ds1302_H
#define _ds1302_H
#include "public.h"
//管脚定义
sbit DS1302_RST = P3^5; // 复位管脚
sbit DS1302_CLK = P3^6; // 时钟管脚
sbit DS1302_IO = P3^4; // 数据管脚
//变量声明
extern unsigned char gDS1302_TIME[7]; // 存储时间
// 函数声明
void ds1302_init(void);
void ds1302_read_time(void);
#endif
#include "ds1302.h"
#include "intrins.h"
//---DS1302写入和读取时分秒的地址命令
//---秒分时日月周年 最低位读写位
unsigned char gREAD_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};
unsigned char gWRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};
//---DS1302时钟初始化2021年5月20日星期四13点51分47秒
//---存储顺序是秒分时日月周年,存储格式是用BCD码
unsigned char gDS1302_TIME[7] = {0x47, 0x51, 0x13, 0x20, 0x04, 0x05, 0x21};
// DS1302写单字节
void ds1302_write_byte(unsigned char addr, unsigned char dat)
{
unsigned char i = 0;
DS1302_RST = 0;
_nop_();
DS1302_CLK = 0; // CLK低电平
_nop_();
DS1302_RST = 1; // RST由低到高变化,复位
_nop_();
for(i = 0; i < 8; i++) // 循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = addr&0x01;
addr >>= 1;
DS1302_CLK = 1;
_nop_();
DS1302_CLK = 0; // 产生下降沿,将数据写入
_nop_();
}
for(i = 0; i < 8; i++) // 循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = dat&0x01;
dat >>= 1;
DS1302_CLK = 1;
_nop_();
DS1302_CLK = 0; // 产生下降沿,将数据写入
_nop_();
}
DS1302_RST = 0; // RST拉低
_nop_();
}
// DS1302读单字节
unsigned char ds1302_read_byte(unsigned char addr)
{
unsigned char i = 0;
unsigned char temp = 0;
unsigned char value = 0;
DS1302_RST=0;
_nop_();
DS1302_CLK = 0; // CLK低电平
_nop_();
DS1302_RST = 1; // RST由低到高变化
_nop_();
for(i = 0; i < 8; i++) // 循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = addr&0x01;
addr >>= 1;
DS1302_CLK = 1;
_nop_();
DS1302_CLK = 0; // CLK由低到高产生一个上升沿,从而写入数据
_nop_();
}
for(i = 0; i < 8; i++) // 循环8次,每次读1位,先读低位再读高位
{
temp=DS1302_IO;
value=(temp<<7) | (value>>1); // 先将value右移1位,然后temp左移7位,最后或运算
DS1302_CLK = 1;
_nop_();
DS1302_CLK = 0;
_nop_();
}
DS1302_RST = 0; // RST拉低
_nop_();
DS1302_CLK = 1; // 对于实物中,P3.4口没有外接上拉电阻的,此处代码需要添加,使数据口有一个上升沿脉冲。
_nop_();
DS1302_IO = 0;
_nop_();
DS1302_IO = 1;
_nop_();
return value;
}
// DS1302初始化时间
void ds1302_init(void)
{
unsigned char i =0;
ds1302_write_byte(0x8E, 0X00); // 写入初始时间
for(i = 0; i < 7; i++)
{
ds1302_write_byte(gWRITE_RTC_ADDR[i], gDS1302_TIME[i]);
}
ds1302_write_byte(0x8E, 0X80);
}
// DS1302读取时间
void ds1302_read_time(void)
{
unsigned char i = 0;
for(i = 0; i < 7; i++)
{
gDS1302_TIME[i] = ds1302_read_byte(gREAD_RTC_ADDR[i]);
}
}
5. 小结
各函数功能在代码内有详细注释, 大家可以参考注释理解。
程序开头定义了3 个数组: gREAD_RTC_ADDR、 gWRITE_RTC_ADDR 和 gDS1302_TIME。 gREAD_RTC_ADDR和 gWRITE_RTC_ADDR 数组内存储的是 DS1302 写入和读取时分秒的地址命令, 这个可以对照前面介绍的寄存器看。
TIME 数组是用来存储初始化时间, 每个数据对应含义在代码内已做注释。 在使用中主要调用 ds1302_init()函数和ds1302_read_time()函数来初始化 DS1302 时钟数据以及读取 DS1302 时钟数据。
在初始化时钟数据时, 首先要禁止写保护即关闭写保护功能, 然后将所需设置的时钟数据写入到对应的时钟寄存器地址内, 最后打开写保护功能, 以防止意外修改 DS1302 内部寄存器。 对于读取时钟数据其实很简单, 只要从对应的时钟寄存器地址内读取数据即可, 然后将读取的数据存储到一个缓存数组中, 方便数据的处理与显示, 这个操作在后面 main.c 文件内会有。
#include "public.h"
#include "smg.h"
#include "ds1302.h"
void main()
{
unsigned char time_buf[8];
ds1302_init(); // 初始化DS1302
while(1)
{
ds1302_read_time(); // 写入数据
time_buf[0] = gsmg_code[gDS1302_TIME[2]/16];
time_buf[1] = gsmg_code[gDS1302_TIME[2]&0x0f];
time_buf[2] = 0x40;
time_buf[3] = gsmg_code[gDS1302_TIME[1]/16];
time_buf[4] = gsmg_code[gDS1302_TIME[1]&0x0f];
time_buf[5] = 0x40;
time_buf[6] = gsmg_code[gDS1302_TIME[0]/16];
time_buf[7] = gsmg_code[gDS1302_TIME[0]&0x0f];
smg_display(time_buf,1);
}
}
主函数代码非常简单, 首先调用外设头文件, 然后初始化 DS1302 并设定好初始时间, 进入 while 循环, 读取 DS1302 时钟数据存储至全局变量数组gDS1302_TIME 中, 最后将读取的数据转换为数码管可显示的段码数据并调用数码管显示函数显示时间。
细心的朋友可能发现, 在处理 DS1302 读取的数据时, 取高低位是使用除 16和取余 16, 并非之前的除 10 和取余 10。 这是因为写入进 DS1302 时是 BCD 码,读取出的数据也是 BCD 码, 而 BCD 码即是 4 位表示一个十进制数, 类似于一个字节的十六进制数据的高 4 位和低 4 位一样, 所以这里是除 16 和取余 16。
2024.7.21 第一次修订
2024.8.22 第二次修订,后期不再维护