Monday, April 18, 2022

More good times with LMIC and an AdaFruit LoRaWAN Feather M0

It is notoriously difficult to allow a node based upon the Arduino port of LMIC to sleep in a low power mode due to having to bring either LMIC or the Arduino core up-to-date with the time when the MCU awakes. It seems to be impossible without modifying either LMIC or the Arduino core.

This post covers the first half of the problem, how to give LMIC a time source that survives standby mode.

The basic problem seems to be that LMIC uses the Arduino micros() function to derive its internal tick value, and the value of micros() is not updated while the MCU is asleep. So if LMIC has a job scheduled, that job will not run at the appropriate time because LMIC (and the Arduino core) are now ‘in the past’ with respect to real-world time.

Various people have had a go at doing this by bring the time in the Arduino core up-to-date but at least in the AdaFruit SAMD21 M0 core I cannot see how to do this without changing the Arduino core because the varaibles are marked static, so cannot be referenced externally. The other thing I don’t like about this solution is that I don’t understand how LMIC derives its ticks from the micros() value, and if I must change something I may as well change code at the upper level (compared to the Ardiuino core code) to something I do understand.

Another problem with naive sleep schemes is that they may not handle cases where the LoRaWAN server causes a bit of a conversation to happen by downloading MAC commands, meaning the node should acknowledge those commands in a subsequent uplink that happens fairly quickly. I have observed up to to extra uplinks at a time caused by these interactions, with the uplinks happening in the space of a minute or two after the node’s own expected uplink. Simply going to sleep for some period of time after the expected uplink is done means these MAC command interchanges do not happen correctly and the server may continue to schedule downlinks to try and configure the node. Again, the next post will look at this in more detail.

Examing the LMIC os_ and hal_ layers of code it seems:

  • LMIC uses an unsigned 32-bit value for its internal ticks.

  • The frequency of the ticks is, by default, 32.768 KHz.

  • LMIC seems to be able to handle the roll-over of the 32-bit value even when it has jobs scheduled so far in the future that the value rolls-over many times.

Given the above, if a means to keep the ticks increasing at the fixed rate when the MCU is asleep can be arranged, LMIC should be able to start processing again when the MCU wakes up.

I took a look at the start-up state of the Feather when setup() is called to see how the oscillators and clock generators were configured, and what peripherals were free. That can be expanded upon in another post, but the good news is:

  • The external 32.768 KHz oscillator (XOSC32K) is enabled, and just needs the RUNSTDBY bit set to keep it going in standby mode to make it perfect for this job.

  • Clock generators 4 - 7 are all free to take a signal from an oscillator and send it to a peripheral.

  • The timer/counter peripherals are free, and can be put into a 32-bit counting mode.

  • The ultra-low-power internal 32.768 KHz oscillator could also be used, and always runs in standby mode, but it may not be accurate enough.

  • XOSC32K is used by the RTCZero library to drive the RTC, and I’m going to use the the RTC so I may as well use XOSC32K myself given it’s already running.

So hooking up XOSC32K to drive timer/counter pair 4/5 as a 32-bit counter, and have LMIC read the 32-bit COUNT register should satisfy the requirements above.

It seems like a cleaner solution to me, giving LMIC exactly what it wants with no bit shifting weirdness, and it just survives standby mode rather than having push time forward in the Arduino core, if you can even access the variables you need without modifying the core code.

The downsides are:

  • You must learn how to set these things up and it isn’t trivial; the datasheet is not easy to read and provides no code examples of how to do anything.

  • You must modify the LMIC hal_time_init() and hal_ticks() functions, meaning you lose compatibility with the standard MCCI Arduino LMIC library, and updating the library will lose your changes.

Here are the changes to hal.cpp in LMIC. Replace the hal_time_init() and hal_ticks() functions with these:

static void hal_time_init () {
    // Do not be tempted to split the register writes into separate statements
    // or use code that reads and writes them - the datasheet is quite specific
    // that some writes must just be whole-register writes or things don't work.

    // This is the same as how XOSC32K looks after the AdaFruit core has initialsed
    // the system, except the RUNSTDBY bit is being set so it keeps running during
    // standby mode.
    SYSCTRL->XOSC32K.reg = SYSCTRL_XOSC32K_RUNSTDBY
                         | SYSCTRL_XOSC32K_EN32K
                         | SYSCTRL_XOSC32K_XTALEN
                         | SYSCTRL_XOSC32K_STARTUP(6)
                         | SYSCTRL_XOSC32K_ENABLE;
    while (GCLK->STATUS.bit.SYNCBUSY);

    GCLK->GENCTRL.reg = GCLK_GENCTRL_ID(4)             // Specifies which generator is being configured
                      | GCLK_GENCTRL_GENEN             // Eenable the generator
                      | GCLK_GENCTRL_SRC_XOSC32K;      // Use XOSC32K as the source for the generator
    while (GCLK->STATUS.bit.SYNCBUSY);

    GCLK->CLKCTRL.reg = GCLK_CLKCTRL_GEN(4)            // Specifies which clock is being configured
                      | GCLK_CLKCTRL_CLKEN             // Enable the clock
                      | GCLK_CLKCTRL_ID(GCM_TC4_TC5);  // Feed the clock into peripheral timer/counter 4&5.
    while (GCLK->STATUS.bit.SYNCBUSY);

    // Disable timer/counter so it can be configured.
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);
    TC4->COUNT32.CTRLA.reg &= ~TC_CTRLA_ENABLE;
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);

    TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32     // Use 32-bit counting mode.
                           | TC_CTRLA_WAVEGEN(0)       // Use NFRQ mode so it just counts.
                           | TC_CTRLA_RUNSTDBY;        // Run when in standby mode.
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);

    // Enable the TC.
    TC4->COUNT32.CTRLA.reg |= TC_CTRLA_ENABLE;
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);

    // The 32-bit COUNT register of timer/counter 4&5 is now being incremented 32768 times a second.
}

u4_t hal_ticks () {
    // Signal we want to read the value of the COUNT register.
    TC4->COUNT32.READREQ.reg = TC_READREQ_RREQ | TC_COUNT32_COUNT_OFFSET;

    // Wait for the register value to be available.
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);

    // Read it.
    return TC4->COUNT32.COUNT.reg;
}

I had to add these #defines to lmic_project_config.h file to get everything to work, it’s too late in either oslmic.h or hal.h.

// LMIC requires ticks to be 15.5μs - 100 μs long
#define OSTICKS_PER_SEC 32768
// 1s/32768 = 0.00003052 so each tick is 30.52 μs, round up to 31 μs.
//
#define US_PER_OSTICK 31

With these changes LMIC has a very clean and simple source for its 32.769 KHz ticks. If you want to keep a 64-bit value for ticks, as LMIC may in future, it is a matter of enabling the overflow interrupt and incrementing the top-half of a uint64_t variable on each overflow, and putting the value of COUNT into the lower half whenever LMIC wants the value.

I have had a sketch running for at least 4 days with with these modifications, sending my own uplinks every 15 minutes, and it has been rock solid. I’ve used LMIC to schedule these 15 minute intervals, and been checking its idea of the time until the next job and it all seems correct. I may have missed the exact roll-overs, but looking back through the output I don’t see unexpected intervals.

No comments: