An Introduction to Interrupts on Arduino
Interrupts have a reputation for being tricky and an unnecessary complication to just get code running. This reputation is however ill-deserved. Interrupts when used correctly simplify code, speed execution on hardware, and allow projects to do more with less powerful hardware.
What is an Interrupt?
Normally a microprocessor executes a set of instructions, your code, in a sequence. Some of those instructions are what we call flow control or branching instructions which cause execution to take different paths, think if-else statement blocks. These instructions, even the branching instructions, are the normal flow of your program. An interrupt stops the microprocessor from following the normal flow and causes it to execute a different block of code, typically resuming the normal flow of your code once done.
Microprocessors typically provide for a range of interrupts which are divided into two flavors, software interrupts and hardware interrupts. A software interrupt is triggered by a software condition such as an error like division by zero. Hardware interrupts are meanwhile triggered by hardware; whether internal such as a real time clock indicating a second has elapsed or external via a signal on an interrupt capable digital IO pin.
Consider an Arduino program which checks a digital IO pin each time through the loop function that toggles a boolean variable if the pin is low and then uses the value of that boolean variable to determine which other actions to take.
#define INPUT_PIN 0 // digital input pin 0 bool state = false; void setup() { ... pinMode(INPUT_PIN, INPUT_PULLUP); ... } void loop() { ... if(LOW == digitalRead(INPUT_PIN)) { state = ~state; // toggle state } if(state) { ... // work } }
This code implements a classic strategy known as polling. Sometimes polling is the only way to get something done; but it should generally be avoided if possible for some equally classic reasons. First and foremost, if state is true and the statements in the if(state) block take much time, then it will take that much time before we check the digital IO pin again. If the pin is connected to a push button the user may have pressed and released the button while the microcontroller is dutifully executing its way through work meaning the button press will be missed. Alternatively, if state is false then the microcontroller will busily check the digital pin wasting power if running on a battery. Using an interrupt to eliminate the polling will fix these issues.
Using Interrupts on Arduino
As interrupts execute outside the normal sequence of instruction execution we first need to provide the code that will run when the microprocessor is interrupted. This means adding a new function to our program, an “interrupt handler”. C++(1), the language used with the Arduino platform requires that we “declare” our function before “defining” it. If you have used a language like Java or Python, this may seem strange and indeed most languages if not all created after C++ have dropped this requirement. The declaration tells the C++ compiler the function’s “signature”: name, return type, number and types of parameters. The signature of interrupt handlers varies between platforms. On Arduino, interrupt handlers must have the signature of no parameters and no return value (type void). E.g.
void interrupt_handler();With the interrupt handler declared, it can then be defined. Interrupt handlers on some platforms including Arduino come with the caveat that they cannot themselves be interrupted. Therefore interrupt handlers must be kept simple in order to execute quickly and return to normal code execution as quickly as possible so future interrupts can be handled. The simplest useful statement is an assignment of a new value to a variable. It is also reasonable to access a scalar (bool, int, double) variable to mutate its value e.g. increment it, though accessing hardware should be avoided in an interrupt handler, e.g. Serial.println() or digitalWrite(). The C++ compiler also comes with a caveat that variables changed in an interrupt handler must be declared with the
volatile
keyword. E.g.
volatile int interrupt_count; /** * interrupt_handler - an Arduino interrupt handler. * Assigns the volatile int interrupt_count * a value one greater than the current value. */ void interrupt_handler() { interrupt_count += 1; }With the interrupt handler in place, it can be “attached” or registered with the Arduino platform code along with a condition that will trigger the interrupt handler to be called. This condition is typically when the voltage on an interrupt enabled digital IO pin falls from HIGH to LOW. Therefore pinMode() must be used to configure the pin before calling attachInterrupt() with the pin. E.g.
pinMode(INPUT_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(INPUT_PIN), interrupt_handler, FALLING);Typically, this setup is performed in the Arduino
setup()
function. Notice that the name of the interrupt handler function is provided as the second parameter of attachInterrupt()
without parentheses as we are passing a pointer to the function interrupt_handler
in the example above rather than invoking the function and passing its return value to attachInterrupt()
.
Putting it all together (with comments) we can rewrite the example from the previous section as:
#define INPUT_PIN 2 //digital io pin 2 /* * Flags we will set in our interrupt handlers indicating * that an event happened */ volatile bool state = false; // Function declarations void changeState(); /** * changeState - is an interrupt service routine (ISR) * as such it has no parameters and must not return * anything all memory shared from the ISR to userland * (setup, loop, etc.) must be marked volatile or the * compiler may optimize userland memory accesses by * pinning the memory to a register (cache it) */ void changeState() { state = ~state; } /** * setup - is a special function defined by the Arduino * platform that is called once after the core firmware * initialization happens but before loop is called for * the first time */ void setup(void) { ... // init interrupts pinMode(INPUT_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(INPUT_PIN), changeState, FALLING); ... } /** * loop - is a special function defined by the Arduino platform * that is called continuously in an infinite loop once * platform specific initialization and setup() have run. */ void loop(void) { if(state) { ... } delay(100); // limit frequency at which loop executes }
Known Issue
Not all board support packages define the digitalPinToInterrupt macro(2). If you compile the code from the previous section against such a board you will see an error along the lines of:
digitalPinToInterrupt not defined in scopeThis can be frustrating for even experienced developers as the documentation for attachInterrupt states that using the macro is highly recommended but the macro is not a core part of the Arduino platform. This is a result of many Arduino boards having a one to one correspondence between digital IO pins and interrupts i.e. digital IO pin 0 is connected to interrupt 0, digital IO pin 1 is connected to interrupt 1, and so on. This is however not the case for some of the older or more limited boards. The Adafruit Flora in particular has eight digital IO pins but only four interrupts. Further the pin number does not correspond to the interrupt number. In this case the developer should create their own macro and conditionally include it in the source code(3, 4). E.g.
/* * the Flora board support package does not include * digitalPinToInterrupt though pin numbers do not correspond * to interrupt numbers :( */ #ifndef digitalPinToInterrupt #ifdef ARDUINO_AVR_FLORA8 #define digitalPinToInterrupt(p) ( (p) == 0 ? 2 : ((p) == 1 ? 3 : ((p) == 2 ? 1 : ((p) == 3 ? 0 : -1))) ) #endif #ifdef SOME_OTHER_BOARD #define digitalPinToInterrupt(p) (p) #endif #endif
Notes
- Some C++ compilers have also done away with the declare before define requirement for functions in some instances. The GNU-AVR toolchain used by the Arduino IDE and PlatformIO however has not as of 2019 May 17.
- A macro is like a function except that instead of being called when your code runs, the compiler, technically the preprocessor which is a stage in the compiler, replaces calls to the macro with the body of the macro. This is often useful when the compiler can optimize away some or all of the macro during the optimization stage.
- Macro code tested with Platformio 3.6.7. Arduino IDE and future versions of PlatformIO may provide different board names.
- Board names can be discovered when using PlatformIO by looking in the appropriate file in
~/.platformio/platforms/${PLATFORM}/boards/
. For the Adafruit flora,flora8
to platformio that is:~/.platformio/platforms/atmelavr/boards/flora8.json
.