--- name: schunix-driver-infrastructure description: "Dynamic IRQ registration, a DMA arena and PCI capabilities exist now; PCI interrupts are level/active-low and the old ioapicenable only did edge/high" metadata: node_type: memory type: project originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225 modified: 2026-08-04T22:21:01.983Z --- **Built 2026-08-04**, before USB, because xHCI, any NIC and virtio-gpu all need the same three things and none of them existed. - **`kernel/dev/irq.c` + `include/irq.h`** — `irq_register(irq, fn, arg, name, level_low)`. `trap.c` used to be a switch listing every interrupt by name, which cannot work for a PCI device: its line comes out of config space at probe time, so there is no constant to write a `case` label for. Chains of up to 4 handlers per line, because PCI INTx pins are shared and a handler must recognise that its own device is idle and return. Registration publishes an entry then bumps `n` with a barrier, so dispatch needs no lock. `/proc/interrupts` (minor 12) shows counts and trigger mode. - **`kernel/core/dma.c` + `include/dma.h`** — `dma_alloc(size, align)` / `dma_free(p, size)` (Linux's contract: caller remembers the size). Carved off the top of physical memory in `main.c` *before* `kinit1`, because kalloc's free list makes no contiguity promise. x86 PCI is cache coherent so no PAT games are needed — but ordering is still your problem. - **`pci_find_cap()` and `pci_msi_enable()`** in `kernel/dev/pci.c`. **Four traps worth remembering:** 1. **PCI interrupts are level-triggered, active low.** `ioapicenable()` hardcoded edge/active-high, which is right for every ISA-era device and wrong for every PCI one — and it fails *silently*: you get exactly one interrupt, then the device holds the line asserted, no further edge ever comes, and it is never heard from again. `ioapicroute(irq, cpu, level_low)` is the real call now. 2. **`IRQ_ERROR` (19) and `IRQ_SPURIOUS` (31) are LAPIC vectors that xv6 numbered as if they were IOAPIC pins**, so `T_IRQ0+19` is already taken. A PCI device routed to pin 19 would collide. `irq_register` refuses them. 3. **`cprintf` has no field widths.** `%04x` is not a directive to it: it prints `%` and `0` literally, consumes no argument, and every argument after it lands in the wrong place. It understands only `%d %u %l %x %p %b %s %%`. 4. **Unaligned MMIO reads return garbage.** Reading a uint32 at BAR offset 2 to get xHCI's HCIVERSION returned zero. Read the aligned dword and shift. 5. **Never let a device raise an interrupt before its line is claimed.** This one cost real time. xHCI transfers completed, completion events sat in the event ring, `USBSTS.EINT` and `IMAN.IP` were both set -- and the interrupt count stayed at zero. Cause: the controller asserted INTx during enumeration, while the IOAPIC entry was still masked. **An interrupt that arrives at a masked pin is dropped, not deferred** (the 82093AA manual says so, and QEMU implements it), and INTx is a *level* -- so the line then sits high forever with no further transition for anyone to notice. Unmasking afterwards finds nothing pending. The fix is ordering: enumerate with controller interrupts off, then `irq_register`, then clear the device's pending status so it drives the line low, and only then enable. The next event is a genuine low-to-high transition. Verify with `pwsh -File tools/bootcheck.ps1 -Usb`. See [[schunix-usb-xhci]], [[schunix-pci-drivers]].