Interrupts are an integral parts of computing (not just in embedded systems) but often misunderstood. Modern computers cannot function without interrupts, as for embedded systems? Well, that depends on your design. Sometimes you won’t need interrupts, and sometimes they will be a critical part of your application. To understand if you need to use interrupts, you need to understand what an interrupt is.
Introduction to Interrupts
Let’s imagine that you are home. You are expecting a parcel; a new computer, a video game, tickets to a concert… Anything you want. Since the local delivery system informed you that delivery will occur somewhere between 8AM and 8PM, you do not have a precise idea when the delivery will occur.
You keep on working throughout the day, but you are eager to know when the delivery occurs. So, once every so often, you stop what you are doing and look at the letterbox. No parcel? Back to work.
Now imagine that the parcel is being delivered by another delivery service, one that knocks on the door. You are busy working, and then you hear the doorbell. You stop what you are doing, go and answer the door, and get your parcel.
This analogy is incorrect for a few reasons, but it does illustrate the idea, and introduces two notions; polling and interrupts.
Constantly looking at the letterbox is known as polling; you are actively looking at something to see if it has changed. In development, this is equal to:
if(readDigitalPin(10) == 1).
You are looking at a value at this specific moment, and looking at the value. The value might have been different before, and might be different later. If you are looking at the state of a pushbutton, it will return the state of the pushbutton to react to a user’s input, but any input done previously might not have been taken into account.
An interrupt is when a notification arrives, forcing you to stop what you are doing. When the doorbell rings, you need to get up and answer it, otherwise the deliveryman will leave a note saying you weren’t home. An interrupt arrives during program execution, and forces the program to stop, while you run an interrupt handler. In the case of an embedded system, your application will immediately be informed of the user activating the pushbutton.
What the deliver analogy fails to explain is the way in which interruptions are handled. When the doorbell rings, your brain stops writing that email, and concentrates on the doorbell. A quick calculation is done, and your brain is informed that there is something else that needs to be done. However, there is a decision that needs to be made.
Your microcontroller is designed to run code. Anything that stops it from performing that task is known as an Exception. Interrupts are exceptions, even a reset is an exception (since, by definition, a reset stops a device from performing the task at hand).
A Vector Table is a location in memory that contains memory addresses. It contains either the memory address of the code to run (known as Predefined), or contains the address of the address to run (known as Fetch). When an exception occurs, the table is read, and depending on the type of table, some actions are performed. Generally, further interrupts are disabled. Then the ISR is called.
The microcontroller is in a specific state. Since interrupts are now disabled, any further input will be ignored, until interrupts are enabled again. Therefore, the ISR has to be as quick as possible. If your pushbutton input is supposed to turn on an LED, then that is fast enough, and can be placed inside the ISR. On the other hand, if the pushbutton initiates a network transfer of a data value, then this has to be handled outside of the ISR. In this case, you would activate a flag, and handle this later.
Once the ISR is complete, interrupts are reactivated, and your microcontroller returns to the memory location where it left off, continuing the program as if nothing happened. Inside your main loop, you can periodically check to see if the flag was raised or not, and then act on that information.
The very first microprocessors only had one interrupt pin, an external chip had the be added to correctly handle more interrupts. In essence, the external chip had several interrupt inputs, and when one arrived, it would output its own interrupt, and wait for the processor to read it’s register to know who exactly raised the interrupt. Today, this is no longer the case. Most microcontrollers have multiple interrupts, and in the case of some Cortex-M devices, each and every GPIO can be connected to an interrupt.
So if multiple interrupts arrive, how do you know which one to handle first?
The more advanced chips will use an NVIC, a Nested Vector Interrupt Controller. This peripheral can handle multiple interrupts, and can assign priorities to each one. It is also highly configurable, so you can program it to fit your needs. In this case, interrupts are not disabled, and continue to run in the background, even if you are currently handling an interrupt.
If a single interrupt arrives, the microcontroller changes context, the interrupt is handled, the context is restored, and program execution continues.
Let’s look at multiple interrupts.
The first case is an interrupt followed by a lesser priority interrupt. First of all, we receive an interrupt (Int1 in the illustration below). A context change occurs, and the Interrupt Service Handler connected with this interrupt (ISR1) is performed. During this time, a lower priority interrupt is received (Int2). The first ISR will finish, and then the second ISR (ISR2) will be run automatically. Once they are finished, the context is restored, and the main program continues to run.
The second case is an interrupt, followed by a higher priority interrupt. Once again, we receive an interrupt (Int1 in the illustration below). A context change occurs, and the Interrupt Service Handler connected with this interrupt (ISR1) is performed. However, during execution, and higher priority interrupt is received. Execution stops, and another context change occurs. Now a second ISR is run, and the system waits until it completes. Then, the context is restored, and control is returned to ISR1, where it left off. Once ISR1 finishes, the context is again restored, and program execution continues.
When to use Interrupts
Polling can take a long time, taking frequent breaks from you main program execution, but these breaks occur when you want them to. Interrupts lets your main program continue running, and to only be interrupted when needed. From a performance point of view, interrupts are more interesting.
When using interrupts, your Interrupt Service Routine needs to be programmed to be fast, and configuring an ISR is more complicated than simply using polling. From a beginner’s point of view, polling is more interesting.
A common misconception is that when you create an ISR, every interrupt must be handled. If your device is a hydraulic press that enables every time you press a button, you don’t necessarily want it to begin an operation while it is currently performing one. In this case, the ISR will be programmed to start the mechanical operation, but it will also disable this particular interrupt. No matter how many times the user presses the start button, nothing will happen. However, not all interrupts are disabled, so the emergency stop button will still work. Once the operation is completed, this particular interrupt is enabled again, and the user can begin a mechanical operation again.
Interrupts are an integral part of every computer system, either microprocessor or microcontroller. Sooner or later, you will probably use them, especially on systems that rely on external data arriving at unknown intervals. We’ll be looking into a few cases of interrupts to see how they change from polling.