一起玩转树莓派(22)——DS1302硬件时钟实践
不知你是否有发现,我们在使用计算机时,除了第一次启动需要同步下时间外,即是没有联网,断电重启后,计算机的时间依然是准确的。这是因为在计算机主机内部有一个自带电源的硬件时钟模块,在同步时间时将当前的时间写入模块后,此硬件时钟模块会自动的维护准确的当前时间。树莓派内部本身没有硬件时钟模块,但是在某些非联网的需求场景中,我们需要准确的记录当前的日期时间,比如之前我们介绍过许多有关气象相关的传感器,在记录气象数据时,也需要记录当时的准确时间。
1. DS1302模块简介
DS1302是一款涓流充电计时芯片。其拥有实时时钟可以计算年,月,日,时,分,秒,星期等信息,可使用的时间间隔为2000年到2100年之间。除此之外,其还拥有一个31*8位的通用暂存RAM,用来存储一些临时的逻辑数据。DS1302采用同步串行通信的方式,简化了处理器的接口,理论上除了电源之外,主需要连接三根线即可实现通信功能,即CE线,I/O线和SCLK串行时钟线。
DS1302芯片有8个引脚,工作电路结构如下图所示:
每个引脚的作用如下:
DS1302采用的是8位的指令命令字,在每次通信前,都需要使用命令字来启动,命令字结构如下:
如上图所示,其中第7位是写入有效控制位,为0时写入无效,为1时允许写入,每次进行数据传输时,必须将此位设置为1,一般我们在使用指令时,此为强制设置为1。
第6位控制芯片功能,为0时表示使用时钟数据,为1时表示使用RAM数据。
第1位到第5位为寄存器选择位,选择要操作的寄存器。
第0位是读写控制位,为0时表示要写数据到寄存器,为1时表示要从寄存器读数据。
了解了DS1302的指令结构,要使用它我们还需要对寄存器的功能做简单的了解,本次实验我们主要使用DS1302的时钟功能,与时钟功能相关的寄存器有7个,芯片手册中给出了示例,如下:
下面我们介绍下这些寄存器的功能和用法。
首先,指令中有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模块,如下图所示:
其中CLK对应SCLK引脚,DAT是IO功能引脚,RST对应CE功能引脚。我们选用树莓派中的16,18和22引脚(物理编码,分别对应BCM编码的23,24和25),连线如下:
DS1302 | 树莓派 |
---|---|
VCC | 3.3V |
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)
上面示例代码中,无论是读数据还是写数据,我们都采用的时钟脉冲串的通信模式,这样我们只需要设置一次指令,即可按需读出所需数据,非常方便。在树莓派上运行上面的代码,效果如下图所示:
4.思考一下
仿照上面的思路,是否可以改成非时钟脉冲串的通信方式来获取日期时间信息呢?试试吧!
专注技术,懂的热爱,愿意分享,做个朋友