diff --git a/DS3231/README.md b/DS3231/README.md index 3acd1bf..08771e3 100644 --- a/DS3231/README.md +++ b/DS3231/README.md @@ -1,13 +1,14 @@ # The DS3231 real time clock chip This is a remarkably inexpensive and easily interfaced battery-backed RTC. It -is an ideal way to rapidly calibrate the Pyboard's RTC which can then achieve +is an ideal way rapidly to calibrate the Pyboard's RTC which can then achieve similar levels of accuracy (+- ~2 mins/year). The chip can also provide accurate time to platforms lacking a good RTC (notably the ESP8266). Two drivers are provided: 1. `ds3231_port.py` A multi-platform driver. - 2. `ds3231_pb.py` A Pyboard-specific driver with RTC calibration facility. + 2. `ds3231_pb.py` A Pyboard-specific driver with RTC calibration facility. For + Pyboard 1.x and Pyboard D. Breakout boards are widely available. The interface is I2C. Pullups to 3.3V (typically 10KΩ) should be provided on the `SCL` and `SDA` lines if these are @@ -15,7 +16,8 @@ not supplied on the breakout board. Both divers use edge detection to achieve millisecond-level precision from the DS3231. This enables relatively rapid accuracy testing of the platform's RTC, -and fast calibration of the Pyboard's RTC. +and fast calibration of the Pyboard's RTC. To quantify this, a sufficiently +precise value of calibration may be acquired in 5-10 minutes. ###### [Main README](../README.md) @@ -23,10 +25,28 @@ and fast calibration of the Pyboard's RTC. This can use soft I2C so any pins may be used. -It is based on the assumption that, where a hardware RTC exists, MicroPython's -local time (`utime.localtime()`) is based on the RTC time. Changes to local -time don't propagate to the RTC which must explicitly be set. This holds for -the Pyboard, ESP8266 and ESP32. +It uses the currently undocumented `RTC.datetime()` method to set and to query +the platform RTC. This appears to be the only cross-platform way to do this. +The meaning of the subseconds field is hardware dependent so this is ignored. +The RTC is checked against the DS3231 by timing the transition of the seconds +field of each clock (using system time to measure the relative timing of the +edges). + +This example ran on a WeMos D1 Mini ESP8266 board, also a generic ESP32. +```python +from ds3231_port import DS3231 +from machine import Pin, I2C +# Pins with pullups on ESP8266: clk=WeMos D3(P0) data=WeMos D4(P2) +i2c = I2C(-1, Pin(0, Pin.OPEN_DRAIN), Pin(2, Pin.OPEN_DRAIN)) +ds3231 = DS3231(i2c) +ds3231.get_time() +``` +Testing the onboard RTC: +``` +ds3231.rtc_test() # Takes 10 minutes +``` +In my testing the ESP8266 RTC was out by 5%. The ESP32 was out by 6.7ppm or +about 12 minutes/yr. Hardware samples will vary. ## 1.1 The DS3231 class @@ -34,35 +54,42 @@ Constructor: This takes one mandatory argument, an initialised I2C bus. Public methods: - 1. `get_time` Optional boolean arg `set_rtc=False`. If `set_rtc` is `True` it - sets the platform's RTC from the DS3231. It returns the DS3231 time as a tuple - in the same format as `utime.localtime()` except that yday (day of year) is 0. - So the format is (year, month, day, hour, minute, second, wday, 0). + 1. `get_time(set_rtc=False)`. If `set_rtc` is `True` it sets the platform's + RTC from the DS3231. It returns the DS3231 time as a tuple in the same format + as `utime.localtime()` except that yday (day of year) is 0. So the format is + (year, month, day, hour, minute, second, wday, 0). Note that on ports/platforms which don't support an RTC, if `set_rtc` is `True`, the local time will be set from the DS3231. - 2. `save_time` No args. Sets the DS3231 time from the platform's local time. - 3. `rtc_test` Optional args: `runtime=600`, `ppm=False`. This tests the - platform's local time against the DS3231 returning the error in parts per - million (if `ppm` is `True`) or seconds per year. A positive value indicates - that the DS3231 clock leads the platform local time. + 2. `save_time()` No args. Sets the DS3231 time from the platform's local time. + 3. `rtc_test(runtime=600, ppm=False, verbose=True)`. This tests the platform's + RTC time against the DS3231 returning the error in parts per million (if `ppm` + is `True`) or seconds per year. A positive value indicates that the DS3231 + clock leads the platform RTC. The `runtime` value in seconds defines the duration of the test. The default of 10 minutes provides high accuracy but shorter durations will suffice on devices with poor RTC's (e.g. ESP8266). + If `machine.RTC` is unsupported a `RuntimeError` will be thrown. # 2. The Pyboard driver The principal reason to use this driver is to calibrate the Pyboard's RTC. This -does not yet support the Pyboard D. +supports the Pyboard 1.x and Pyboard D. Note that the RTC on the Pyboard D is +much more accurate than that on the Pyboard 1.x but can still be in error by up +to 20ppm. It can benefit from calibration. For this to work reliably on the D a +firmware build later than V1.12 is required: use a daily build if a later +release is not yet available. -This assumes that the DS3231 is connected to the hardware I2C port on the `X` -or `Y` side of the board, and that the Pyboard's RTC is set to the correct time -and date. +The sample below assumes that the DS3231 is connected to the hardware I2C port +via I2C(2) but any I2C may be used including soft I2C. Ensure that the Pyboard +RTC is set to the correct time and date. Usage to calibrate the Pyboard's RTC. Takes 5 minutes. ```python from ds3231_pb import DS3231 -ds3231 = DS3231('X') +import machine +i2c = machine.I2C(2) # Connected on 'Y' side Y9 clk Y10 data +ds3231 = DS3231(i2c) ds3231.save_time() # Set DS3231 to match Pyboard RTC ds3231.calibrate() ``` @@ -73,19 +100,19 @@ used the RTC will run accurately in the event of a power outage. ## 2.1 The DS3231 class Constructor: -This takes one mandatory argument, a string identifying the Pyboard side in use -('X' or 'Y'). +This takes one mandatory argument, an I2C bus instantiated using the `machine` +library. Public methods: - 1. `get_time` Optional boolean arg `set_rtc=False`. If `set_rtc` is True it - sets the Pyboard's RTC from the DS3231. It returns the DS3231 time as a tuple - in the same format as `utime.localtime()` except that yday (day of year) is 0. - namely (year, month, day, hour, minute, second, wday, 0). - 2. `save_time` No args. Sets the DS3231 time from the Pyboard's RTC. - 3. `calibrate` Optional arg `minutes=5`. The time to run. This calculates the - calibration factor and applies it to the Pyboard. It returns the calibration - factor which may be stored in a file if the calibration needs to survive an - outage of all power sources. + 1. `get_time(set_rtc=False)`. If `set_rtc` is `True` it sets the Pyboard's RTC + from the DS3231. It returns the DS3231 time as a tuple in the same format as + `utime.localtime()` except that yday (day of year) is 0. + Namely (year, month, day, hour, minute, second, wday, 0). + 2. `save_time()` No args. Sets the DS3231 time from the Pyboard's RTC. + 3. `calibrate(minutes=5)`. The time to run. This calculates the calibration + factor and applies it to the Pyboard. It returns the calibration factor which + may be stored in a file if the calibration needs to survive an outage of all + power sources. 4. `getcal(minutes=5, cal=0, verbose=True)` Measures the performance of the Pyboard RTC against the DS3231. If `cal` is specified, the calibration factor is applied before the test is run. The default is to zero the calibration and diff --git a/DS3231/ds3231_pb.py b/DS3231/ds3231_pb.py index e2dd129..c86760c 100644 --- a/DS3231/ds3231_pb.py +++ b/DS3231/ds3231_pb.py @@ -1,47 +1,36 @@ # 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 -# delta method now operates to 1mS precision # precison of calibration further improved by timing Pyboard RTC transition # 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 +# 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. import utime -import pyb +import machine import os d_series = os.uname().machine.split(' ')[0][:4] == 'PYBD' if d_series: - pyb.Pin.board.EN_3V3.value(1) + machine.Pin.board.EN_3V3.value(1) DS3231_I2C_ADDR = 104 class DS3231Exception(OSError): pass -rtc = pyb.RTC() +rtc = machine.RTC() -def now(): # Return the current time from the RTC in millisecs from year 2000 - secs = utime.time() - if d_series: - ms = rtc.datetime()[7] // 1000 - else: - ms = 1000 * (255 -rtc.datetime()[7]) >> 8 - if ms < 50: # Might have just rolled over - secs = utime.time() - return 1000 * secs + ms - -def nownr(): # Return the current time from the RTC: caller ensures transition has occurred - if d_series: - return 1000 * utime.time() + rtc.datetime()[7] // 1000 - return 1000 * utime.time() + (1000 * (255 -rtc.datetime()[7]) >> 8) +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 def get_us(s): # For Pyboard D: convert datetime to μs. Caller handles rollover - return (s[7] + s[6] * 1_000_000 + s[5] * 60_000_000 + s[4] * 3_600_000_000 ) -# Driver for DS3231 accurate RTC module (+- 1 min/yr) needs adapting for Pyboard -# source https://github.com/scudderfish/uDS3231 + return s[7] + s[6] * 1_000_000 + s[5] * 60_000_000 + s[4] * 3_600_000_000 + def bcd2dec(bcd): return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f)) @@ -49,25 +38,25 @@ def dec2bcd(dec): tens, units = divmod(dec, 10) return (tens << 4) + units +def tobytes(num): + return num.to_bytes(1, 'little') + class DS3231: - def __init__(self, side = 'X'): - side = side.lower() - if side == 'x': - bus = 1 - elif side == 'y': - bus = 2 - else: - raise ValueError('Side must be "X" or "Y"') - self.ds3231 = pyb.I2C(bus, mode=pyb.I2C.MASTER, baudrate=400000) + def __init__(self, i2c): + self.ds3231 = i2c self.timebuf = bytearray(7) if DS3231_I2C_ADDR not in self.ds3231.scan(): raise DS3231Exception("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR) - def get_time(self, set_rtc = False): + def get_time(self, set_rtc=False): if set_rtc: - data = self.await_transition() # For accuracy set RTC immediately after a seconds transition + self.await_transition() # For accuracy set RTC immediately after a seconds transition else: - data = self.ds3231.mem_read(self.timebuf, DS3231_I2C_ADDR, 0) # don't wait + 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 ss = bcd2dec(data[0]) mm = bcd2dec(data[1]) if data[2] & 0x40: @@ -89,31 +78,25 @@ class DS3231: return (YY, MM, DD, hh, mm, ss, wday -1, 0) # Time from DS3231 in time.time() format (less yday) def save_time(self): - (YY, MM, DD, wday, hh, mm, ss, subsecs) = rtc.datetime() - self.ds3231.mem_write(dec2bcd(ss), DS3231_I2C_ADDR, 0) - self.ds3231.mem_write(dec2bcd(mm), DS3231_I2C_ADDR, 1) - self.ds3231.mem_write(dec2bcd(hh), DS3231_I2C_ADDR, 2) # Sets to 24hr mode - self.ds3231.mem_write(dec2bcd(wday), DS3231_I2C_ADDR, 3) # 1 == Monday, 7 == Sunday - self.ds3231.mem_write(dec2bcd(DD), DS3231_I2C_ADDR, 4) + (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 if YY >= 2000: - self.ds3231.mem_write(dec2bcd(MM) | 0b10000000, DS3231_I2C_ADDR, 5) - self.ds3231.mem_write(dec2bcd(YY-2000), DS3231_I2C_ADDR, 6) + 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))) else: - self.ds3231.mem_write(dec2bcd(MM), DS3231_I2C_ADDR, 5) - self.ds3231.mem_write(dec2bcd(YY-1900), DS3231_I2C_ADDR, 6) - - def delta(self): # Return no. of mS RTC leads DS3231 - self.await_transition() - rtc_ms = now() - t_ds3231 = utime.mktime(self.get_time()) # To second precision, still in same sec as transition - return rtc_ms - 1000 * t_ds3231 + self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM))) + self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900))) def await_transition(self): # Wait until DS3231 seconds value changes - data = self.ds3231.mem_read(self.timebuf, DS3231_I2C_ADDR, 0) - ss = data[0] - while ss == data[0]: - data = self.ds3231.mem_read(self.timebuf, DS3231_I2C_ADDR, 0) - return data + 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 # 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. @@ -134,24 +117,28 @@ class DS3231: 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 - tus = pyb.micros() + tus = utime.ticks_us() st = rtc.datetime()[7] while rtc.datetime()[7] == st: # Wait for RTC to change pass - t1 = pyb.elapsed_micros(tus) # t1 is duration (μs) between DS and RTC change (start) - rtcstart = nownr() # RTC start time in mS - dsstart = utime.mktime(self.get_time()) # DS start time in secs - pyb.delay(minutes * 60000) + 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) + self.await_transition() # DS second boundary - tus = pyb.micros() + tus = utime.ticks_us() st = rtc.datetime()[7] while rtc.datetime()[7] == st: pass - t2 = pyb.elapsed_micros(tus) # t2 is duration (μs) between DS and RTC change (end) - rtcend = nownr() - dsend = utime.mktime(self.get_time()) + 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()) dsdelta = (dsend - dsstart) * 1000000 # Duration (μs) between DS edges as measured by DS3231 - rtcdelta = (rtcend - rtcstart) * 1000 + t1 -t2 # Duration (μs) between DS edges as measured by RTC and corrected + 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 ppm = (1000000* (rtcdelta - dsdelta))/dsdelta if cal: verbose and print('Error {:4.1f}ppm {:4.1f}mins/year.'.format(ppm, ppm * 1.903)) @@ -169,13 +156,15 @@ class DS3231: 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) - dsstart = utime.mktime(self.get_time()) # DS start time in secs - pyb.delay(minutes * 60_000) + dsstart = utime.mktime(self.convert()) # DS start time in secs + + utime.sleep(minutes * 60) + 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) - dsend = utime.mktime(self.get_time()) # DS start time in secs + dsend = utime.mktime(self.convert()) # DS end time in secs 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 diff --git a/DS3231/ds3231_port.py b/DS3231/ds3231_port.py index aeba034..cd71924 100644 --- a/DS3231/ds3231_port.py +++ b/DS3231/ds3231_port.py @@ -96,13 +96,37 @@ class DS3231: # one-seond boundaries and using ticks_ms() to time the RTC. # For a 10 minute measurement +-1ms corresponds to 1.7ppm or 53s/yr. Longer # runtimes improve this, but the DS3231 is "only" good for +-2ppm over 0-40C. - def rtc_test(self, runtime=600, ppm=False): - factor = 1000000 if ppm else 31557600 # seconds per year - self.await_transition() # Start on transition - rtc_start = utime.ticks_ms() # and get RTC time NOW - ds3231_start = utime.mktime(self.convert()) + def rtc_test(self, runtime=600, ppm=False, verbose=True): + if rtc is None: + raise RuntimeError('machine.RTC does not exist') + verbose and print('Waiting {} minutes for result'.format(runtime//60)) + factor = 1_000_000 if ppm else 114_155_200 # seconds per year + + self.await_transition() # Start on transition of DS3231. Record time in .timebuf + t = utime.ticks_ms() # Get system time now + ss = rtc.datetime()[6] # Seconds from system RTC + while ss == rtc.datetime()[6]: + pass + ds = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC + ds3231_start = utime.mktime(self.convert()) # Time when transition occurred + t = rtc.datetime() + rtc_start = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0 + utime.sleep(runtime) # Wait a while (precision doesn't matter) - self.await_transition() - d_rtc = utime.ticks_diff(utime.ticks_ms(), rtc_start) - d_ds3231 = 1000 * (utime.mktime(self.convert()) - ds3231_start) - return (d_ds3231 - d_rtc) * factor / d_ds3231 + + self.await_transition() # of DS3231 and record the time + t = utime.ticks_ms() # and get system time now + ss = rtc.datetime()[6] # Seconds from system RTC + while ss == rtc.datetime()[6]: + pass + de = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC + ds3231_end = utime.mktime(self.convert()) # Time when transition occurred + t = rtc.datetime() + rtc_end = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0 + + d_rtc = 1000 * (rtc_end - rtc_start) + de - ds # ms recorded by RTC + d_ds3231 = 1000 * (ds3231_end - ds3231_start) # ms recorded by DS3231 + ratio = (d_ds3231 - d_rtc) / d_ds3231 + ppm = ratio * 1_000_000 + verbose and print('DS3231 leads RTC by {:4.1f}ppm {:4.1f}mins/yr'.format(ppm, ppm*1.903)) + return ratio * factor