Streamline your Arduino code with Interrupts - the simple way of reacting to real-time events!

Recommended Level

Intermediate

 

We Interrupt This Broadcast...

As it turns out, there's a great (and underutilized) mechanism built into all Arduinos that is ideal for monitoring these kinds of real-time events. This mechanism is called an Interrupt. An Interrupt's job is to make sure that the processor responds quickly to important events. When a certain signal is detected, an Interrupt (as the name suggests) interrupts whatever the processor is doing, and executes some code designed to react to whatever external stimulus is being fed to the Arduino. Once that code has wrapped up, the processor goes back to whatever it was originally doing as if nothing happened!

What's awesome about this is that it structures your system to react quickly and efficiently to important events that aren't easy to anticipate in software. Best of all, it frees up your processor for doing other stuff while it's waiting on an event to show up. 

Button Interrupts

Let's start with a simple example - using an Interrupt to monitor a button press. To start, we'll take a sketch you've likely seen before - the "Button" example sketch included with all Arduinos. (You can find this in the "Examples" sketchbook. Check out "File > Examples > Digital > Button".)

                    const int buttonPin = 2;     // the number of the pushbutton pin
const int ledPin =  13;      // the number of the LED pin

// variables will change:
int buttonState = 0;         // variable for reading the pushbutton status

void setup() {
  // initialize the LED pin as an output:
  pinMode(ledPin, OUTPUT);
  // initialize the pushbutton pin as an input:
  pinMode(buttonPin, INPUT);
}

void loop() {
  // read the state of the pushbutton value:
  buttonState = digitalRead(buttonPin);

  // check if the pushbutton is pressed.
  // if it is, the buttonState is HIGH:
  if (buttonState == HIGH) {
    // turn LED on:
    digitalWrite(ledPin, HIGH);
  }
  else {
    // turn LED off:
    digitalWrite(ledPin, LOW);
  }
}
                  

  Download Code  


What you're seeing here is nothing shocking or amazing - all the program is doing, over and over again, is running through `loop()`, and reading the value of `buttonPin`. Suppose for a moment that you wanted to do something else in `loop()` - something besides just reading a pin. That's where Interrupts come in. Instead of just watching that pin all the time, we can farm the work of monitoring that pin to an Interrupt, and free up `loop()` to do whatever we need it to do in the meantime! The new code would look something like this:

                    const int buttonPin = 2;     // the number of the pushbutton pin
const int ledPin =  13;      // the number of the LED pin

// variables will change:
volatile int buttonState = 0;         // variable for reading the pushbutton status

void setup() {
  // initialize the LED pin as an output:
  pinMode(ledPin, OUTPUT);
  // initialize the pushbutton pin as an input:
  pinMode(buttonPin, INPUT);
  // Attach an interrupt to the ISR vector
  attachInterrupt(0, pin_ISR, CHANGE);
}

void loop() {
  // Nothing here!
}

void pin_ISR() {
  buttonState = digitalRead(buttonPin);
  digitalWrite(ledPin, buttonState);
}

                  

  Download Code  


Loops and Interrupt Modes

You'll notice a few changes here. The first, and most obvious of these, is that `loop()` doesn't contain any instructions! We can get away with this because all of the work that was previously done by an `if/else` statement is now handled by a new function, `pin_ISR()`. This type of function is called an _interrupt service routine_ - its job is to run quickly and handle the interrupt and let the processor get back to the main program (i.e. the contents of `loop()`). There are a few important things to consider when writing an interrupt service routine, which you can see reflected in the code above:

  • ISRs should be short and sweet. You don't want to derail the main loop for too long!
  • There are no input variables or returned values. All changes have to be made on global variables. 

You're probably wondering - how do we know when an interrupt gets run? What triggers it? The third function in the `setup()` routine is what sets up the interrupt for the whole system. This function, `attachInterrupt()`, takes three arguments:

1. The interrupt vector, which determines what pin can generate an interrupt. This isn't the number of the pin itself - it's actually a reference to where in memory the Arduino processor has to look to see if an interrupt occurs. A given space in that vector corresponds to a specific external pin, and not all pins can generate an interrupt! On the Arduino Uno, pins 2 and 3 are capable of generating interrupts, and they correspond to interrupt vectors 0 and 1, respectively. For a list of what pins are available as interrupt pins, check out the Arduino documentation on `attachInterrupt()`.  

2. The function name of the interrupt service routine - this determines the code that gets run when the interrupt condition is met. 

3. The interrupt mode, which determines what pin action triggers an interrupt. The Arduino Uno supports four interrupt modes: 

  * `RISING`, which activates an interrupt on a rising edge of the interrupt pin,

  * `FALLING`, which activates on a falling edge,

  * `CHANGE`, which responds to any change in the interrupt pin's value, 

  * `LOW`, which triggers any time the pin is a digital low.

Just to recap - our setting of `attachInterrupt()` is setting us up to monitor interrupt vector 0/pin 2, to respond to interrupts using `pin_ISR()`, and to call `pin_ISR()` whenever we see any change of state on pin 2. 

Volatile - Do Not Shake!

One more quick thing to point out - our ISR uses the variable `buttonState` to store pin state. Check out the definition of `buttonState` - instead of type `int`, we've defined it as type `volatile int`. What's the deal here? `volatile` is a C keyword applied to variables. It means that the value of that variable is not entirely within a program's control. It reflects that the value of `buttonState` could change, and change on something that the program itself can't predict - in this case, user input. 

One more useful thing that the `volatile` keyword does is protect us from any accidental compiler optimization. Compilers, as it turns out, have a few purposes in addition to turning your source code into a machine executable. One of their tasks is to trim unused source code variables out of machine code. Since the variable `buttonState` is not used or called directly in the `loop()` or `setup()` functions, there is a risk that the compiler might remove it as an unused variable. Obviously, this is wrong - we need that variable! The `volatile` keyword has the side effect of telling the compiler to cool its jets and hang on to that variable - it's not a fat finger error!

(Aside: pruning unused variables from code is a feature, not a bug, of compilers. People will occasionally leave unused variables in source code, which takes up memory. This isn't such a big deal if you were writing a C program on a computer, with gigabytes of RAM. On an Arduino, however, RAM is limited, and you don't want to waste any! Even C programs on computers will do this as well, with tons of system memory available. Why? The same reason people clean up campgrounds - it's good practice to not leave garbage behind!) 

Wrapping Up

Interrupts are a simple way to make your system more responsive to time sensitive tasks. They also have the added benefit of freeing up your main `loop()` to focus on some primary task in the system. (I find that this tends to make my code a little more organized when I use them - it's easier to see what the main chunk of code was designed for, while the interrupts handle periodic events.) The example shown here is just about the most basic case for using an interrupt - you can use them for reading an I2C device, sending or receiving wireless data, or even starting or stopping a motor. 

Got any cool projects with interrupts? Leave us a comment below to let us know!

Comments

12 Comments


  • Canobi 2015-11-06

    Thank you for sharing smile

    I’m just learning to code for a project I’m working on which needs a momentary button and accelerometer so this article couldn’t have been more perfect if it tried.

    It’s also very well written and easy to understand so I will definitely keep my eye open for any more of yours.

  • kesarraghav 2016-02-22

    The above code is not working !. I am sorry but i tried your code with little modification but it didnot work.


    please see the modified code


    const int buttonPin = 2;    // the number of the pushbutton pin
    const int ledPin =  13;    // the number of the LED pin

    // variables will change:
    volatile int buttonState = 0;      // variable for reading the pushbutton status

    void setup() {
    Serial.begin(9600);
    // initialize the LED pin as an output:
    pinMode(ledPin, OUTPUT);
    // initialize the pushbutton pin as an input:
    pinMode(buttonPin, INPUT);
    // Attach an interrupt to the ISR vector
    attachInterrupt(0, pin_ISR, CHANGE);
    }

    void loop() {
    // Nothing here!
    while(1)
    {
    Serial.println("Normal Working 1");
    delay(1000);
    Serial.println("Normal Working 2");
    delay(1000);
    }

    }

    void pin_ISR() {
    buttonState = digitalRead(buttonPin);
    digitalWrite(ledPin, buttonState);
    Serial.println("INTERRUPT C A L L E D");
    }

    ////////////////////////////////////////////////////////////////////////////////////////////
    I tried your concept only but ! as you explained it just not worked

    • kesarraghav 2016-02-22

      once i called the interrupt ! it blinks the led but it will not jump back to the main program!

      • cushyChicken 2016-02-22

        Well, as it turns out, your modifications are actually ones with pretty serious ramifications for interrupt service routines! Serial.println() in the Arduino codebase is very similar to printf(), which has some known re-entrance problems. It’s generally not a good idea to call printf() style functions from interrupt service routines as a result - that’s why I used an LED in this example to show when the ISR fires.

        If you absolutely have to have serial output in your program in response to an ISR, I would recommend using the ISR to set a bit in a separate flag variable. Your main loop would then poll that variable; when it find that variable’s bit has been set, it can then do two things:

        1) clear the bit in your flag variable (set it back to zero), and
        2) print your message saying that the interrupt has occurred.

        Before you do that, however, I would strongly recommend that you copy the original code into your Arduino IDE and verify that everything is working properly. I tested the code on an Arduino Nano and an Arduino Uno before I posted it here, so I know it’s working correctly!

  • kroome88 2016-03-10

    thanks for sharing….

  • wooja 2016-03-17

    good day sir! why is attachInterrupt used in void setup(), why is it not used in the void loop()?

    • cushyChicken 2016-03-17

      Because an interrupt is triggered based on a hardware condition. An interrupt service routine is a function that gets run when that hardware condition is met. You only have to specify what function you want run during your interrupt once. Everything else after that happens in hardware.

      Does that make sense?

  • Arjun07 2016-07-26

    Best introduction to interrupts.

  • Micky Hickbert 2016-10-04

    Thx for sharing, learned a lot but it doesn’t quite solve my Problem. How would i also break the loop, as soon as the interrupt occurs (the button is pushed) and let it start again when the button is released? Right now the loop runs over and over again AND the LED started by the interrupt is high.
    Another weird thing: the LED stays high for some more Moments (ca 2 s) when the Button is released. Where does the delay come from?
    Many THX in advance from a complete newbie smile

    here is my code: https://gist.github.com/anonymous/df522b90b4386103faaf52c3e3ab34d2

  • MacAriOi 2016-11-01

    Very elegant and well presented information. Appreciated.

    In a comment you made (I can’t reply to it for some reason), you said:
    “If you absolutely have to have serial output in your program in response to an ISR, I would recommend using the ISR to set a bit in a separate flag variable. Your main loop would then poll that variable”

    Doesn’t that actually cancel the hardware interrupt function concept completely??
    Do you mean that the main loop should ‘monitor’ that variable? Why not monitor the pin state directly then?

    • cushyChicken 2016-11-02

      You’re correct that this effectively sets up the main loop to monitor another variable, and yes, it’s a slight redundancy. This sample code is a poor example of this, but interrupts are frequently used to respond to time sensitive events. Think for a moment if your embedded system were running code as a big “superloop”, with a ton of program steps executed serially inside a big loop (as opposed to nothing in this example). For some time critical applications, there’s a chance that you could miss the window in which to react to that event just due to which stage of the loop you’re in!  In contrast, serial communications are actually really, really slow - the Arduino, by default, only sends 9600 ASCII characters per second. This is way, WAY slower than a lot of stuff that the processor in the Arduino was designed to control. (For example, the internal clock is 8MHz, and the GPIO toggle rate maxes out at about 5MHz.) I guess the point I’m trying to make is that you really shouldn’t be using the serial console to signal high-speed events, as you run the risk of missing things otherwise!

      Guessing you’ve read the other comment threads - did you see my reply to kesarraghav? Printing inside of an ISR can cause serious issues with program flow, the largest one being preventing your program from returning to normal execution.

      • MacAriOi 2016-11-05

        Can’t thank you enough for your time..

        Just because my project is small and simple, I gave it a shot anyway:
        -An Arduino Nano samples power consumption data every 0.5sec
        -At a certain point it activates a digital output, to power-up an ESP8266. And keeps sampling..
        -When the ESP8266 has connected to the MQTT broker, it issues a Hardware-Interrupt to the Arduino.
        -Arduino serial-prints the average-Power
        -4 seconds later shuts-down the ESP8266.
        Sampling continues at all times. Averaging gets stored at the end of every minute.

        It works alright for 2 days now. Probably it is too short and simple to cause issues.
        Thank you for sharing the knowledge.