Dealing with Interrupts

This article is written by Francis Perea, the author of the book Arduino Essentials.

In all our previous projects, we have been constantly looking for events to occur. We have been polling, but looking for events to occur supposes a relatively big effort and a waste of CPU cycles to only notice that nothing happened.

In this article, we will learn about interrupts as a totally new way to deal with events, being notified about them instead of looking for them constantly.

Interrupts may be really helpful when developing projects in which fast or unknown events may occur, and thus we will see a very interesting project which will lead us to develop a digital tachograph for a computer-controlled motor.

Are you ready? Here we go!

(For more resources related to this topic, see here.)

The concept of an interruption

As you may have intuited, an interrupt is a special mechanism the CPU incorporates to have a direct channel to be noticed when some event occurs.

Most Arduino microcontrollers have two of these:

  • Interrupt 0 on digital pin 2
  • Interrupt 1 on digital pin 3

But some models, such as the Mega2560, come with up to five interrupt pins.

Once an interrupt has been notified, the CPU completely stops what it was doing and goes on to look at it, by running a special dedicated function in our code called Interrupt Service Routine (ISR).

When I say that the CPU completely stops, I mean that even functions such as delay() or millis() won't be updated while the ISR is being executed.

Interrupts can be programmed to respond on different changes of the signal connected to the corresponding pin and thus the Arduino language has four predefined constants to represent each of these four modes:

  • LOW: It will trigger the interrupt whenever the pin gets a LOW value
  • CHANGE: The interrupt will be triggered when the pins change their values from HIGH to LOW or vice versa
  • RISING: It will trigger the interrupt when signal goes from LOW to HIGH
  • FALLING: It is just the opposite of RISING; the interrupt will be triggered when the signal goes from HIGH to LOW

The ISR

The function that the CPU will call whenever an interrupt occurs is so important to the micro that it has to accomplish a pair of rules:

  • They can't have any parameter
  • They can't return anything
  • The interrupts can be executed only one at a time

Regarding the first two points, they mean that we can neither pass nor receive any data from the ISR directly, but we have other means to achieve this communication with the function.

We will use global variables for it. We can set and read from a global variable inside an ISR, but even so, these variables have to be declared in a special way. We have to declare them as volatile as we will see this later on in the code.

The third point, which specifies that only one ISR can be attended at a time, is what makes the function millis() not being able to be updated. The millis() function relies on an interrupt to be updated, and this doesn't happen if another interrupt is already being served.

As you may understand, ISR is critical to the correct code execution in a microcontroller. As a rule of thumb, we will try to keep our ISRs as simple as possible and leave all heavy weight processing that occurs outside of it, in the main loop of our code.

The tachograph project

To understand and manage interrupts in our projects, I would like to offer you a very particular one, a tachograph, a device that is present in all our cars and whose mission is to account for revolutions, normally the engine revolutions, but also in brake systems such as Anti-lock Brake System (ABS) and others.

Mechanical considerations

Well, calling it mechanical perhaps is too much, but let's make some considerations regarding how we are going to make our project account for revolutions.

For this example project, I have used a small DC motor driven through a small transistor and, like in lots of industrial applications, an encoded wheel is a perfect mechanism to read the number of revolutions. By simply attaching a small disc of cardboard perpendicularly to your motor shaft, it is very easy to achieve it.

By using our old friend, the optocoupler, we can sense something between its two parts, even with just a piece of cardboard with a small slot in just one side of its surface.

Here, you can see the template I elaborated for such a disc, the cross in the middle will help you position the disc as perfectly as possible, that is, the cross may be as close as possible to the motor shaft. The slot has to be cut off of the black rectangle as shown in the following image:
Arduino Essentials

The template for the motor encoder

Once I printed it, I glued it to another piece of cardboard to make it more resistant and glued it all to the crown already attached to my motor shaft. If yours doesn't have a surface big enough to glue the encoder disc to its shaft, then perhaps you can find a solution by using just a small piece of dough or similar to it.

Once the encoder disc is fixed to the motor and spins attached to the motor shaft, we have to find a way to place the optocoupler in a way that makes it able to read through the encoder disc slot.

In my case, just a pair of drops of glue did the trick, but if your optocoupler or motor doesn't allow you to apply this solution, I'm sure that a pair of zip ties or a small piece of dough can give you another way to fix it to the motor too.

In the following image, you can see my final assembled motor with its encoder disc and optocoupler ready to be connected to the breadboard through alligator clips:
Arduino Essentials

The complete assembly for the motor encoder

Once we have prepared our motor encoder, let's perform some tests to see it working and begin to write code to deal with interruptions.

A simple interrupt tester

Before going deep inside the whole code project, let's perform some tests to confirm that our encoder assembly is working fine and that we can correctly trigger an interrupt whenever the motor spins and the cardboard slot passes just through the optocoupler.

The only thing you have to connect to your Arduino at the moment is the optocoupler; we will now operate our motor by hand and in a later section, we will control its speed from the computer.

The test's circuit schematic is as follows:
Arduino Essentials

A simple circuit to test the encoder

Nothing new in this circuit, it is almost the same as the one used in the optical coin detector, with the only important and necessary difference of connecting the wire coming from the detector side of the optocoupler to pin 2 of our Arduino board, because, as said in the preceding text, the interrupt 0 is available only through that pin.

For this first test, we will make the encoder disc spin by hand, which allows us to clearly perceive when the interrupt triggers.

For the rest of this example, we will use the LED included with the Arduino board connected to pin 13 as a way to visually indicate that the interrupts have been triggered.

Our first interrupt and its ISR

Once we have connected the optocoupler to the Arduino and prepared things to trigger some interrupts, let's see the code that we will use to test our assembly.

The objective of this simple sketch is to commute the status of an LED every time an interrupt occurs. In the proposed tester circuit, the LED status variable will be changed every time the slot passes through the optocoupler:

/*
 Chapter 09 - Dealing with interrupts
 A simple tester
 By Francis Perea for Packt Publishing
*/
 
// A LED will be used to notify the change
#define ledPin 13
 
// Global variables we will use
// A variable to be used inside ISR
volatile int status = LOW;
 
// A function to be called when the interrupt occurs
void revolution(){
  // Invert LED status
  status=!status;
}
 
// Configuration of the board: just one output
void setup() {
  pinMode(ledPin, OUTPUT);
  // Assign the revolution() function as an ISR of interrupt 0
  // Interrupt will be triggered when the signal goes from
  // LOW to HIGH
  attachInterrupt(0, revolution, RISING);
}
 
// Sketch execution loop
void loop(){ 
  // Set LED status
  digitalWrite(ledPin, status);
}

Let's take a look at its most important aspects.

The LED pin apart, we declare a variable to account for changes occurring. It will be updated in the ISR of our interrupt; so, as I told you earlier, we declare it as follows:

volatile int status = LOW;

Following which we declare the ISR function, revolution(), which as we already know doesn't receive any parameter nor return any value. And as we said earlier, it must be as simple as possible. In our test case, the ISR simply inverts the value of the global volatile variable to its opposite value, that is, from LOW to HIGH and from HIGH to LOW.

To allow our ISR to be called whenever an interrupt 0 occurs, in the setup() function, we make a call to the attachInterrupt() function by passing three parameters to it:

  • Interrupt: The interrupt number to assign the ISR to
  • ISR: The name without the parentheses of the function that will act as the ISR for this interrupt
  • Mode: One of the following already explained modes that define when exactly the interrupt will be triggered

In our case, the concrete sentence is as follows:

attachInterrupt(0, revolution, RISING);

This makes the function revolution() be the ISR of interrupt 0 that will be triggered when the signal goes from LOW to HIGH.

Finally, in our main loop there is little to do. Simply update the LED based on the current value of the status variable that is going to be updated inside the ISR.

If everything went right, you should see the LED commute every time the slot passes through the optocoupler as a consequence of the interrupt being triggered and the revolution() function inverting the value of the status variable that is used in the main loop to set the LED accordingly.

A dial tachograph

For a more complete example in this section, we will build a tachograph, a device that will present the current revolutions per minute of the motor in a visual manner by using a dial.

The motor speed will be commanded serially from our computer by reusing some of the codes in our previous projects.

It is not going to be very complicated if we include some way to inform about an excessive number of revolutions and even cut the engine in an extreme case to protect it, is it?

The complete schematic of such a big circuit is shown in the following image. Don't get scared about the number of components as we have already seen them all in action before:
Arduino Essentials

The tachograph circuit

As you may see, we will use a total of five pins of our Arduino board to sense and command such a set of peripherals:

  • Pin 2: This is the interrupt 0 pin and thus it will be used to connect the output of the optocoupler.
  • Pin 3: It will be used to deal with the servo to move the dial.
  • Pin 4: We will use this pin to activate sound alarm once the engine current has been cut off to prevent overcharge.
  • Pin 6: This pin will be used to deal with the motor transistor that allows us to vary the motor speed based on the commands we receive serially. Remember to use a PWM pin if you choose to use another one.
  • Pin 13: Used to indicate with an LED an excessive number of revolutions per minute prior to cutting the engine off.

There are also two more pins which, although not physically connected, will be used, pins 0 and 1, given that we are going to talk to the device serially from the computer.

Breadboard connections diagram

There are some wires crossed in the previous schematic, and perhaps you can see the connections better in the following breadboard connection image:
Arduino Essentials

Breadboard connection diagram for the tachograph

The complete tachograph code

This is going to be a project full of features and that is why it has such a number of devices to interact with. Let's resume the functioning features of the dial tachograph:

  • The motor speed is commanded from the computer via a serial communication with up to five commands:
    • Increase motor speed (+)
    • Decrease motor speed (-)
    • Totally stop the motor (0)
    • Put the motor at full throttle (*)
    • Reset the motor after a stall (R)
  • Motor revolutions will be detected and accounted by using an encoder and an optocoupler
  • Current revolutions per minute will be visually presented with a dial operated with a servomotor
  • It gives visual indication via an LED of a high number of revolutions
  • In case a maximum number of revolutions is reached, the motor current will be cut off and an acoustic alarm will sound

With such a number of features, it is normal that the code for this project is going to be a bit longer than our previous sketches. Here is the code:

/*
 Chapter 09 - Dealing with interrupt
 Complete tachograph system
 By Francis Perea for Packt Publishing
*/
 
#include <Servo.h>
 
//The pins that will be used
#define ledPin 13
#define motorPin 6
#define buzzerPin 4
#define servoPin 3
 
#define NOTE_A4 440
// Milliseconds between every sample
#define sampleTime 500
// Motor speed increment
#define motorIncrement 10
// Range of valir RPMs, alarm and stop
#define minRPM  0
#define maxRPM 10000
#define alarmRPM 8000
#define stopRPM 9000
 
// Global variables we will use
// A variable to be used inside ISR
volatile unsigned long revolutions = 0;
// Total number of revolutions in every sample
long lastSampleRevolutions = 0;
// A variable to convert revolutions per sample to RPM
int rpm = 0;
// LED Status
int ledStatus = LOW;
// An instace on the Servo class
Servo myServo;
// A flag to know if the motor has been stalled
boolean motorStalled = false;
// Thr current dial angle
int dialAngle = 0;
// A variable to store serial data
int dataReceived;
// The current motor speed
int speed = 0;
// A time variable to compare in every sample
unsigned long lastCheckTime;
 
// A function to be called when the interrupt occurs
void revolution(){
  // Increment the total number of
  // revolutions in the current sample
  revolutions++;
}
 
// Configuration of the board
void setup() {
  // Set output pins
  pinMode(motorPin, OUTPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);
  // Set revolution() as ISR of interrupt 0
  attachInterrupt(0, revolution, CHANGE);
  // Init serial communication
  Serial.begin(9600);
  // Initialize the servo
  myServo.attach(servoPin);
  //Set the dial
  myServo.write(dialAngle);
  // Initialize the counter for sample time
  lastCheckTime = millis();
}
 
// Sketch execution loop
void loop(){ 
  // If we have received serial data
  if (Serial.available()) {
    // read the next char
     dataReceived = Serial.read();
     // Act depending on it
     switch (dataReceived){
       // Increment speed
       case '+':
         if (speed<250) {
           speed += motorIncrement;
         }
         break;
       // Decrement speed
       case '-':
         if (speed>5) {
           speed -= motorIncrement;
         }
         break;        
       // Stop motor
       case '0':
         speed = 0;
         break;    
       // Full throttle   
       case '*':
         speed = 255;
         break;
       // Reactivate motor after stall
       case 'R':
         speed = 0;
         motorStalled = false;
         break;
     }
    //Only if motor is active set new motor speed
    if (motorStalled == false){
      // Set the speed motor speed
      analogWrite(motorPin, speed);
    }
  }
  // If a sample time has passed
  // We have to take another sample
  if (millis() - lastCheckTime > sampleTime){
    // Store current revolutions
    lastSampleRevolutions = revolutions;
    // Reset the global variable
    // So the ISR can begin to count again
    revolutions = 0;
    // Calculate revolution per minute
    rpm = lastSampleRevolutions * (1000 / sampleTime) * 60;
    // Update last sample time
    lastCheckTime = millis();
    // Set the dial according new reading
    dialAngle = map(rpm,minRPM,maxRPM,180,0);
    myServo.write(dialAngle);
  }
  // If the motor is running in the red zone
  if (rpm > alarmRPM){
    // Turn on LED
    digitalWrite(ledPin, HIGH);
  }
  else{
    // Otherwise turn it off
    digitalWrite(ledPin, LOW);
  }
  // If the motor has exceed maximum RPM
  if (rpm > stopRPM){
    // Stop the motor
    speed = 0;
    analogWrite(motorPin, speed);
    // Disable it until a 'R' command is received
    motorStalled = true;
    // Make alarm sound
    tone(buzzerPin, NOTE_A4, 1000);
  }
  // Send data back to the computer
  Serial.print("RPM: ");
  Serial.print(rpm);
  Serial.print(" SPEED: ");
  Serial.print(speed);
  Serial.print(" STALL: ");
  Serial.println(motorStalled);
}

It is the first time in this article that I think I have nothing to explain regarding the code that hasn't been already explained before.

I have commented everything so that the code can be easily read and understood.

In general lines, the code declares both constants and global variables that will be used and the ISR for the interrupt.

In the setup section, all initializations of different subsystems that need to be set up before use are made: pins, interrupts, serials, and servos.

The main loop begins by looking for serial commands and basically updates the speed value and the stall flag if command R is received.

The final motor speed setting only occurs in case the stall flag is not on, which will occur in case the motor reaches the stopRPM value.

Following with the main loop, the code looks if it has passed a sample time, in which case the revolutions are stored to compute real revolutions per minute (rpm), and the global revolutions counter incremented inside the ISR is set to 0 to begin again.

The current rpm value is mapped to an angle to be presented by the dial and thus the servo is set accordingly.

Next, a pair of controls is made:

  • One to see if the motor is getting into the red zone by exceeding the max alarmRPM value and thus turning the alarm LED on
  • And another to check if the stopRPM value has been reached, in which case the motor will be automatically cut off, the motorStalled flag is set to true, and the acoustic alarm is triggered

When the motor has been stalled, it won't accept changes in its speed until it has been reset by issuing an R command via serial communication.

In the last action, the code sends back some info to the Serial Monitor as another way of feedback with the operator at the computer and this should look something like the following screenshot:
Arduino Essentials

Serial Monitor showing the tachograph in action

Modular development

It has been quite a complex project in that it incorporates up to six different subsystems: optocoupler, motor, LED, buzzer, servo, and serial, but it has also helped us to understand that projects need to be developed by using a modular approach.

We have worked and tested every one of these subsystems before, and that is the way it should usually be done.

By developing your projects in such a submodular way, it will be easy to assemble and program the whole of the system.

As you may see in the following screenshot, only by using such a modular way of working will you be able to connect and understand such a mess of wires:
Arduino Essentials

A working desktop may get a bit messy

Summary

I'm sure you have got the point regarding interrupts with all the things we have seen in this article.

We have met and understood what an interrupt is and how does the CPU attend to it by running an ISR, and we have even learned about their special characteristics and restrictions and that we should keep them as little as possible.

On the programming side, the only thing necessary to work with interrupts is to correctly attach the ISR with a call to the attachInterrupt() function.

From the point of view of hardware, we have assembled an encoder that has been attached to a spinning motor to account for its revolutions.

Finally, we have the code. We have seen a relatively long sketch, which is a sign that we are beginning to master the platform, are able to deal with a bigger number of peripherals, and that our projects require more complex software every time we have to deal with these peripherals and to accomplish all the other necessary tasks to meet what is specified in the project specifications.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Arduino Essentials

Explore Title