Devices
Device trait
#![allow(unused)] fn main() { pub trait Device: Send { fn read(&mut self, offset: usize, size: usize) -> XResult<Word>; fn write(&mut self, offset: usize, size: usize, value: Word) -> XResult; fn tick(&mut self) {} fn irq_line(&self) -> bool { false } fn notify(&mut self, _irq_lines: u32) {} } }
Five methods. Default-no-op for tick, irq_line, notify — device
authors override only what they need.
Bus
#![allow(unused)] fn main() { pub struct Bus { ram: Ram, mmio: Vec<MmioRegion>, plic_idx: Option<usize>, } struct MmioRegion { name: &'static str, range: Range<usize>, dev: Box<dyn Device>, irq_source: u32, // 0 = no IRQ } }
Every access goes through Bus::read / Bus::write:
- Fast path — RAM. Static dispatch, no vtable. Typed-read bypass for aligned 1/2/4/8-byte accesses (Phase P6).
- Slow path — MMIO. Linear scan for the covering region, then
dispatch via
dyn Device.
tick() split
#![allow(unused)] fn main() { pub fn tick(&mut self) { // ... ACLINT every step (fast path) ... // ... UART + PLIC every 64 steps (slow path) ... } }
ACLINT fires on every step because the Mtimer deadline check is on the critical path. UART and PLIC tick less frequently — their state rarely changes per-instruction.
Inside ACLINT, the Mtimer deadline gate (Phase P3) short-circuits 99.99 % of checks:
#![allow(unused)] fn main() { if self.mtime < self.next_fire_mtime { return; } self.check_all(); // slow path only when a deadline has arrived }
IRQ collection
The Bus collects level-triggered IRQ lines:
#![allow(unused)] fn main() { let mut irq_lines: u32 = 0; for r in &mut self.mmio { r.dev.tick(); if r.irq_source > 0 && r.dev.irq_line() { irq_lines |= 1 << r.irq_source; } } if let Some(i) = self.plic_idx { self.mmio[i].dev.notify(irq_lines); } }
The PLIC is the only device whose notify is overridden — it
receives the full IRQ-line bitmap and evaluates MEIP/SEIP.