Interrupts are an integral part of most MCUs, but using them incorrectly can introduce problems to your design. Here, Stuart looks at what interrupts are, how they work, and how to avoid some of the potential pitfalls.
An interrupt is a signal that temporarily changes what an MCU is doing, because of some hardware event, such as receipt of a byte of data or rollover of a timer. In response to an interrupt, the MCU stops whatever code it is currently executing, executes some other code for a while, then returns to whatever was interrupted.
An interrupt is analogous to a traffic stoplight. At a red light, all the traffic on one street stops, while traffic on the cross street proceeds. If you think of the cars in your lane of traffic as a software task, your progress is stopped while the cross street “task” executes. Once the light turns green, you proceed as if nothing had happened.
From your perspective, your progress is temporarily delayed. You still reach your destination, but just a little later. Your brief delay helps to organize the overall traffic flow. The traffic system is analogous to the software in an MCU, which has many tasks to complete. Although I have been known to complain that traffic lights seem to be synchronized for maximum frustration, they keep all the intersections from getting gridlocked, for the price of a little additional travel time. I was in a situation once where flooding caused excessive traffic. People ignored the traffic lights and gridlocked the intersections, and a normally 15-minute drive took 4 hours.
Like the stoplight, an interrupt in an MCU temporarily executes a different task for a while. For example, a timer might expire, generating an interrupt that causes the MCU to reload and restart the timer, blink an LED, or take some other action that needs to occur periodically. An interrupt generated by a magnetic pickup on a motor shaft might trigger a task to measure or adjust RPM. Figure 1 shows a simplified timeline of an interrupt. Some task or routine (Task A) is executing. An interrupt occurs, which might be from an internal peripheral, such as a USB controller or an I/O pin connected to that motor sensor. When the interrupt occurs, Task A is stopped and the interrupt routine (called an Interrupt Service Routine, or ISR) is executed. When the ISR completes, Task A resumes execution where it was interrupted.

Simple interrupt timing example
HOW AN INTERRUPT WORKS
When the interrupt occurs, an interrupt controller in the MCU directs CPU execution to a new routine at a new address. This normally uses a vector table in flash memory that contains the addresses of all the ISRs. The interrupt hardware saves the return address (the address of the interrupted instruction) on the system stack. Note that some processors such as the TI TM4C series of ARM processors may have more than one stack, and the stack used depends on what mode the MCU is in. But the general principle still applies; the return address is saved on the stack.
One of the things that must happen in response to an interrupt is that the context must be saved. The CPU registers have to be saved (usually pushed onto the stack), and any other state values that may change during the ISR have to be saved. If you are programming in C or a similar language, there is a mechanism to identify ISRs to the compiler, and the compiler adds the code to save the context. For example, on the TI TM4C series of ARM processors, the setup for an interrupt handler looks like the following, for an ISR function named TickISR(), which services a timer that generates a periodic tick interrupt:
— ADVERTISMENT—
—Advertise Here—
Create the code (ISR function) for TickISR().
Call a function to register TickISR as the tick timer ISR.
Enable the interrupt.
This is a very simplified overview, and a more detailed example will be presented later but it captures the steps that must be performed. Once the compiler recognizes TickISR() as an ISR, it adds the code to save any context that is needed.
The ISR executes when the associated interrupt occurs, and then returns. Again, in C, if the function is registered as an ISR, the compiler will add the code needed to restore the context on exit. Note that ISRs don’t return values; there is no way to process a return value since execution resumes at the point where the code was interrupted.
If some hardware state, such as the state of some external device, is not inside the MCU, the compiler won’t be aware of it. If it is necessary to save such context, it must be added to your ISR code. I worked on a system some years ago that required writing an address register in hardware peripheral, and then reading/writing a data register. If an interrupt occurred between writing the address register and accessing the data register, and if the ISR was using the same peripheral, then there was a risk of a race condition in the software:
Task A contains the following:
Write 01 to address register of peripheral
Write something to the data register
The ISR contains this:
Write 05 to address register of peripheral
Read data register
If the interrupt occurs between the two Task A writes, this is what happens:
Write 01 to address register (Task A)
—– Interrupt occurs here ——
Write 05 to address register (ISR)
Read data register (ISR)
——- ISR exits ——————–
Write something to the data register (Task A)
You can see that Task A will now be writing to peripheral register 5, instead of to register 1, because the ISR changed the address register contents. In a case like this, the ISR needs to read and save the address register before writing it, and then restore the value before exiting. This would have to be added to your ISR code, along with whatever save/restore functionality the compiler adds. The same concern applies to shared memory, such as a buffer that is written by the ISR and read by non-ISR code.
WHEN TO USE INTERRUPTS
A lot of MCU systems can be implemented without using interrupts. I frequently do this for simple applications. But there are two cases where interrupts are needed. The first one is easy, since some MCUs have internal peripherals that can only be serviced by use of an interrupt. There is no way to poll the interrupt condition to see if it is set.
— ADVERTISMENT—
—Advertise Here—
The second case is where interrupts are needed to prioritize the execution of the software. Figure 2 shows a flowchart for a simple system. This system has an input device that is receiving bytes from an external source. There are two functions, one that reads the input data, and a processing step that does some mathematical computations on the incoming data. When input bytes are received, they are stored in a buffer for processing. When 4 bytes are collected, the computation step does a computation on them.

Flowchart for simple compute system
Now, suppose that the computation step is usually brief, but sometimes it takes longer to run. In fact, it takes so long that at least one input byte is missed. Figure 3 shows a timeline for this. Four bytes are received, and after the fourth read-the-byte function executes, and the computation piece starts. It completes before the next byte is received.

Timing for compute system, showing a timing problem
Four bytes later, the entire process happens again, except that this time the computation function takes longer than 2-byte intervals to complete. One byte is missed.
Figure 4 shows how we can prevent this problem, using an interrupt to activate an ISR that receives the incoming bytes. Here, the longer-than-normal computation is interrupted when a byte is available, ensuring that no data is lost.

Interrupt-driven compute system
This change causes additional bytes to be stored, and they are stored in the middle of the computation. The software will have to use alternating buffers or a first-in, first-out (FIFO) buffer or some other method to handle the additional bytes. The byte read and compute functions have to handle the case where there are more bytes stored than the four being processed, and the new bytes are being stored while computation is progressing.
Adding the interrupt solves the missing byte problem, but it requires increased complexity in storing extra bytes and synchronizing the data to the compute functionality. This is one of the prices you pay for using interrupts—sometimes other parts of the code have to be more complex to handle whatever unpredictability is added.
Another impact of adding the interrupt is that the longer compute piece gets broken up into four smaller time segments, instead of a single longer one. The time it takes to run is the same but is now spread across more elapsed time, since it is interleaved with the interrupt servicing. In fact, if the computation time were just a little longer, it would stretch into another 4-byte interval. This could create an entirely new set of problems because the computations would get backed up, and the input buffer management potentially would become more complex.
The described example is somewhat contrived to illustrate the reasons you would use an interrupt and the potential issues it can create. The more interrupts you use, the more the complexity can grow.
One of the key rules for using interrupts is that they nearly always increase the unpredictability of the system. An interrupt can occur during any portion of code execution—literally between any two machine-level instructions—and asynchronous interrupts can all occur simultaneously. This means that a piece of code can be interrupted at any time, and may not resume executing until two or more simultaneous interrupts have been serviced.
The word “simultaneous” doesn’t mean that interrupts have to occur at the same instant; they just have to appear simultaneously to the software. This is illustrated in Figure 5. Each ISR starts after the previous one completes, and the interrupted function does not resume until the last ISR has completed. Even though the interrupts occur at slightly different times, the effect on the interrupted function is the same as if they all occur simultaneously. Any system using multiple asynchronous interrupts has to allow for the possibility that they will be simultaneous—because eventually, they will be.

Simultaneous interrupts
To answer the original question of when to use interrupts, you normally use them when some hardware event needs to be handled immediately to prevent missed events. You might also use interrupts in an event-driven system such as an real-time operating system (RTOS), where everything is interrupt-driven to allow for preemption.
TYPES OF INTERRUPTS
Interrupts come in two types: “edge sensitive” and “level sensitive.” An edge-sensitive interrupt recognizes a rising or falling edge to trigger the ISR. The interrupt condition can stay active after the interrupt is serviced, but the interrupt won’t be recognized again until the interrupt signal goes inactive and then active again. Edge-sensitive interrupts are useful for detecting things such as motor encoders, where the interrupt signal remains low or high for an unknown period of time, but you are only interested in the rising or falling edge transition.
A level-sensitive interrupt will produce an interrupt whenever the interrupt signal is at an active level. So, if an interrupt is active when high, and your ISR services it, but the interrupt remains high after the ISR completes, then the interrupt will again be recognized and the ISR will be executed again. This may or may not be a problem, depending on what you wanted. For example, a serial port on an MCU might have a hardware FIFO buffer that can hold several bytes. If there are 2 bytes in the FIFO, you might want the interrupt to remain active after the first byte is read, so the ISR will execute twice to empty the FIFO.
Most microcontrollers have input pins that can be configured as interrupts, and these will be edge- or level-sensitive, sometimes programmable as either. Depending on the MCU, a pin configured as edge sensitive may be able to detect rising, falling, or both edges.
Most MCUs synchronize input pins with the CPU clock, so a signal with a long high time and a short low time (or vice-versa) might result in the transition being missed. Typically the shortest high or low time is on the order of four CPU clocks. So if you are using such a signal as an interrupt input, check the MCU datasheet to make sure the shortest pulse width isn’t too short.
— ADVERTISMENT—
—Advertise Here—
Some MCU peripherals require you to clear the interrupt condition in software before exiting the ISR. This is not uncommon with timers, where a timer rollover interrupt is not automatically cleared when the ISR is executed. Other peripherals on the same MCU may clear the interrupt condition in hardware when the condition that caused the interrupt goes away, such as when the serial port input FIFO goes empty.
INTERRUPT PRIORITY
In some MCUs, when an interrupt occurs, it disables all other interrupts until the ISR is completed. In such cases, the MCU sometimes requires an explicit “re-enable interrupts” function to be performed. Normally this is handled by the compiler, if you are using a high-level language such as C.
Other, more complex, MCUs allow you to prioritize interrupts. So, if a lower-priority ISR is executing, and a higher-priority interrupt occurs, the lower-priority ISR is, itself, interrupted to run the higher-priority ISR. In the reverse case, where a high-priority ISR is executing, a lower-priority interrupt has to wait until the higher-priority ISR completes before it can execute.
The TI TM4C1233 MCU, for example, has eight interrupt priority levels. Interrupts may be grouped into different priority levels, and sub-priority levels are available in the groups to prioritize interrupts within each group. The original Intel 8051 MCU had two priority levels and five interrupt sources. If all interrupts were set to the same level, the priority was determined in hardware. Newer variants of the 8051 have more flexibility in interrupt prioritization.
INTERRUPT CONFIGURATION EXAMPLE
The Listing 1 shows how you might set up an interrupt for the TI TM4C family of MCUs to use the Systick interrupt as a 1,220Hz timer event. The Systick is an internal timer, specifically intended for use with an RTOS or similar applications. It can be programmed with a divisor to generate an interrupt at a regular interval. The snippets are from code that I used in an earlier Circuit Cellar November 2018 issue with my article “Add GPS to Your Embedded System: MCU-Based Design” [1]. First, I defined the ISR code:
// Systick interrupt handler
void SysTick_IntHandler(void)
{
SysTickInt = 1 ;
}
This code sets a flag, SysTickInt, when the interrupt occurs; subsequent code in the main loop handles the actual interrupt by polling for SysTickInt to be set. SysTickInt is a static or global variable. You could also increment SysTickInt instead of setting it, so you can tell if any systick interrupts occurred but weren’t processed. The code that processes the SysTickInt flag is part of a bigger while() forever loop and is shown in Listing 1.
Listing 1
Using 1220Hz timer interrupt to blink LED
if (SysTickInt)
{
SysTickInt = 0 ;
ticker ++ ;
if (610 == ticker) HBLEDon; // Heartbeat LED on
if (1220 == ticker)
{
HBLEDoff; // Heartbeat LED off.
ticker = 0 ;
// Other 1 Hz ticker stuff goes here.
}
// Other 100 Hz ticker stuff goes here
}
This is just the first part of the code, which blinks a heartbeat LED. Ticker is a 16-bit static variable to count 1,220Hz ticks. HBLEDon and HBLEDoff are #defines to turn the heartbeat LED on and off. Note that SysTickInt is set to zero in the systick processing. Without that, every time the main loop happened, this section of code would execute again, and the LED would blink at a rate too fast to see.
Finally, the code in the initialization section that gets called at startup, configures the systick interrupt for 1,220Hz operation, installs the SysTick_IntHandler() function as the ISR, and enables the interrupt (Listing 2).
Listing 2
Registering and enabling timer interrupt
// Configure SysTick for 1220 Hz interrupt
// with 80 MHz CPU.
// Register SysTick_IntHandler as systick ISR.
SysTickIntRegister(SysTick_IntHandler);
SysTickPeriodSet(65536) ; // Set 1220 Hz interrupt
IntMasterEnable() ; // Enable interrupts
SysTickIntEnable() ; // Enable systick interrupt
SysTickEnable() ; // Start systick running
All this code was written in TI’s Code Composer Studio, using the TivaWare peripheral driver libraries from TI [2]. These libraries provide functions to manipulate the on-chip peripherals of the MCU.
ISR TIMING
This particular snippet wouldn’t need a systick to be an interrupt; it could be polled. In a more complex application, the systick ISR could do more things. However, one of the guidelines for using interrupts is to keep the ISR as short as possible, to minimize the amount of time the ISR is blocking the rest of the system functionality. Obviously, there are cases in which you need to do significant processing inside the ISR, but the less time spent in the ISR, the less impact it has on whatever was interrupted.
As an example, imagine a system that is getting high-speed data from an internal UART or SPI peripheral. The peripheral has a 16-byte FIFO buffer that is configured to generate an interrupt when 8 bytes have been received. You could empty the FIFO in the ISR. But an alternative is to set a flag in the ISR to indicate that the FIFO is half full and let the main polling loop check the flag and empty the FIFO. Functionally, both are the same, but the second method limits the time that the ISR is delaying whatever function was interrupted. This only works, however, if you know the rest of the code will get to that FIFO before an additional 8 bytes come in and it overflows.
In a RTOS, an interrupt will typically exit through the system. The RTOS will then determine if the event that triggered the ISR is a higher priority than the code that was interrupted, and either will execute the FIFO-emptying task if it is the highest priority, or else delay the FIFO-emptying task and return control to whatever was interrupted. In the second case, the FIFO-emptying task would execute after the higher-priority task finishes. If you use interrupts without an RTOS, the ISR will always return to whatever code was interrupted; there is no way to redirect the ISR return.
As Albert Einstein said, everything should be made as simple as possible, but no simpler. Similarly, an ISR should be as short as possible, but no shorter. Sometimes an ISR has to do significant processing to avoid losing data, or for some other high-priority reason. So the rule about keeping an ISR short has to be tempered with the priority of whatever the ISR is responding to. You just have to be sure that the worst-case ISR execution time doesn’t break something else.
INTERRUPT DISABLE
Earlier, I mentioned a system in which a peripheral saves the value of the address register in the ISR and then restores it before return, to avoid corrupting the address register for the interrupted function. Another way to handle this is to disable interrupts:
Disable interrupts
Write HW address register
Read or write HW data register
Enable interrupts
This method works and is how the software engineers actually implemented the code in this case. If an interrupt occurs between writing the address register and reading/writing the data register, it will be ignored until interrupts are re-enabled. But use caution; leaving interrupts disabled for too long can create other timing issues. And if the disable is in a function that is called by another function that had already disabled interrupts, this function may re-enable interrupts prematurely. So if you use this method, make sure you are aware of all the ramifications in timing and in other functions.
CONCLUSION
Interrupts aren’t always necessary, but they provide a way to prioritize responses to hardware events and enable your embedded system to keep up with everything it has to do. Use them correctly to avoid unexpected behaviors.
REFERENCES
[1] Add GPS to Your Embedded System: MCU-Based Design, Circuit Cellar November 2018
[2] TI TivaWare Peripheral Driver Library: https://www.ti.com/lit/ug/spmu298e/spmu298e.pdf?ts=1646005527718&ref_url=https%253A%252F%252Fwww.google.com%252F
RESOURCES
Texas Instruments | www.ti.com
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • AUGUST 2022 #385 – Get a PDF of the issue
Sponsor this ArticleStuart Ball recently retired from a 40+ year career as an electrical engineer and engineering manager. His most recent position was as a Principal Engineer at Seagate Technologies.