X86 Assembly/Programmable Interrupt Controller
The original IBM PC contained a chip known as the Programmable Interrupt Controller to handle the incoming interrupt requests from the system, and to send them in an orderly fashion to the MPU for processing. The original interrupt controller was the 8259A chip, although modern computers will have a more recent variant. The most common replacement is the APIC (Advanced Programmable Interrupt Controller) which is essentially an extended version of the old PIC chip to maintain backwards compatibility. This page will cover the 8259A.
The function of the 8259A is actually relatively simple. Each PIC has 8 input lines, called Interrupt Requests (IRQ), numbered from 0 to 7. When one of these lines goes high, the PIC alerts the CPU and sends the appropriate interrupt number. This number is calculated by adding the IRQ number (0 to 7) to an internal "vector offset" value. The CPU uses this value to execute an appropriate Interrupt Service Routine. (For further information, see Advanced Interrupts).
Of course, it's not quite as simple as that, because each system has two PICS, a "master" and a "slave". So when the slave raises an interrupt, it's actually sent to the master, which sends that to the CPU. In this way, interrupts cascade and a processor can have 16 IRQ lines. Of these 16, one is needed for the two PIC chips to interface with each other, so the number of available IRQs is decreased to 15.
While cli and sti can be used to disable and enable all hardware interrupts, it's sometimes desirable to selectively disable interrupts from certain devices. For this purpose, PICs have an internal 8-bit register called the Interrupt Mask Register (IMR). The bits in this register determine which IRQs are passed on to the CPU. If an IRQ is raised but the corresponding bit in the IMR is set, it is ignored and nothing is sent to the CPU.
Of the 15 usable IRQs, some are universally associated with one type of device:
- IRQ 0 ‒ system timer
- IRQ 1 — keyboard controller
- IRQ 3 — serial port COM2
- IRQ 4 — serial port COM1
- IRQ 5 — line print terminal 2
- IRQ 6 — floppy controller
- IRQ 7 — line print terminal 1
- IRQ 8 — RTC timer
- IRQ 12 — mouse controller
- IRQ 13 — math co-processor
- IRQ 14 — ATA channel 1
- IRQ 15 — ATA channel 2
Each of the system's two PICs are assigned a command port and a data port:
Normally, reading from the data port returns the IMR register (see above), and writing to it sets the register. We can use this to set which IRQs should be ignored. For example, to disable IRQ 6 (floppy controller) from firing:
in ax, 0x21 or ax, (1 << 6) out 0x21, ax
In the same way, to disable IRQ 12 (mouse controller):
in ax, 0xA1 or ax, (1 << 4) ;IRQ 12 is actually IRQ 4 of PIC2 out 0xA1, ax
Another common task, often performed during the initialization of an operating system, is remapping the PICs. That is, changing their internal vector offsets, thereby altering the interrupt numbers they send. The initial vector offset of PIC1 is 8, so it raises interrupt numbers 8 to 15. Unfortunately, some of the low 32 interrupts are used by the CPU for exceptions (divide-by-zero, page fault, etc.), causing a conflict between hardware and software interrupts. The usual solution to this is remapping the PIC1 to start at 32, and often the PIC2 right after it at 40. This requires a complete restart of the PICs, but is not actually too difficult, requiring just eight
mov al, 0x11 out 0x20, al ;restart PIC1 out 0xA0, al ;restart PIC2 mov al, 0x20 out 0x21, al ;PIC1 now starts at 32 mov al, 0x28 out 0xA1, al ;PIC2 now starts at 40 mov al, 0x04 out 0x21, al ;setup cascading mov al, 0x02 out 0xA1, al mov al, 0x01 out 0x21, al out 0xA1, al ;done!