Secret Code

Interrupts

Make Your Code More Responsive To Outside Stimulus

Oliver Higgins

Issue 8, February 2018

In a world full of constant interruptions how do we decide what is the most important?

In all computer systems, we need to deal with events that occur at all levels and that are both internal and external to the core system. These events may be as simple as a keyboard stroke or a mouse input. We never know when the system will need to wake up and deal with these new data inputs, which means sometimes we waste resources by making the system sit and listen for them. So how can we have these external inputs tell us that they need attention without slowing down or making our systems unstable? We do this by using interrupts.

THE BROAD OVERVIEW

At its simplest, an interrupt request is simply a way of telling the system that some input is happening, and it needs to stop what it’s doing and focus on a new task. In the microcontroller world, this is a fundamental idea that is often overlooked at implementation. The FreeBSD definition is: “an event external to the currently executing process that causes a change in the normal flow of instruction execution; usually generated by hardware devices external to the CPU”.

Key to remember is that it runs asynchronous to the current process; that is, out of sync to the loop or process that the CPU may be running. An asynchronous event is something that occurs outside of the regular flow of your program – it can happen at any time, no matter what your code is doing. So at its simplest, we can use an interrupt to stop our loop doing something else.

Let’s take a real-world example. Imagine you are busy working hard on your next project. You are diligently working away at your desk enjoying the challenge presented, but you are expecting your next issue of DIYODE to be delivered to your door at any moment. If you were a traditional Arduino style program, you would have to get up every five minutes to go and check the front door to see if it had been delivered. This would get pretty tedious pretty quickly, not to mention the resources and time you would lose in the process. What if there was an easier way? For example, we decide to install a doorbell, and when the delivery arrives, we hear a “ding-dong” noise, which lets us know the delivery has arrived. That is the power of the interrupt. Once we hear the bell, you can pause your work, turn off the soldering iron and go and answer the door. We can then return to our work and pick up where we left off. This is an interrupt service routine, and it allows us an efficient and fast way to get things done. It also allows us to ensure events occur correctly, and really are the only way to go if your project requires precise timing. Most microprocessors do not allow for parallel programing, and as such cannot do multiple things at once.

Types of interrupts

There are two distinct types of interrupts: hardware-based interrupts that occur as a response to an external event; and a software interrupt, which occurs in response to an instruction sent in the software.

Hardware

Hardware interrupts are used by devices that require attention from our software or the operating system. In the land of desktop computers and laptops, this would be a mouse click, keystroke or disk controller. In the maker microcontroller space, this can be as simple as a push button or more complex devices. Hardware-based interrupts are always asynchronous; by their nature they must interrupt outside of the normal cycles and loops to be effective.

Software

The software can occur at several levels within microprocessor architecture, from low down at the processor itself, right up to the software layer that we may be implementing. These low-level interrupts are also often called “traps” or “exceptions”, and are used for errors. For example, you ask a user to input two numbers for division and one of them is a zero. This will always cause a divide by zero error and depending on your software, will cause a “crash”. In reality, it does not actually cause a crash, although the results may look like it does. Instead, your processor will have “thrown” an “exception”, which we can “trap” and tell our program to do something else, thus avoiding our “crash” or runtime error.

Interrupt service routines

All processors have a list of interrupt sources, which cover what hardware can trigger an interrupt. These interrupt sources are also called “vectors”. When we enable an interrupt for use in our software, we have to tell the code where in a specific point in the program memory, it has to jump to. This is the “interrupt vector”. By creating an interrupt service routine (ISR) and placing a link to the vector location, we create a path to tell our code to do something specific when our interrupt is triggered.

The act of initiating an interrupt is known as an “interrupt request”, which is commonly abbreviated to “IRQ”. Interrupts have a hierarchy as to the order of their importance and these lines are often identified by an index with the format of IRQ followed by a number. In an 8-bit bus, you would have 8 interrupt lines, IRQ0 through to IRQ7. However, in the Arduino world, these are often referred to as INT.0 through to INT.5 depending on your board and chipset.

Can we interrupt an interrupt?

Interrupts can supersede each other and follow a hierarchy. So if we raise the interrupt at IRQ3 or INT.3 raising another at IRQ1 or INT.1 will interrupt the original. This higher interrupt will execute before returning to the original. However, a word of warning: it is possible for multiple interrupt devices to share one interrupt line. For example, this may be applicable to a device such as keyboard where there is only ever one key being pressed at a time; but if you have two different devices sharing one interrupt, any software you write will be unable to distinguish which of the devices takes priority over the other, and you will most likely end up in a runtime error.

Edge-Triggered

Edge-triggered interrupts are triggered by the rise or fall of a signal level, either a falling edge (high to low) or a rising edge (low to high). The simplest way here is to think of the logic or 5V line with a simple push button. When the button is pressed, the input line goes from 0V to 5V. This sharp rise is the leading edge. When the button is released the voltage or signal drops from 5V to 0V. This is the falling edge. We can trigger an interrupt to occur on either of these, depending on what we require. This style of input is used for things such as game pad device inputs where you have multiple buttons that need to react to the user’s input as quickly as possible. Polling through the buttons would introduce a significant lag into the system and make for a poor user experience.

Level-Triggered

A level-triggered interrupt is an interrupt signalled by maintaining the interrupt line at a high or low logic level [1]. The device signals the IRQ and the interrupt is deemed active while the line is held either high or low. It holds this level until it has finished its commands and then ceases to assert the line level logic. While this makes sharing interrupts between devices even easier, you must be mindful that different devices may hold the line in a high state, even after others have ceased, leading to runtime issues.

1
figure 1

Practical Demonstration

Both the Arduino and Raspberry Pi development environments support the use of interrupts. For the Arduino environment, it will depend on which board and chipset you are using as to how many interrupts you have available to work with. For the UNO, NANO and other 328p based boards you have 2 interrupts, located on pins 2 and 3. The Mega2560 has 6 interrupts located on pins 2,3,18,19,20 and 21. However, be mindful that when we address the interrupt initially, we do so by the pin assignment, yet the IRQ are assigned by the hierarchy. For example, regarding the UNO, pin 2 is INT.0 (IRQ0) and pin 3 is INT.1 (IRQ1). For Due, Zero, MKR1000 and 101 boards, the interrupt number = pin number.

When using ISRs in the Arduino environment, you do need to be aware of a few things. Browsing the code, the ISR does resemble a normal function, but there are some limitations. The ISR cannot have any parameters, and it should not return anything. The idea behind an ISR is that it is as short as possible. If your sketch ends up having multiple ISRs then remember, only one can be running at a time and it will depend on their priority order. If you are relying on millis() or delay() then neither will work or increment while the ISR is working. You can use micros() but it will start to be inaccurate after 1 to 2us. The only way around these timing issues is to use delayMicroseconds() as this does not use any counters.

The use of globals is not only recommended, but they are encouraged to pass data between the ISR and the main sketch. To make sure that they are accessible between the ISR and main program, variables should be declared as volatile.

Our basic syntax for attaching an interrupt for use is:

attachInterrupt(digitalPinToInterrupt(PIN),ISR, MODE);

Parameters

PIN: The pin number, an integer or whole number.

ISR: The ISR to call when the interrupt occurs; this function must take no parameters and return nothing. In the example below our ISR or function to call is blink.

MODE: Defines when the interrupt should be triggered. Five constants are predefined as valid values:

  1. LOW to trigger the interrupt whenever the pin is low
  2. CHANGE to trigger the interrupt whenever the pin changes value
  3. RISING to trigger when the pin goes from low to high
  4. FALLING for when the pin goes from high to low
  5. HIGH to trigger the interrupt whenever the pin is high (for Due, Zero and MKR1000).

For the purpose of the below example, assume an input button on pin 2.

const byte ledPin = 13; 
const byte interruptPin = 2; 
volatile byte state = LOW; 

This is our initialisation. We are using the built-in LED on pin 13, an input on pin 2, and we are creating a byte variable called state to toggle the state. Note that it is declared with “volatile” as this is best practice when using interrupts.

void setup() { 
  pinMode(ledPin, OUTPUT); 
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt
(interruptPin), blink, CHANGE); 
}   

Our setup routine assigns our pins and declares the interrupt attachment. Note that interruptPin is pin 2, which is used by the pinMode assignment and in the attach interrupt. Also note that in this case we will be using CHANGE as the mode for the interrupt. This means whenever the state of the button changes, the interrupt will be called.

void loop() { 
  digitalWrite(ledPin, state); 

This is where it starts to get interesting. The main loop only contains one line of code, and it is not related to the interrupt at all. The only thing this line does is to change the LED to on or off depending on the value of the variable state.

Our ISR is where the magic happens. Following is the blink function. You will note that it takes no arguments nor does it return a value. It serves a single purpose, and that is to change the variable indicating the current state of the function.

void blink() { 
  state = !state; 
}   

Python Example

Using the Raspberry Pi’s GPIO ports, we can use any of the digital pins and use them as an interrupt source. We need to make sure that the GPIO ports are configured as general purpose input. It is difficult to draw direct comparisons between Python and C++ in this context, as the underlying hardware is different and the programmer’s interface level is quite varied. However, the principles are still the same, and the ARM processor will accept the same interrupt sensing:

  • Level sensitive high/low
  • Rising/falling edge
  • Asynchronous rising/falling edge.

Level interrupts maintain the status of the interrupt until the level has been cleared by the system software. The normal rising/falling has a small amount of synchronisation built into it. The attachment syntax differs from Arduino’s implementation, but the principles are still the same; although one positive thing the GPIO library allows is the use of debouncing the switch, to stop any false triggers.

A NOTE ON IRQS AND ISRS IN LINUX: As the Raspberry Pi uses the Linux operating system, the OS itself interfaces with the kernel and true hardware interrupts. We could rewrite the OS from scratch, but that is a mammoth task that is well beyond the scope of this article. Python on Linux is a thread on its own and we cannot truly shut down or pause one ISR over another at runtime. We need to be mindful of this and use our code and variables to ensure interrupts are handled efficiently. The libraries in question here often refer to the ISR as “callback”.

The attachment syntax differs from Arduino’s implementation, but the principles are still the same; although one positive thing the GPIO library allows is the use of debouncing the switch, to stop any false triggers.

We need to assign the pins that we are using, then assign the ISR. The below code sequence can be put together to create one overall instruction. It assumes you have put a push button on the GPIO pins 23 and 24, where 23 is connected to ground and 24 is connected to 3.3V.

GPIO.add_event_detect(PIN, MODE, callback=ISR, bouncetime=300) 
import RPi.GPIO as GPIO 
  GPIO.setmode(GPIO.BCM)

We start by setting the GPIO 23 as an input, pulled up to avoid false detection. The port is wired to connect to GND on button press. We will be setting it to falling edge soon.

GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)

Next, we set up GPIO 24 as an input, pulled down, connected to 3.3V on button press.

GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 

Next, we define a threaded callback function called ourCallback. This will run in another thread when our events are detected. When a falling edge is detected on pin 23, regardless of whatever else is happening in the program, the function ourCallback will be run.

def ourCallback(channel):
  print "falling edge detected on 23" 

As this is the starting point of our main code, we attach the interrupt to our callback or ISR, which is named ourCallback. The parameter bouncetime=300 is to prevent switch bounce causing multiple IRQs.

GPIO.add_event_detect(23, GPIO.FALLING, callback=ourCallback, bouncetime=300) 

The last section here waits for a rising edge on pin 24, and the final 2 lines are used to clean up the use of the GPIO port, which is best practice when using Python on the Raspberry Pi.

try: 
  GPIO.wait_for_edge(24, GPIO.RISING) 
  print ("Rising edge detected")
except KeyboardInterrupt: 
  GPIO.cleanup()        
GPIO.cleanup()  

Trap or an interrupt?

It is very easy to become complacent and use interrupts for everything in your project. However the interrupt is not always the right tool for the job. Systems can generate what are called traps. Many languages from the low level assembly up to Java contain variations of the Trap.

If we were to make a blind division of the difference, a trap is generated by software or a user process whereas an interrupt is instigated by a hardware related input. A simple example of a software or user process trap is a divide by zero. No computer system can divide by zero as it generates an error, similarly would be an invalid memory designation. Even the best coding in the world, trying to avoid getting a divide by zero error can inevitably fail if the end user of your project intentionally does put a zero in a division statement. However if we know what to expect we can “TRAP” this error and divert the code to another subroutine that will perform actions, such as giving the user a warning, and then continue on. More often than not a divide by zero error will cause a program to crash thus resulting in a non-functioning product.

C++/Arduino does not have a direct code example to catch an exception and trap them. However Python does. Below is an example of using the “try” block to trap the divide by zero:

try:
  print 1/0
except ZeroDivisionError:
  print "You can't divide by zero"

Conclusion

Once you have spent some time working with interrupts, they are quite easy to implement but will make a big difference to the human interface elements of your projects. It can stop lagging and sluggish behaviour, and bring radical changes to your ideas. Just remember the amount of interrupts you have at your disposal, and be mindful that sharing the lines can lead to interrupts interrupting interrupts.