Architecture overview

The step loop

CPU::step is the per-instruction driver:

CPU::step()
  1. bus.tick()                   — ACLINT every step, UART/PLIC every 64
  2. sync_interrupts()             — merge irq_state → mip
  3. check_pending_interrupts()    — raise trap if priority/gating allows
  4. fetch → decode (icache) → execute
  5. retire + commit_trap()        — commit npc, enter trap vector if any

The loop owns the Bus directly — no Arc<Mutex<Bus>>. Field-level borrow splitting lets MMU and Bus be accessed simultaneously without locking. This is Phase P1 of the perf roadmap; see performance.md.

Dispatch diagram

                ┌──────────────────────────────────────────────┐
                │                 xdb::main                    │
                │  (monolithic under LTO + codegen-units = 1)  │
                └───────────────┬──────────────────────────────┘
                                │
                    ┌───────────▼────────────┐
                    │  CPU<Core, Bus>        │
                    │   ├── Core: CoreOps    │   ← arch-agnostic trait
                    │   └── Bus: owned       │
                    └───────────┬────────────┘
                                │
                    ┌───────────▼────────────┐
                    │  RVCore (CoreOps impl) │
                    │   ├── GPR / PC / NPC   │
                    │   ├── csr: CsrFile     │
                    │   ├── privilege        │
                    │   ├── mmu: Mmu         │
                    │   ├── pmp: Pmp         │
                    │   ├── icache           │
                    │   └── pending_trap     │
                    └───────────┬────────────┘
                                │
                    ┌───────────▼──────────────────────────┐
                    │  Bus                                 │
                    │   ├── Ram [0x8000_0000, 1 GiB]        │
                    │   ├── ACLINT [0x0200_0000]            │
                    │   ├── PLIC   [0x0C00_0000]            │
                    │   ├── UART   [0x1000_0000]            │
                    │   ├── VirtIO [0x1000_1000]            │
                    │   └── Test   [0x0010_0000] (test-only)│
                    └──────────────────────────────────────┘

Four-layer memory access

A guest load/store walks:

vaddr ─► align check ─► MMU.translate ─► paddr ─► PMP.check ─► Bus.access
                             │                         ▲
                             └── page walk:  pte_paddr ┘  (PMP checks PTE reads too)

Responsibility split (see ../spec/mm/SPEC.md for the canonical table):

LayerKnows aboutDoes NOT know about
BusPhysical addresses, device regionsVirtual addresses, privilege, traps, PMP
MmuPage tables, TLB, PTE bits, SUM / MXRTrap codes, PMP (receives &Pmp for walks)
PmpPhysical-address permissions, privilegeVirtual addresses, page tables
RVCoreOrchestrates: privilege, MPRV, trap mappingInternal device state

Lock-free IRQ delivery

Devices raise interrupts through IrqState — a shared Arc<AtomicU64> bitmap. Each bit maps to an mip hardware bit:

BitSource
1SSIP (ACLINT SSWI)
3MSIP (ACLINT MSWI)
7MTIP (ACLINT MTIMER)
9SEIP (PLIC context 1)
11MEIP (PLIC context 0)

sync_interrupts() merges this into the CPU's mip register at the top of each step. No locks, no downcasts.