The PLIC (Platform-Level Interrupt Controller) is a hardware device that sits between all the I/O devices and the CPU harts. When a device like the UART or the virtio disk needs attention, it doesn’t signal the CPU directly — it tells the PLIC, and the PLIC decides which hart to deliver the interrupt to.
Like the UART, the PLIC is memory-mapped — you configure it by reading and writing to specific physical addresses starting at PLIC (0x0C000000).
plicinit
*(uint32*)(PLIC + UART0_IRQ*4) = 1;
*(uint32*)(PLIC + VIRTIO0_IRQ*4) = 1;This sets the priority of two interrupt sources — the UART and the virtio disk — to 1. Every device connected to the PLIC has a priority register at an offset based on its IRQ number. A priority of 0 means “disabled, don’t deliver this interrupt ever.” Setting it to 1 means “this interrupt is active.” xv6 doesn’t use different priority levels — everything is just 1. The only distinction that matters is zero (off) vs nonzero (on).
This is global setup, done once by hart 0. It tells the PLIC which devices exist and are worth listening to.
plicinithart
This is per-hart — each hart runs it independently.
*(uint32*)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);The PLIC has a separate enable bitmask for each hart. This tells the PLIC “this specific hart wants to receive interrupts from the UART and the virtio disk.” A hart that didn’t run this line would never get device interrupts, even though the devices have nonzero priority.
*(uint32*)PLIC_SPRIORITY(hart) = 0;This sets the hart’s priority threshold to 0. The PLIC only delivers interrupts with priority above the threshold. Since the threshold is 0 and device priorities are 1, everything gets through. If you set the threshold to 1, nothing would be delivered. It’s basically a per-hart “minimum priority filter.”
The two helper functions
plic_claim reads which IRQ is waiting to be handled on this hart. When the PLIC delivers an interrupt, the trap handler calls plic_claim to find out which device caused it — was it the UART (someone typed) or the virtio disk (a read/write completed)? Reading the claim register also tells the PLIC “I’m handling this one now, don’t send it to another hart.”
plic_complete writes the IRQ number back to the same register after the handler is done. This tells the PLIC “I’m finished, this device is allowed to interrupt again.” Without this, the device could only interrupt once and then go silent forever.
You can see both being used in devintr in the trap code:
int irq = plic_claim();
if(irq == UART0_IRQ) uartintr();
else if(irq == VIRTIO0_IRQ) virtio_disk_intr();
if(irq) plic_complete(irq);Claim, dispatch to the right handler, complete. That’s the full lifecycle of a device interrupt through the PLIC.
Why the split between plicinit and plicinithart
Same pattern as everything else in main(). plicinit is global — device priorities don’t change per hart. plicinithart is per-hart — each hart has its own enable mask and priority threshold in the PLIC hardware. The other harts call plicinithart after waking up so the PLIC knows to send interrupts to them too, not just hart 0.