It may seem surprising to some that we will start talking about interrupts so early in this tutorial, but I am convinced that it makes sense to do so now. All the peripherals that we will later learn about can be serviced using interrupts, and in many if not most circumstances that is the best way to service them. If this tutorial went through the use of the common microcontroller peripherals without discussing interrupts, and only then introduced the subject of interrupts, then all the peripherals would have to be revisited to discuss how they would be used with interrupts. It is much better and more organized, I think, to lay a solid foundation regarding interrupts first, so that the topic can be included in the discussion of each of the peripheral types later.
Besides, interrupts are just very cool, and you deserve to get to the cool stuff ASAP.
What is an Interrupt?
An interrupt is a signal (an "interrupt request") generated by some event external to the CPU , which causes the CPU to stop what it is doing (stop executing the code it is currently running) and jump to a separate piece of code designed by the programmer to deal with the event which generated the interrupt request. This interrupt handling code is often called an ISR (interrupt service routine). When the ISR is finished, it returns to the code that was running prior to the interrupt, which then resumes running with no awareness that it has been pre-empted by the interrupt code. It is this ability to run the appropriate code for an external event at any point in time that is both the chief benefit of, and the potential source of difficulties from, interrupts.
A computer interrupt can be compared to interruptions in everyday life. For example, the ringing of your telephone or a knock on your door are interrupt-type events. Your phone or your doorbell can ring at any point during your day, and when they do ring you will typically stop what you are doing, deal with the reason behind the phone call or front door visit, and then go back to what you were doing. Likewise, just as you can ignore a phone call or doorbell if what you are doing requires your immediate and undivided attention, so it is possible to program the CPU to defer interrupts during certain critical sections of code, or even to ignore them entirely.
Interrupts are one of the most powerful and useful features one can employ in embedded systems. They can make the system more efficient and more responsive to critical events, and they can also make the software easier to write and understand. However, they can also be a confusing and error-prone feature in a program, and some people avoid them for that reason. But every embedded programmer should be at home with interrupts, should consider them as useful tools and not monsters in the closet, and use them whenever appropriate. It is fairly uncommon that an embedded system does not use at least one interrupt.
An interrupt request is generated when some device external to the CPU (but not necessarily external to the microcontroller – it may be an on-board peripheral) sends a signal (the interrupt request signal) to the CPU. The CPU, if configured by the software to do so, will respond to this interrupt request by finishing the current instruction it is executing, and then jumping or “vectoring” to the correct interrupt service routine (ISR) for this interrupt. This jumping or vectoring is similar to a subroutine call but not identical. The CPU will save the program counter (on the hardware stack or in a special register), and depending on design may also save some status information (perhaps on the stack), and then begin executing the interrupt code that the user has designated for this interrupt source. At the end of the interrupt code there will be a return-from-interrupt instruction which will restore any automatically-saved status information followed by the saved program counter, which then results in the previously running code being resumed without any indication that the interrupt has occurred. Beyond any state information that is automatically saved and restored in the interrupt process, it is up to the programmer to save and restore any additional resources such as registers that are used in the ISR.
Note that it is critical to return to the interrupted code with the processor in the same state as when it was interrupted. All the flags and registers should be unaltered, either by having not been modified in the ISR, or, if they are modified in the ISR, by having been saved before being modified, and then restored before the ISR returns. Failure to preserve the processor state across an interrupt has the effect on the interrupted code of having registers or condition flags “magically” change at random points, which is understandably quite a serious error, and very difficult to debug. Some of the example programs in this section will demonstrate different ways in which incorrectly written programs that use interrupts can generate such errors.
So to sum up, the general sequence for an interrupt is as follows:
- Foreground code is running, interrupts are enabled
- Interrupt event sends an interrupt request to the CPU
- After completing the current instruction(s), the CPU begins the interrupt response
- automatically saves current program counter
- automatically saves some status (depending on CPU)
- jump to correct interrupt service routine for this request
- ISR code saves any registers and flags it will modify
- ISR services the interrupt and re-arms it if necessary
- ISR code restores any saved registers and flags
- ISR executes a return-from-interrupt instruction or sequence
- return-from-interrupt instruction restores automatically-saved status
- return-from-interrupt instruction recovers saved program counter
- Foreground code continues to run from the point it responded to the interrupt
As usual, the details of this process will depend on the CPU design. Many devices use the hardware stack for all saved data, but RISC designs typically save the PC in a register (the link register). Many designs also have separate duplicate registers that can be used for interrupt processing, thus reducing the amount of state data that must be saved and restored.
Note that saving and restoring the foreground code state is generally a two-step process for reasons of efficiency. The hardware response to the interrupt automatically saves the most essential state, but the first lines of ISR code are usually dedicated to saving additional state (usually in the form of saving condition flags if not saved by the hardware, along with saving additional registers). This two-step process is used because every ISR will have different requirements for the number of registers it needs, and thus every ISR may need to save save different registers, and different numbers of registers, assuring all appropriate state data is saved without wasting time saving registers unnecessarily (that is, saving registers that are not modified in the ISR and thus didn’t need to be saved). A very simple ISR may not need to use any registers, another ISR may need to use only one or two registers, while a more complicated ISR may need to use a large number of registers. In every case, the ISR should only save and restore those registers it actually uses.
Some common examples of events that can generate interrupts include: a timer overflows or reaches an assigned value, a serial input device has received a new character, a serial output device is ready to send a new character, an input pin has changed state, the system voltage has dropped below a safe level, or an ADC (analog to digital converter) has finished a new conversion. This list is by no means all-encompassing. Each microcontroller has its own set of peripherals and events that can generate an interrupt, which will be described in its datasheet.
To summarize, the characteristics of an interrupt are:
- The software to handle the interrupting event (ISR) is executed as quickly as possible after the event occurs.
- The foreground code can be interrupted at almost any point in its execution.
- The immediate state of the interrupted code must be saved and restored so that the interrupted code can resume running without error. Some of this saving and restoring is automatic, while the rest must be done by the ISR.
- The “extended” state of the system will be changed by the ISR. Thus both foreground code and interrupt code must take precautions to insure that all data and resources affected by the interrupt remain uncorrupted.
Enabling and Disabling Interrupts
Every interrupt source will have some way to enable or disable it - usually an enable bit in a configuration register. At the end of the interrupt configuration sequence for that peripheral, the interrupt will be enabled. But this will not cause interrupts to begin to be serviced. The CPU will also have a global interrupt enable mechanism (again, usually a bit in a configuration register) which allows it to process interrupts. This global interrupt enable will typically be set after all the individual interrupt enables have been set, which is to say, after all the individual interrupt sources have been configured. The initialization sequence will look something like this:
- Configure and enable interrupt source 1
- Configure and enable interrupt source 2
- Configure and enable interrupt source N
- Enable Global Interrupts
After being enabled, individual interrupt sources can be disabled and re-enabled as needed at any point in the program, and global interrupts can also be disabled and re-enabled at any point in the program. Briefly disabling global interrupts is one way to assure atomic access to ISR-accessed data, as will be discussed below.
Some Common Interrupt Sources
To give you some ideas of the common sources of interrupts on a microcontroller, here is a list that is suggestive but by no means comprehensive. If a uC contains additional onboard peripherals those will almost certainly be able to generate interrupts as well. As you can see, all of these events are asynchronous, meaning that they can happen at any time, or at least at an unknown time, relative to the executing code.
- Input pin state change
- Timer overflow
- Timer compare/match
- Timer capture
- UART RX char ready
- UART TX ready
- UART TX complete
- SPI transfer
- I2C transfer
- ADC conversion complete
Setting up Interrupt Service Routines and Interrupt Vectors
Every uC will have a documented list of interrupts that can be generated by on-chip or external events. It will also have a list of fixed addresses (often called vectors) corresponding to each of these interrupts. An interrupt vector can take one of two forms. It may be an address where actual code resides (usually, just a jump to the ISR), or it may be an address that holds the address of the ISR. So if your uC supports an interrupt N, it will have a fixed vector address for that interrupt, call it address VN. Supposing you write an ISR to handle this interrupt and the ISR is located in memory starting at address VISR, then the vector for interrupt N (the memory starting at address VN) will either consist of an instruction jump-to-VISR or it will just consist of the address VISR.
Vector holds jump to ISR
.org This_Vector_Address VN: jmp VISR ;jump to ISR … VISR: ISR for Interrupt N …
Vector holds address of ISR
.org This_Vector_Address VN: VISR ;address of ISR … VISR: ISR for Interrupt N …
As far as the structure of an ISR, the important considerations are that all registers that will be used in the ISR must first be saved (usually, pushed onto a stack), that at the end of the ISR all the saved registers must be restored (if on a stack, popped off the stack in the reverse order they were pushed on), and finally that the correct return-from-interrupt instruction be executed, which will not only resume execution of the code that was interrupted, but will also correctly restore or adjust any status bits that were altered by the interrupt response hardware. It is this restoration of status bits that differentiates a return-from-interrupt instruction from the standard return-from-subroutine instruction.
If the ISR is written in assembly language then the saving and restoring of registers, and the return-from-interrupt instruction, will be evident in the code. If the ISR is written in C, the compiler will insert the correct code invisible to the programmer, but to do so, the compiler must be informed that the function is an ISR and not a regular function. Different compilers use different keywords to make this distinction, so you’ll have to look in your documentation to see how to do it. Note that while this additional ISR code is invisible as far as the C source code goes, it will be evident if you look at an assembly listing of the ISR.
Interrupt No-Nos and Fixes
Since interrupts can occur at just about any point in the execution of a program, in most cases the execution time of the interrupt service code should be as short as possible. All other processing related to the interrupt should be moved outside the ISR and into the foreground code.
In addition, interrupts introduce possibilities for data corruption which must be explicitly accounted for in both the interrupt code and the background code. Among the steps necessary to prevent data corruption are:
- ISR saves and restores any working registers it uses
- ISR saves and restores status/condition code register
- Foreground code uses atomic accesses for any data it shares with ISR
The Need for Atomic Access
Imagine this scenario: foreground program, running on an 8-bit uC, needs to examine a 16-bit variable, call it X. So it loads the high byte and then loads the low byte (or the other way around, the order doesn’t matter), and then examines the 16-bit value. Now imagine an interrupt with an associated ISR that modifies that 16-bit variable. Further imagine that the value of the variable happens to be 0x1234 at a given time in the program execution. Here is the Very Bad Thing that can happen:
- foreground loads high byte (0x12)
- ISR occurs, modifies X to 0xABCD
- foreground loads low byte (0xCD)
- foreground program sees a 16-bit value of 0x12CD.
The problem is that a supposedly indivisible piece of data, our variable X, was actually modified in the process of accessing it, because the CPU instructions to access the variable were divisible. And thus our load of variable X has been corrupted. You can see that the order of the variable read does not matter. If the order were reversed in our example, the variable would have been incorrectly read as 0xAB34 instead of 0x12CD. Either way, the value read is neither the old valid value (0x1234) nor the new valid value (0xABCD).
Writing ISR-referenced data is no better. This time assume that the foreground program has written, for the benefit of the ISR, the previous value 0x1234, and then needs to write a new value 0xABCD. In this case, the VBT is as follows:
- foreground stores new high byte (0xAB)
- ISR occurs, reads X as 0xAB34
- foreground stores new low byte (0xCD)
Once again the code (this time the ISR) sees neither the previous valid value of 0x1234, nor the new valid value of 0xABCD, but rather the invalid value of 0xAB34.
If this sounds like an incredibly unlikely problem, don’t be fooled. Interrupts are like ants. They will find the smallest hole in your code, and they will trigger off at exactly that point. And they will trash any non-atomic data accesses. And you will weep and wail trying to figure out what went wrong. Bank on it.
What’s more, going to larger bit-size processors, which move data in larger indivisible pieces, will not solve the problem. A 16-bit or 32-bit processor would not experience the corruption mechanism illustrated for variable X, but can experience interrupt corruption in other ways, for example (let X = 1000):
- foreground loads X (1000) in one operation
- interrupt occurs and ISR increments X: 1000+1=1001
- foreground decrements its no-longer-valid copy of X: 1000-1=999
- foreground saves its modified copy of X (999)
At this point X is not valid. After one increment and one decrement X should be 1000, but it is 999. The ISR increment has been lost. Our indivisible 16-bit loads and saves didn’t help us in this case, because the foreground was performing a divisible read-modify-write operation on the data when the interrupt occurred.
And the problem is even worse than suggested above. It is not just corruption of individual multi-byte values that is at stake, it is also the corruption of larger data aggregates which are comprised of multiple data values. For example, we may have a 3-dimensional mechanism in which every location is designated by a structure consisting of an x-location byte, a y-location byte and a z-location byte. While the loading or storing of the individual x, y and z components cannot be corrupted by an interrupt, it is still possible to corrupt the entire data aggregate, just as in the 2-byte value examples given earlier.
All of these cases are examples of what is known as a critical section. A critical section is a section of code which must have complete and undisturbed access to a block of data or any other resource(s).
The simplest solution to this problem is obvious enough. When modifying or inspecting a given piece of data, don’t let any interrupts occur that might also modify or inspect that data (other interrupts are not a problem). In our examples above, if we had prevented the interrupt between the foreground load or store of the first byte and the load or store of the second byte, the problems would not have occurred.
Disabling interrupts before entering a critical section is effective but rather heavy handed. We do not need to disable all interrupts, only any interrupts that could potentially affect the data accessed in the critical section. By disabling all interrupts, which is sometimes easier than only disabling selected interrupts, we are disabling "innocent" interrupts as well as the one(s) we need to disable. This makes the entire system less responsive than it could be. For this reason, there are more advanced techniques to protect data and resources in critical sections with less potential loss of responsiveness, but any discussion of these will need to wait until much later sections of this tutorial.
What should an ISR do?
“Depends” is probably not the answer you were looking for, right? So I’ll try to be a little more helpful. An ISR should do the immediate work necessary to process the interrupting event, and should signal the main program if there is other work to be done or data to be handled.
As an example, imagine a sensor that interrupts the uC when it has a new temperature value to report. The ISR would then input this new temperature value, save the value in an appropriate location, and if the sensor required it, acknowledge to the sensor that it has received the new value. After that the ISR would set a flag telling the main program that a new temperature value is waiting, and then the ISR should finish and exit.
Now the main program can look for the new-temperature-available message in an appropriate section of its loop (remember, all embedded programs are an endless loop), and process the new value as and when needed.
So the key to writing ISRs is, what must be done now, do now, in the ISR. What can or should be done later, send a message to that effect (often just setting a simple flag) and do later, outside of the ISR.
Since interrupts can happen at just about any time, there is a possibility that interrupt requests from two different sources will be generated at the same time, or to be more precise, before the CPU checks for any pending interrupts (typically this happens between each instruction). If the CPU checks and finds two pending interrupts from different sources, it will use the assigned priorities of each interrupt source to determine which interrupt to run first. The other interrupt will then run when the first ISR is finished (unless, that is, yet another higher-priority interrupt request has come along since).
Some devices have interrupt priorities built into their hardware design, but for other devices the programmer can specify the priorities of the different interrupt sources. This allows the programmer to control which interrupts get the fastest servicing.
Interrupt Vectoring vs. Polling
There is nothing inherent in the concept of interrupts that requires that each interrupt source is vectored to its own private ISR. While this is a great convenience, it is also possible to have multiple interrupt sources all connected to the same interrupt request line. Any of the sources can request an interrupt by asserting the line, and the ISR must then poll each device that is on the interrupt request line to see which device (or devices) requested an interrupt. Earlier microcontrollers had such simplistic, polled interrupt systems, but these days, some form of interrupt vectoring will be found in just about every uC family. Even so, there may be cases where two or more external devices are connected to a single interrupt pin (perhaps they are identical devices and require the same ISR code), so even on a uC with interrupt vectoring, it is possible to add polling into the mix as well.
You might well wonder what happens if a second interrupt occurs while the ISR for another interrupt is being executed. Well, somebody is going to have to get in line and wait. Either the ISR for the second interrupt will not run until the code for the first interrupt has finished, or the second interrupt will interrupt the ISR for the first interrupt and the second ISR will run, and thus the ISR for the first interrupt will be delayed in finishing. Which of these two scenarios plays out will depend on the design of the device, and on how the programmer has configured it.
Example: Button Interrupt
Actually reading switches and buttons directly via interrupts is generally not a good idea, for the simple reason that switch bouncing can generate a large number of interrupts for a single switch event. But it’s a good way to try your first interrupt handling.
The sample programs use two button and one LED, much like the earlier program #NNN. One button turns the LED ON and the other button turns the LED off. But this time, instead of monitoring the buttons in a loop, the buttons will generate interrupts and the LED will be turned ON and OFF inside the corresponding ISRs. The problem of multiple interrupts from a single switch event does not pertain in this case, since turning an LED ON or OFF multiple times is no different than turning it ON or OFF just once.
Now we will take a slightly different approach to controlling the LED via the interrupts. Instead of turning the LED ON or OFF directly inside the ISR, the ISRs will set or clear an LED flag, and the main loop will monitor this flag and turn the LED ON or OFF accordingly.
When does the Interrupt Stop?
A very important question to ask and answer is, once an interrupt request is triggered, when does the interrupt request cease? If the interrupt request did not cease then as soon as the ISR finished, the interrupt request would cause the ISR to be re-entered, trapping the code in an endless interrupt response to a single interrupt request. Clearly, as part of the servicing of an interrupt, the interrupt request must be cleared so this endless response does not happen. As usual, the details of this process depend greatly on the particular device and family, but in general there are two ways in which the interrupt request gets cleared. The first way is automatic – when the CPU vectors to the ISR, it automatically clears the interrupt request. For interrupt sources that work this way, there is nothing for the software to do, since the request is cleared by the hardware. The second way requires that code in the ISR clear the interrupt request “manually”. This usually involves clearing an interrupt request flag in an interrupt control/status register associated with the interrupt source. It is common that clearing such flags is accomplished by actually writing a ‘1’ to the flag bit location. The reason for this approach has to do with our old interrupt problem of corrupting data through two different paths of access. If the interrupt flag were cleared through a read-modify-write process (read the flag register, clear the bit, write the flag register), it is possible that a different interrupt, setting a different interrupt request flag in the same register, could be missed. The corruption sequence for two interrupts (INT1, INT2) could look like this:
- INT1 triggers, setting INT1 bit in INT flags register, causing ISR1 to run
- ISR1 reads INT flags register, in which INT1 bit is set, INT2 bit is clear
- INT2 now triggers, setting INT2 bit in the INT flags register. ISR2 does not run since ISR1 is running
- ISR1 writes out INT flags register, overwriting INT2 bit that was just set. ISR2 never runs
By replacing the read-modify-write sequence with a single write of a single ‘1’ bit, the possibility of losing an interrupt request is eliminated.
Edge-triggered vs. Level-triggered Interrupts
External interrupts, which are triggered by a voltage on a pin, are unique in one aspect. In many device families, external interrupts can be configured as either edge-triggered or as level-triggered. With an edge-triggered interrupt, the interrupt request is generated once for each edge (depending on design and configuration this could be a negative-going or falling edge, a positive-going or rising edge, or either edge). With a level-triggered interrupt, it is not the edge but the level that causes the interrupt request. What this means is that if one has an external interrupt signal that goes active (let us say it goes LOW) and stays active, if the external interrupt was configured for edge-triggering (LOW edge triggering), exactly one interrupt request would be generated, no matter how long the interrupt line was held low. But if the external interrupt was configured for level-triggering (LOW level triggering), interrupt requests would continue to be generated for as long as the interrupt line was held low. As soon as the ISR returned from one such interrupt, the CPU would vector to the ISR again (unless, of course, other higher priority interrupts were pending).