Interrupts in an embedded system usually notify the CPU of some external event that requires immediate attention. An interrupt temporarily redirects the program flow to service whatever condition caused the interrupt.
There are two common reasons for using interrupts. The first would be to ensure that high-priority tasks are executed immediately-one example is a byte received by a UART, where the data must be read before the next byte is received to prevent the first one from being overwritten. A second reason is to reduce hardware cost and complexity by implementing functions in software. Examples of this would be a regular interrupt for timekeeping, or the use of a regular timer interrupt to cause the software to multiplex a multidigit numeric LED display.
Interrupts are the best and sometimes only solution to real-time requirements, but they also reduce the stability of a system. Interrupts do not necessarily make a system unstable, but they usually make it less stable.
Interrupts come in two flavors: edge- and level-sensitive. A level-sensitive interrupt is recognized when it is in the active state. Once the interrupt is serviced, it is expected to return to the inactive state, or else it will be continuously serviced. Edge-sensitive interrupts are recognized in the transition from the inactive to the active state and typically must remain in the active state until serviced. An edge-sensitive interrupt may remain in the active state once serviced; it is the transition that causes the interrupt. Edge-sensitive interrupts are often left in the active state and pulsed to the inactive state to generate an interrupt.
The hardware components of an interrupt consist of the external event that generates an interrupt request; the interrupt controller, and the CPU. The software components include an interrupt vector table; the interrupt service routine (ISR) and the system stack. When the software executes a program as an external event occurs, a request is generated. The interrupt controller may have several interrupt inputs, but it has only one interrupt output that it uses to request an interrupt from the CPU. Assuming the software has enabled interrupts, the CPU will save the return address of the program-the next instruction that would have been executed-on the stack and execute an interrupt acknowledge cycle.
In response to the interrupt acknowledge, the controller will pass a vector to the CPU, with each interrupt input typically having a unique vector. The CPU uses the vector as an index into the interrupt vector table, usually in RAM, and the interrupt vector table contains pointers to the various ISRs. After the ISR executes, it must notify the interrupt controller when it is done. The ISR code re-enables interrupts and executes an interrupt return. The CPU pops the return address from the stack and execution of the program resumes where it left off, with some delay.
Even this brief overview of their structure and operation allows us to derive at least two immutable laws of interrupts. The first law is that an interrupt can occur at any time-it is asynchronous to the program. The second law is that regardless of programming technique, all interrupts use one global resource: time.
While the majority of difficult interrupt problems are caused by the effects of these two laws, there are some variations on the interrupt structure just described that may modify them slightly. Many microcontrollers, such as the 8031, 6805 and PIC17C42, do not have an interrupt vector table. Instead, each interrupt sources vectors to a specific execution address. Some processors have a unique interrupt return instruction that returns from the ISR and re-enables interrupts. Some CPUs (for example, the 80186) have an internal controller, while others require an external controller (the 8086).
Some of the implications of the first law can be seen in shared memory applications where a counter variable is shared between ISR and non-ISR code and is counting interrupts for some unknown reason. The ISR increments the count and the non-ISR code decrements the count when it processes whatever action the interrupt requested. The logic for the ISR and non-ISR code is straightforward:
For ISR logic the sequence is read counter, increment and write counter. The steps for non-ISR logic are read counter, decrement and write counter.
If the interrupt occurs between the time the non-ISR code reads the counter and the time it writes the new value, the counter value will be incorrect. This problem can be fixed. One way is by protecting the non-ISR code where it must be indivisible. Another solution is using indivisible read-decrement-store instruction if the CPU has one.
The lesson to be learned from this is that in such shared memory designs it is important to avoid variables that are written by both ISR and non-ISR code. Values written by an ISR should be read-only to non-ISR code and vice-versa. In this example, the counter could be implemented by a pair of counters. The ISR would increment one and the non-ISR code would increment the other. The actual count is the difference between the two.
In many cases, it is necessary for both ISR and non-ISR code to access the device. If the interrupt occurs between the time the non-ISR code writes the address register and the time it accesses the data register, the non-ISR code will get data from whatever register the ISR code last accessed. The fix for this problem is to protect the non-ISR accesses with disable/enable pairs.
Out of these and other situations come two rules that are important in avoiding ISR problems. The first is to avoid having any variable or hardware written by both ISR and non-ISR code; variables should be written by one and read by the other. When shared variables are necessary, never allow ISR and non-ISR code to attempt a simultaneous write. For example, use a semaphore that is written by ISR code only to indicate that data is available and by non-ISR code only to indicate that the data has been processed.
The second rule is that when it is impossible to avoid shared variables or hardware, identify and protect all non-ISR code that needs to be indivisible.
But what if you follow the rules and still have problems? Let's look at one situation in which this might occur, where the following non-ISR pseudo code uses a variable, X:
If X = 1, do something.
If x = 2, do something else.
If x = 3, do a third thing.
The intent is that one of the three things will be performed on each pass through this section of code. However, if the ISR modifies X, then an interrupt during this sequence can cause bizarre and apparently impossible results, since two of the somethings can be performed in a single pass through the code.
This scenario is not as unrealistic as it may appear; for example, the 8031 microcontroller does not have a compare instruction. To perform the comparison as shown the variable to be tested must be placed in the accumulator register and an XOR operation must be performed. This alters the contents of the accumulator, so it must be reloaded for the next comparison.
The fix, of course, is to read X at the beginning of the code section, store it someplace and use the copy. But this scenario leads us to a third rule: Always assume that an ISR-modified variable can change between any two successive reads. Sooner or later, it will.
A number of problems caused by ISR delays leads to another important rule: The real world keeps happening while interrupts are being serviced.
For example, consider a system that uses a 16-bit counter for time tagging. Every so often, the software reads the count. Since the CPU is 8 bits, it must read the counter twice to get the full 16-bit count. An unrelated interrupt that occurs between the two paired reads can cause the code to get the wrong counter value.
The fixes for this are straightforward: Read the count twice to verify a good read; protect counter reads with disable/enable; and use the counter with separate "freeze" register.
Another useful rule (No. 5) can be derived from situations where there are interrupt stackups: In any system with more than one active interrupt sooner or later the interrupts will stack up. Count on it.
Consider a system with three interrupts and three ISRs and where the code is executing in the background when Interrupt 1 occurs. During execution of ISR 1, Interrupt 2 occurs. As soon as ISR 1 is finished, ISR 2 will be executed. During ISR 2, Interrupt 3 occurs, so ISR 2 is followed by ISR 3. The background code stops execution for the total time it takes ISR 1, ISR 2 and ISR 3 to execute. Note that the interrupts do not have to occur simultaneously to stack up and appear the same time as the non-ISR code.
Such situations also lead to a sixth rule to minimize the impact of the execution time on the rest of the system: Keep ISRs as short and simple as possible.
For example, in a system that processes commands from a host PC via a serial port, don't put the command processor in the ISR that services the UART receive interrupts. Instead, let the ISR buffer the incoming data in a software FIFO and let the background code perform the command processing.
Other problems to look out for are related to interrupt latency, which may be loosely defined as the time from when an interrupt occurs until it is serviced. This time can vary for a number of reasons. First, the CPU will always finish the current instruction before servicing an interrupt and some instructions can take longer than others. The CPU may be executing a sequence of instructions protected by a disable/enable pair. Second, the CPU may be executing another ISR, which often has interrupts disabled.
Other interrupt problems that have to be considered but that are not related to timing concerns are situations such as stack overflow, caused by having insufficient stack space to support both interrupts and normal program execution. Another potential problem is subroutines shared between ISR and non-ISR code that are not re-entrant.
Finally, don't get into the trap of ignoring symptoms in favor of a theory. For example, if interrupts seem to stop being serviced for a few seconds, you might theorize that the software turns interrupts off and doesn't turn them back on. However, if you notice that the heartbeat LED, which is controlled by a timer ISR, keeps blinking, then you need a new theory.