Interrupts are how a processor responds to the world β a key press, a network packet, a timer tick. On RISC-V the interrupt story has two layers: a clean, minimal trap mechanism in the core ISA, and a set of interrupt controllers (CLINT, PLIC, and the modern AIA) that decide what reaches each hart. This guide untangles all three.

Traps: The Foundation
In RISC-V, interrupts (asynchronous, from external events) and exceptions (synchronous, like an illegal instruction or page fault) are handled by the same underlying machinery: the trap. When a trap fires, the hart:
- Saves the current PC into
mepc/sepc. - Records the reason in
mcause/scause. - Updates
mstatus/sstatus(disabling further interrupts, saving the previous privilege). - Jumps to the handler address in
mtvec/stvec.
Your handler runs, then executes mret/sret to restore state and resume. The relevant CSRs β mtvec, mcause, mepc, mstatus, mie, mip β are the same regardless of which interrupt controller delivered the signal. This clean trap model sits right alongside the privilege architecture.
The Three Interrupt Sources
RISC-V defines three standard interrupt causes per privilege level:
- Software interrupts β used for inter-processor interrupts (one hart waking another)
- Timer interrupts β fired when the machine timer reaches a comparison value
- External interrupts β from peripherals (UART, NIC, storage, GPIOβ¦)
The first two are core-local; the third comes from the outside world and needs routing. That split is exactly why RISC-V has two classic controllers.
The CLINT: Timer and Software Interrupts
The CLINT (Core-Local Interruptor) is the simple one. Per hart it provides:
mtimeβ a free-running machine timer, andmtimecmpβ write a future value here and a timer interrupt fires whenmtimereaches it. This is the heartbeat behind the OS scheduler tick.msipβ a software-interrupt pending bit; writing it triggers an IPI to that hart.
The kernel does not poke the CLINT directly in M-mode; it asks the firmware via the SBI (sbi_set_timer, IPI calls), which keeps the OS portable across platforms.
The PLIC: External Device Interrupts
The PLIC (Platform-Level Interrupt Controller) handles everything external. It:
- Accepts interrupt lines from many devices
- Assigns each a priority and a threshold per hart
- Lets a hart claim the highest-priority pending interrupt and complete it when done
The classic claim/complete handshake:
uint32_t irq = *PLIC_CLAIM; // read = claim highest-priority pending IRQ
handle_device(irq); // service the device
*PLIC_CLAIM = irq; // write back = signal completionThe PLIC is simple and has served well, but it has limits β notably no native message-signaled interrupts and weak virtualization support. That is what the AIA fixes.
The AIA: The Modern Architecture
The Advanced Interrupt Architecture (AIA) is the modern replacement designed for high-core-count servers and virtualization β increasingly important as RISC-V scales into the datacenter. It has two main pieces:
- IMSIC (Incoming MSI Controller) β per-hart, receives message-signaled interrupts (MSIs). Instead of a dedicated wire, a device signals an interrupt by writing to a memory address β the same model PCIe uses. This scales far better to many cores and many devices.
- APLIC (Advanced PLIC) β handles traditional wired interrupts and can convert them into MSIs for the IMSIC.
Crucially, the AIA was designed with the hypervisor extension in mind, so interrupts can be delivered directly to guest virtual machines with low overhead β essential for cloud and confidential computing workloads.
CLINT/PLIC vs AIA: Which Will You See?
- Embedded and microcontroller parts typically use CLINT + PLIC (or a vendor CLIC variant for fast vectored interrupts). Simple, small, perfect for the job.
- Application-class and server SoCs are moving to the AIA (IMSIC/APLIC) for MSI support and virtualization.
Both deliver into the same core trap machinery β only the routing layer differs, which is why the same kernel can support both with the right drivers.
A Minimal Handler Sketch
In M-mode, a trap handler reads mcause to decide what happened:
trap_handler:
csrr t0, mcause
bltz t0, interrupt # high bit set => interrupt, not exception
# ... handle synchronous exception ...
interrupt:
# mask off the interrupt code in t0 and dispatch:
# timer -> reschedule
# software -> handle IPI
# external -> claim from PLIC/IMSIC and service device
mretYou can single-step this logic in QEMU and a debugger to watch the CSRs change as traps fire.
The Bottom Line
RISC-V keeps the trap mechanism beautifully simple and pushes complexity out to interrupt controllers. The CLINT handles core-local timer and software interrupts; the PLIC routes external device interrupts; and the modern AIA (IMSIC + APLIC) brings MSIs and virtualization for servers. Learn the trap CSRs once and the rest is just understanding which controller is delivering the signal β knowledge that pays off whether you are writing firmware, an RTOS, or a kernel driver.
Part of my RISC-V series. See also the boot flow and debugging RISC-V.



