pimoroni-pico icon indicating copy to clipboard operation
pimoroni-pico copied to clipboard

PCF85063A library: can we set RTC timers longer than 255 ticks?

Open lowercasename opened this issue 1 year ago • 5 comments

I'm having a lot of fun with the Inky Frame, but I've hit a stumbling block: how would I go about sleeping the Frame for longer than 255 seconds? I'd like it to update once an hour to conserve battery life.

This code:

HOLD_VSYS_EN_PIN = 2
hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT)
hold_vsys_en_pin.value(True)

i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000)
rtc = PCF85063A(i2c)

UPDATE_INTERVAL = 60 * 60
rtc.enable_timer_interrupt(True)
rtc.set_timer(UPDATE_INTERVAL)
hold_vsys_en_pin.init(Pin.IN)
time.sleep(UPDATE_INTERVAL)

Returns this error:

ValueError: ticks out of range. Expected 0 to 255

lowercasename avatar Aug 02 '22 11:08 lowercasename

You have two choices:

  1. You can adjust the clock frequency of the timer to make 1 tick != 1 second and adjust your ticks amount accordingly.
  2. You can calculate a future date and use the alarm feature instead

The number of ticks is otherwise hard-limited to 255 by the 8-bit register in the PCF86063A.

You can accomplish 1 using the ttp argument of set_timer though the available values - afaict - are 64Hz and 4096Hz.

In your case 60 * 60 could be:

rtc.set_timer(60, ttp=TIMER_TICK_64HZ)

Which I think is equivalent to 60 * 64 and roughly converts the set_timer value to minutes instead of seconds.

I spent some time trying to accomplish 2 cleanly, and have some code I'll have to dig up. Adding dates/times together is non-trivial and the comprehensive datetime library is rather big.

Gadgetoid avatar Aug 02 '22 14:08 Gadgetoid

Here's a really cut down datetime.py:

_DBM = (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)
_DIM = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)


def _leap(y):
    return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)


def _dby(y):
    # year -> number of days before January 1st of year.
    Y = y - 1
    return Y * 365 + Y // 4 - Y // 100 + Y // 400


def _dim(y, m):
    # year, month -> number of days in that month in that year.
    if m == 2 and _leap(y):
        return 29
    return _DIM[m]


def _dbm(y, m):
    # year, month -> number of days in year preceding first day of month.
    return _DBM[m] + (m > 2 and _leap(y))


def _ymd2o(y, m, d):
    # y, month, day -> ordinal, considering 01-Jan-0001 as day 1.
    return _dby(y) + _dbm(y, m) + d


def _o2ymd(n):
    # ordinal -> (year, month, day), considering 01-Jan-0001 as day 1.
    n -= 1
    n400, n = divmod(n, 146_097)
    y = n400 * 400 + 1
    n100, n = divmod(n, 36_524)
    n4, n = divmod(n, 1_461)
    n1, n = divmod(n, 365)
    y += n100 * 100 + n4 * 4 + n1
    if n1 == 4 or n100 == 4:
        return y - 1, 12, 31
    m = (n + 50) >> 5
    prec = _dbm(y, m)
    if prec > n:
        m -= 1
        prec -= _dim(y, m)
    n -= prec
    return y, m, n + 1


MINYEAR = 1
MAXYEAR = 9_999


class timedelta:
    def __init__(
        self, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0
    ):
        s = (((weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds
        self._us = round((s * 1000 + milliseconds) * 1000 + microseconds)

    def __neg__(self):
        return timedelta(0, 0, -self._us)

    def tuple(self):
        d, us = divmod(self._us, 86_400_000_000)
        s, us = divmod(us, 1_000_000)
        h, s = divmod(s, 3600)
        m, s = divmod(s, 60)
        return d, h, m, s, us


def _date(y, m, d):
    if MINYEAR <= y <= MAXYEAR and 1 <= m <= 12 and 1 <= d <= _dim(y, m):
        return _ymd2o(y, m, d)
    elif y == 0 and m == 0 and 1 <= d <= 3_652_059:
        return d
    else:
        raise ValueError


def _time(h, m, s, us):
    if (
        0 <= h < 24
        and 0 <= m < 60
        and 0 <= s < 60
        and 0 <= us < 1_000_000
    ) or (h == 0 and m == 0 and s == 0 and 0 < us < 86_400_000_000):
        return timedelta(0, s, us, 0, m, h)
    else:
        raise ValueError


class datetime:
    def __init__(
        self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tz=0):
        self._d = _date(year, month, day)
        self._t = _time(hour, minute, second, microsecond)

    def __add__(self, other):
        us = self._t._us + other._us
        d, us = divmod(us, 86_400_000_000)
        d += self._d
        return datetime(0, 0, d, 0, 0, 0, us)

    def __sub__(self, other):
        if isinstance(other, timedelta):
            return self.__add__(-other)
        elif isinstance(other, datetime):
            d, us = self._sub(other)
            return timedelta(d, 0, us)
        else:
            raise TypeError

    def _sub(self, other):
        # Subtract two datetime instances.
        dt1 = self
        dt2 = other
        os1 = dt1.utcoffset()
        os2 = dt2.utcoffset()
        if os1 != os2:
            dt1 -= os1
            dt2 -= os2
        D = dt1._d - dt2._d
        us = dt1._t._us - dt2._t._us
        d, us = divmod(us, 86_400_000_000)
        return D + d, us

    def tuple(self):
        d = _o2ymd(self._d)
        t = self._t.tuple()[1:]
        return d + t

And an example adding 6 hours:

import time
import datetime

d = datetime.timedelta(hours=6)
t = datetime.datetime(*time.localtime()) + d
target_time = t.tuple()

print(target_time)

This can be used with set_alarm.

Gadgetoid avatar Aug 08 '22 10:08 Gadgetoid

This is very helpful, thank you - I will use it elsewhere.

I may have gone about this a naive way but I ended up simply taking the current hour, adding 1 modulo 24, and using that as the next hour for set_alarm, copying the current minutes and seconds from the RTC. It means the update time drifts by about a minute each time as it takes that length of time for the frame to wake up and update, but it seems to do the trick - unless I've missed something fudnamental!

lowercasename avatar Aug 08 '22 10:08 lowercasename

Based on https://github.com/pimoroni/pimoroni-pico/blob/ac2fa97e9673d0f59ccb0ac2b41eaa2a7283f033/libraries/inky_frame/inky_frame.cpp#L112-L120

I think

In your case 60 * 60 could be:

rtc.set_timer(60, ttp=TIMER_TICK_64HZ)

should be

rtc.set_timer(60, ttp=PCF85063A.TIMER_TICK_1_OVER_60HZ)

for 60 minute RP2040-power-off sleep. Having said that none of this code is working for me at the moment...

kevinjwalters avatar Aug 21 '22 13:08 kevinjwalters

OK, so I'd really like to use an alarm rather than sleep and then have a callback function, but I can't figure out if this is even possible - am I looking in the wrong place for the PCF85063A python documentation? I can't find it anyway.

emmanorling avatar Aug 31 '22 10:08 emmanorling

Might be helpful to someone else: I found I had to call rtc.reset() before setting the RTC timer; otherwise, it only entered deep sleep the first time, and after subsequent awakenings, it would never deep sleep again. I guess that might be obvious to some, but I didn't see it documented in the guide or examples.

Using this with rtc.set_timer(60, ttp=PCF85063A.TIMER_TICK_1_OVER_60HZ) (as mentioned above), I have it reliably sleeping for (close to) an hour at a time :)

turley avatar Oct 21 '22 23:10 turley

I got it working in the end in https://github.com/kevinjwalters/micropython-examples/blob/master/pico-w/sdslideshow.py. I also am using rtc.reset() but I've forgotten the details of the fiddling around I had to do to get this working.

Relevant parts of the code for rtc/sleeping are:

https://github.com/kevinjwalters/micropython-examples/blob/490f5d0cffc3d813dfd6fea0efce3cd90be148df/pico-w/sdslideshow.py#L133-L137

https://github.com/kevinjwalters/micropython-examples/blob/490f5d0cffc3d813dfd6fea0efce3cd90be148df/pico-w/sdslideshow.py#L237-L245

@turley Did you do any timing to check accuracy of the clock? I've given away the one I did some timings on but I was getting 146 seconds for a 180 second rtc sleep. I've got a replacement now so I look at this again...

kevinjwalters avatar Oct 22 '22 14:10 kevinjwalters

@turley Did you do any timing to check accuracy of the clock? I've given away the one I did some timings on but I was getting 146 seconds for a 180 second rtc sleep. I've got a replacement now so I look at this again...

@kevinjwalters I have noticed that it tends to wake up earlier than intended, but no, I haven't done any proper measurements. For my project, the timing isn't super critical. Given that it does always seem early though, I assume one could measure and compensate for at least some of the inaccuracy when setting the timer.

turley avatar Oct 22 '22 20:10 turley

RTC things should be much easier to do with the new Inky Frame helper module, so closing this one for now.

helgibbons avatar Apr 05 '23 13:04 helgibbons