Sunday, April 2, 2017

Arduino Interrupt on Button Press

Hardware: Adafruit Feather 32u4 Radio (RFM69HCW)

Software: Arduino IDE 1.8.1.0 / Windows 10

I can't properly describe how I feel about the documentation on "interrupts" on Arduino.  Even if you don't get into "pin change interrupts" (i.e. NOT THE SAME THING!!!)  it still isn't straightforward.  Add in the fact that the reason you might be looking to handle an external interrupt is because you probably want to put the unit to sleep and now the levels of complexity are notably higher.

I have implemented a fairly simple solution to get past the issues commonly encountered and included it below.  This all revolves around a board going to sleep indefinitely and then waking up on a button press.

Basic Setup

In this scenario I connected up a single button to pin 3 on an Adafruit Feather 32u4 RFM69HCW board.  Of course, this should work without modification with any of the Feather 32u4 boards and probably most 32u4 boards in general.


The button in this example is wired to pin 3 (red wire in photo).  It definitely matters which pins you choose when dealing with interrupts, again don't confuse them with pin change interrupts.  For example, this is the layout for the Adafruit Feather 32u4 boards:

Look at the "Interrupt pins" in the legend (e.g. INT0, INT1, etc.)

The only pins you can use for interrupts on this board are 0, 1, 2 and 3.  However, 0 and 1 are by default tied to serial communications.  So by default, only 2 and 3 are usable for button interrupts.  It is possible still use 0 and 1, but to keep this simple, let's stick with pin 3.

Understanding Interrupts

On a basic level, Arduino Interrupts stop the main loop{} from processing (no matter what it is doing at that moment) and jumps to a function you designate (this is done when you enable the Interrupt).  Once that function ends the application continues where it left off.  

While that sounds pretty straightforward, you will quickly realize the danger this could bring.  It takes a good understanding of what interrupt really means in the world of Arduino.  In fact, interrupt is about as accurate of a description as I've ever encountered.

Interrupts just stop everything else for a moment.  Then when the interrupt is done, everything goes back to running as before.  Unless of course you have changed certain things within your interrupt code.  Don't think of Interrupts as stopping everything permanently.  Once your Interrupt is done, all things return to the very next line of where your original code was when the Interrupt occurred.

As well, trying to do any kind of delay() in your interrupt routine will fail.  You'll need to finish "handling" the interrupt and return to the main execution code before you can do this.

Another thing that seems to throw people off is that Interrupts are referenced using different numbers than the pin numbers.  In fact, most examples have the interrupts hard-coded.  Which is...stupid.  Particularly when the digitalPinToInterrupt() function exists.  Sending this function the normal button ID automatically translates the standard pin to an interrupt.  I'll use this in the example below. 

After spending hours (in fact...days) searching through various forums and sample code (where a majority hardcoded their Interrupts), I concluded that there is no default method for handling interrupts properly.  In fact, given some of the highly convoluted methods I saw implemented it seems that understanding of these issues is notably limited.  It made me question even the most basic examples I encountered because many of them also displayed a lack of understanding of Interrupts as a concept on Arduino.

From my own perspective (and having fallen for it myself), people seem to get highly trapped in the "event based" model once it is adopted.  This leads to notable difficulty in dealing with bounces or ghost events for button presses.  They discover Interrupts, then get all excited about doing everything event-based, then things quickly devolve.  

To get started on the right foot and avoid a lot of misinformation out there, follow what I've outlined below.

Basic setup

To set up an interrupt you need to use the attachInterrupt function.  While that particular function is well-utilized, people tend to ignore detachInterrupt.  In fact, detachInterrupt is essential.

A basic design

The key to properly using Interrupts is really to just disable the Interrupt as soon as you receive one.  Then when you are done with your discrete code that you don't want interrupted, turn it back on.  This very simple methodology will resolve many of the normal issues encountered relating to bounce or ghost presses.

So again, don't forget to detach the Interrupt after it occurs and only re-enable it when you're ready to allow it happen again.

Basic Example


By default, Arduino apps generally just repeat the loop() function indefinitely.  Until an Interrupt happens of course.  


#define BUTTON        3
void setup() {
   pinMode(BUTTON, INPUT);
   digitalWrite(BUTTON, HIGH);
}
void loop() {
   attachInterrupt(digitalPinToInterrupt(BUTTON), wakeUp, LOW);
   // Put your board to sleep somehow
   // Do a bunch of stuff that you do on a button press
}
void wakeUp() {
  detachInterrupt(digitalPinToInterrupt(BUTTON));
}

This very basic example is a good starting point for how to handle things.  Your application will essentially start up, assign an Interrupt to a pin, disable the Interrupt the first press, then go back to the main loop and eventually start the whole process all over.

But while this can be compiled and written to a board it doesn't really do anything...yet.

Add Sleep / Power down

NOTE:  If you're doing this incrementally then at the end of this you will need to skip down to the Troubleshooting section below because now your board is "asleep" and the USB port is shut down.  How will you ever load software on it again?!

In the code below I've included a sleep function from RocketScream.  Why?  Well, because while the Adafruit site pushes their SleepyDog code, it seems...incomplete...at best.  In fact, considering that they incorrectly reference their own boards this code works with (their ReadMe doesn't list the 32u4) and changed their own site to remove references to other people's more functional GitHub code, I can say that something else is going on here.  

Regardless of the reasons, their code *appears* to require a duration for sleeping.  Since my example is based on sleeping forever until an event occurs, their design doesn't appear useful.  After reading another annoying number of Google searches and blog posts I settled on RocketScream's, but also considered/tested (it worked) the LowPowerLab function as well.

To follow the code below download the RocketScream Low-Power .zip, and unzip it to (by default) ..\Documents\Arduino\libraries\Low-Power (e.g. remove "-master" from the end).  Then pull up your Arduino IDE and put in the following:


#include "LowPower.h"
#define BUTTON        3
void setup() {
   pinMode(BUTTON, INPUT);
   digitalWrite(BUTTON, HIGH);
}
void loop() {
   attachInterrupt(digitalPinToInterrupt(BUTTON), wakeUp, LOW);
   LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
   // We resume here after the Interrupt
   // Do a bunch of stuff that you want to happen after a button press
}
void wakeUp {
  detachInterrupt(digitalPinToInterrupt(BUTTON));
}

Now this is getting close to being useful.  This will work to power down your board and then wait for a button press.  After the press it will...do nothing.  Well, it does something, but nothing you'd notice externally.  We're getting there.  

NOTE:  Remember when I said you need to jump down to the Troubleshooting section?  

Add LED

I'm sure you know how to light up some LEDs, but for the sake of completeness I'll include it here.  When you do a button press you're going to want to do "something" and in this case we're going to flash the LED a few times.  I've connected my LED to the same pin as the onboard LED (e.g. pin 13) in this case.  You don't have to wire anything up (since the onboard LED will light up) but in my photo above I have mine wired (yellow wire) to the LED on my button as well.

#include "LowPower.h"
#define BUTTON        3
#define LEDPIN        13
void setup() {
    pinMode(BUTTON, INPUT);
    pinMode(LEDPIN, OUTPUT);
}
void loop() {
   attachInterrupt(digitalPinToInterrupt(BUTTON), wakeUp, LOW);
   LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
   //We resume here after the Interrupt
   Blink(LEDPIN,100,3);
}
void wakeUp() {
  detachInterrupt(digitalPinToInterrupt(BUTTON));
}
void Blink(byte ledPin, int msDelay, byte loops)
{
  for (byte i=0; i<loops; i++)
  {
    digitalWrite(ledPin,HIGH);
    delay(msDelay);
    digitalWrite(ledPin,LOW);
    delay(msDelay);
  }
}

So now we have something actually more useful.  The application starts, sets up the Button and LED pins for input and output respectively, puts the unit to sleep and waits for a button press to wake up and blink the LED a few times, then puts the unit back to sleep.  Lather, rinse, repeat.

Troubleshooting

Using different pins

The most likely thing that goes wrong here is that you decide to mix this up and switch pins around.  Again, you can use other pins for the Button/Interrupt, but as I explained above, only certain pins work with Interrupts.  And again, Interrupts are not the same as Pin Change Interrupts.  Totally different animals, but similar enough when (poorly) documented that you can easily confuse them.  Just stick with pins 2 or 3 and you won't get into the weeds until you need more than two buttons/Interrupts.


Board not seen by PC / Cannot upload sketches

When you get to the Add Sleep / Power down section you might notice that your board disconnects from your PC when it goes to sleep.  Which is actually a good thing.  The board shuts down the USB data connection to save power.  HOWEVER, this means that when you decide to load different code on the board it will not respond to you attempting to load new software on it.  First off, in the Arduino IDE you need to enable verbose output during upload.
Just check this box

After that, simply open up your sketch and press the Upload icon in the Arduino IDE as you would normally.  Then (while it is trying to upload) press/release the Reset button on your Feather 32u4.  The board will synch up, take the upload and everything will work as expected.  For other boards you will need to see their particular reference on how to get them to reload software after deep sleep.

Ghost presses / Button Bounce

The next thing you might run into if you aren't doing something that takes a second or two after you wake up is button bounce or ghost presses.  Any EE probably spent a chunk of their schooling on handling these exact kinds of issues via circuits.  You can even get A LOT better understanding of tactics and the problem in general see this article written by Jack Ganssle (whom I now owe one internet beer to).  In fact, I suggest you subscribe to his newsletter and let him merge in front of you whenever you see him on the road.

You can also be exceedingly lazy and try out Bounce.h instead of worrying your pretty little head with learning stuff.

Regardless, for the most part this simple method should handle such situations fairly reliably for simple applications.  As long as what you are doing after you wake up takes ~100-200ms or more then you are in pretty good shape.  Since I'm using a board above that includes an 868/915mhz radio you can (correctly) assume that I'm also sending out a radio signal on a button press.  But that is a story for another post...

8 comments:

  1. Hi, John! Thanks for this tutorial. I am trying to do approximately the same thing (sleep until interrupt) on the Feather M0 RFM95 LoRa Radio. This specific device uses an ATSAMD21G18 microcontroller, but the LowPowerClass doesn't seem to work with it. Do you have any suggestions for how to get sleep/wake to work for the Feather M0? Thanks!

    ReplyDelete
    Replies
    1. If I had one here I'd test but unfortunately I do not. I'd order one but I'm grumpy at Adafruit per them not restocking a particular item in a manner I'd consider "timely". :(

      I see some discussion that appears relevant per this at:

      http://www.arduino.org/forums/zero/m0-standby-mode-power-consumption-580

      Not the same board, but the same chip.

      What exact error are you running into? Can you copy/paste it here?

      Delete
    2. I don't think there's anything defined for ATSAMD21G18 in the LowPower.h file. Here is the error:

      interrupt:14: error: 'class LowPowerClass' has no member named 'powerDown'

      LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);

      ^

      exit status 1
      'class LowPowerClass' has no member named 'powerDown'

      Delete
    3. Patricia, I actually believe I solved a similar problem w/ this as I had to alter the static values I was passing into this function around the particular board. For example, I got similar errors when trying to get the unit to sleep for 4s but leave one of the timers running. I had to check the source code. At a glance, it looks like the section on the SAMD21G18 is remarked out in their code.

      --- (excerpt from LowPower.h) ---

      #elif defined (__arm__)

      #if defined (__SAMD21G18A__)
      void idle(idle_t idleMode);
      void standby();
      #else

      Delete
  2. How much battery life do you get with the 32u4 and deep sleep?

    ReplyDelete
    Replies
    1. That's actually the exact problem I am wrestling with now. Currently I'm getting about 45 days of life from a 2600mAh battery. I was testing w/ some Lithium Thionyl 3.6V "AA" form factor ones and have just moved back to similar capacity Lithium Polymer 3.7V (4.2V at full capacity) rechargeables to see how they differ.

      Delete
    2. The answer is: they mostly don't. I a now reviewing some additional code changes to see is I can improve this and also trying Adafruit's SleepyDog library. I doubt it will improve notably.

      Problem seems to be power draw during sleep.

      Delete
  3. Hi John, thank you for the tutorial! I want to do exactly the same thing with mit Adafruit Feather 32u4 but unfortunately it is still not working... the only thing that I have changed to your code is the state HIGH instead of LOW in "attachInterrupt" because my switch is HIGH when I press it and LOW if it is open. My switch generally works at Pin 3, I tested it with a program turning my LED on and off. Do you have any idea what I did wrong??

    Thank you!

    ReplyDelete