Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The PIC — Programmable Interrupt Controller

The 8259 PIC is one of the most important chips in the x86 architecture. Without it, x86 would not be an interrupt-driven architecture.

What It Does

The PIC manages hardware interrupts and routes them to the CPU in order. When a keyboard registers a key hit, it sends a pulse along its interrupt line (IRQ1) to the PIC. The PIC translates the IRQ into a system interrupt number and tells the CPU to stop what it’s doing and handle it.

Without a PIC, you’d have to constantly poll every device to see if it wants to do anything. With the PIC, devices come to you when they’re ready — the CPU can sleep between events.

Two PICs: Master and Slave

Modern x86 systems use two cascaded 8259 PICs:

  • PIC1 (Master) — Handles IRQ0–7
  • PIC2 (Slave) — Handles IRQ8–15, connected to PIC1 via IRQ2

This gives 15 usable IRQs (IRQ2 is used for the cascade connection, so hardware that would have used IRQ2 is wired to IRQ9 instead).

The Remapping Problem

The BIOS maps hardware IRQs to interrupt numbers 8–15 by default. But the CPU reserves interrupts 0–31 for its own exceptions. This creates a conflict:

Default MappingProblem
IRQ0 (Timer) → Interrupt 8Interrupt 8 is “Double Fault”
IRQ1 (Keyboard) → Interrupt 9Interrupt 9 is “Coprocessor Segment Overrun”

When the keyboard fires at interrupt 9, the CPU thinks it’s a CPU exception. Bad things happen.

Solution: Remap the PIC to interrupts 32–47.

IRQ0 (Timer)    → Interrupt 32   ✓ No conflict
IRQ1 (Keyboard) → Interrupt 33   ✓ No conflict
...
IRQ15           → Interrupt 47   ✓ No conflict

Remapping the PIC

The PIC initialisation sequence sends four “Initialisation Command Words” (ICWs):

#![allow(unused)]
fn main() {
pub fn remap() {
    unsafe {
        // ICW1: Start initialisation
        outb(PIC1_COMMAND, ICW1_INIT);  // 0x11
        io_wait();
        outb(PIC2_COMMAND, ICW1_INIT);
        io_wait();

        // ICW2: Set vector offsets
        outb(PIC1_DATA, 32);  // IRQ0-7 → INT 32-39
        io_wait();
        outb(PIC2_DATA, 40);  // IRQ8-15 → INT 40-47
        io_wait();

        // ICW3: Tell PICs about each other
        outb(PIC1_DATA, 4);   // PIC2 is at IRQ2 (bitmask: 00000100)
        io_wait();
        outb(PIC2_DATA, 2);   // PIC2's cascade identity
        io_wait();

        // ICW4: Set 8086 mode
        outb(PIC1_DATA, ICW4_8086);  // 0x01
        io_wait();
        outb(PIC2_DATA, ICW4_8086);
        io_wait();

        // Mask all IRQs except keyboard (IRQ1)
        outb(PIC1_DATA, 0xFD);  // 11111101 — only IRQ1 enabled
        io_wait();
        outb(PIC2_DATA, 0xFF);  // All slave IRQs disabled
        io_wait();
    }
}
}

What io_wait() Does

#![allow(unused)]
fn main() {
unsafe fn io_wait() {
    outb(0x80, 0);  // Write to an unused port
}
}

On old hardware, the PIC needs a small delay to process each command. Writing to port 0x80 (unused during normal operation, used by BIOS for POST codes) introduces a ~1–4 microsecond delay. The Linux kernel uses the same technique.

The Mask: 0xFD

0xFD = 11111101 binary
       ││││││└─ IRQ0 (Timer) = 1 (DISABLED)
       │││││└── IRQ1 (Keyboard) = 0 (ENABLED)  ← The only one we want
       ││││└─── IRQ2 (Cascade) = 1 (DISABLED)
       ...

Disabling the timer is important — if the timer fires with no handler installed, it triple-faults and reboots the machine. That was a real bug.

End of Interrupt (EOI)

After handling an interrupt, you must tell the PIC you’re done. Otherwise it won’t send any more interrupts.

#![allow(unused)]
fn main() {
pub fn send_eoi(irq: u8) {
    unsafe {
        if irq >= 8 {
            outb(PIC2_COMMAND, PIC_EOI);  // Slave PIC acknowledgement
        }
        outb(PIC1_COMMAND, PIC_EOI);      // Master PIC acknowledgement
    }
}
}

PIC_EOI = 0x20 — The “End of Interrupt” command code.

For IRQs 8–15 (from the slave PIC), you must send EOI to both PICs — the slave PIC, and the master PIC that received the cascade signal.

Interrupt Mask Register

Each PIC has an 8-bit Interrupt Mask Register (IMR). A bit set to 1 masks (disables) that IRQ; 0 enables it.

#![allow(unused)]
fn main() {
unsafe fn irq_set_mask(irq_line: u8) {
    let (port, line) = if irq_line < 8 {
        (PIC1_DATA, irq_line)
    } else {
        (PIC2_DATA, irq_line - 8)
    };
    let value = inb(port) | (1 << line);
    outb(port, value);
}

unsafe fn irq_clear_mask(irq_line: u8) {
    let (port, line) = if irq_line < 8 {
        (PIC1_DATA, irq_line)
    } else {
        (PIC2_DATA, irq_line - 8)
    };
    let value = inb(port) & !(1 << line);
    outb(port, value);
}
}

Spurious IRQs

Occasionally the PIC will send a spurious IRQ — interrupt 7 (master) or interrupt 15 (slave) — when there’s no actual interrupt. This happens due to electrical noise or timing issues.

The correct handling: check the In-Service Register (ISR). If the bit for that IRQ isn’t set, it’s spurious — acknowledge it but don’t do any real processing. For spurious IRQ15 specifically, send EOI to the master PIC (because the master didn’t know it was spurious) but not the slave.

In our implementation, the default interrupt handler catches these without crashing the kernel.