Sleep Deeply, Wake Briefly

Microcontrollers typically use on the order of milliamps when they are running, so if you want to make a battery last more than a few days, then you need to make sure that the processor is running as little as possible. This means you should almost always be sleeping.

On the AVR microcontrollers, this typically means setting the WatchDog timer to wake you up, and then going to sleep. You can set the WatchDog to wake up anywhere from 60 times per second, to only once every 8 seconds depending on what you need to do.

Once the WatchDog triggers, you need to wake up as fast as you can, then do what you need to do, then go back to sleep.

It turns out that correctly setting the SUT fuse bits can have a huge impact on how long it takes you to wake up each time, and this can have a huge impact on how much power you use overall.

Correctly setting the SUT fuses is critical for minimizing power when using sleep modes.

The default setting adds a delay of 62 milliseconds to every wake up, and the processor uses about 5mA during the delay. 

Lets take a look at a possibly the smallest possible wake up task to highlight this. All this program does when it wakes up is turn on a pin (so we know it is working), get everything ready to go back to sleep, turn off the pin, and sleep.

Here is what that looks like on the scope…

We are just needlessly burning juice during this entire 67ms delay!
We are just needlessly burning juice during this entire 67ms delay!

The little “T” is spot where we turned on the pin (and triggered the scope). The pin is on for such a sort amount of that we can not even see the yellow trace go up at this scale (10ms/div).

The trace is showing the chip’s current usage. See how it the chip starts using power more than 67ms before it actually wakes and runs our code? This is terrible! What is going on?

It turns out that the chip has a feature that delays the chip from running any code for a while right after it wakes up. The idea behind this is to let everything settle down and smooth out before it starts. By default, it waits ~62 milliseconds. This is a good idea when, say, you are running off a power supply that takes some time to stabilize. It is a terrible idea when you are just waking up for a tiny moment to do something and then go back to bed.

According to the math, we are wasting about 0.25 milliampere seconds every time we wake up. That adds up fast. If we are waking up 4 times per second, the delay will be wasting an average of 1mA all the time!

Since this delay happens before the chip even starts up, the only place to configure it is by setting the SUT (Start Up Delay) fuse bits using a hardware programmer.  Here is the default setting…

SUT65

The Ognite runs off a nice stable battery , so we don’t need any delay. Even if we did, it would be much better to maybe use the Brown Out Detector to delay our startup since we could then disable it once we were up and running and then not need to endure the delay everytime we woke up from a WatchDog. 

If you’ve read this far, I assume you are very interested in power saving strategies on the AVR microcontrollers.  In that case, here is some code that demonstrates how you can use only WatchDog resets to wake up periodically with very high power efficiently  …

/*
 * This is a framework for very low power AVR applications that spend most time
 * sleeping and do ongoing work only when woken by a WatchDog reset.
 *
 * For best power efficiency, program the SUT fuses for 0ms startup delay
 *
 * Created: 11/18/2013 10:45:43 PM
 * Author: josh
 * Website; https://ognite.wordpress.com
 */
#include <avr/io.h>

#include <avr/power.h>

#include <avr/sleep.h>

#include <avr/wdt.h>

#include <avr/wdt.h>                                // Watchdog Functions


// Set these according to your project,
// but make sure SUT1 and SUT0 are set or you
// waste a lot of time waiting for start up delay

FUSES = {
        
    .low = (FUSE_SUT1 & FUSE_SUT0 & FUSE_CKSEL3 & FUSE_CKSEL2 & FUSE_CKSEL0),                        // Startup with clock/8, no startup delay, 8Mhz internal RC
    .high = HFUSE_DEFAULT,
    .extended = EFUSE_DEFAULT
        
};
        
void init0 (void) __attribute__ ((naked)) __attribute__ ((section (".init0")));

// This code will be run immedeately on reset, before any initilization or main()

void init0(void) {

 asm( "in __tmp_reg__ , %[mcusr] " : : [mcusr] "I" (_SFR_IO_ADDR(MCUSR)) ); // Get the value of the MCUSR register into the temp register
 asm( "sbrc __tmp_reg__ ,%[wdf] " : : [wdf] "I" (WDRF) ); // Test the WatchDog bit and skip the next instruction if the bit is not set
 asm( "rjmp warmstart" : : ); // If we get to here, the the WDT bit was set so we are waking up warm, so jump to warm code

 // On power up, This code will fall though to the normal .init seconds and set up all the variables and get ready for main() to start
}

// Put any code you want to run once on power-up here....
// Any global variables set in this routine will persist to the WatchDog routine
// Note that I/O registers are initialized on every WatchDog reset the need to be updated inside userWakeRountine()

// This is "static inline" so The code will just be inserted directly into the main() code avoiding overhead of a call/ret

static inline void userStartup(void) {

 // Your start-up code here....

}

// Main() only gets run once, when we first power up

int main(void)
{
 userStartup();   // Do this first, because if we turned on WDT first it might reset on us
 wdt_enable(WDTO_15MS); // Could do this slightly more efficiently in ASM, but we only do it one time- when we first power up
 // The delay set here only matters for the first time we reset to get things started, so we set it to the shortest available so we don't wait to wait too long...
 // In warmstart we will set it recurring timeout. 

 // Note that we could save a tiny ammount of time if we RJMPed right into the warmstart() here, but that
 // could introduce a little jitter since the timing would be different on the first pass. Better to
 // always enter warmstart from exactly the same reset state for consistent timing.

 MCUCR = _BV( SE ) | _BV(SM1 ) | _BV(SM0); // Sleep enable (makes sleep instruction work), and sets sleep mode to "Power Down" (the deepest sleep)

 asm("sleep");

 // we should never get here

}

// Put any code you want to run on every wake up here....
// Any global variables used in this routine will persist
// All I/O Registers are reset to their initial values (per the datasheet) everytime we wake up
// If you plan to do work for longer than the WatchDog timer, then you need to do a WatchDogReset (WDR) occasionally to keep the timer from expiring on you. Note that this can add jitter if consistant timing is important.

// This is "static inline" so The code will just be inserted directly into the warmstart code avoiding overhead of a call/ret

static inline void userWakeRoutine(void) {

 // Just for testing, lets twiddle PORTD0 bit on and off with a little delay between...

 DDRD |= _BV(0);   // Note that we can't do this in startup because IO registers get cleared everytime we reset
 PORTD |= _BV(0);

 for( unsigned x=0; x<100;x++ ) {asm("nop");}

 PORTD &= ~_BV(0);

}

void __attribute__ ((naked)) warmstart(void) {

 // Set the timeout to the desired value. Do this first because by default right now it will be at the initial value of 16ms
 // which might not be long enough for us to do what we need to do.

WDTCR = _BV( WDP1) | _BV( WDP0) ; // This sets a 125ms timeout. See the table of timeout values in the datasheet. Note that you can delete this line completely if you want the default 16ms timeout.

// Now do whatever the user wants...

 userWakeRoutine();

 // Now it is time to get ready for bed. We should have gotten here because there was just a WDT reset, so...
 // By default after any reset, the watchdog timeout will be 16ms since the WDP bits in WDTCSR are set to zero on reset. We'd need to set the WDTCSR if we want a different timeout
 // After a WDT reset, the WatchDog should also still be on by default because the WDRF will be set after a WDT reset, and "WDE is overridden by WDRF in MCUSR. See “MCUSR – MCU Status Register” on page 45for description of WDRF. This means thatWDE is always set when WDRF is set."

 MCUCR = _BV( SE ) | _BV(SM1 ) | _BV(SM0); // Sleep enable (makes sleep instruction work), and sets sleep mode to "Power Down" (the deepest sleep)

 asm("sleep");

 // we should never get here

 }

First look at the main(). This is pretty standard C code for setting the WatchDog up to wake us, then enabling sleep, and then actually sleeping. The main() only gets called once at power-up. 

Now look above it at the .init0 routine. This little bit of assembly code gets run right after a reset before anything else happens.  It checks to see if we just woke up (as opposed to just powering up)  and if we did, it jumps straight into our warmstart() code. It skips eveything that normally happens on a reset like setting up registers and initialing variables.  This stuff is slow and would delay us from doing the work that we woke up to do, and worse it messes up variables and registers that it might be nice to keep intact between wake ups so we don’t have to keep figuring them out over and over again.

You could also do this by having the WatchDog timeout trigger an Interrupt routine, but since we are just going to be sleeping between WatchDogs, this is wasteful. Interrupt routines assume that they will be “interrupting” other running code so they do a lot of work saving and restoring stuff that we just don’t need. We want a quick wake-do-sleep!

Once into warmstart() we are ready to immediately do our work. All global variables are preserved between between wake ups. The only overhead in warmstart() is getting ready to sleep again, which is a single assignment to MCUCR and setting the timeout again in WDTCR. You might wonder how we can get away with not needing to enable to the WatchDog again (which can be a complicated affair). It turns out that if we leave the WatchDog Reset Flag alone, it will override the WatchDog enable bit and let the WatchDog continue to repeat fire automatically! Handy! 

What about setting the WatchDog timeout? In this case, I am using the 125ms timeout, but you could change this to anything you want. If you happened to want the 62ms timeout, you can actually delete this line altogether since that corresponds to all zero bits, and this will be the default value in this register after reset.

Note that anytime we reset (on power-up, or waking with a WatchDog) all I/O registers are set to their initial values. For most things (like DDR and PORT registers), this is zero, but you can look on the data sheet for the chip to see what the initial value for any individual register is. In Ognite this is great since it saves us a little work zeroing out the DDR registers.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s