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.)
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:
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:
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:
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.
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.
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:
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:
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.
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:
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.
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:
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.
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:
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:
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.
There are some wires crossed in the previous schematic, and perhaps you can see the connections better in the following breadboard connection image:
Breadboard connection diagram for the tachograph
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:
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:
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:
Serial Monitor showing the tachograph in action
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:
A working desktop may get a bit messy
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.
Further resources on this subject: