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 Mapping | Problem |
|---|---|
| IRQ0 (Timer) → Interrupt 8 | Interrupt 8 is “Double Fault” |
| IRQ1 (Keyboard) → Interrupt 9 | Interrupt 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.