Technical Article

Using Interrupts on Arduino

August 12, 2015 by Nash Reilly

We've all been there - you've spent hours and hours trying to get that sketch working, but somehow, you're not reacting well to time critical events in your system. Maybe it's a wireless peripheral indicating when a packet is ready. Maybe it's an external Flash device signaling when data is ready to be read out. Maybe it's as simple as a button push that you need to react quickly to! But, try as you might, it seems the best way to see these sort of events is to just digitalRead() that pin over and over in your main loop() function until something interesting happens. ...or is it?

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);
  }
}

arduino1.zip

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);
}

Arduino2.zip

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.

There are other ways to implement interrupts; specifically by using registers and register masks that enables you to use all other pins of the Arduino board. 

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

30 Comments
  • Canobi November 06, 2015

    Thank you for sharing 😊

    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.

    Like. Reply
  • kesarraghav February 22, 2016

    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

    Like. Reply
    • kesarraghav February 22, 2016
      once i called the interrupt ! it blinks the led but it will not jump back to the main program!
      Like. Reply