Using Sleep mode when delaying

Wrote code to put Arduino into Power Down sleep mode when calling a delay. Seems to work ok. Comments appreciated. I haven’t learnt how do libraries yet but eventually, I’ll turn it into a library and also put it in the Playground.

Features are:

  • Calibration of timer: calibrate(). Watchdog timer can be 10% off and varies with temperature. This will calibrate watchdog timer with clock timer and adjusts delay so that it is closer to actual time. You can call the calibrate function every hour or so to readjust.

  • Replacement for millis(). Since clock stops during sleep, I have provided estMillis() which will give you an estimated actual time. What this does is to keep the accumulated sleep time (calibrated) and adds it to millis() to give you actual time.

  • Delay function sleepCPU_delay(long millis). Will sleep for the specified time. Note that sleep granularity is 16ms so that is the smallest amount of time it will sleep and it will always sleep in multiples of 16ms.

#include <avr/sleep.h>
#include <avr/wdt.h>

long timeSleep = 0;  // total time due to sleep
float calibv = 0.93; // ratio of real clock with WDT clock
volatile byte isrcalled = 0;  // WDT vector flag

// Internal function: Start watchdog timer
// byte psVal - Prescale mask
void WDT_On (byte psVal)
{
  // prepare timed sequence first
  byte ps = (psVal | (1<<WDIE)) & ~(1<<WDE);
  cli();
  wdt_reset();
  /* Clear WDRF in MCUSR */
  MCUSR &= ~(1<<WDRF);
  // start timed sequence
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  // set new watchdog timeout value
  WDTCSR = ps;
  sei();
}

// Internal function.  Stop watchdog timer
void WDT_Off() {
  cli();
  wdt_reset();
  /* Clear WDRF in MCUSR */
  MCUSR &= ~(1<<WDRF);
  /* Write logical one to WDCE and WDE */
  /* Keep old prescaler setting to prevent unintentional time-out */
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  /* Turn off WDT */
  WDTCSR = 0x00;
  sei();
}

// Calibrate watchdog timer with millis() timer(timer0)
void calibrate() {
  // timer0 continues to run in idle sleep mode
  set_sleep_mode(SLEEP_MODE_IDLE); 
  long tt1=millis();
  doSleep(256);
  long tt2=millis();
  calibv = 256.0/(tt2-tt1);
}

// Estimated millis is real clock + calibrated sleep time
long estMillis() {
  return millis()+timeSleep;
}

// Delay function
void sleepCPU_delay(long sleepTime) {
  ADCSRA &= ~(1<<ADEN);  // adc off
  PRR = 0xEF; // modules off

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  int trem = doSleep(sleepTime*calibv);
  timeSleep += (sleepTime-trem);

  PRR = 0x00; //modules on
  ADCSRA |= (1<<ADEN);  // adc on
}

// internal function.  
int doSleep(long timeRem) {
  byte WDTps = 9;  // WDT Prescaler value, 9 = 8192ms

  isrcalled = 0;
  sleep_enable();
  while(timeRem > 0) {
    //work out next prescale unit to use
    while ((0x10<<WDTps) > timeRem && WDTps > 0) {
      WDTps--;
    }
    // send prescaler mask to WDT_On
    WDT_On((WDTps & 0x08 ? (1<<WDP3) : 0x00) | (WDTps & 0x07));
    isrcalled=0;
    while (isrcalled==0) {
      // turn bod off
      MCUCR |= (1<<BODS) | (1<<BODSE);
      MCUCR &= ~(1<<BODSE);  // must be done right before sleep
      sleep_cpu();  // sleep here
    }
    // calculate remaining time
    timeRem -= (0x10<<WDTps);
  }
  sleep_disable();
  return timeRem;
}

// wdt int service routine
ISR(WDT_vect) {
  WDT_Off();
  isrcalled=1;
}

Sleeping is good for saving power, right? Can a hardware interrupt (pin 2 or 3 if I recall correctly) wake it from sleep earlier than the delay you're setting?

A hardware interrupt will execute your service routine but this code will continue sleeping for the duration of the delay. I could add a function to cancel the sleep which you could then call from your interrupt routine.

EDIT: Just to clarify, whenever some other interrupt occurs, this code will immediately put the Arduino back to power down sleep after the interrupt routine has completed. So if you issue a sleepCPU_delay(1000), this function will not return for 1008ms.

What if you created a pointer to the Sleeptime variable… And your interrupt changed it to 0… Would it wake up within 16ms of the interrupt occuring? :slight_smile: I like your code, it’s way above my head though, as I’m just getting into the Arduino. I see you have comments all over that code, that’s very helpful for understanding it. :slight_smile:

What if you created a pointer to the Sleeptime variable.. And your interrupt changed it to 0.. Would it wake up within 16ms of the interrupt occuring?

That would work but then estMillis() would be inaccurate. It would be better to have a cancel function so that I can cancel gracefully. I've already written the function and turned this all into a library. I'll post it up when I have fully tested it.

I have three suggestions...

  1. Use _WD_CONTROL_REG instead of WDTCSR

  2. Use _WD_CHANGE_BIT instead of WDCE

  3. Use "unsigned long" for time value instead of "long"

Thanks for those suggestions. Out of interest, where did you get the info on _WD_xx from? I searched the datasheet and there's no mention of them there. The code I use in the WDT_On/Off functions are a direct copy/paste from the AVR datasheet, chapter 10.8.2.

Digging through the AVR-GCC header files.

Looking through the header files, I notice an obvious problem with my code which is WDP3 is not available on all MPUs. What's the recommended way to handle this? Is there a way to find out dynamically in code or do I just use a macro as follows?

if defined(WDP3)

byte WDTps = 9; // WDT Prescaler value, 9 = 8192ms

else

byte WDTps = 7; // WDT Prescaler value, 7 = 2048ms

endif

Macros / conditional compilation are definately the way to go.