STRUCTURED PROGRAMMING

Chimney-Pipe Interruptions

Jeff Duntemann KG7JF

Mr. Horny is back. He announced his return in spectacular style one recent weekday night at 3 a.m. or so, by landing on the perforated metal spark-catcher cap that encloses the top of the master-bedroom fireplace chimney pipe, and proceeding to do what owls do for reasons best known to themselves: HOO-HOO! HOOOOOOOOOOOOOO!

It's not for nothing that owls can be heard a long way off, and a chimney pipe can do wonders in conducting sound waves. Carol and I sat bolt upright in bed, sure the Martians were invading. Mr. Byte sailed off his spot at the foot of the bed and ran to the fireplace, determined to defend his home from hostile aliens, and made his most fearsome noises right into the hearth until I hauled him bodily back to bed.

That was that. Sound amplifies both ways through a chimney pipe. We've heard Mr. Horny since then, at considerably greater distance.

Living Better Asynchronously

Sometimes I think I would define life as a series of interruptions, from owls and other things. We set up a sequential itinerary for ourselves, begin to pursue it, and then the phone rings. Or the oven timer beeps. Or the dog throws up on the brand new living room rug. We trudge through life, answering phones, burning roasts, and wiping up dog urp, thinking sequentially while struggling against the universe's insistence on operating asynchronously.

Should we expect our machines to operate any differently?

I sometimes see us, the structured programming gang, as living in a fool's paradise. We start our programs at the top, run them through to the bottom, and assume nothing untoward happens along the way. But in fact, the C and assembler crazies have almost totally masked reality for us: Interruptions are happening all the time, from clocks and disks and printers and modems and network controllers and numerous other things. The BIOS, operating system, and installable device drivers do their work well, so well that we can sometimes squint a little and forget that such things as machine interrupts even exist.

Nonetheless, they do. They're essential. I think, moreover, that we should understand how interrupts work, and be ready to write our own interrupt handlers when the occasion arises.

A Tap on the Shoulder

The nature of The Box That Follows a Plan is to begin at the top of a sequence of machine instructions and follow them sequentially to their end, making branches and jumps in a rational manner. An interrupt is nothing more than a tap on the CPU's shoulder, with a directive to hold that thought, and duck over here for a second to take care of something else right now.

Some interrupts happen at predictable times (the clock tick interrupt being the best example), but the real hallmark of interrupts is that they happen when they happen, generally on no schedule and without warning. I can sit here and stare at the screen doing nothing for as long as I choose. But at some point (at least if I ever expect to make a nickel writing again) I have to reach out and press a key. Bang! There's an interrupt. The machine must set aside what it's doing for a moment and go fetch the code for the key I just pressed. It does some necessary processing on that key (more than you might imagine, although that's another story) before putting a key code in the keyboard buffer and taking up its previous work once more.

Like everything else connected with the 86-family Intel CPU product line, interrupts have evolved over time. What I'm going to describe here are things as they exist in the 8086/8088 CPU itself, without getting into the enhancements specific to the 286, 386, and 486. Perhaps another time.

The 8086 has the machinery to handle as many as 256 different interrupts. A handful of them perform special services baked right into the CPU chip, and a few more serve the PC hardware and operating environment, but most lie simply unused. Each of those 256 different interrupts represent a possible "something else" for the CPU to do when it receives its tap on the shoulder. We'll come back to the machinery of the shoulder tap itself. It's easier to begin by understanding what happens when an interrupt is received by the CPU.

The Interrupt Vector Table

Interrupts are numbered from 0 to 255. Regardless of where an interrupt comes from, it has a number in that range. Down in the very lowest area of the 8086 memory address space is a table of 256 4-byte slots for containing addresses, one address for each of the 256 possible interrupts. This table is called the interrupt vector table, and is 1024 bytes in size, located in the very first 1024 bytes of memory. Most of the slots in the interrupt vector table are empty and consist of 4 bytes of zeros. A valid address in the table is called an interrupt vector.

The vectors in the table are full 32-bit addresses, consisting of a 16-bit segment and a 16-bit offset. The offset portion is first (lower) in memory followed by the segment portion. I've sketched out the order of the addresses and their component parts in Figure 1. You don't need to memorize these things; most of the time, you'll be dealing with interrupt vectors as indivisible wholes.

As I said, most of the slots in the interrupt vector table are zeroed out and considered empty. At power-up time and occasionally later, DOS, the BIOS, a driver, or an application will place a valid vector in the vector table. "Vector" really means "pointer," and that's a good way to conceptualize the vectors placed in the interrupt vector table. They are pointers to little code sequences located somewhere else in memory. These code sequences are called interrupt service routines (ISRs), and are the "something else" that the CPU must do when an interrupt occurs.

Something to keep in mind is that any interrupt service routine can always be located, no matter where it actually is in memory, simply by knowing the interrupt number it serves. The address of the ISR that serves interrupt 6 exists in segment 0, at an offset of 6 x 4, or 24 (hexadecimal $18). The ISR itself is not there, but the ISR's address is. The CPU simply has to multiply an interrupt's number by four (which is an easy thing for the CPU to do, since multiplies by powers of two are simply bit-shifts) and jump to the address it finds at the resulting offset from 0. It will then be executing the interrupt's ISR.

Hold Everything

From a height then, what happens when the CPU receives interrupt N is this: It saves the bare essentials of what it is currently executing, locates the address of interrupt N, and then branches to the code existing at that address. At that point it is executing the interrupt's ISR.

What gets saved, and how? Remarkably, the CPU only saves two things when an interrupt happens: The machine flags and the 32-bit instruction pointer. The machine flags comprise a 16-bit word containing information about actions in progress, such as whether the last arithmetic operation resulted in a carry or a borrow, whether the last operation forced the accumulator (AX) register to 0, and so on. In a sense, the flags retain the essential what of the CPU's previous work. Next, the CPU saves the where of its previous work, by saving the address of the instruction it was about to execute when the interrupt came in. This address consists of the Code Segment register (CS) and the Instruction Pointer register (IP). The CPU does not automatically save the contents of the machine registers like AX, DX, BP, or SI. If the registers are to be saved, the ISR itself must save them. Obviously, if the ISR leaves the registers alone, it needn't save them. However, if it intends to reuse them or otherwise change values that exist in them, it had better save them, and in most cases ISRs do save one or more registers that they intend to use.

The CPU saves what it saves by pushing it on the system stack. The stack is nothing more than an area of memory addressed by two registers, SS and SP. SS and SP are initially set up by DOS, and as Pascal programmers you should only change them in dire need. Altering the stack incorrectly (or unsuccessfully) is the fastest road I can think of to a Big Red Switch crash.

The stack is an interesting creature that I won't fully describe here. The essence of a stack is that it is a last-in, first-out mechanism. The last thing pushed onto the stack is the first thing popped off. In other words, things come off the stack in the reverse order that they go on. For example, the CPU pushes the flags on the stack first, followed by CS, and then IP. When it retrieves them later on, it will first pop IP, then CS, and finally the flags.

To recap: When interrupt N taps the CPU on the shoulder, the CPU first pushes the flags on the stack, then pushes CS, followed by IP. The CPU then calculates the address of interrupt vector N, reads the interrupt vector from low memory, and places the vector into CS and IP.

As a result, the next instruction the CPU fetches for execution is the first instruction of interrupt N's interrupt service routine. The CPU is then off and running on the interrupt.

Coming Home Again

As I mentioned earlier, if the ISR intends to use any of the registers, it must push their current values onto the stack, so it can restore those values before returning control to what the CPU was doing before the interrupt.

After saving any registers it intends to use, the ISR does what it must. As I'll say again and again, it had better be quick. Creating complex ISRs that take a long time to execute is asking for trouble. There are also special considerations you have to keep in mind when writing ISRs in order to stay out of various kinds of trouble. I'll get into these later on. (Can you say, "reentrancy?")

When the ISR is finished with its specific tasks, it must return control gracefully to whatever work was in progress when the interrupt happened. The advice, "pop whatever you push" is applicable here. Anything the ISR pushed onto the stack must be popped off again. If the ISR pushed three machine registers onto the stack, it had better pop three registers back off again, or you'll hear the crash in the next county.

The final switch from ISR back to ordinary application code is handled by a special machine instruction called an interrupt return instruction, or IRET. The IRET pops the IP value from the stack back into the CPU's internal instruction pointer register, pops the CS value back into the code segment register, and finally restores the prior state of the various machine flags by popping the flags' values from the stack into the flags themselves.

At this point (assuming the ISR didn't "trash" any registers or memory that the code-in-progress was using) things should be just as they were before the interrupt happened, and work (like life) goes on -- at least until the next interrupt.

Software Interrupts

There are some minor details that we'll come back to, but in general, all interrupts are handled pretty much that way. And so we return to the question of where interrupts actually come from; that is, who taps the CPU's shoulder to kick off an interrupt?

Interrupts can come from two different places: software and hardware. Software interrupts are intriguing and I'll spend some time on them in a future column. But quite briefly, you can kick off an interrupt just by using a special machine instruction created for that purpose. Executing an INT N instruction forces the CPU to go through the interrupt process just described for interrupt N.

Far trickier, but more useful in many ways are interrupts generated by the hardware. The CPU chip has a pin dedicated to interrupt generation. Ordinarily, this pin is held idle at a logic 0. A hardware gadget of some sort may be attached to the interrupt pin, and when that gadget even momentarily raises the level on the interrupt pin to logic 1, a hardware interrupt occurs, and once again the sequence described above happens.

Sharing a Pin

So conceptually, interrupts are pretty simple. You can almost consider them subroutines whose addresses can be found in a table at a predictable location, and for software interrupts that's pretty close to the whole truth.

Hardware interrupts, however, get complicated for this reason: There is only one general-purpose interrupt pin on the CPU chip. As soon as you want to connect more than one interrupt-capable peripheral to the PC, you have to consider how to keep the peripherals from fighting over that one pin.

It's not enough to put some sort of eight-input OR-gate on the interrupt pin and then give everybody an input to the OR-gate. That allows up to eight people to knock, but the CPU still has no way of knowing who's there. (Not to mention the problem of what to do when two or three people knock at once....) This problem requires another chip to solve, and that chip is the 8259 Programmable Interrupt Controller (PIC) device manufactured by Intel, National Semiconductor, and other firms. The 8259 has three jobs to do:

  1. It allows up to eight devices to access the CPU's interrupt pin and it tells the CPU which device is interrupting.
  2. It allows the programmer to "mask out" any of those eight interrupts, so that when desired, the masked interrupts will not be passed through to the CPU.
  3. It handles the problem of what to do when another interrupt request comes in while a prior interrupt request is still being serviced. Understand the three tasks performed by the 8259, and you've got PC interrupts in your hip pocket.

Those IRQ Numbers

At one point or another you've run into a serial port problem (no probablies about it; serial port problems are as common as corrupt congressmen) and had someone ask, "Well, is the port set up for IRQ3 or IRQ4?" Perhaps you peeked at the DIP switches and were able to report the truth, but you might also have wondered just what that meant.

The IRQ numbers are the identifiers of the eight inputs to the 8259 PIC chip. They run from IRQ0 to IRQ7, and they represent literal input pins on the physical 8259 chip as well as the names of signals passing through the chip.

It's important to remember that the IRQ numbers do not correspond to interrupt vector numbers. IRQ0, for example, does not make use of interrupt vector 0, nor does IRQ1 make use of vector 1, and so on. In truth, the IRQ interrupts are "mapped onto" interrupt vectors 8 through 15, where IRQ0 uses vector 8, IRQ1 uses vector 9, and so on. I've summarized the first 16 PC interrupt vectors in Table 1, including their memory addresses, applicable IRQ numbers, and standard uses, if any.

Table 1: The first 16 interrupt vectors and their uses

  Vector#   Vector offset from segment 0  Standard use
----------------------------------------------------------------
  0                   $0000               Divide by 0 (internal)
  1                   $0004               Single step (internal)
  2                   $0008               Non-Maskable Interrupt
  3                   $000C               Breakpoint interrupt
  4                   $0010               Divide overflow
  5                   $0014               Print screen
  6                   $0018               IBM Reserved
  7                   $001C               IBM Reserved
  8                   $0020               IRQ0 Timer tick
  9                   $0024               IRQ1 Keyboard
  A                   $0028               IRQ2 AT 8259 pass-through
  B                   $002C               IRQ3 COM2:
  C                   $0030               IRQ4 COM1:
  D                   $0034               IRQ5 Hard disk controller
  E                   $0038               IRQ6 Diskette controller
  F                   $003C               IRQ7 Parallel port

The first several interrupts are special-purpose in nature. Some of them are built into the CPU. A divide by 0 operation, for example, will automatically trigger an interrupt to vector 0. You don't have to code it up, and the interrupt pin on the CPU is not involved. If the DIV instruction microcode detects a divide by 0, it does what amounts to a software interrupt to vector 0.

The Non-Maskable Interrupt (NMI) uses vector 2. NMI has its own dedicated pin on the CPU, and generally is used to report catastrophic hardware failure. On those occasions when you've seen PARITY ERROR on your screen just before the system locked up, you've witnessed a nonmaskable interrupt in action, reporting a bad memory location somewhere. The NMI is not something programmers ordinarily mess with, so I won't describe it further.

Cascading Controllers

What I've described so far is pretty much the way things exist on the older PC and XT machines based on the 8088. Starting with the AT in 1984, however, IBM added a second 8259 PIC chip to the motherboard. This added eight interrupt lines to the system, for a total of 15. (One line is taken in connecting the two 8259 chips to one another, or there would be 16.)

The second 8259's output pin is connected to the IRQ2 input of the original 8259. This prevents IRQ2 from being used for any specific hardware device, but it adds the eight inputs from the second 8259. The second set of interrupt inputs are known as IRQ8-IRQ15. IRQ8 is used by the AT's real-time clock chip, and IRQ9 is used by local area network adapter boards. Most of the other IRQs are undedicated or reserved.

When an interrupt comes in from one of the second set of IRQs, the second 8259 enters an interrupt to IRQ2 of the first 8259. Then some additional protocols must be followed to inform the CPU which of the second set of IRQ's was the ultimate source of the interrupt. Yes, it does get hairy, but the second eight IRQs don't really involve serial communications in any way, and I won't be discussing them further.

Masking Out Interrupts

Apart from the NMI and two internal interrupts, all of the interrupts in the PC architecture are maskable, meaning that the CPU can be made to ignore them, even when an external hardware device attempts to trigger them.

The CPU can mask out interrupts generally through the use of the Interrupt Flag (IF) and the two machine instructions that toggle it. The STI instruction sets IF, and the CLI instruction clears IF. When IF is cleared, all maskable interrupts will be ignored by the CPU. (This includes software interrupts, but again, not the NMI.) IF is automatically cleared when an interrupt is recognized by the CPU to prevent a second interrupt from happening until the CPU is ready to deal with it. We'll come back to this shortly.

Masking out individual interrupts while allowing others to go through is done by way of the machinery inside the 8259 chip. Inside the 8259 is an 8-bit register cleverly named Operation Control Word 1, or OCW1 for short. Don't forget that, despite its name, OCW1 is a byte in size, and not a word. Each of the 8 bits in OCW1 masks or enables one of the eight IRQ interrupt signals controlled by the 8259. It's a simple relationship: Bit 0 controls IRQ0, bit 1 controls IRQ1, and so on, through all eight IRQs.

When a bit in OCW1 is 1, the corresponding interrupt is disabled. (We say "masked.") When a bit in OCW1 is 0, the corresponding interrupt is enabled, Enabling COM1: means setting the bit for IRQ4 to 0; enabling COM2: means setting the bit for IRQ3 to 0. (See Table 1 for IRQ numbers and what they do.)

Working With OCW1

OCW1 is a read/write register accessed through I/O port number $21. I've summarized the bit numbers, IRQ numbers, and mask values associated with OCW1 in Figure 2. One thing never to forget in programming OCW1 is that you can't just write a mask value to it. Writing $10 directly to OCW1 will disable IRQ4 -- and enable all other IRQs, regardless of whether they were enabled before!

You must make sure that what you write affects only the mask bits and thus IRQs that you wish to affect. This is best done by reading OCW1, ANDing or ORing the OCW1 contents with the mask bit you wish to change, and then writing the whole value back to OCW1. For example, to disable interrupts at IRQ4 (to turn off COM1: interrupts) you might use the Pascal statements in Example 1(a). The OR operator writes the single 1-bit in IRQ4Mask to OCW1 without affecting any of the other bits either way. Enabling interrupts at IRQ4 would be done as in Example 1(b).

Example 1: (a) Disabling interrupts at IRQ4; (b) enabling interrupts at IRQ4

  (a)  OCW1 := $21;
       IRQ4Mask := $10;

       Port [OCW1] := Port [OCW1] OR IRQ4Mask;

  (b)  Port [OCW1] := Port [OCW1] AND (NOT IRQ4Mask);

Note that you must first invert the mask value via NOT. The idea in enabling an interrupt is to force the OCW1 mask bit in question to 0, and the significant bit in the mask values are all 1-bits. The AND operator will then force the mask bit in question to a 0, since the significant bit in the inverted mask value is the only 0-bit in the inverted mask value.

Hold That Thought

Once again, the subject at hand far outweighs a single column's worth of magazine pages. There's a lot more to be said about the 8259 before we can write a simple interrupt-driven version of the POLLTERM program I presented a few columns back. I'd almost say that in regard to interrupt driven serial port I/O, the 8259 is a more significant challenge than the CPU itself.

More coming. Stay tuned.


Copyright © 1991, Dr. Dobb's Journal