Hardware Sleep-Wake Toggle

While working on some holiday projects, I needed on off switches for some JeeNode based thingies. It took me a couple of hours to get it working well, and I learned some stuff while doing it.

I’ll include the code here, but if you want nicer formatting see my blog post: goo.gl/BNccf (you’ll have to paste that b/c I’m not allowed to post a link on my first post)

The code is simple, small and written in an instructional style.

-Kael

/*
  SleepWakeToggle
  
  SUMMARY 
  Toggles an atmega*8 state from running to lowest power sleep
  mode using a hardware interrupt, e.g.  a switch.  Suitable for
  Arduino, JeeNode, etc.

  WHY
  Battery powered devices can last double-plus-much-longer if you power
  the chip down when not in use.

  HOW IT WORKS
  Interrupts are a way of having your program flow altered by simple
  real world events (e.g. passage of time or activation of switches).

  This program demonstrates using a single switch to accomplish two
  things: putting the chip in sleep mode and then waking it up.  When
  a switch is activated, an interrupt is triggered and program
  control is shifted to a function you specify (a callback).  Two
  hardware interrupts are available INT0 and INT1.
  
  Boldly rebinding INT0/INT1 callbacks to the wake and sleep functions
  in the callbacks themselves is bad magic, and doesn't seem to work
  :-).  So the sleep function is called from loop() after a callback
  sets a flag.  This way the callback which triggers sleep (here:
  'toggle') is done before 'void sleep()' actually runs.  This is good
  because, the interrupt has to be rebound to 'wake' just before going
  in to sleep_mode.

  HARDWARE SETUP 
  Lowest power sleep mode, SLEEP_MODE_PWR_DOWN, can only respond to a
  pin going from high to low. Hence, tie desired pin (2 or 3) to +
  with a resistor (i.e. a "pull-up" resistor), and connect a momentary
  switch to ground and the pin.

  SOFTWARE SETUP 
  You will want to provide functions that do something interesting and
  that cause the system to be in your desired state before actually
  powering down.  These are: "void importantThing()" and "void
  prepareToSleep()" below.  The state that is achieved after power up
  or reset is selected by setting the global flag buttonPushed in the
  setup function.

  Hereby placed in the public domain, December 2010
  by Kael Fischer <kael.fischer@gmail.com>

 */

// hardware interrupt stuff
#include <avr/sleep.h>
#define INT0 0 
#define INT0_PIN 2
#define INT1 1 
#define INT1_PIN 3

// application specific stuff
#define BLINK_PIN 13

// global flags
int buttonPushed;

void importantThing(){
  // the 'work' the program does
  digitalWrite(BLINK_PIN, HIGH);
  delay(1000);
  digitalWrite(BLINK_PIN, LOW);
  delay(1000);           
}

void prepareToSleep(){
  // When a button is pushed
  // do some prep before sleeping 
  // to get in the desired state
  digitalWrite(13, LOW);
}

void wake(){

  // This is the callback that runs when the 
  // chip is waking up.
  buttonPushed=0;

  // Can set variables and check stuff.  Cannot do fancy stuff.
  // Program flow returns to line after 'sleep_mode()' in sleep.

  // No code is required here.
}

void toggle(){
  // set a flag telling system to sleep on next loop iteration.
  // this is the normal running state callback triggered by the 
  // switch.
  detachInterrupt(1);    // this seems to be allowed in callback
  if (buttonPushed ==0){ // just in case, ignore switch if already 
    buttonPushed=1;      // doing it 
  }
}

void sleep(){
  prepareToSleep();  //application specific

  // YOU ARE GETTING SLEEPY.....
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  
  attachInterrupt(INT1,wake,LOW); // rebind interrupt
  sleep_mode(); // sleeping
  
  // program returns here, after 'wake' callback
  
  // now back to loop
}

void setup() {                
  // On Off Toggle setup
  pinMode(INT1_PIN, INPUT);  // set switch pin to input 
                             // to limit current 
  attachInterrupt(INT1,toggle,LOW);
  buttonPushed=1; // set to 1 = sleep after reset
                  //        0 = run loop after reset

  // app specific setup here
  pinMode(BLINK_PIN, OUTPUT);  
}

void loop() {
  if (buttonPushed == 1) {
    sleep();
  } else {
    // reset button events to sleep
    attachInterrupt(INT1,toggle,LOW);
  }
  // do important things
  importantThing();
}

Edit: modified per discussion below.

Looks good, I'll save this one for later use :)

Just a question, does the code in the loop that resets the button events:

// reset button events to sleep attachInterrupt(INT1,toggle,LOW); buttonPushed=0;

have to be called all the time or can you actually put it in the wake() function? If not because wake() can't do special things, how about after sleep_mode() is called?

Putting the attachInterrupt call in the "void wake()" callback angers the Flying Spaghetti Monster. This is one of many configurations that work once, and then one of the bindings fail and you are stuck in one state. In this case it is the 'on' state.

You can put buttonPushed=0; in "wake" and then you could have the attachInterrupt in an else in loop(). But it still executes almost every time. The assignment would only run on wake up however.

It's a modest improvement. I changed above and my original blog post to:

void loop() {
  if (buttonPushed == 1) {
    sleep();
  } else {
    // reset button events to sleep
    attachInterrupt(INT1,toggle,LOW);
  }
  // do important things
  importantThing();
}

Don't forget to move buttonPushed=0 to wake as well.

Thanks.

-K

Thanks, so having attachInterrupt(INT1,toggle,LOW); in the loop is kind of a safety mechanism so we don't get stuck in the on state forever because it might not register the first attachInterrupt of toggle we send it?