Arduino Interrupts and Timers
Ancient Knowledge
This article is getting old. It was written in the ancient times and the world of software development has changed a lot since then. I'm keeping it here for historical purposes, but I recommend you check out the newer articles on my site.
Outside of the traditional method of gathering values from Arduino inputs, there are a couple of other excellent tools in the workshop we can use. Interrupts provide the means to allow hardware devices to trigger a routine in your sketch and perform additional processing outside of the main loop. This trigger usually happens when an input changes state, allowing you to monitor an input pin without having to check it each time the main loop runs. Timers provide a way to run code at predefined intervals outside the main loop. Instead of calling millis() during the loop and constantly checking whether enough time has passed, you can create timers to move that logic outside of the main program execution. Just like hardware interrupts, a timer interrupt runs asynchronously and independent of the main loop.
Reading Inputs
Let's start by taking a look at the traditional method of reading inputs in a sketch.
#define SENSOR 5
void setup(){
Serial.begin(9600);
pinMode(SENSOR, INPUT); // enable input on sensor pin
digitalWrite(SENSOR, HIGH); // enable internal pull ups
}
void loop(){
byte val = digitalRead(SENSOR); // traditional read
Serial.write(val);
delay(1000);
}
Here we define our pin (5) and enable the internal pull up resistor (to prevent random floating values). When the loop begins, we read the value of pin 5 and write it out to the serial port. After a delay of 1000ms, we rinse and repeat it all over again.
Interrupt Pins
With some devices, such as a water flow sensor, you want the device itself to trigger and action when its state changes. You could check this input every iteration of the main loop, but its much more useful to use an hardware interrupt to let the device tell you when it has changed instead. This has the added benefit of really cleaning up the code in your main loop. The water flow sensor has 3 leads; V+, GND, and a signal lead. The signal lead is normally pulled high with an external pull up resistor and connected to an input pin. Each time the sensor pulses, it momentarily grounds the input pin. Each time the input pin goes to ground, it triggers our interrupt and we can execute a method outside of the main loop and perform some additional processing. On the Arduino Uno there are 2 interrupts available, interrupt 0 is pin 2 and interrupt 1 is pin 3. Other ATMEGA's may have more or less, so if you're using a different version, be sure to check out the documentation. Let's take a look at how to use a simple interrupt in the following sketch.
#define SENSORINT 0
volatile int k = 0;
void setup(){
Serial.begin(9600);
attachInterrupt(SENSORINT, ISR, FALLING); // create an interrupt
}
void loop(){
console.write(k);
delay(1000);
}
void ISR(){
k++;
}
In this sketch we use attachInterrupt to tell our program which method we want use as our interrupt service routine (ISR) (I am using the name ISR but you can use whatever name you want for the actual method). The third parameter of that method indicates that we want to interrupt each time the pin changes from HIGH to LOW. You have the option of triggering whenever the pin changes, on a low or high, or on a rising or falling state. Inside an ISR you can use any normal code you might use in the main loop, but best practice is to keep your ISR as short (quick) as possible, usually just increment a value to keep track of the number of interrupts. Notice that our interrupt counter k
is defined as volatile. This is because we don't want the compiler to optimize the usage of this variable; since it changes outside the main loop, the compiler may optimize it in such a way that it wouldn't see the change immediately. So volatile
is our way of telling the compiler that the variable must be reevaluated each time it is accessed.
Using a Timer
Timers let you define a set of code that runs periodically outside of the main loop. To make an LED blink for example, you would normally be checking the loop to see how much time has passed. Timer interrupts take care of the scheduling for you, so you only need to change the state each time. Here is the way you might perform blinking an LED without a timer.
#define LED 13;
void setup() {
pinMode(LED, OUTPUT);
}
// the loop routine runs over and over again forever:
void loop() {
digitalWrite(LED, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
Obviously, if you have this crazy 1000ms delay all over the place, when is the rest of your code going to execute? What we really need is a way to tell that LED to keep on blinking regardless of what is going on inside the main program loop.
#define LED 13
void setup(){
Serial.begin(9600);
OCR0A = 0xAF; // use the same timer as the millis() function
TIMSK0 |= _BV(OCIE0A);
}
void loop(){
// do something amazing
}
int ticks = 0;
ISR(TIMER0_COMPA_vect){
ticks++;
if(ticks >= 1000){
ticks = 0;
if(digitalRead(LED) == HIGH)
digitalWrite(LED, LOW);
else
digitalWrite(LED, HIGH);
}
}
Yikes! What is that crazy TIMSK0 |= _BV(OCIE0A);
code all about? This is a shorthand way of hijacking the same timer that the millis() function uses, and is the simplest way of using a 1 millisecond timer in our sketch. You're definitely going to need more flexibility, so be sure to check out this really great post from EngBlaze that covers everything about setting them up for various situations.