一起玩转树莓派(22)——DS1302硬件时钟实践

不飞则已,一飞冲天;不鸣则已,一鸣惊人。这篇文章主要讲述一起玩转树莓派(22)——DS1302硬件时钟实践相关的知识,希望能为你提供帮助。
一起玩转树莓派(22)——DS1302硬件时钟实践不知你是否有发现,我们在使用计算机时,除了第一次启动需要同步下时间外,即是没有联网,断电重启后,计算机的时间依然是准确的。这是因为在计算机主机内部有一个自带电源的硬件时钟模块,在同步时间时将当前的时间写入模块后,此硬件时钟模块会自动的维护准确的当前时间。树莓派内部本身没有硬件时钟模块,但是在某些非联网的需求场景中,我们需要准确的记录当前的日期时间,比如之前我们介绍过许多有关气象相关的传感器,在记录气象数据时,也需要记录当时的准确时间。
1. DS1302模块简介DS1302是一款涓流充电计时芯片。其拥有实时时钟可以计算年,月,日,时,分,秒,星期等信息,可使用的时间间隔为2000年到2100年之间。除此之外,其还拥有一个31*8位的通用暂存RAM,用来存储一些临时的逻辑数据。DS1302采用同步串行通信的方式,简化了处理器的接口,理论上除了电源之外,主需要连接三根线即可实现通信功能,即CE线,I/O线和SCLK串行时钟线。
DS1302芯片有8个引脚,工作电路结构如下图所示:

一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

每个引脚的作用如下:
一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

DS1302采用的是8位的指令命令字,在每次通信前,都需要使用命令字来启动,命令字结构如下:
一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

如上图所示,其中第7位是写入有效控制位,为0时写入无效,为1时允许写入,每次进行数据传输时,必须将此位设置为1,一般我们在使用指令时,此为强制设置为1。
第6位控制芯片功能,为0时表示使用时钟数据,为1时表示使用RAM数据。
第1位到第5位为寄存器选择位,选择要操作的寄存器。
【一起玩转树莓派(22)——DS1302硬件时钟实践】第0位是读写控制位,为0时表示要写数据到寄存器,为1时表示要从寄存器读数据。
了解了DS1302的指令结构,要使用它我们还需要对寄存器的功能做简单的了解,本次实验我们主要使用DS1302的时钟功能,与时钟功能相关的寄存器有7个,芯片手册中给出了示例,如下:
一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

下面我们介绍下这些寄存器的功能和用法。
首先,指令中有5位来标识要操作的寄存器的地址,上图中直接给我了操作寄存器的读写指令。
第1个寄存器的地址为0b0,因此其读指令为0b1000 0001,写指令为0b1000 0000,转换成16进制,即上图中的0x81与0x80。此寄存器的最高位CH是一个标志位,每次上电后,我们可以读取一下这一位,如果是1表示断电后始终系统已经不工作了,我们需要重新校准时间,如果是0表示备用电源正常,始终持续正常运行。第4到第6位用来表示时间秒的十位数据,第0到第3位用来表示秒的个位数据,因为秒的十位数据最大为5,3个二进制位足够使用。
第2个寄存器的地址为0b1,其最高位为保留位,目前无用,第4位到第6位用来藐视分钟的十位,第0位到第3位表示分钟的个位,同样分钟的十位数据最大为5,3个二进制位足够使用。
第3个寄存器的地址为0b10,其最高位为1表示当前使用的是12小时制,最高位是0表示使用的是24小时制。第6位固定为0,没有意义。第5位在12小时制的模式下,为1表示的是下午,为0表示的是上午。在24小时制的模式下,第4位和第5位一起表示小时的十位。第0位到第3位表示了小时的个位。
第4个寄存器的地址为0b11,其第7位和第6位为固定的0,没有意义。第5位和第4位一起表示了日期的十位,第0位到第3位表示了日期的个位。
第5个寄存器的地址为0b100,其第5位到第7位为固定的0,没有意义。第4位表示了月的十位,第0到第3位表示月的个位。
第6个寄存器的地址为0b101,其高5位固定为0,没有意义。第3位表示了星期。
第7个寄存器的地址为0b110,高4位表示了年的十位,第4位表示了年的个位,可以表示0-99之间的值,即2000年到2099年。
第8个寄存器的地址为0b111,这是一个控制寄存器,在写数据时,这个寄存器的最高位WP必须是0,如果此位为1,则给其他寄存器的写操作都将禁止,用来做保护。
如果要使用到RAM功能,则指令的前两位必须是11,因此RAM的第一个寄存器的读写指令为0b1100 0000或0b1100 0001 ,即0xC0和0xC1。RAM存储寄存器有31个,地址为0x00到0x1F,对应的命令字从0xC0到0xFF。
无论是时钟模式,还是RAM模式,DS1302都支持采用脉冲串的方式来读写数据,时钟脉冲串指令为0xBF与0xBE,RAM脉冲串为0xFF与0xFE。数据会按照寄存器的顺序依次写入和读出。
明白了指令与寄存器的使用逻辑,下面还剩下一点需要明确,那就是如何进行数据的输入和读取。RST引脚用来控制数据读写,RTS从低电平变成高电平触发一次数据传输过程。
2. 实验连线本次实验,我们使用的是封装好的DS1302模块,如下图所示:
一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

其中CLK对应SCLK引脚,DAT是IO功能引脚,RST对应CE功能引脚。我们选用树莓派中的16,18和22引脚(物理编码,分别对应BCM编码的23,24和25),连线如下:
DS1302 树莓派
VCC 3.3V
GND GND
CLK GPIO23(BCM编码)
DAT GPIO24(BCM编码)
RST GPIO25(BCM编码)
3.实验编码现在,我们只需要按照前面介绍的模块用法进行编码即可,示例代码如下:
#coding:utf-8import time import RPi.GPIO from datetime import datetime# 使用物理编码 SCL = 16 IO = 18 RST = 22# 数据读写的间隔 CLK_PERIOD = 0.00001# 关闭GPIO警告 RPi.GPIO.setwarnings(False) # 配置树莓派GPIO接口 使用物理编码 RPi.GPIO.setmode(RPi.GPIO.BOARD)# 写入一个字节的数据 def writeByte(Byte): for Count in range(8): # 将SCL置为低电平 开启一次传输 time.sleep(CLK_PERIOD) RPi.GPIO.output(SCL, 0) # 取一位数据进行写入 Bit = Byte % 2 Byte = int(Byte / 2) # 通过IO引脚进行写入 time.sleep(CLK_PERIOD) RPi.GPIO.output(IO, Bit) # 将SCL置为高电平 结束一次传输 time.sleep(CLK_PERIOD) RPi.GPIO.output(SCL, 1)# 读取一个字节的数据 def readByte(): # 将IO引脚设置为输入 RPi.GPIO.setup(IO, RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN) Byte = 0 for Count in range(8): # 先将SCL重置为高电平 time.sleep(CLK_PERIOD) RPi.GPIO.output(SCL, 1) # 将SCL置为低电平 开启一次传输 time.sleep(CLK_PERIOD) RPi.GPIO.output(SCL, 0) # 读取一位数据 time.sleep(CLK_PERIOD) Bit = RPi.GPIO.input(IO) Byte |= ((2 ** Count) * Bit) return Byte# 重置一些数据 def resetDS1302(): # SCL引脚设置为输出 RPi.GPIO.setup(SCL, RPi.GPIO.OUT, initial=0) # RST引脚设置为输出 RPi.GPIO.setup(RST, RPi.GPIO.OUT, initial=0) # IO引脚设置为输出 RPi.GPIO.setup(IO, RPi.GPIO.OUT, initial=0) # SCL和IO都置为低电平 RPi.GPIO.output(SCL, 0) RPi.GPIO.output(IO, 0) time.sleep(CLK_PERIOD) # RST置为高电平 RPi.GPIO.output(RST, 1)# 结束操作 def endDS1302(): # SCL引脚设置为输出 RPi.GPIO.setup(SCL, RPi.GPIO.OUT, initial=0) # RST引脚设置为输出 RPi.GPIO.setup(RST, RPi.GPIO.OUT, initial=0) # IO引脚设置为输出 RPi.GPIO.setup(IO, RPi.GPIO.OUT, initial=0) # SCL和IO都置为低电平 RPi.GPIO.output(SCL, 0) RPi.GPIO.output(IO, 0) time.sleep(CLK_PERIOD) # RST置为低电平 RPi.GPIO.output(RST, 0)# 进行时间校准 def setDatetime(year, month, day,hour, minute, second, dayOfWeek): # 引脚重置 resetDS1302() # 设置写始终数据脉冲指令 writeByte(int("10111110", 2))# 开始依次写数据 # 写入秒数据,*16的作用是把十位右移4位 下面同 writeByte((second % 10) | int(second / 10) * 16) # 写入分钟数据 writeByte((minute % 10) | int(minute / 10) * 16) # 写入小时数据 writeByte((hour % 10) | int(hour / 10) * 16) # 写入日期数据 writeByte((day % 10) | int(day / 10) * 16) # 写入月份数据 writeByte((month % 10) | int(month / 10) * 16) # 写入星期数据 writeByte(dayOfWeek) # 写入年份数据 writeByte((year % 100 % 10) | int(year % 100 / 10) * 16) # 结束数据写入 writeByte(int("00000000", 2)) # 结束任务 endDS1302()# 获取DS1302硬件时钟实践 def getDatetime(): # 重置引脚 resetDS1302() # 0xBF指令,开始时钟脉冲串读取数据 writeByte(int("10111111", 2))Datahttps://www.songbingjia.com/android/= ""# 依次读取 # 先读出秒数据 Byte = readByte() second = (Byte % 16) + int(Byte / 16) * 10 # 分钟数据 Byte = readByte() minute = (Byte % 16) + int(Byte / 16) * 10 # 小时数据 Byte = readByte() hour = (Byte % 16) + int(Byte / 16) * 10 # 日期数据 Byte = readByte() day = (Byte % 16) + int(Byte / 16) * 10 # 月份数据 Byte = readByte() month = (Byte % 16) + int(Byte / 16) * 10 # 星期数据 Byte = readByte() day_of_week = (Byte % 16) # 年数据 Byte = readByte() year = (Byte % 16) + int(Byte / 16) * 10 + 2000# 结束任务 endDS1302() return datetime(year, month, day, hour, minute, second)# 时间格式化 def format_time(dt): if dt is None: return "" fmt = "%m/%d/%Y %H:%M" return dt.strftime(fmt)def parse_time(s): fmt = "%m/%d/%Y %H:%M" return datetime.strptime(s, fmt)# 初始化工作resetDS1302()# 写保护已关闭。 writeByte(int("10001110", 2)) # 结束指令执行 writeByte(int("00000000", 2)) # 涓流充电模式被关闭。 writeByte(int("10010000", 2)) # 结束指令执行 writeByte(int("00000000", 2)) #结束任务 endDS1302()current = datetime.now()year = current.year month = current.month day = current.day hour = current.hour minute = current.minute second = current.second week = current.weekday()setDatetime(year,month,day,hour,minute,second,week)while True: time.sleep(1) dt = getDatetime() print(dt)

上面示例代码中,无论是读数据还是写数据,我们都采用的时钟脉冲串的通信模式,这样我们只需要设置一次指令,即可按需读出所需数据,非常方便。在树莓派上运行上面的代码,效果如下图所示:
一起玩转树莓派(22)——DS1302硬件时钟实践

文章图片

4.思考一下仿照上面的思路,是否可以改成非时钟脉冲串的通信方式来获取日期时间信息呢?试试吧!

    推荐阅读