logo头像

学如逆水行舟

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

一起玩转树莓派(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 GND
CLK GPIO23(BCM编码)
DAT GPIO24(BCM编码)
RST GPIO25(BCM编码)

3.实验编码

现在,我们只需要按照前面介绍的模块用法进行编码即可,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#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.思考一下

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

专注技术,懂的热爱,愿意分享,做个朋友

QQ:316045346