MicroPython-On-ESP8266——时钟模块DS3231的使用方法
1. 模块DS3231介绍
1.1. 模块基本参数
DS3231是一块低成本高精度的时钟RTC芯片(模块),采集IIC总线方式通讯(从机地址为0x68),包含了电池输入端能支持断开主电源也可保持的计时功能。
芯片的实时时钟提供了秒、分、时、星期、日、月、年信息,带闰年补偿。还可以设定两个闹钟,可输出方波来驱动蜂鸣器等。
另外芯片还内置了湿度传感器,温度分辨率为0.25度。
没找到模块对应的官方网站,只从百度文库中找到了一份模块的介绍,见此地址
1.2. 芯片各寄存器地址及参数含义
从文档资料里面截取了下面这个寄存器说明表:
需要注意:
- 日期和时间寄存器里面将8个字节拆分开存储,低4字节存个位,高4字节存十位
- year那里存储的范围是0~99,所以在设置年份的时候就不能设置为2021这样的长时间格式,用21就可以了
- 温度用了两个字节来存,有效数据共10位,11H存储了高8位、12H在大端存储了低两位。因为温度存储要兼顾负数,所以存储值用了2的补码形式。
- 温度分辨率为0.25度,也就是得出的10位有效数值再乘0.25(或者除4),就是最终温度
1.3. DS3231寄存器读写方式
1.3.1. 从设备接收模式(DS3231写模式)
官方文档对写寄存器方式的描述为:
通过SDA和SCL接收串行数据和时钟。收到每个字节后,发送应答位。START和STOP条件作为串行传输的开始和结束。
在收到从设备地址和传输方向位之后,由硬件实现地址识别。
(1). 主设备产生START条件后,从设备地址字节是收到的第一个字节。从设备地址字节包括7位DS3231地址,即1101000,接着是传输方向位(R/W)。该位为0,表示写操作。在收到并译码从设备地址字节后,DS3231向SDA发出应答信号。
(2). 在DS3231应答从设备地址+写位之后,主设备发送一个字节地址至DS3231。这用于设定DS3231的寄存器指针,DS3231对该传输做出应答。
(3). 主设备随后可以发送0或者更多字节的数据,DS3231应答每个收到的字节。每个数据字节传输完后,寄存器指针自动递增。主设备产生STOP条件以终止数据写人。
抛开I2C总线传输方向和应答不讲(因为micropython已经封装好了),单纯看数据流的话,写寄存器就很简单了。从主机(咱们的esp8266模块)向从机(ds3231模块)依次向送的数据流为:“从机I2C地址0x68
+ 寄存器地址
+ 设定值
”。
1.3.2. 从设备发送模式(DS3231读模式)
官方文档对读取寄存器数据方式的描述为:
接收和处理首字节的方式与从设备接收模式相同。但是,在这种模式下,方向位指示的传输方向是相反的。DS3231向SDA发送串行数据,并由SCL输入串行时钟。START和STOP条件作为串行传输的开始和结束。在收到从设备地址和方向位后,由硬件进行地址识别。
(1). 主设备产生START条件后,从设备地址字节是收到的首字节。从设备地址字节包括7位DS3231地址,即1101000,接下来是方向位(R/W)。该位为1,表示读操作。在接收和译码从设备地址字节后,DS3231向SDA发出应答信号。
(2). 然后DS3231开始发送数据,并从寄存器指针所指向的寄存器地址开始。如果在启动读模式之前未写寄存器指针,所读取的首地址是最后存储的寄存器指针值。DS3231必须收到非应答信号以结束读操作。
读寄存器的话,重点在描述里面的第(2)点。原本主机(esp8266)向从机发送地址后,从机就会应答数据了,但是我们要读取的是指定寄存器的值呀,不能乱给数据。按后面“所读取的首地址是最后存储的寄存器指针值”,这就意味着,咱们要给从机指定寄存器发送个寂寞,然后再从从机那里读取指定长度的值,从机这时会在前面刚被写了个寂寞的地方开始回数据给主机。
1)主机向从机指定地址存数据,只指定地址不用发数据。主机发送 从机I2C地址0x68
+ 寄存器地址
2)主机读取从机指定长度的缓存值
(这里咱们只读取一位,因为底层要单独封装年、月、日、时、分、秒等)
2. DS3231驱动库封装
2.1. ds3231寄存器的读写
按照1.3.节文档对寄存器读取的描述和个人的理解,咱们使用micropython对ds3231的寄存器进行读写可以总结为下述方法。
写寄存器
buf = bytearray(2)
buf[0] = reg_addr # eg. 0x00
buf[1] = 'int-data-to-set' # eg. 59
i2c.writeto(ds3231_addr, buf)
读寄存器
i2c.writeto(ds3231_addr, reg_addr)
buf = i2c.readfrom(ds3231_addr, 1)
2.2. ds3231封装库
按照上面文档中对寄存器读取方式的说明,我们使用micropython的I2C总线方式对模块进行封装。
目前我们只测试时钟的设置与获取、温度获取,其他闹钟设置、方波输出和复位设置等先不做研究。
ds3231库,主要参考了别人家使用pyboard时封装的库,初始化方法和函数名等少部分地方做了修改。
from micropython import const
from machine import Pin, I2C
DS3231_ADDR = const(0x68)
DS3231_REG_SEC = b'\x00'
DS3231_REG_MIN = b'\x01'
DS3231_REG_HOUR = b'\x02'
DS3231_REG_WEEKDAY = b'\x03'
DS3231_REG_DAY = b'\x04'
DS3231_REG_MONTH = b'\x05'
DS3231_REG_YEAR = b'\x06'
DS3231_REG_A1SEC = b'\x07'
DS3231_REG_A1MIN = b'\x08'
DS3231_REG_A1HOUR = b'\x09'
DS3231_REG_A1DAY = b'\x0A'
DS3231_REG_A2MIN = b'\x0B'
DS3231_REG_A2HOUR = b'\x0C'
DS3231_REG_A2DAY = b'\x0D'
DS3231_REG_CTRL = b'\x0E'
DS3231_REG_STA = b'\x0F'
DS3231_REG_OFF = b'\x10'
DS3231_REG_TEMPM = b'\x11'
DS3231_REG_TEMPL = b'\x12'
class DS3231(object):
def __init__(self, gpio_scl=5, gpio_sda=4, freq=100000, i2c=None):
# 初始化i2c总线,可以外部传入也可以指定管脚在内部创建
self.i2c = i2c or I2C(scl=Pin(gpio_scl), sda=Pin(gpio_sda), freq=freq)
def Date(self, dat=[]):
'''读取或设置当前日期'''
if dat == []:
t = []
t.append(str(self.year()))
t.append(str(self.month()))
t.append(str(self.day()))
return t
else:
self.year(dat[0])
self.month(dat[1])
self.day(dat[2])
def Time(self, dat=[]):
'''读取或设置当前时间'''
if not dat:
t = []
t.append(str(self.hour()))
t.append(str(self.min()))
t.append(str(self.sec()))
return t
else:
self.hour(dat[0])
self.min(dat[1])
self.sec(dat[2])
def DateTime(self, dat=[]):
'''读取或设置当前日期与时间'''
if dat == []:
return self.Date() + self.Time()
else:
self.year(dat[0])
self.month(dat[1])
self.day(dat[2])
self.hour(dat[3])
self.min(dat[4])
self.sec(dat[5])
def _set_reg(self, reg, dat, trans_dec=True):
'''写寄存器'''
# 转换成寄存器需要的大小端分离格式
dat = (int(dat / 10) << 4) + (dat % 10) if trans_dec else dat
# 待发送数据:寄存器地址 + 设定值
buf = bytearray(2)
buf[0] = reg[0]
buf[1] = dat
self.i2c.writeto(DS3231_ADDR, buf)
def _get_reg(self, reg, trans_dec=True):
'''读寄存器'''
# 指定待读取寄存器地址
self.i2c.writeto(DS3231_ADDR, reg)
# 读取1位数据
t = self.i2c.readfrom(DS3231_ADDR, 1)[0]
if trans_dec: # 将大小端分离的格式数据转换为正常十进制
return (t >> 4) * 10 + (t % 16)
else:
return t
def sec(self, sec=''):
if sec == '':
return self._get_reg(DS3231_REG_SEC)
else:
self._set_reg(DS3231_REG_SEC, sec)
def min(self, min=''):
if min == '':
return self._get_reg(DS3231_REG_MIN)
else:
self._set_reg(DS3231_REG_MIN, min)
def hour(self, hour=''):
if hour == '':
return self._get_reg(DS3231_REG_HOUR)
else:
self._set_reg(DS3231_REG_HOUR, hour)
def day(self, day=''):
if day == '':
return self._get_reg(DS3231_REG_DAY)
else:
self._set_reg(DS3231_REG_DAY, day)
def month(self, month=''):
if month == '':
return self._get_reg(DS3231_REG_MONTH)
else:
self._set_reg(DS3231_REG_MONTH, month)
def year(self, year=''):
if year == '':
return self._get_reg(DS3231_REG_YEAR)
else:
self._set_reg(DS3231_REG_YEAR, year)
def Temperature(self):
'''获取温度
# t1是高8位,本来要左移两位的,因为温度分辨率还得除4,相当于不用移位了。
# t2是低2位,但放置在了12H的最高位置上,所以需要右移6位(除以64),再加上分辨率因素(除4),所以t2/256就是实际值了。
'''
t1 = self._get_reg(DS3231_REG_TEMPM, trans_dec=False)
t2 = self._get_reg(DS3231_REG_TEMPL, trans_dec=False)
if t1 > 0x7F: # 11H最高位为1代表负数
return t1 - t2 / 256 - 256 # 补码简化算法,得数-256
else:
return t1 + t2 / 256
2.3. 封装库使用方法
from ds3231 import DS3231
ds = DS3231(gpio_scl=5, gpio_sda=4) # 这里根据实际接线的GPIO管脚来修改
ds.DateTime() # 获取日期、时间
ds.DateTime([21, 8, 18, 12, 0, 0]) # 设置日期时间(注意用短年份)
ds.Time([12, 1, 0]) # 仅设置时间
ds.Temperature() # 获取当前温度
3. 实验:简易时钟与温湿度显示器
3.1. 需求
- 将日期时间显示到oled屏幕上
- 时钟每天联网校准一次
- 同步显示当前环境温湿度
3.2. 多I2C从机的接线方式
我们这里ESP8266作为主机来使用,上面RTC时钟是个I2C从机,现在要将获取到的时间显示在oled屏幕上,我们之前用过的SSD1306驱动的0.96寸oled屏幕也是I2C总线协议。那这里就使用到了一主多从的模式,按下图所示的串接方式连接:
因为咱们使用的各个模块内部都已经封装好了时钟线和数据线连接上拉电阻到正极电源线,我们就不用管这里了,只需要按照VIN/GND/SCL/SDA
标识,将各模块串接起来。
设备清单:
- 主机:ESP8266
- 从机1:DS3231时钟模块
- 从机2:AHT10温湿度模块
- 从机3:SSD1306显示屏
接线实物图:
3.3. 实验分析与代码
SSD1306驱动在micropython已经内置了,DS3231驱动见上,ATH10驱动咱们前面文章已经分析和做出来了,再加上前面也学习了在boot.py
中配置wifi,这里实验其实就容易了,把各个驱动库引入进来,使用urequests.py
定期获取网络时间来校准,然后定义一个Timer定时器来刷新oled屏幕就可以啦。
直接上完整代码,不拆解介绍了。
import urequests
import json
import time
from machine import Pin,I2C
from ds3231 import DS3231
from aht import AHT10
from ssd1306 import SSD1306_I2C
from machine import Timer
# 初始化各个模块
i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000)
ds = DS3231(i2c=i2c)
aht = AHT10(i2c)
display = SSD1306_I2C(128,64,i2c,addr=60)
NOW_DATE = ''
# 网络校时
def reset_time():
url = 'http://quan.suning.com/getSysTime.do'
res=urequests.get(url).text
print(res)
j=json.loads(res)
t2_date = j['sysTime2'].split()[0] #日期
t2_time = j['sysTime2'].split()[1] #时间
global ds
ds.Date([int(x) for x in t2_date[2:].split('-')]) #设置初始日期年、月、日
ds.Time([int(x) for x in t2_time.split(':')]) #设置初始时间时、分、秒
# 获取待显示值
def get_content():
global NOW_DATE
# 读取时钟模块的日期和时间,拼接成正常的时间格式
_date = '20'+'-'.join(ds.Date())
_time = ':'.join(ds.Time())
# 读取AHT10的温湿度
_,_temperature,_humidity = aht.measure(),aht.temperature(),aht.humidity()
# 判定是否需要校准时间,每天0时触发
if NOW_DATE != _date:
reset_time()
NOW_DATE = _date
return _date,_time,_temperature,_humidity
# 刷新屏幕
def flash_display(t):
global display
_date,_time,_temperature,_humidity = get_content()
display.fill(0) # 清屏
display.text(_date, 3, 5)
display.text(_time, 3, 16)
display.text(str(_temperature) + " 'c", 3, 27)
display.text(str(_humidity) + " %", 3, 38)
display.show()
tim = Timer(-1)
tim.init(period=1000, mode=Timer.PERIODIC, callback=flash_display)
看看效果: