MicroPython-On-ESP时钟模块DS3231的使用方法
1. 模块DS3231介绍
1.1. 模块基本参数
DS3231是一个低成本、高精度的时钟RTC采集芯片(模块)IIC总线通信(从机地址为0x68)包括电池输入端可以支持断开主电源或保持的计时功能。 芯片的实时时钟提供秒、分、时、周、日、月、年的信息,并提供闰年补偿。还可以设置两个闹钟,输出方波驱动蜂鸣器等。 此外,芯片还内置了湿度传感器,温度分辨率为0.25度。
没有找到模块对应的官方网站,只从百度图书馆找到了一个模块介绍,看到这个地址
1.2. 芯片寄存器地址及参数含义
以下寄存器说明表从文档中截取: 需要注意:
- 将8个字节分开存储在日期和时间寄存器中,低4字节存储个位,高4字节存储10位
- year存储范围为0~99,所以在设定年份的时候,不能设定为2021这样的长时间格式,用21就可以了
- 两个字节存储温度,共10个有效数据,11H存储高8位,12位H低两位存储在大端。由于温度存储应考虑负数,因此存储值采用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从设备地址+写位回应后,主设备将字节地址发送至DS设置3231。DS寄存器指针3231,DS3231应对传输。 (3). 主设备可以发送0或更多字节数据,DS3231响应每个收到的字节。每个数据字节传输后,寄存器指针自动增加。生成主要设备STOP终止数据写人的条件。
抛开I2C因为总线的传输方向和响应没有讲(因为micropython已经包装好了),单纯看数据流,写寄存器很简单。从主机(我们的)esp8266模块(ds3231模块231模块数据流为:从机I2C地址0x68
寄存器地址
设定值
”。
1.3.2. 从设备发送模式出发(DS3231读模式)
官方文存器数据的官方文件描述如下:
接收和处理首字节的方式与设备接收模式相同。但在这种模式下,方向位指示的传输方向相反。DS3231向SDA并由发送串行数据SCL输入串行时钟。START和STOP条件是串行传输的开始和结束。硬件在收到设备地址和方向后识别地址。 (1). 主设备产生START从设备地址字节中收到的第一个字节。从设备地址字节包括7位DS3231地址,即1101000,其次是方向位(R/W)。该位置为1,表示读取操作。从设备地址字节接收和翻译后,DS3231向SDA发出响应信号。 (2). 然后DS从寄存器指针指向的寄存器地址开始,3231开始发送数据。如果在启动读取模式之前没有写寄存器指针,读取的第一个地址是最终存储的寄存器指针值。DS3231必须收到结束读取操作的非应答信号。
读寄存器时,重点是描述中的第(2)点。原本主机(esp8266)将地址发送到机器后,机器将响应数据,但我们需要读取指定寄存器的值,而不是给数据。根据后面的读取的第一个地址是最后存储的寄存器指针值,这意味着我们应该向指定的从机寄存器发送一个孤独,然后读取指定长度的值。此时,我们将开始在前面写一个孤独的地方返回数据给主机。
1)主机向从机指定地址存数据,只指定地址不用发数据。主机发送 从机I2C地址0x68
寄存器地址
2)主机读取从机指定长度的缓存值
(这里我们只读一个,因为底层应该分别包装年、月、日、时、分、秒等。
2. DS3231驱动库封装
2.1. ds3231寄存器读写
按照1.3.我们使用节文档对寄存器读取的描述和个人理解micropython对ds读写3231寄存器可以概括为以下方法。
写寄存器
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)
看看效果: