Interrupts are a very powerful and valuable mechanism in processors and automata. Arduino, of course, is no exception. In this entry, we will see what interrupts are and how to use them in our code.
To understand the utility and necessity of interrupts, let’s assume we have Arduino connected to a sensor (for example, an optical encoder that counts the revolutions of a motor, a detector that emits an alarm for the water level in a tank, or a simple stop button).
If we want to detect a change in state in this input, the method we have used so far is to use digital inputs to repeatedly poll the value of the input, with a time interval (delay) between polls.
This mechanism is called “polling,” and it has 3 clear disadvantages:
- It assumes continuous processor and energy consumption, as it must continuously ask for the state of the input.
- If the action needs to be addressed immediately (for example, in a collision alert), waiting until the point in the program where the check occurs may be unacceptable.
- If the pulse is very short, or if the processor is busy performing another task while it occurs, we may miss the trigger and never get to see it.
To solve these types of problems, microprocessors incorporate the concept of interrupts, which is a mechanism that allows associating a function with the occurrence of a specific event. This associated callback function is called ISR (Interruption Service Routine).
When the event occurs, the processor “exits” immediately from the normal flow of the program and executes the associated ISR function, completely ignoring any other tasks (hence it is called an interrupt). Once the associated ISR function finishes, the processor returns to the main flow, at the exact point where it was interrupted.
As we can see, interrupts are a very powerful and convenient mechanism that improves our programs and allows us to perform actions that would not be possible without the use of interrupts.
To use interrupts in physical devices (such as buttons, optical sensors, … ) we must first eliminate the “bounce” effect, as you can see in the entry Apply debounce when using interrupts in Arduino
Interrupts in Arduino
Arduino has two types of events for defining interrupts. On one hand, we have timer interrupts (which we will see in due time when talking about timers). On the other hand, we have hardware interrupts, which respond to events occurring on certain physical pins.
Within hardware interrupts, which are the focus of this entry, Arduino can detect the following events.
RISING
, occurs on the rising edge fromLOW
toHIGH
FALLING
, occurs on the falling edge fromHIGH
toLOW
CHANGING
, occurs when the pin changes state (rising + falling)LOW
, executes continuously while inLOW
state
The pins capable of generating interrupts vary depending on the Arduino model.
On Arduino and Nano, there are two interrupts, 0 and 1, associated with digital pins 2 and 3. The Arduino Mega has 6 interrupts on pins 2, 3, 21, 20, 19, and 18 respectively. Arduino Due has interrupts on all its pins.
Model | INT0 | INT1 | INT2 | INT3 | INT4 | INT5 |
---|---|---|---|---|---|---|
UNO | 2 | 3 | ||||
Nano | 2 | 3 | ||||
Mini Pro | 2 | 3 | ||||
Mega | 2 | 3 | 21 | 20 | 19 | 18 |
Leonardo | 3 | 2 | 0 | 1 | 7 | |
Due | On all pins |
The ISR Function
The function associated with an interrupt is called ISR (Interruption Service Routines) and, by definition, it must be a function that receives nothing and returns nothing.
Two ISRs cannot execute simultaneously. If another interrupt is triggered while one ISR is executing, the ISR function will execute one after the other.
The ISR, the shorter the better
When designing an ISR, we must keep the execution time as short as possible, since while the ISR is executing, the main loop and all other functions are halted.
Imagine, for example, that the main program has been interrupted while a motor was moving an arm to pick up an object. A long interrupt could cause the arm not to stop in time, knocking over or damaging the object.
Frequently, the function of the ISR will be limited to activating a flag, incrementing a counter, or modifying a variable. This modification will be addressed later in the main thread, when appropriate.
Do not use time-consuming processes in an ISR. This includes complex calculations, communication (serial, I2C, and SPI), and, as much as possible, changing both digital and analog inputs or outputs.
ISR Variables as “Volatile”
To modify an external variable within the ISR, we must declare it as “volatile”.
The “volatile” indicator tells the compiler that the variable must always be checked before use, as it may have been modified outside the normal flow of the program (which is precisely what an interrupt does).
By marking a variable as volatile, the compiler disables certain optimizations, resulting in a loss of efficiency. Therefore, we should only mark as volatile the variables that truly require it, that is, those used in both the main loop and within the ISR.
Effects of Interrupts and Time Measurement
Interrupts have effects on Arduino’s time measurement, both outside and inside the ISR, because Arduino uses timer-type interrupts to update time measurement.
Effects Outside the ISR
During the execution of an interrupt, Arduino does not update the value of the millis and micros functions. That is, the execution time of the ISR is not counted, and Arduino has a lag in time measurement.
If a program has many interrupts and they take a long time to execute, the measurement of Arduino’s time can become very distorted compared to reality (again, a reason to keep ISRs short).
Effects Inside the ISR
Within the ISR, other interrupts are disabled. This means:
- The millis function does not update its value, so we cannot use it to measure time within the ISR. (we can use it to measure time between two different ISRs)
- Consequently, the delay() function does not work, as it relies on the millis() function.
- The micros() function updates its value within an ISR, but it starts giving inaccurate time measurements after the 500us range.
- Consequently, the delayMicroseconds function works in that time range, although we should avoid its use because we should not introduce delays within an ISR.
Creating Interrupts in Arduino
To define an interrupt in Arduino, we use the function:
attachInterrupt(interrupt, ISR, mode);
Where interrupt is the number of the interrupt we are defining, ISR is the associated callback function, and mode is one of the available options (FALLING
, RISING
, CHANGE
, and LOW
)
However, it is cleaner to use the digitalPinToInterrupt() function, which converts a Pin to the equivalent interrupt. This way, it facilitates changing board models without modifying the code.
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
Other interesting functions for managing interrupts are:
DetachInterrupt(interrupt)
, disables the interrupt.NoInterrupts()
, disables the execution of interrupts until further notice (equivalent to cli())Interrupts()
, re-enables interrupts (equivalent to sei())
Testing Interrupts in Arduino
To test interrupts in Arduino, we will use a digital output from Arduino to emulate a digital signal.
In the real world, it would be another device (a sensor, another processor…) that generates this signal, and we would capture it with Arduino.
We connect digital pin 10 to digital pin 2, associated with interrupt 0.
Blinking an LED through Interrupts
In the following code, we define digital pin 10 as output, to emulate a square wave with a period of 300ms (150ms ON and 150ms OFF).
To visualize the operation of the interrupt, on each active edge of the simulated pulse, we turn the onboard LED on/off, so the LED blinks at intervals of 600ms (300ms ON and 300ms OFF).
At this point, seeing an LED blink may not seem very spectacular, but it’s not as simple as it seems. We are not turning the LED on with a digital output; rather, it is the interrupt that triggers that turns the LED on and off (the digital pin merely emulates an external signal).
const int emuPin = 10;
const int LEDPin = 13;
const int intPin = 2;
volatile int state = `LOW`; // define as volatile
void setup() {
pinMode(emuPin, OUTPUT);
pinMode(LEDPin, OUTPUT);
pinMode(intPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(intPin), blink, CHANGE);
}
void loop() {
//this part is to emulate the output
digitalWrite(emuPin, HIGH);
delay(150);
digitalWrite(emuPin, LOW);
delay(150);
}
void blink() {
state = !state; // change the state to make it blink
digitalWrite(LEDPin, state);
}
Counting Interrupt Triggers
In the following code, we use the same digital pin to emulate a square wave, this time with a 2s interval (1s ON and 1s OFF).
On each interrupt, we update the value of a counter. Later, in the main loop, we check the value of the counter, and if it has been modified, we display the new value.
When we run the code, we will see consecutive numbers printed in the serial monitor at intervals of two seconds.
const int emuPin = 10;
const int intPin = 2;
volatile int ISRCounter = 0;
int counter = 0;
void setup()
{
pinMode(emuPin, OUTPUT);
pinMode(intPin, INPUT_PULLUP);
Serial.begin(9600);
attachInterrupt(digitalPinToInterrupt(intPin), interruptCount, FALLING);
}
void loop()
{
//this part is to emulate the output
digitalWrite(emuPin, HIGH);
delay(1000);
digitalWrite(emuPin, LOW);
delay(1000);
if (counter != ISRCounter)
{
counter = ISRCounter;
Serial.println(counter);
}
}
void interruptCount()
{
ISRCounter++;
}
Logically, our goal is to use hardware interrupts not only with other digital devices but also with physical devices such as buttons, optical sensors…
However, as we have mentioned, these devices generate a lot of noise during state changes, which causes interrupts to trigger multiple times. This phenomenon is called “bounce,” and we will learn how to eliminate it in the next entry.
Download the Code
All the code for this entry is available for download on Github.