|
||||
September 1, 1997 Don't
interrupt your computer: Clive "Max" Maxfield, Intergraph Computer Systems Ideally, you want to create a program that can concentrate on the task for which it was intended without interruptions from the outside world. However, when an external situation meriting action does arise, then the computer's response should be fast and furious. Suppose that you've just taken possession of a brand-new car equipped with an onboard computer, whose tasks include closing the windows when you instruct it to do so and activating the air bags in the event of a crash. Now, assume that you're merrily cruising down the highway and you flick the "Close Window" button, which causes the computer to enter a loop saying, "Is the window closed yet? If not, I'll keep on closing it." Suddenly, as if from nowhere, a herd of rampaging buffalo appears! Swerving to avoid them, you rocket off the road, screech across a farmyard, and collide rather forcibly with an extremely large pile of manure. It's fortunate indeed that you're wearing your seat belt, because your air bags sadly fail to make an appearance: Your computer is still looping around saying, "Is the window closed yet?" Thanking your lucky stars, you reach for the steaming-hot coffee that you recently acquired from a well-known purveyor of fast foods. But at the same moment that you raise the coffee to your lips, the window finishes closing, and the computer finally gets around to check what's happening in the outside world. Realizing that there's a problem, the computer immediately activates the air bags, and you unexpectedly find yourself taking a somewhat larger gulp of coffee than you intended. You're well on the way to having one of those days. The point is that, although this scenario may be less than common, it can be tricky to ensure that a computer becomes aware of external events quickly enough to handle them appropriately. Using a polling strategy Assume that you have a rudimentary QWERTY-keyboard device that you've plugged into one of your computer's 8-bit input ports. This keyboard contains an 8-bit latch that stores the ASCII code associated with whichever key you last pressed. Also, assume that the act of reading from the keyboard automatically clears the latch to contain a null code of $00, where "$" indicates a hexadecimal value. Now, assume that you create a simple program to loop around reading characters from the QWERTY keyboard and writing them to an output display. Further assume that, while performing this task, you also want your computer to act as a burglar alarm that monitors the state of a switch connected to the front door of your house. Opening the door causes the switch to close, in which case you want the computer to respond by ringing a bell.
Your program loops around reading from the input port connected to the keyboard until it sees a nonzero value indicating that you have pressed a key (Figure 2a). When the program detects a nonzero value, the program writes that value to the display and then returns to look for the next key. The act of reading a value from this input port automatically clears the latch in the keyboard. A first-pass solution might simply add the test to read the alarm switch onto the end of your original program, but this solution is a bad idea because the program checks the state of the switch only after you activate a key (Figure 2b). So, while you ponder which key to press, a burglar could have entered your abode and be creeping up behind you. Imagine instead that the following scenario occurs when you eventually press a key: The bell rings, and you leap to your feet thinking, "Don't panic! We've got a burglar! Don't panic!" You turn around, and there he is! (This may well be the time when you contemplate investing in a better alarm system.) As an alternative scenario, the burglar could slip into your house and close the door while you contemplate the keyboard. In this case, the alarm doesn't sound even after you press a key, because the door closes by the time the computer finally comes to look at it. So, now you've got a burglar roaming wild and free through your house while your computer is essentially saying, "Don't worry about a thing my little fruit bat, because the front door is safely closed." A key aspect of external signals, such as the switch that forms your burglar alarm, is that they're typically asynchronous. Thus, these signals can occur at any time and are not synchronized to the computer system's clock. Therefore, you usually have to latch such signals. In this scenario, you could place a latch between the switch and the port. The act of opening the door changes the state of the latch, which retains this new state even when someone again closes the door. Thus, when your program eventually manages to limp around to check the state of the door, the value in the latch tells it that the door either is open or was open. (You could also arrange the circuit so that the act of reading from this port would automatically reset the latch.) Unfortunately, even if you add a latch to your circuit, the program still does not warn you that the door is open until you press a key, which makes the circuit next to useless as a burglar alarm. The solution is to check for the state of the door every time you go around the loop that tests to see whether you have pressed a key (Figure 2c).
You now must consider the relative priority of the various signals. For example, you might decide that checking whether the house is afire takes precedence over testing to see whether someone presses a key. You have to prioritize all external signals and determine the order in which the computer should evaluate them. Another consideration is that your program contains only one simple loop, but this loop can be a portion of a program containing a multitude of loops and subloops. In this case, you'd probably bundle all of the switch and sensor tests into a subroutine and then call this subroutine at the appropriate point or points from within each loop. The end result is that, if you're not careful, you might spend more time thinking about when to call the tests for the external signals than you do creating the rest of the program. Also, if you add or remove any new switches or sensors, then you must reprioritize everything and update all of the programs that include these tests. Last, your programs might expend more effort checking the switches and sensors than they do performing their intended tasks. This approach is inefficient, especially when the external conditions occur infrequently; that is, how often does the house catch fire on an average day? Thus, you have a dilemma: You don't want your programs to spend most of their time checking for conditions that rarely transpire. On the other hand, when something important--such as a smoke detector's being activated--does arise, you want the computer to respond quickly and effectively. Interrupt-request input
When the IRQ enters its active state, this occurrence is stored in a latching circuit inside the CPU, thereby circumventing the problem of the IRQ's going inactive before the CPU manages to check it. You can program this interrupt latch in some CPUs to look for active-high (Logic 1) or active-low (Logic 0) signals, but many simply assume that the IRQ's active state is Logic 0. The CPU also contains the interrupt mask, a status flag that enables or disables interrupts that you can set or clear under program control. By default, the CPU powers up with the interrupt mask in its inactive state, which is typically a Logic 0. Thus, for the CPU to be able to "see" an IRQ, the programmer has to use a set-interrupt-mask instruction to place the mask in its active state. Similarly, a programmer subsequently wishing to prevent the CPU from responding to IRQs can use a clear-interrupt-mask instruction to return the mask to its inactive state. Whenever it completes a machine-code-level instruction, the CPU checks the state of the interrupt mask. If the mask is inactive, the CPU simply proceeds to the next instruction. However, if the mask is active, the CPU takes a peek inside the interrupt latch to determine whether an interrupt has been requested. When servicing an interrupt, the CPU must perform a sequence of tasks. It must at least push a copy of the current contents of the program counter followed by a copy of the contents of the status register onto the top of the stack. The CPU next places the interrupt mask into its inactive state, thereby preventing any subsequent activity on the IRQ input from confusing the issue. Some CPUs also push copies of one or more of the other internal registers, such as the accumulator and the index register, onto the stack, because the act of servicing the interrupt probably modifies the contents of these registers. If the CPU doesn't automatically perform this task, then the programmer must save the contents of any registers upon entering the interrupt-service routine (ISR).
The location in memory that the hard-wired address identifies contains the first byte of the interrupt-vector address, which, in turn, points to the first instruction in the ISR. Thus, the CPU effectively uses its hard-wired address to perform an unconditional jump using the indirect-addressing mode, which eventually leaves the CPU at the beginning of the ISR. You can store the interrupt vector and the ISR in either RAM or ROM, depending on the system's application. Once the ISR performs the necessary actions to handle the interrupt, you can terminate it using a return-from-interrupt (RFI) instruction, which is similar to a return-from-subroutine instruction, except that the RFI reloads the status register with whatever byte is on top of the stack before loading the program counter with the return address from the stack. Also, if the CPU automatically pushes the contents of any other registers onto the stack following an IRQ, then the CPU also restores these registers from the stack before loading the program counter with the return address. One advantage of this interrupt strategy is that the ISR is largely distinct from the main program, which, therefore, facilitates the ISR's development and maintenance. Also, you can design your program to concentrate on a task without explicitly monitoring what's occurring in the outside world. When an external event occurs that requires attention, the CPU automatically hands control to the ISR, and, upon handling the interrupt, the ISR returns control to the main program, which picks up where it left off. Nonmaskable interrupts In addition to the IRQ input, many processors also sport a nonmaskable interrupt (NMI), which has its own latch within the CPU. An active event on the NMI always causes the CPU to respond, regardless of the state of the interrupt mask, and the NMI, therefore, has higher priority than an IRQ. The system responds to an NMI in much the same way that it handles an IRQ; the only difference is that the NMI has its own hard-wired address inside the CPU. This new hard-wired address points to a separate interrupt vector in the system's memory, and this second interrupt vector points to its own ISR. You typically use the NMI in mission-critical circumstances. For example, you might decide that an alert from a smoke detector takes priority over a warning that the Jacuzzi is overflowing. Now, assume that your Jacuzzi does begin to overflow and that the resulting deluge shorts out a power point and starts a fire. Upon "seeing" the IRQ generated by the Jacuzzi, the CPU immediately leaps into action and starts performing the appropriate ISR. But one of the first things the CPU does when it responds to an IRQ is to disable the interrupt mask, thereby hiding any other IRQs. So, if the smoke detector were to also generate an IRQ, the computer wouldn't see it because the CPU would be too busy working on the Jacuzzi problem. However, if the smoke detector generates an NMI, that NMI takes precedence over anything else that the computer is doing, including servicing an IRQ. (If the CPU has NMI input but you're not using it, then you can just "tie it off" to its inactive state.) Software interrupts Some CPU instruction sets include software interrupts (SWIs), instructions that trigger an interrupt from within the program. If the CPU supports both IRQs and NMIs, an equivalent number of SWI instructions exists for each type. SWIs have many uses, not the least of which is that they allow programmers to test the ISRs without physically triggering an external interrupt (such as burning the house down). Also, these instructions may find application in debugging. For example, you could create an ISR whose only task was to display the current values of the CPU's registers on an output device, such as a memory-mapped display. You could then insert SWI instructions at strategic locations within your program, such that whenever the CPU sees one of these instructions it leaps to the ISR, displays the current contents of the registers, and then returns to the program. (This is one way to implement source-level debuggers.)
Once you assemble this code into machine code, it causes the CPU to continuously perform unconditional jumps back to itself (Figure 6b). In this example, assume that the "dummy" label occurs at address $4F05, so the resulting machine code contains a $C1 operation code at $4F05. (Also, assume that $C1 equates to an unconditional-jump instruction.) The two operand bytes, $4F and $05, cause the CPU to return to address $4F05, whence it reads the $C1 operating code again, and so forth. The only way to break out of this loop is to call an interrupt or to reset the computer. Unfortunately, when you call an interrupt, the CPU automatically pushes the return address, $4F05, onto the top of the stack. So, after completing its task, the ISR returns control to address $4F05, and the CPU returns to mindlessly looping around, which means that it can never proceed to the instruction following the loop. You could address this problem by causing the ISR to finagle the return address on the top of the stack, but this approach is not aesthetically pleasing. Instead, you should replace your dummy loop with a HALT instruction, which uses the implied addressing mode and occupies only 1 byte of memory. Upon seeing a HALT instruction, the CPU stops executing the program and commences to generate internal no-operation instructions. Once again, the only way to break out of the HALT is to call an interrupt or to reset the computer. However, while reading the HALT operation code, the CPU automatically increments the program counter to point to the next instruction. Thus, when an interrupt occurs, the return address on the stack is for the instruction following the HALT. Interrupt-acknowledge output The sources of the IRQs so far have been simple devices, such as switches and sensors. In some circumstances, however, the IRQ may come from a more sophisticated device, and this device may have more than a passing interest in knowing when the CPU begins to respond to its request. Thus, CPUs typically have interrupt-acknowledge (IACK) outputs. Assume that all control signals are active-low, which is often the case. When an external device places a Logic 0 value on either the IRQ or the NMI inputs, the CPU responds by driving a Logic 0 onto the IACK output. This action informs the external device that the CPU has heard and is responding to the external device's plea for attention. After servicing the IRQ, the CPU returns the IACK output to a Logic 1, which tells the external devices that the CPU is now willing and able to accept a new interrupt. Interrupt-driven input and output One technique for handling devices such as a QWERTY keyboard is to use a polling strategy. For example, you can create a program that loops around reading the port connected to the keyboard until you press a key. It then passes the code for this key to an output device and returns to looping around waiting for the next key. However, a modern computer can execute millions of instructions a second, which means that, most of the time, your CPU is just hanging around twiddling its thumbs. This approach is acceptable if you want to perform simple tasks, such as copying characters from the keyboard to the display. However, instead of recklessly squandering all of this processing power, you might employ it in a gainful way. For example, while the CPU is waiting for you to press the next key, you could be using the CPU to perform some useful task, such as reformatting the contents of the display to align the words. However, if you create a routine to reformat the screen, the routine must continue to check the keyboard to see whether you've pressed another key. A better approach would be to leave the reformatting routine free to perform its machinations and interrupt the routine when you press a key. You could easily achieve this goal by equipping your keyboard with the ability to generate an interrupt whenever you press a key. In this scenario, the CPU can perform a task without monitoring the keyboard. When you press a key, the keyboard issues an IRQ, which would cause the CPU to hand control to the associated ISR. In turn, this routine would read the input port connected to the keyboard, copy the resulting value to the display, and then return control to the main program. Also, upon responding to the IRQ, the CPU would activate its IACK output, thereby informing the keyboard that things were on the move. When the ISR terminates, the CPU returns the IACK to its inactive state, which would inform the keyboard that it is now free to clear its internal latch. Handling multiple IRQ signals
The idea is to modify each of the external devices so that when they aren't calling for an interrupt, they effectively disconnect themselves from the IRQ signal. The pullup resistor then forces the IRQ signal to a weak Logic 1 value (its inactive state). However, if one of the devices wants to call an interrupt, it can overpower the pullup resistor by driving a strong Logic 0 onto the wire. Also, you can connect the IACK output from the CPU to both of the external devices, allowing them to tell the CPU which of them called an interrupt. The advantage of this scheme is that it's relatively easy to hook additional devices to the IRQ signal. The disadvantage is that when the CPU receives an IRQ, it doesn't know which of the devices called the IRQ. So, the ISR uses a polling strategy to determine which device wants the CPU's attention. An alternative technique for handling multiple interrupts is to simply equip the CPU with more IRQ inputs, each with its own interrupt latch, hard-wired address, interrupt vector, and ISR. In this case, the CPU's status register now contains interrupt-mask flags for each of the IRQ inputs. Priority handling
If you assume that your CPU has a 16-bit address bus and an 8-bit data word, then each interrupt vector occupies 2 bytes in memory, which means that the CPU must multiply by two the value from the priority encoder before adding the value to the hard-wired address. (The CPU can easily perform this task by automatically shifting the value left by 1 bit.) For example, assume that the base address of the interrupt-vector table resides at address $9000, which is therefore the value that the CPU's hard-wired address represents. Thus, the first interrupt vector occupies addresses $9000 and $9001, the second interrupt vector occupies $9002 and $9003, the third occupies $9004 and $9005, and so forth. Now, assume that the external device connected to the XIRQ[2] signal requests an interrupt, which causes the priority encoder to activate the main IRQ signal to the CPU. After completing its instruction, the CPU pushes the values in the program counter and status register onto the stack and then reads a value from the priority encoder. As XIRQ[2] was the signal that called the interrupt, the code generated by the encoder is $02 (00000010 in binary). The CPU multiplies this value by two (by shifting it 1 bit to the left) to generate $04 (00000100 in binary). The CPU then adds this value to the hard-wired address to generate a new address, $9004, which the CPU loads into the program counter to point to the appropriate interrupt vector. Finally, the CPU performs an unconditional jump to address $9004 using the indirect-addressing mode, which causes the CPU to end up at the first instruction in the relevant ISR. Now, consider what happens if the priority encoder receives multiple requests on its 16 XIRQ[15:0] inputs, of which there are 216=65,536 potential combinations. Also, assume that, by default, the priority encoder considers XIRQ[0] to have a higher priority than XIRQ[1], which, in turn, has a higher priority than XIRQ[2], and so forth. Thus, if the priority encoder simultaneously receives IRQs on XIRQ[15], XIRQ[12], and XIRQ[9], the value the encoder eventually hands over to the CPU is the $09 (00001001 in binary) corresponding to XIRQ[9], because this input has the highest priority. Also, if the system is handling an interrupt when another, higher priority interrupt occurs, you can permit this new signal to interrupt the first. The CPU can write values to the priority encoder, because, in addition to acting like an input port, the encoder can also behave like an output port. One common scenario is that the priority encoder contains its own 16-bit interrupt-mask register (distinct from the interrupt mask in the CPU), thereby giving programmers the power to enable or disable the external IRQs individually. For example, if you load this interrupt-mask register to contain 0000 0100 0000 1001 in binary, then the priority encoder responds only to IRQs on the XIRQ[10], XIRQ[3], and XIRQ[0] signals. |
||||
|
||||
| EDN Access | Feedback | Table of Contents | |
||||
| Copyright © 1997 EDN Magazine, EDN Access. EDN is a registered trademark of Reed Properties Inc, used under license. EDN is published by Cahners Publishing Company, a unit of Reed Elsevier Inc. | ||||