rocketscream / Low-Power

Low Power Library for Arduino
www.rocketscream.com
1.26k stars 345 forks source link

Suggestion for improving "sleep" time accuracy. #112

Open MojaveTom opened 3 years ago

MojaveTom commented 3 years ago

This suggestion is similar to issue #111 in that a flag is to be set when the watch dog timer interrupt service routine is executed, but it this case, it is used to re-sleep the CPU if it is awakened by some other interrupt to ensure that the power saving function does not exit before the WDT interrupt happens. As I see it, the problem is that when the CPU wakes up in powerDown it doesn't know which interrupt awakened it, so it just assumes that it was the WDT. Since the interrupt service routine for the WDT is in that same module, it is easy to add a flag that is set when the WDT interrupt is entered, and cleared when the powerDown function first sleeps the CPU. The CPU is just put back to sleep if it is awakened and the wdtFinished flag is still false. When the wdtFinished flag is true, the powerDown function exits as usual.

With this modification, the time in the powerDown function is not short circuited by an unexpected interrupt.

// add new wdtFinished flag class variable
volatile bool LowPowerClass::wdtFinished;

...

void    LowPowerClass::powerDown(period_t period, adc_t adc, bod_t bod)
{
    if (adc == ADC_OFF) ADCSRA &= ~(1 << ADEN);

    if (period != SLEEP_FOREVER)
    {
        wdt_enable(period);
        WDTCSR |= (1 << WDIE);
    }
    if (bod == BOD_OFF)
    {
        wdtFinished = false;        //  added code
        while (!wdtFinished)        //  added code
        #if defined (__AVR_ATmega328P__) || defined (__AVR_ATmega168P__)
            lowPowerBodOff(SLEEP_MODE_PWR_DOWN);
        #else
            lowPowerBodOn(SLEEP_MODE_PWR_DOWN);
        #endif
        wdtFinished = false;        //  added code
    }
    else
    {
        wdtFinished = false;        //  added code
        while (!wdtFinished)        //  added code
            lowPowerBodOn(SLEEP_MODE_PWR_DOWN);
        wdtFinished = false;        //  added code
    }

    if (adc == ADC_OFF) ADCSRA |= (1 << ADEN);
}

...

 // set wdt finished flag
ISR (WDT_vect)
{
    // WDIE & WDIF is cleared in hardware upon entering this ISR
    wdt_disable();
    LowPowerClass::wdtFinished = true;        //  added code
}

The powerSave, powerStandby, and powerExtStandby functions could be modified in a similar manner.

MojaveTom commented 3 years ago

When I implemented these suggestions, I realized two things:

  1. The wdtFinished flag cannot be defined in the header file as a class variable; the interrupt service routine is not a class function (and cannot be one) and so does not have access to private or protected attributes of the class. I think it would be very dangerous to make this attribute public. The solution is to make the variable local to the .cpp file.
  2. If the user specifies to SLEEP_FOREVER, the routines would never return since the WDT interrupt in not enabled. The solution is to not loop if SLEEP_FOREVER is specified.

LowPower.cpp

// variable global to this file.
volatile static bool wdtFinished;

...

void    LowPowerClass::powerDown(period_t period, adc_t adc, bod_t bod)
{
    if (adc == ADC_OFF) ADCSRA &= ~(1 << ADEN);

    // set wdtFinished true if SLEEP_FOREVER so single test will terminate sleep
    wdtFinished = period == SLEEP_FOREVER;  // assigned before IE to prevent race
    if (period != SLEEP_FOREVER)
    {
        wdt_enable(period);
        WDTCSR |= (1 << WDIE);
    }

    do {    // Do this at least once even if SLEEP_FOREVER
        if (bod == BOD_OFF)
        {
                #if defined (__AVR_ATmega328P__) || defined (__AVR_ATmega168P__)
                    lowPowerBodOff(SLEEP_MODE_PWR_DOWN);
                #else
                    lowPowerBodOn(SLEEP_MODE_PWR_DOWN);
                #endif
        }
        else
        {
            lowPowerBodOn(SLEEP_MODE_PWR_DOWN);
        }
    } while (!wdtFinished);

    if (adc == ADC_OFF) ADCSRA |= (1 << ADEN);
}

...

ISR (WDT_vect)
{
    // WDIE & WDIF is cleared in hardware upon entering this ISR
    wdt_disable();
    wdtFinished = true;
}

The other functions would have similar modifications.