# Device interrupts CPU exceptions ([interrupts.md](interrupts.md)) are the kernel reacting to its own mistakes. **Device interrupts** are the opposite: hardware asking for attention — a timer firing, a key pressed, a packet arriving. They share the IDT, but differ in one fundamental way: an exception here is terminal (we report and halt), while a device interrupt is *handled and returned from*, so the interrupted code resumes as if nothing happened. This is danos's first code that takes an interrupt and comes back — the same mechanism a scheduler will later use to preempt tasks. The first device we bring up is the **timer**, because it's the simplest: it lives entirely on the CPU's local interrupt controller, needing no external routing. It's all x86_64-specific, behind the [arch](arch.md) boundary. ## The APIC, not the PIC Interrupt delivery on modern x86 goes through the **APIC**, not the legacy 8259 PIC. There are two halves; we only need one so far: - The **Local APIC** (per-CPU, memory-mapped at physical `0xFEE00000`) handles the CPU's own timer and receives interrupts routed to it. `src/arch/x86_64/apic.zig`. - The **IO-APIC** routes *external* device lines (keyboard, etc.) to LAPIC vectors. Not needed for the timer — it'll arrive with the keyboard. The old PIC has to be dealt with first, though: left alone it would deliver interrupts on vectors `0x08-0x0F`, which **collide with the CPU exception vectors** — a spurious IRQ would look like a double fault. So `init` remaps the PIC's vectors to `0x20-0x2F` and masks every line, taking it out of the picture. Then the LAPIC is enabled in two places: the `IA32_APIC_BASE` MSR's global-enable bit, and the LAPIC's own spurious-vector register (bit 8 = software enable). The spurious vector is `0x2F` — low nibble `F` by convention, and inside our gate range so a stray spurious interrupt lands on a valid no-op. ## The timer The LAPIC timer is three register writes (`initTimer`): a divide setting, then the LVT-timer entry giving it a **vector** (32) and **periodic** mode, then an initial count that becomes the reload value. From then on it fires vector 32 repeatedly, on its own, forever. > The count isn't calibrated to real time yet — the tick *rate* is arbitrary > (bus-clock dependent). Turning it into a known frequency (say 100 Hz) needs a > reference clock to measure against (the PIT, HPET, or the TSC). That's a later > step; for now it just needs to tick. ## Two kinds of vector, one dispatch The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every gate still funnels through the same stub tail (`isr_common`), which calls one dispatcher that branches on the vector (`interruptDispatch` in `idt.zig`): ```zig if (state.vector < 32) { on_fault(state); // exception: report and halt (never returns) } else if (handlers[state.vector]) |handler| { handler(); // device: run the registered handler apic.eoi(); // ...acknowledge the LAPIC } // else: spurious/unhandled — deliberately no EOI ``` Two things make device interrupts *return* where exceptions don't: 1. **The handler returns.** The timer handler just bumps a tick counter. Control flows back to `isr_common`, which restores every register it saved and executes `iretq` — resuming the interrupted instruction exactly. (This is why the stub saves *all* the general registers.) 2. **End-of-interrupt.** After handling, we write the LAPIC's EOI register. Miss this and the LAPIC thinks we're still busy and never delivers the next interrupt. It's the single most common "my timer fired once and stopped" bug. A device handler is a plain `fn () void` — a timer or keyboard handler doesn't need the interrupted registers. (Note: the stubs don't save the SSE/vector registers, so a handler must not use them; ours don't.) ## Turning them on Exceptions can't be masked, which is why they worked all along. Maskable device interrupts don't fire until the CPU's interrupt flag is set — so the final step is `sti` (`arch.enableInterrupts()`), after the APIC and timer are configured. From that instant the kernel has a heartbeat, and its idle `hlt` loop ([halting.md](halting.md)) wakes on every tick and dozes off again. ## Verifying it The `timer` test (see [testing.md](testing.md)) is the proof that an interrupt both *fires* and *returns*: it records the tick count, busy-waits, and checks the count advanced on its own. ``` $ python3 test/qemu_test.py timer timer ... PASS (matched 'DANOS-TEST-RESULT: PASS') ``` If the APIC weren't enabled, or `sti` were missing, or EOI were forgotten, the count would stay put and the test would fail. That it advances — while the CPU was spinning in unrelated code — is the whole mechanism working end to end. ## What's next (not done here) - **The keyboard**: bring up the IO-APIC, route its IRQ to a vector, and read scancodes from the PS/2 controller — the first *input* device. - **A calibrated timer** at a known frequency, and a monotonic clock. - **Uncacheable MMIO**: the LAPIC page is currently mapped writeback-cacheable like the rest of the identity map. QEMU tolerates it, but real hardware wants MMIO marked uncacheable (via the page's cache bits or an MTRR). - **Preemption**: once there are tasks, the timer handler is where the scheduler decides to switch — the reason a *returning* interrupt matters.