Interrupts might seem basic, but many programmers still avoid them
When I first started working with microcontrollers, interrupts seemed like a magical
mechanism that only a rare few really understood. Intimidated, I religiously avoided them
and stuck with polled I/O. After a while, though, I decided to become one of the few, the
proud, the programmers that not only used interrupts but exploited all their capabilities.
Just like taking apart a toaster oven to see how it works, I disassembled all the
interrupt-driven code I could find. Now, interrupts are easy, fun and above everything
else, extremely necessary for today's embedded systems.
In case you're wondering why I'm taking a column to talk about something so basic, the
answer is twofold: First, in the past few months, I've examined (and chased bugs in)
several examples of code by people who were supposed to know what they were doing
with interrupts, yet they consistently got part of the implementation wrong. Second, in my
company the one part of an instrument's operating system that I always receive questions
on is how it manages interrupts. Therefore, it's time for a review.
Obviously, each microcontroller and microprocessor handles interrupts somewhat
differently, yet they all share some common functionality. Basically, an interrupt causes
a program to suspend its current operation and branch to a location elsewhere in memory.
Then, after the program handles the event that caused the interrupt, the interrupt service
routine (ISR) must restart the program from where it had previously been suspended.
Electrically, an interrupt is a digital signal into a CPU that indicates some event has
happened. For example, receiving serial information, pressing a key or a timer expiring
can all generate an interrupt. Expanding on the keypress example, a common way of
generating an interrupt is with the venerable 74C923 20-position keypad encoder. When you
press a key, this chip's debounced Data Available (DA) signal goes High and stays there
until you release the key. By tying this signal to one of the CPU's interrupt inputs, the
processor can sense a keypress as soon as it happens.
Interrupts come in three flavors: edge-triggered, level-triggered and a combination of
both. As the name indicates, edge-triggered interrupts happen on the transition of a
signal from one state to another, primarily from a Zero to a One. This type of interrupt
is useful for a fleeting signal that doesn't last long enough for the processor to
recognize it using polled I/O or for when the signal can last a long time, but the
significant event is when that signal first goes active. Again, the keypress is an
excellent example of an application that calls for an edge-triggered interrupt. From an
interrupt's viewpoint, the amount of time you hold the key down doesn't matter. All that's
important is detecting when the event first happens.
By contrast, level-triggered interrupts are in some ways like polling except that the
CPU manages the polling without program intervention. Typically, the processor samples the
interrupt input at predefined times during each bus cycle such as state T2 for the Z80
microprocessor. If the interrupt isn't active when the processor samples it, the CPU
doesn't see it. One possible use for this type of interrupt is to minimize spurious
signals from a noisy interrupt line.
The last interrupt type is a hybrid, where the hardware not only looks for a
transition, but it also verifies that the interrupt signal stays active for a certain
period of time. A common hybrid interrupt is the NMI (non-maskable interrupt) input.
Because NMIs generally signal major-or even catastrophic-system events, a good
implementation of this signal tries to ensure that the interrupt is valid by verifying
that it remains active for a period of time. This 2-step approach helps to eliminate false
interrupts from affecting the system.
Processing an interrupt
Okay, now you have an interrupt signal at the CPU, the next step is to see how it
affects the processor's operation. No single technique describes all possible forms of
interrupt processing, so I'll start with a simple description and work up to more
Interrupt processing has some basic requirements from the CPU. Before it can respond to
an interrupt, the processor must wait for an "interruptible" state in its
processing. For example, if the processor's writing to memory, it must wait until the
write is done before processing the interrupt. Once the CPU detects the interrupt, its
first action is to save all the information it will need to resume normal processing once
the interrupt is over. At a minimum, the chip saves the Program Counter (PC). This process
is analogous to you placing a finger in a book when someone interrupts you while you're
reading. After the interruption goes away, you know exactly where to continue.
After saving this "bookmark" information, the CPU changes the PC value to a
fixed location in the processor's memory that contains a pointer to the
instructions-called an interrupt service routine (ISR)-that tells the processor how to
deal with the interrupt. When the ISR finishes, the processor restores the original PC and
merrily continues on as if nothing had happened.
An NMI is an excellent example of this most-basic form of interrupt. Many processors
have a special interrupt pin reserved for flagging such catastrophic events as a power
failure. Using this interrupt might allow you a couple of milliseconds in which to save
crucial operating information or shut down a system before power dies completely.
Beyond these basics, interrupts often sport some refinements that make them even more
useful. For example, one common feature is to multiplex one interrupt input by allowing
the interrupt source to send an identifying code along with the signal that allows the
processor to tell which interrupt occurred. This code might take the form of an address
where the processor can find the ISR for that particular interrupt, or an index into a
table where the processor can look up the ISR's address. The code typically goes into the
processor on the data lines.
When the interrupt occurs, the processor also sends out an acknowledge signal analogous
to the CPU memory read signal that other parts of the system use to place information onto
the processor's data bus. I've seen processors issue anywhere from one to four of these
acknowledge signals for each interrupt. Such multiple signals allow the processor to get
additional information about the interrupt from the hardware.
Depending on the microprocessor, this information can take the form of either a vector
address or an executable opcode. The latter can lend itself to some rather peculiar
implementations. I once worked on a very early floppy-disk system-big 8" disks that
held all of 90k bytes-that required considerable horsepower from the processor. As each
new byte came off the disk every 35 µsec, the processor had to store the data away as it
came in. At the time, this speed was much too fast for an ISR, so instead we implemented a
halt-interrupt technique that involved executing a halt instruction and then waiting for
an interrupt-on this processor the only way to exit the halt. When the next data byte
became available, the hardware issued an interrupt. The interrupt acknowledge read a NOP
(no operation) opcode that the processor executed, and then the processor started
executing the next instruction after the halt. This technique allowed us to quickly read
all the data while staying synchronized-but just try to document the technique in the code
Finally, no interrupt primer would be complete without a discussion of what goes on
inside an ISR. Basically there are two fundamental rules. First, the ISR must save then
restore all CPU, memory and I/O resources that it uses. For example, this data might
include CPU registers or memory data such as the scratch area for a floating-point package
(many of which aren't reentrant). Second, it must get back out of the ISR as quickly as
possible. The reason for this rule is that ISRs should generally block new interrupts
until after the ISR has completed running. Therefore, an ISR should do as little as
possible so that interrupts are off for as little time as possible. For the processors I
typically use, I like to see ISRs run in less than 40 µsec. Obviously, a 100-MHz Pentium
and a 4-MHz 8051 have very different time scales, but the concept's still the same-get out