2016-01-06 09:39:06 +00:00
|
|
|
# Pyboard driver for DS3231 precison real time clock.
|
|
|
|
# Adapted from WiPy driver at https://github.com/scudderfish/uDS3231
|
|
|
|
# Includes routine to calibrate the Pyboard's RTC from the DS3231
|
2016-01-19 17:46:15 +00:00
|
|
|
# precison of calibration further improved by timing Pyboard RTC transition
|
2020-01-25 17:36:58 +00:00
|
|
|
# Adapted by Peter Hinch, Jan 2016, Jan 2020 for Pyboard D
|
|
|
|
|
|
|
|
# Pyboard D rtc.datetime()[7] counts microseconds. See end of page on
|
|
|
|
# https://pybd.io/hw/pybd_sfxw.htm
|
2020-01-27 09:58:35 +00:00
|
|
|
# Note docs for machine.RTC are wrong for Pyboard. The undocumented datetime
|
|
|
|
# method seems to be the only way to set the RTC and it follows the same
|
|
|
|
# convention as the pyb RTC's datetime method.
|
2020-01-25 17:36:58 +00:00
|
|
|
|
|
|
|
import utime
|
2020-01-27 09:58:35 +00:00
|
|
|
import machine
|
2020-01-25 17:36:58 +00:00
|
|
|
import os
|
|
|
|
|
|
|
|
d_series = os.uname().machine.split(' ')[0][:4] == 'PYBD'
|
|
|
|
if d_series:
|
2020-01-27 09:58:35 +00:00
|
|
|
machine.Pin.board.EN_3V3.value(1)
|
2016-01-06 09:39:06 +00:00
|
|
|
|
|
|
|
DS3231_I2C_ADDR = 104
|
|
|
|
|
|
|
|
class DS3231Exception(OSError):
|
|
|
|
pass
|
|
|
|
|
2020-01-27 09:58:35 +00:00
|
|
|
rtc = machine.RTC()
|
2016-01-06 09:39:06 +00:00
|
|
|
|
2020-01-27 09:58:35 +00:00
|
|
|
def get_ms(s): # Pyboard 1.x datetime to ms. Caller handles rollover.
|
|
|
|
return (1000 * (255 - s[7]) >> 8) + s[6] * 1000 + s[5] * 60_000 + s[4] * 3_600_000
|
2016-01-06 09:39:06 +00:00
|
|
|
|
2020-01-25 17:36:58 +00:00
|
|
|
def get_us(s): # For Pyboard D: convert datetime to μs. Caller handles rollover
|
2020-01-27 09:58:35 +00:00
|
|
|
return s[7] + s[6] * 1_000_000 + s[5] * 60_000_000 + s[4] * 3_600_000_000
|
|
|
|
|
2016-01-06 09:39:06 +00:00
|
|
|
def bcd2dec(bcd):
|
2016-01-06 10:45:20 +00:00
|
|
|
return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f))
|
2016-01-06 09:39:06 +00:00
|
|
|
|
|
|
|
def dec2bcd(dec):
|
2016-01-07 07:50:06 +00:00
|
|
|
tens, units = divmod(dec, 10)
|
|
|
|
return (tens << 4) + units
|
2016-01-06 09:39:06 +00:00
|
|
|
|
2020-01-27 09:58:35 +00:00
|
|
|
def tobytes(num):
|
|
|
|
return num.to_bytes(1, 'little')
|
|
|
|
|
2016-01-06 09:39:06 +00:00
|
|
|
class DS3231:
|
2020-01-27 09:58:35 +00:00
|
|
|
def __init__(self, i2c):
|
|
|
|
self.ds3231 = i2c
|
2016-01-24 12:31:05 +00:00
|
|
|
self.timebuf = bytearray(7)
|
2016-01-06 09:39:06 +00:00
|
|
|
if DS3231_I2C_ADDR not in self.ds3231.scan():
|
|
|
|
raise DS3231Exception("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR)
|
|
|
|
|
2020-01-27 09:58:35 +00:00
|
|
|
def get_time(self, set_rtc=False):
|
2016-01-06 10:45:20 +00:00
|
|
|
if set_rtc:
|
2020-01-27 09:58:35 +00:00
|
|
|
self.await_transition() # For accuracy set RTC immediately after a seconds transition
|
2016-01-06 10:45:20 +00:00
|
|
|
else:
|
2020-01-27 09:58:35 +00:00
|
|
|
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) # don't wait
|
|
|
|
return self.convert(set_rtc)
|
|
|
|
|
|
|
|
def convert(self, set_rtc=False):
|
|
|
|
data = self.timebuf
|
2016-01-06 10:45:20 +00:00
|
|
|
ss = bcd2dec(data[0])
|
|
|
|
mm = bcd2dec(data[1])
|
2016-01-06 09:39:06 +00:00
|
|
|
if data[2] & 0x40:
|
2016-01-06 10:45:20 +00:00
|
|
|
hh = bcd2dec(data[2] & 0x1f)
|
2016-01-06 09:39:06 +00:00
|
|
|
if data[2] & 0x20:
|
|
|
|
hh += 12
|
|
|
|
else:
|
|
|
|
hh = bcd2dec(data[2])
|
|
|
|
wday = data[3]
|
|
|
|
DD = bcd2dec(data[4])
|
|
|
|
MM = bcd2dec(data[5] & 0x1f)
|
|
|
|
YY = bcd2dec(data[6])
|
|
|
|
if data[5] & 0x80:
|
|
|
|
YY += 2000
|
|
|
|
else:
|
|
|
|
YY += 1900
|
|
|
|
if set_rtc:
|
|
|
|
rtc.datetime((YY, MM, DD, wday, hh, mm, ss, 0))
|
|
|
|
return (YY, MM, DD, hh, mm, ss, wday -1, 0) # Time from DS3231 in time.time() format (less yday)
|
|
|
|
|
|
|
|
def save_time(self):
|
2020-01-27 09:58:35 +00:00
|
|
|
(YY, MM, mday, hh, mm, ss, wday, yday) = utime.localtime() # Based on RTC
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss)))
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm)))
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month
|
2016-01-06 09:39:06 +00:00
|
|
|
if YY >= 2000:
|
2020-01-27 09:58:35 +00:00
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM) | 0b10000000)) # Century bit
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-2000)))
|
2016-01-06 09:39:06 +00:00
|
|
|
else:
|
2020-01-27 09:58:35 +00:00
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM)))
|
|
|
|
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900)))
|
2016-01-06 09:39:06 +00:00
|
|
|
|
2020-01-25 17:36:58 +00:00
|
|
|
def await_transition(self): # Wait until DS3231 seconds value changes
|
2020-01-27 09:58:35 +00:00
|
|
|
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)
|
|
|
|
ss = self.timebuf[0]
|
|
|
|
while ss == self.timebuf[0]:
|
|
|
|
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)
|
|
|
|
return self.timebuf
|
2016-01-06 09:39:06 +00:00
|
|
|
|
|
|
|
# Get calibration factor for Pyboard RTC. Note that the DS3231 doesn't have millisecond resolution so we
|
|
|
|
# wait for a seconds transition to emulate it.
|
|
|
|
# This function returns the required calibration factor for the RTC (approximately the no. of ppm the
|
|
|
|
# RTC lags the DS3231).
|
2016-01-19 17:46:15 +00:00
|
|
|
# Delay(min) Outcome (successive runs). Note 1min/yr ~= 2ppm
|
|
|
|
# 5 173 169 173 173 173
|
|
|
|
# 10 171 173 171
|
|
|
|
# 20 172 172 174
|
|
|
|
# 40 173 172 173 Mean: 172.3
|
2016-01-06 09:39:06 +00:00
|
|
|
# Note calibration factor is not saved on power down unless an RTC backup battery is used. An option is
|
|
|
|
# to store the calibration factor on disk and issue rtc.calibration(factor) on boot.
|
|
|
|
|
2020-01-25 17:36:58 +00:00
|
|
|
def getcal(self, minutes=5, cal=0, verbose=True):
|
|
|
|
if d_series:
|
|
|
|
return self._getcal_d(minutes, cal, verbose)
|
|
|
|
verbose and print('Pyboard 1.x. Waiting {} minutes for calibration factor.'.format(minutes))
|
|
|
|
rtc.calibration(cal) # Clear existing cal
|
|
|
|
self.save_time() # Set DS3231 from RTC
|
|
|
|
self.await_transition() # Wait for DS3231 to change: on a 1 second boundary
|
2020-01-27 09:58:35 +00:00
|
|
|
tus = utime.ticks_us()
|
2016-01-19 17:46:15 +00:00
|
|
|
st = rtc.datetime()[7]
|
2020-01-25 17:36:58 +00:00
|
|
|
while rtc.datetime()[7] == st: # Wait for RTC to change
|
2016-01-19 17:46:15 +00:00
|
|
|
pass
|
2020-01-27 09:58:35 +00:00
|
|
|
t1 = utime.ticks_diff(utime.ticks_us(), tus) # t1 is duration (μs) between DS and RTC change (start)
|
|
|
|
rtcstart = get_ms(rtc.datetime()) # RTC start time in mS
|
|
|
|
dsstart = utime.mktime(self.convert()) # DS start time in secs as recorded by await_transition
|
|
|
|
|
|
|
|
utime.sleep(minutes * 60)
|
|
|
|
|
2020-01-25 17:36:58 +00:00
|
|
|
self.await_transition() # DS second boundary
|
2020-01-27 09:58:35 +00:00
|
|
|
tus = utime.ticks_us()
|
2016-01-19 17:46:15 +00:00
|
|
|
st = rtc.datetime()[7]
|
|
|
|
while rtc.datetime()[7] == st:
|
|
|
|
pass
|
2020-01-27 09:58:35 +00:00
|
|
|
t2 = utime.ticks_diff(utime.ticks_us(), tus) # t2 is duration (μs) between DS and RTC change (end)
|
|
|
|
rtcend = get_ms(rtc.datetime())
|
|
|
|
dsend = utime.mktime(self.convert())
|
2020-01-25 17:36:58 +00:00
|
|
|
dsdelta = (dsend - dsstart) * 1000000 # Duration (μs) between DS edges as measured by DS3231
|
2020-01-27 09:58:35 +00:00
|
|
|
if rtcend < rtcstart: # It's run past midnight. Assumption: run time < 1 day!
|
|
|
|
rtcend += 24 * 3_600_000
|
|
|
|
rtcdelta = (rtcend - rtcstart) * 1000 + t1 - t2 # Duration (μs) between DS edges as measured by RTC and corrected
|
2016-01-06 09:39:06 +00:00
|
|
|
ppm = (1000000* (rtcdelta - dsdelta))/dsdelta
|
2020-01-25 17:36:58 +00:00
|
|
|
if cal:
|
|
|
|
verbose and print('Error {:4.1f}ppm {:4.1f}mins/year.'.format(ppm, ppm * 1.903))
|
|
|
|
return 0
|
|
|
|
cal = int(-ppm / 0.954)
|
|
|
|
verbose and print('Error {:4.1f}ppm {:4.1f}mins/year. Cal factor {}'.format(ppm, ppm * 1.903, cal))
|
|
|
|
return cal
|
|
|
|
|
|
|
|
# Version for Pyboard D. This has μs resolution.
|
|
|
|
def _getcal_d(self, minutes, cal, verbose):
|
|
|
|
verbose and print('Pyboard D. Waiting {} minutes for calibration factor.'.format(minutes))
|
|
|
|
rtc.calibration(cal) # Clear existing cal
|
|
|
|
self.save_time() # Set DS3231 from RTC
|
|
|
|
self.await_transition() # Wait for DS3231 to change: on a 1 second boundary
|
|
|
|
t = rtc.datetime() # Get RTC time
|
|
|
|
# Time of DS3231 transition measured by RTC in μs since start of day
|
|
|
|
rtc_start_us = get_us(t)
|
2020-01-27 09:58:35 +00:00
|
|
|
dsstart = utime.mktime(self.convert()) # DS start time in secs
|
|
|
|
|
|
|
|
utime.sleep(minutes * 60)
|
|
|
|
|
2020-01-25 17:36:58 +00:00
|
|
|
self.await_transition() # Wait for DS second boundary
|
|
|
|
t = rtc.datetime()
|
|
|
|
# Time of DS3231 transition measured by RTC in μs since start of day
|
|
|
|
rtc_end_us = get_us(t)
|
2020-01-27 09:58:35 +00:00
|
|
|
dsend = utime.mktime(self.convert()) # DS end time in secs
|
2020-01-25 17:36:58 +00:00
|
|
|
if rtc_end_us < rtc_start_us: # It's run past midnight. Assumption: run time < 1 day!
|
|
|
|
rtc_end_us += 24 * 3_600_000_000
|
|
|
|
|
|
|
|
dsdelta = (dsend - dsstart) * 1_000_000 # Duration (μs) between DS3231 edges as measured by DS3231
|
|
|
|
rtcdelta = rtc_end_us - rtc_start_us # Duration (μs) between DS edges as measured by RTC
|
|
|
|
ppm = (1_000_000 * (rtcdelta - dsdelta)) / dsdelta
|
|
|
|
if cal: # We've already calibrated. Just report results.
|
|
|
|
verbose and print('Error {:4.1f}ppm {:4.1f}mins/year.'.format(ppm, ppm * 1.903))
|
|
|
|
return 0
|
|
|
|
cal = int(-ppm / 0.954)
|
|
|
|
verbose and print('Error {:4.1f}ppm {:4.1f}mins/year. Cal factor {}'.format(ppm, ppm * 1.903, cal))
|
|
|
|
return cal
|
2016-01-06 09:39:06 +00:00
|
|
|
|
2016-01-07 07:50:06 +00:00
|
|
|
def calibrate(self, minutes=5):
|
|
|
|
cal = self.getcal(minutes)
|
|
|
|
rtc.calibration(cal)
|
|
|
|
print('Pyboard RTC is calibrated. Factor is {}.'.format(cal))
|
|
|
|
return cal
|