DS3231 Support Pyboard D. Various updates and fixes.

pull/15/head
Peter Hinch 2020-01-27 09:58:35 +00:00
rodzic c259792a7c
commit f62a6d8e1c
3 zmienionych plików z 148 dodań i 108 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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