一起玩转树莓派(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位为寄存器选择位,选择要操作的寄存器。

第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-8

import 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))

    Data = ""
    
    # 依次读取
    # 先读出秒数据
    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.思考一下

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

专注技术,懂的热爱,愿意分享,做个朋友
上一篇:自上而下的理解网络(4)——TCP篇


下一篇:一起玩转树莓派(19)——红外遥控控制实验