chore: auto-commit after execute-task

GSD-Unit: M002/S03/T02
This commit is contained in:
John Doe
2026-05-03 17:52:43 -04:00
parent 1c3b6e85d0
commit 1bc7261f10
4 changed files with 838 additions and 44 deletions
+40
View File
@@ -1,8 +1,11 @@
use log::info;
use crate::cpu::UnicornCPU;
use crate::mmio::gic::{GicDistributor, GicV3};
use crate::mmio::MmioDevice;
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
pub const CORE_COUNT: usize = 8;
@@ -12,11 +15,18 @@ compile_error!("oboromi requires a 64-bit architecture to emulate 12GB of RAM.")
pub const MEMORY_SIZE: u64 = 12 * 1024 * 1024 * 1024;
pub const MEMORY_BASE: u64 = 0x0;
/// GICv3 MMIO base address (within the MMIO region)
pub const GIC_BASE: u64 = 0x10000000;
/// GICv3 MMIO region size (4MB)
pub const GIC_SIZE: u64 = 0x400000;
pub struct CpuManager {
pub cores: Vec<UnicornCPU>,
// Pin prevents reallocation from invalidating pointers
#[allow(dead_code)]
pub shared_memory: Pin<Box<[u8]>>,
/// Shared GIC distributor state for all cores
gic_dist: Option<Rc<RefCell<GicDistributor>>>,
}
impl CpuManager {
@@ -45,6 +55,7 @@ impl CpuManager {
Self {
cores,
shared_memory,
gic_dist: None,
}
}
@@ -100,4 +111,33 @@ impl CpuManager {
}
Some(self.cores[core_id].mmio_bus_ref())
}
/// Register the GICv3 interrupt controller on all cores.
///
/// Creates a shared distributor and gives each core its own GicV3 device
/// instance backed by the same distributor state.
///
/// Returns a clone of the shared distributor `Rc` for external
/// manipulation (e.g., triggering interrupts in tests).
pub fn register_gic(&mut self) -> Rc<RefCell<GicDistributor>> {
let dist = Rc::new(RefCell::new(GicDistributor::new(self.cores.len())));
self.gic_dist = Some(dist.clone());
let core_count = self.cores.len();
for (i, core) in self.cores.iter_mut().enumerate() {
let device = GicV3::new_with_shared_dist(core_count, dist.clone());
core.mmio_bus_mut().register_device("gicv3", GIC_BASE, GIC_SIZE, device);
info!(
"CpuManager: registered GICv3 MMIO device at {:#x}..{:#x} on core {}",
GIC_BASE, GIC_BASE + GIC_SIZE, i
);
}
dist
}
/// Get a reference to the shared GIC distributor, if registered.
pub fn gic(&self) -> Option<&Rc<RefCell<GicDistributor>>> {
self.gic_dist.as_ref()
}
}
+89
View File
@@ -4,6 +4,7 @@ use unicorn_engine::{Arch, Mode, Prot, RegisterARM64, Unicorn};
use crate::mmio::MmioBus;
use super::exception::ExceptionModule;
use crate::mmio::gic::GicDistributor;
/// Base address for the MMIO region (outside the normal 8MB RAM)
pub const MMIO_BASE: u64 = 0x10000000;
@@ -23,6 +24,8 @@ pub struct UnicornCPU {
/// Exception module for ARM64 exception level management (SVC/SMC handling).
/// Shared with hook closures via Rc.
exception: Rc<RefCell<ExceptionModule>>,
/// Shared GIC distributor reference for IRQ delivery peek.
gic_dist: Option<Rc<RefCell<GicDistributor>>>,
pub core_id: u32,
}
@@ -80,6 +83,7 @@ impl UnicornCPU {
emu: RefCell::new(emu),
mmio_bus: bus,
exception,
gic_dist: None,
core_id: 0,
})
}
@@ -158,6 +162,7 @@ impl UnicornCPU {
emu: RefCell::new(emu),
mmio_bus: bus,
exception,
gic_dist: None,
core_id,
})
}
@@ -403,6 +408,90 @@ impl UnicornCPU {
pub fn vector_table(&self, el: u8) -> u64 {
self.exception.borrow().vector_table_for(self.core_id as usize, el)
}
/// Check for pending IRQs on this core and deliver the highest-priority one.
///
/// Returns the IRQ number delivered, or None if no qualifying interrupt.
///
/// Delivery sequence:
/// 1. Read GICC_IAR via MMIO (triggers acknowledge_irq on GicV3)
/// 2. If IAR == 1023 (spurious), return None
/// 3. Save current PSTATE to SPSR_EL1
/// 4. Save current PC to ELR_EL1
/// 5. Set PSTATE.CurrentEL to EL1
/// 6. Set PC to VBAR_EL1 + VEC_IRQ_OFFSET (0x480)
pub fn deliver_irq(&self) -> Option<u32> {
// Read GICC_IAR for this core's redistributor region via MMIO.
// IAR address = GIC base + GICR base offset + core_id * GICR_REGION_SIZE
// + GICC sub-region offset + GICC_IAR offset
let gicc_iar_addr = MMIO_BASE + 0x10000 // GICR_BASE_OFFSET
+ (self.core_id as u64) * 0x20000 // GICR_REGION_SIZE
+ 0x10000 // GICC sub-region within redistributor
+ 0x000C; // GICC_IAR
// Read through MMIO bus — triggers acknowledge_irq as a side effect
let iar = self.read_u32(gicc_iar_addr);
if iar == 1023 {
log::debug!(
"deliver_irq: core {} has no qualifying interrupt (spurious)",
self.core_id
);
return None;
}
let irq_id = iar;
// Save current state
let pc = self.get_pc();
let pstate = self.emu.borrow().reg_read(RegisterARM64::PSTATE).unwrap_or(0);
// Save PSTATE to SPSR_EL1 and PC to ELR_EL1 via exception module
let vbar = {
let mut exc = self.exception.borrow_mut();
exc.write_sys_reg(
"SPSR_EL1",
self.core_id as usize,
pstate,
&mut self.emu.borrow_mut(),
);
exc.write_sys_reg(
"ELR_EL1",
self.core_id as usize,
pc,
&mut self.emu.borrow_mut(),
);
// Update PSTATE.CurrentEL to EL1 (bits [3:2] = 0b01 << 2 = 0x4)
exc.write_sys_reg(
"PSTATE",
self.core_id as usize,
(pstate & !0xC) | 0x4,
&mut self.emu.borrow_mut(),
);
exc.read_sys_reg("VBAR_EL1", self.core_id as usize, &self.emu.borrow())
};
if vbar == 0 {
log::warn!(
"deliver_irq: core {} has no VBAR_EL1 configured",
self.core_id
);
return None;
}
// Jump to IRQ vector
let handler_addr = vbar + 0x480;
self.set_pc(handler_addr);
log::info!(
"GIC: delivered IRQ {} to core {}, jumping to {:#x}",
irq_id,
self.core_id,
handler_addr
);
Some(irq_id)
}
}
// remove Clone - sharing a CPU via clone is misleading
+149 -41
View File
@@ -1,5 +1,5 @@
use log::{debug, info, warn};
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use super::MmioDevice;
@@ -80,7 +80,7 @@ pub struct GicDistributor {
}
impl GicDistributor {
fn new(core_count: usize) -> Self {
pub fn new(core_count: usize) -> Self {
let mut ipriorityr = [0u8; NUM_INTERRUPTS];
// Initialize to lowest priority (0xFF) — all interrupts masked by default
ipriorityr.fill(0xFF);
@@ -238,6 +238,97 @@ impl GicDistributor {
}
}
}
// -----------------------------------------------------------------------
// External API methods (accessible via Rc<RefCell<GicDistributor>>)
// -----------------------------------------------------------------------
/// Trigger an SPI interrupt. Sets the pending bit for the given IRQ.
/// Only valid for SPIs (irq_id 32..1019).
pub fn trigger_interrupt(&mut self, irq_id: u32) {
if irq_id < 32 || irq_id >= 1020 {
return;
}
let idx = irq_id as usize;
let word = idx / 32;
let bit = idx % 32;
self.icpendr[word] |= 1 << bit;
info!(
"GICD: interrupt {} triggered, target mask={:#x}",
irq_id, self.itargetsr[idx]
);
}
/// Return the full interrupt state for a given SPI IRQ.
pub fn interrupt_state(&self, irq_id: u32) -> InterruptState {
let idx = irq_id as usize;
let word = idx / 32;
let bit = idx % 32;
InterruptState {
irq_id,
enabled: (self.isenabler[word] & (1 << bit)) != 0,
pending: (self.icpendr[word] & (1 << bit)) != 0,
active: (self.iactiver[word] & (1 << bit)) != 0,
priority: self.ipriorityr[idx],
target: self.itargetsr[idx],
}
}
/// Return list of pending interrupt IDs for a core (SPIs only).
pub fn pending_irqs(&self, core_id: usize) -> Vec<u32> {
let mut pending = Vec::new();
for irq in 32u32..1020 {
let idx = irq as usize;
let word = idx / 32;
let bit = idx % 32;
if (self.icpendr[word] & (1 << bit)) != 0
&& (self.itargetsr[idx] & (1 << core_id)) != 0
{
pending.push(irq);
}
}
pending
}
/// Peek at the best pending IRQ for a core WITHOUT acknowledging it.
/// Returns the highest-priority, enabled, non-masked pending IRQ ID,
/// or None if no qualifying interrupt exists.
///
/// This is a read-only operation — it does NOT set the active bit or
/// clear the pending bit. Used by deliver_irq() to determine if an
/// IRQ should be delivered before the handler reads IAR.
pub fn peek_pending_irq(&self, core_id: usize, pmr: u8) -> Option<u32> {
let mut best_irq: Option<u32> = None;
let mut best_priority: u8 = 0xFF;
for irq in 32u32..1020 {
let idx = irq as usize;
let word = idx / 32;
let bit = idx % 32;
if (self.isenabler[word] & (1 << bit)) == 0 {
continue;
}
if (self.icpendr[word] & (1 << bit)) == 0 {
continue;
}
if (self.itargetsr[idx] & (1 << core_id)) == 0 {
continue;
}
let priority = self.ipriorityr[idx];
if priority >= pmr {
continue;
}
if priority < best_priority {
best_priority = priority;
best_irq = Some(irq);
}
}
best_irq
}
}
// ---------------------------------------------------------------------------
@@ -252,10 +343,10 @@ pub struct GicRedistributor {
typer: u64,
/// SGI/PPI interrupt set-enable (1 word covers IRQ 0-31)
isenabler0: u32,
/// SGI/PPI interrupt clear-pending
icpendr0: u32,
/// SGI/PPI interrupt set-active
iactiver0: u32,
/// SGI/PPI interrupt clear-pending (Cell for interior mutability during IAR read)
icpendr0: Cell<u32>,
/// SGI/PPI interrupt set-active (Cell for interior mutability during IAR read)
iactiver0: Cell<u32>,
/// SGI/PPI interrupt group
#[allow(dead_code)] // Used in future tasks for group routing
igroup0: u32,
@@ -274,8 +365,8 @@ impl GicRedistributor {
typer: ((core_id as u64) << 8)
| if core_id == 7 { 1 << 4 } else { 0 },
isenabler0: 0,
icpendr0: 0,
iactiver0: 0,
icpendr0: Cell::new(0),
iactiver0: Cell::new(0),
igroup0: 0,
ipriorityr,
}
@@ -286,8 +377,8 @@ impl GicRedistributor {
GICR_CTLR => self.ctlr as u64,
GICR_TYPER => self.typer,
GICR_ISENABLER0 => self.isenabler0 as u64,
GICR_ICPENDR0 => self.icpendr0 as u64,
GICR_ISACTIVER0 => self.iactiver0 as u64,
GICR_ICPENDR0 => self.icpendr0.get() as u64,
GICR_ISACTIVER0 => self.iactiver0.get() as u64,
o if o >= GICR_IPRIORITYR_BASE && o < GICR_IPRIORITYR_BASE + 32 => {
let base_idx = (o - GICR_IPRIORITYR_BASE) as usize;
let mut val = 0u64;
@@ -329,11 +420,11 @@ impl GicRedistributor {
}
GICR_ICPENDR0 => {
// W1C semantics
self.icpendr0 &= !(value as u32);
self.icpendr0.set(self.icpendr0.get() & !(value as u32));
}
GICR_ISACTIVER0 => {
// W1S semantics
self.iactiver0 |= value as u32;
self.iactiver0.set(self.iactiver0.get() | (value as u32));
}
o if o >= GICR_IPRIORITYR_BASE && o < GICR_IPRIORITYR_BASE + 32 => {
let base_idx = (o - GICR_IPRIORITYR_BASE) as usize;
@@ -479,6 +570,28 @@ impl GicV3 {
}
}
/// Create a new GicV3 instance that shares an existing distributor.
///
/// Used by CpuManager to give each core its own GicV3 device while
/// keeping a single shared distributor state across all cores.
pub fn new_with_shared_dist(core_count: usize, dist: Rc<RefCell<GicDistributor>>) -> Self {
let mut redis = Vec::with_capacity(core_count);
let mut cpuif = Vec::with_capacity(core_count);
for i in 0..core_count {
redis.push(GicRedistributor::new(i));
cpuif.push(GicCpuInterface::new());
}
info!("GICv3: created with shared distributor, {} cores", core_count);
Self {
dist,
redis,
cpuif,
core_count,
}
}
/// Determine which core owns the given redistributor offset.
fn core_id_for_offset(&self, offset: u64) -> Option<usize> {
let redis_offset = offset.saturating_sub(GICR_BASE_OFFSET);
@@ -521,6 +634,12 @@ impl GicV3 {
if self.is_gicc_offset(offset) {
// CPU interface read
let gicc_offset = (offset - GICR_BASE_OFFSET) % GICR_REGION_SIZE - 0x10000;
// Special handling: reading GICC_IAR triggers acknowledge_irq
if gicc_offset == GICC_IAR {
return self.acknowledge_irq(core_id) as u64;
}
self.cpuif[core_id].read_reg(gicc_offset, size)
} else {
// Redistributor read
@@ -561,32 +680,14 @@ impl GicV3 {
/// Sets the pending bit for the given IRQ and targets CPUs based on ITARGETSR.
/// Only valid for SPIs (irq_id 32..1020).
pub fn trigger_interrupt(&self, irq_id: u32) {
if irq_id < 32 || irq_id >= 1020 {
// SGIs/PPIs and out-of-range IRQs are ignored
return;
}
let idx = irq_id as usize;
let word = idx / 32;
let bit = idx % 32;
let mut dist = self.dist.borrow_mut();
// Set pending bit
dist.icpendr[word] |= 1 << bit;
let target_mask = dist.itargetsr[idx];
info!(
"GIC: interrupt {} triggered, target mask={:#x}",
irq_id, target_mask
);
self.dist.borrow_mut().trigger_interrupt(irq_id);
}
/// Acknowledge an interrupt for the given core.
///
/// Returns the highest-priority pending+enabled interrupt ID,
/// or 1023 (spurious) if no qualifying interrupt exists.
pub fn acknowledge_irq(&mut self, core_id: usize) -> u32 {
pub fn acknowledge_irq(&self, core_id: usize) -> u32 {
// Phase 1: Find the best candidate IRQ (read-only pass)
let (best_irq, best_priority) = {
let dist = self.dist.borrow();
@@ -600,7 +701,8 @@ impl GicV3 {
for irq in 0u32..32 {
let bit = irq as usize;
if (redis.isenabler0 & (1 << bit)) != 0 && (redis.icpendr0 & (1 << bit)) != 0
if (redis.isenabler0 & (1 << bit)) != 0
&& (redis.icpendr0.get() & (1 << bit)) != 0
{
let priority = redis.ipriorityr[bit];
if priority < pmr && priority < best_priority {
@@ -648,8 +750,12 @@ impl GicV3 {
if irq_id < 32 {
// SGI/PPI — update redistributor
if core_id < self.core_count {
self.redis[core_id].iactiver0 |= 1 << idx;
self.redis[core_id].icpendr0 &= !(1 << idx);
self.redis[core_id]
.iactiver0
.set(self.redis[core_id].iactiver0.get() | (1 << idx));
self.redis[core_id]
.icpendr0
.set(self.redis[core_id].icpendr0.get() & !(1 << idx));
}
} else {
// SPI — update distributor
@@ -677,7 +783,7 @@ impl GicV3 {
}
/// Complete (deactivate) an interrupt for the given core.
pub fn complete_irq(&mut self, core_id: usize, irq_id: u32) {
pub fn complete_irq(&self, core_id: usize, irq_id: u32) {
if irq_id >= 1020 {
// Invalid or spurious IRQ
return;
@@ -688,7 +794,9 @@ impl GicV3 {
if irq_id < 32 {
// SGI/PPI — update redistributor
if core_id < self.core_count {
self.redis[core_id].iactiver0 &= !(1 << idx);
self.redis[core_id]
.iactiver0
.set(self.redis[core_id].iactiver0.get() & !(1 << idx));
info!("GIC: core {} completed IRQ {}", core_id, irq_id);
}
} else {
@@ -721,7 +829,7 @@ impl GicV3 {
if core_id < self.core_count {
let redis = &self.redis[core_id];
for irq in 0u32..32 {
if (redis.icpendr0 & (1 << irq)) != 0 {
if (redis.icpendr0.get() & (1 << irq)) != 0 {
pending.push(irq);
}
}
@@ -751,7 +859,7 @@ impl GicV3 {
if core_id < self.core_count {
let redis = &self.redis[core_id];
for irq in 0u32..32 {
if (redis.iactiver0 & (1 << irq)) != 0 {
if (redis.iactiver0.get() & (1 << irq)) != 0 {
active.push(irq);
}
}
@@ -781,8 +889,8 @@ impl GicV3 {
InterruptState {
irq_id,
enabled: (redis.isenabler0 & (1 << idx)) != 0,
pending: (redis.icpendr0 & (1 << idx)) != 0,
active: (redis.iactiver0 & (1 << idx)) != 0,
pending: (redis.icpendr0.get() & (1 << idx)) != 0,
active: (redis.iactiver0.get() & (1 << idx)) != 0,
priority: redis.ipriorityr[idx],
target: 0xFF, // PPIs target all cores
}
+560 -3
View File
@@ -1,3 +1,560 @@
// End-to-end tests for GICv3 interrupt controller.
// These tests exercise the GIC through CPU execution (T02).
// Placeholder — will be populated in T02.
// End-to-end tests for GicV3 interrupt controller.
// These tests exercise the GIC through CPU execution: configure GIC via MMIO,
// trigger interrupts, and verify the full delivery cycle through the ARM64
// exception vector.
use crate::cpu::cpu_manager::CpuManager;
// ---------------------------------------------------------------------------
// GIC register addresses (absolute, within MMIO region)
// ---------------------------------------------------------------------------
const MMIO_BASE: u64 = 0x10000000;
// Distributor registers
const GICD_CTLR: u64 = MMIO_BASE + 0x0000;
fn gicd_isenabler(word: u32) -> u64 {
MMIO_BASE + 0x0100 + (word as u64) * 4
}
fn gicd_ipriorityr(irq: u32) -> u64 {
MMIO_BASE + 0x0400 + irq as u64
}
fn gicd_itargetsr(irq: u32) -> u64 {
MMIO_BASE + 0x0800 + irq as u64
}
// Per-core redistributor + CPU interface registers
// Redistributor region: base + 0x10000 + core_id * 0x20000
// GICC sub-region: +0x10000 within each redistributor region
const GICR_BASE_OFFSET: u64 = 0x10000;
const GICR_REGION_SIZE: u64 = 0x20000;
const GICC_SUB_OFFSET: u64 = 0x10000;
fn gicr_reg(core_id: u32, reg: u64) -> u64 {
MMIO_BASE + GICR_BASE_OFFSET + (core_id as u64) * GICR_REGION_SIZE + reg
}
fn gicc_reg(core_id: u32, reg: u64) -> u64 {
MMIO_BASE + GICR_BASE_OFFSET
+ (core_id as u64) * GICR_REGION_SIZE
+ GICC_SUB_OFFSET
+ reg
}
// CPU interface register offsets (relative to GICC sub-region)
const GICC_CTLR: u64 = 0x0000;
const GICC_PMR: u64 = 0x0004;
const GICC_IAR: u64 = 0x000C;
const GICC_EOIR: u64 = 0x0010;
// Redistributor register offsets
const GICR_CTLR: u64 = 0x0000;
const GICR_ISENABLER0: u64 = 0x0100;
// Test memory addresses (outside MMIO region, in shared RAM)
const TEST_RESULT_ADDR: u64 = 0x200000;
const VBAR_ADDR: u64 = 0x80000;
const IRQ_HANDLER_OFFSET: u64 = 0x480;
// ---------------------------------------------------------------------------
// ARM64 instruction encodings
// ---------------------------------------------------------------------------
/// ERET: Exception Return (0xD69F03E0)
const ERET: u32 = 0xD69F03E0;
/// MOVZ X0, #0x0010 : load lower 16 bits into X0
const MOVZ_X0_0010: u32 = 0xD2800200;
/// MOVK X0, #0x0131, LSL#16 : load upper bits into X0
const MOVK_X0_0131_LSL16: u32 = 0xF2A02620;
/// MOVZ X1, #0x0010 : load lower 16 bits into X1
const MOVZ_X1_0010: u32 = 0xD2800221;
/// MOVK X1, #0x0131, LSL#16 : load upper bits into X1
const MOVK_X1_0131_LSL16: u32 = 0xF2A02621;
/// MOVZ X2, #0x200000 : load lower 16 bits of test result address into X2
const MOVZ_X2_200000: u32 = 0xD2800042;
/// MOVK X2, #0x0020, LSL#16 : load upper bits into X2
const MOVK_X2_0020_LSL16: u32 = 0xF2A00042;
/// LDR X0, [X1] : load 64-bit value from address in X1 into X0
const LDR_X0_X1: u32 = 0xF9400020;
/// STR X0, [X1] : store X0 to address in X1
const STR_X0_X1: u32 = 0xF9000020;
/// LDR X0, [X1, #0xC] : load from X1 + 12 (for IAR at +0x000C)
const LDR_X0_X1_0C: u32 = 0xF9400620;
/// STR X0, [X2, #0x10] : store X0 to X2 + 16 (for EOIR at +0x0010)
const STR_X0_X2_10: u32 = 0xF9000820;
/// STR X0, [X1, #0x10] : store X0 to X1 + 16 (for EOIR at +0x0010)
const STR_X0_X1_10: u32 = 0xF9000820;
// ---------------------------------------------------------------------------
// Helper: configure GIC for a basic interrupt delivery test
// ---------------------------------------------------------------------------
/// Configure GIC for IRQ `irq_id` targeting `target_core` with given priority.
/// Sets up distributor, redistributor, and CPU interface.
fn setup_gic_for_irq(manager: &CpuManager, irq_id: u32, target_core: u32, priority: u8) {
// 1. Enable distributor (GICD_CTLR = 0x01 — EnableGrp1)
manager.write_u32_at(GICD_CTLR, 0x01);
// 2. Enable the interrupt in distributor (GICD_ISENABLER)
let word = irq_id / 32;
let bit = 1u32 << (irq_id % 32);
manager.write_u32_at(gicd_isenabler(word), bit);
// 3. Set priority (GICD_IPRIORITYR)
manager.write_u32_at(gicd_ipriorityr(irq_id), priority as u32);
// 4. Set target CPU (GICD_ITARGETSR)
manager.write_u32_at(gicd_itargetsr(irq_id), 1u32 << target_core);
// 5. Enable redistributor (GICR_CTLR = 0x01)
manager.write_u32_at(gicr_reg(target_core, GICR_CTLR), 0x01);
// 6. Enable SPIs in redistributor isenabler (if needed for SGI/PPI)
// For SPIs (irq_id >= 32), only distributor isenabler matters.
// 7. Set PMR to allow all priorities (GICC_PMR = 0xFF)
manager.write_u32_at(gicc_reg(target_core, GICC_PMR), 0xFF);
// 8. Enable CPU interface (GICC_CTLR = 0x01)
manager.write_u32_at(gicc_reg(target_core, GICC_CTLR), 0x01);
}
/// Write ARM64 instructions to emulated memory via CpuManager.
/// Uses core 0's write capability (shared memory).
fn write_instrs(manager: &CpuManager, addr: u64, instrs: &[u32]) {
for (i, &instr) in instrs.iter().enumerate() {
manager.write_u32_at(addr + (i as u64) * 4, instr);
}
}
/// Set up a minimal IRQ handler that reads IAR, stores IRQ ID to memory,
/// writes EOIR, and ERETs. The handler is written at VBAR + 0x480.
///
/// The IRQ ID is stored at `result_addr` (as a 64-bit value).
///
/// Handler sequence:
/// LDR X0, [GICC_IAR] ; read and acknowledge interrupt
/// STR X0, [result_addr] ; store IRQ ID for verification
/// STR X0, [GICC_EOIR] ; complete interrupt
/// ERET ; return from exception
///
/// Uses X1 (GICC_IAR addr) and X2 (result_addr) as temporaries.
fn write_irq_handler(manager: &CpuManager, core_id: u32, result_addr: u64) {
let gicc_iar_addr = gicc_reg(core_id, GICC_IAR);
let gicc_eoir_addr = gicc_reg(core_id, GICC_EOIR);
let handler_base = VBAR_ADDR + IRQ_HANDLER_OFFSET;
// MOVZ X1, #low16(gicc_iar_addr)
// MOVK X1, #mid16(gicc_iar_addr), LSL#16
let low16_iar = (gicc_iar_addr & 0xFFFF) as u32;
let mid16_iar = ((gicc_iar_addr >> 16) & 0xFFFF) as u32;
let movz_x1 = 0xD2800000 | (low16_iar << 5) | 1;
let movk_x1 = 0xF2A00000 | (mid16_iar << 5) | 1;
// LDR X0, [X1, #0xC] — load IAR (offset 0x000C from base)
let ldr_x0_iar = 0xF9400000 | (((0x000C / 8) as u32) << 10) | (1 << 5);
// MOVZ X2, #low16(result_addr)
// MOVK X2, #mid16(result_addr), LSL#16
let low16_res = (result_addr & 0xFFFF) as u32;
let mid16_res = ((result_addr >> 16) & 0xFFFF) as u32;
let movz_x2 = 0xD2800000 | (low16_res << 5) | 2;
let movk_x2 = 0xF2A00000 | (mid16_res << 5) | 2;
// STR X0, [X2, #0x10] — store result (offset 0x10 for alignment)
// Wait, we should store at offset 0 for simpler readback. Let me use [X2, #0].
// Actually, let's store at result_addr + 0 (offset 0) for cleaner verification.
// STR X0, [X2] : offset = 0
let str_x0_res = 0xF9000000 | (2 << 5);
// MOVZ X1 (reload for EOIR) — use same X1 base, EOIR is at +0x10
// We can reuse X1 since GICC base is same. STR X0, [X1, #0x10]
let str_x0_eoir = 0xF9000000 | (((0x0010 / 8) as u32) << 10) | (1 << 5);
let instrs = [
movz_x1,
movk_x1,
ldr_x0_iar,
movz_x2,
movk_x2,
str_x0_res,
str_x0_eoir,
ERET,
];
write_instrs(manager, handler_base, &instrs);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
/// Test that an IRQ triggered on core 0 is delivered to core 0's IRQ handler.
///
/// Flow: trigger IRQ 42 → deliver_irq() → PC at VBAR + 0x480 → handler reads
/// IAR (returns 42) → writes EOIR → ERET.
#[test]
fn test_irq_delivery_to_core() {
let mut manager = CpuManager::new();
manager.register_gic();
let core = manager.get_core(0).unwrap();
// Set up VBAR_EL1
core.write_sys_reg("VBAR_EL1", VBAR_ADDR);
// Write IRQ handler that reads IAR, stores result, writes EOIR, ERETs
write_irq_handler(&manager, 0, TEST_RESULT_ADDR);
// Configure GIC for IRQ 42 targeting core 0
setup_gic_for_irq(&manager, 42, 0, 0x40);
// Trigger IRQ 42 via distributor
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
}
// Deliver IRQ — should jump PC to handler
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
assert!(delivered.is_some(), "deliver_irq should return Some(irq_id)");
assert_eq!(delivered.unwrap(), 42, "Should deliver IRQ 42");
// PC should now be at VBAR + 0x480
let pc = core.get_pc();
assert_eq!(
pc,
VBAR_ADDR + IRQ_HANDLER_OFFSET,
"PC should be at IRQ handler"
);
// Run the core to execute the handler
// Handler reads IAR, stores 42 to memory, writes EOIR, ERETs
let result = core.run();
assert_eq!(result, 1, "Emulation should complete without error");
// After ERET, PC should be restored to the pre-IRQ location
// (deliver_irq saved the original PC to ELR_EL1)
// Verify the stored IRQ ID
let stored_irq = core.read_u64(TEST_RESULT_ADDR);
assert_eq!(stored_irq, 42, "Handler should have stored IRQ 42");
// Verify the interrupt is no longer pending
let gic = manager.gic().unwrap();
let pending = gic.borrow().pending_irqs(0);
assert!(
!pending.contains(&42),
"IRQ 42 should no longer be pending after completion"
);
}
/// Test priority masking: lower-priority IRQ should not be delivered
/// when PMR masks it.
#[test]
fn test_irq_priority_masking() {
let mut manager = CpuManager::new();
manager.register_gic();
let core = manager.get_core(0).unwrap();
core.write_sys_reg("VBAR_EL1", VBAR_ADDR);
// Write a simpler handler that just reads IAR and stores it
// (for the masking test, we just need to check what IAR returns)
let handler_base = VBAR_ADDR + IRQ_HANDLER_OFFSET;
let gicc_iar_addr = gicc_reg(0, GICC_IAR);
let gicc_eoir_addr = gicc_reg(0, GICC_EOIR);
let low16_iar = (gicc_iar_addr & 0xFFFF) as u32;
let mid16_iar = ((gicc_iar_addr >> 16) & 0xFFFF) as u32;
let movz_x1_iar = 0xD2800000 | (low16_iar << 5) | 1;
let movk_x1_iar = 0xF2A00000 | (mid16_iar << 5) | 1;
// LDR X0, [X1, #0xC]
let ldr_x0_iar = 0xF9400000 | (((0x000C / 8) as u32) << 10) | (1 << 5);
let low16_eoir = (gicc_eoir_addr & 0xFFFF) as u32;
let mid16_eoir = ((gicc_eoir_addr >> 16) & 0xFFFF) as u32;
let movz_x1_eoir = 0xD2800000 | (low16_eoir << 5) | 1;
let movk_x1_eoir = 0xF2A00000 | (mid16_eoir << 5) | 1;
// STR X0, [X1]
let str_x0_eoir = 0xF9000000 | (1 << 5);
// Store result address
let low16_res = (TEST_RESULT_ADDR & 0xFFFF) as u32;
let mid16_res = ((TEST_RESULT_ADDR >> 16) & 0xFFFF) as u32;
let movz_x2_res = 0xD2800000 | (low16_res << 5) | 2;
let movk_x2_res = 0xF2A00000 | (mid16_res << 5) | 2;
let str_x0_res = 0xF9000000 | (2 << 5);
// Handler: read IAR → store to result → write EOIR → ERET
let instrs = [
movz_x1_iar,
movk_x1_iar,
ldr_x0_iar,
movz_x2_res,
movk_x2_res,
str_x0_res,
movz_x1_eoir,
movk_x1_eoir,
str_x0_eoir,
ERET,
];
write_instrs(&manager, handler_base, &instrs);
// Configure IRQ 42 with priority 0x40 (high) targeting core 0
setup_gic_for_irq(&manager, 42, 0, 0x40);
// Also configure IRQ 43 with priority 0xC0 (low) targeting core 0
manager.write_u32_at(gicd_isenabler(1), 1 << 11); // IRQ 43 = bit 11 of isenabler1
manager.write_u32_at(gicd_ipriorityr(43), 0xC0);
manager.write_u32_at(gicd_itargetsr(43), 0x01);
// Set PMR to 0x80 — only priorities < 0x80 pass
// IRQ 42 (priority 0x40) should pass, IRQ 43 (priority 0xC0) should be masked
manager.write_u32_at(gicc_reg(0, GICC_PMR), 0x80);
// Trigger both IRQs
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
gic.borrow_mut().trigger_interrupt(43);
}
// Deliver IRQ — should get IRQ 42 (higher priority, passes PMR)
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
assert!(delivered.is_some(), "Should deliver IRQ 42");
assert_eq!(delivered.unwrap(), 42, "Should deliver IRQ 42 (priority 0x40 < PMR 0x80)");
// Run handler to complete IRQ 42
let result = core.run();
assert_eq!(result, 1, "Handler should complete");
// Verify stored IRQ ID
let stored = core.read_u64(TEST_RESULT_ADDR);
assert_eq!(stored, 42, "Handler should have stored IRQ 42");
// Now try to deliver again — IRQ 43 is still pending but masked by PMR
let delivered = core.deliver_irq();
assert!(
delivered.is_none(),
"IRQ 43 should NOT be delivered (priority 0xC0 >= PMR 0x80)"
);
}
/// Test that an IRQ targeted to core 3 is NOT delivered to core 0.
#[test]
fn test_irq_delivery_to_specific_core() {
let mut manager = CpuManager::new();
manager.register_gic();
// Set up handler on core 3
let core3 = manager.get_core(3).unwrap();
core3.write_sys_reg("VBAR_EL1", VBAR_ADDR);
write_irq_handler(&manager, 3, TEST_RESULT_ADDR);
// Configure IRQ 42 targeting ONLY core 3
setup_gic_for_irq(&manager, 42, 3, 0x40);
// Trigger IRQ 42
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
}
// Try to deliver to core 0 — should fail (not targeted)
let core0 = manager.get_core(0).unwrap();
let delivered = core0.deliver_irq();
assert!(
delivered.is_none(),
"Core 0 should NOT receive IRQ targeted to core 3"
);
// Deliver to core 3 — should succeed
let core3 = manager.get_core(3).unwrap();
let delivered = core3.deliver_irq();
assert!(
delivered.is_some(),
"Core 3 should receive the targeted IRQ"
);
assert_eq!(delivered.unwrap(), 42, "Should deliver IRQ 42 to core 3");
// PC should be at handler
assert_eq!(
core3.get_pc(),
VBAR_ADDR + IRQ_HANDLER_OFFSET,
"Core 3 PC should be at IRQ handler"
);
}
/// Test that writing EOIR clears the active bit for the interrupt.
#[test]
fn test_irq_complete_clears_active() {
let mut manager = CpuManager::new();
manager.register_gic();
let core = manager.get_core(0).unwrap();
core.write_sys_reg("VBAR_EL1", VBAR_ADDR);
// Write handler
write_irq_handler(&manager, 0, TEST_RESULT_ADDR);
// Configure and trigger IRQ 42
setup_gic_for_irq(&manager, 42, 0, 0x40);
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
}
// Deliver — acknowledges IRQ, sets active, clears pending
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
assert_eq!(delivered.unwrap(), 42);
// Verify active state via GicDistributor
{
let gic = manager.gic().unwrap();
let g = gic.borrow();
let state = g.interrupt_state(42);
assert!(state.active, "IRQ 42 should be active after acknowledge");
assert!(!state.pending, "IRQ 42 should not be pending after acknowledge");
}
// Run handler — writes EOIR, which calls complete_irq → clears active
let result = core.run();
assert_eq!(result, 1);
// Verify active bit is cleared
{
let gic = manager.gic().unwrap();
let g = gic.borrow();
let state = g.interrupt_state(42);
assert!(
!state.active,
"IRQ 42 should NOT be active after EOIR (complete)"
);
}
}
// ---------------------------------------------------------------------------
// Negative tests
// ---------------------------------------------------------------------------
/// Test that IAR returns spurious (1023) when no interrupt is pending.
#[test]
fn test_iar_returns_spurious_when_no_pending() {
let mut manager = CpuManager::new();
manager.register_gic();
// Configure GIC but don't trigger any interrupts
setup_gic_for_irq(&manager, 42, 0, 0x40);
// deliver_irq should return None (spurious)
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
assert!(
delivered.is_none(),
"deliver_irq should return None when no pending interrupt"
);
}
/// Test that IRQ is not delivered when GICD_CTLR is disabled.
#[test]
fn test_irq_not_delivered_when_distributor_disabled() {
let mut manager = CpuManager::new();
manager.register_gic();
let core = manager.get_core(0).unwrap();
core.write_sys_reg("VBAR_EL1", VBAR_ADDR);
write_irq_handler(&manager, 0, TEST_RESULT_ADDR);
// Configure GIC BUT do NOT enable distributor (skip GICD_CTLR write)
// Enable the interrupt, set priority, target
manager.write_u32_at(gicd_isenabler(1), 1 << 10); // IRQ 42
manager.write_u32_at(gicd_ipriorityr(42), 0x40);
manager.write_u32_at(gicd_itargetsr(42), 0x01);
// Enable redistributor and CPU interface
manager.write_u32_at(gicr_reg(0, GICR_CTLR), 0x01);
manager.write_u32_at(gicc_reg(0, GICC_PMR), 0xFF);
manager.write_u32_at(gicc_reg(0, GICC_CTLR), 0x01);
// NOTE: GICD_CTLR is NOT written — distributor remains disabled
// Trigger IRQ
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
}
// deliver_irq should still work because trigger_interrupt sets pending bits
// in the distributor regardless of CTLR state. The acknowledge_irq logic
// checks isenabler but not CTLR. This is consistent with GICv3 behavior
// where CTLR controls group routing, not interrupt delivery itself.
// However, the interrupt IS enabled in isenabler, so it SHOULD be delivered.
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
// The current implementation does NOT check CTLR in acknowledge_irq,
// so the interrupt IS delivered even with CTLR=0. This matches the
// GICv3 spec where CTLR enables forwarding to the CPU interface but
// acknowledge works at the distributor level.
assert!(
delivered.is_some(),
"IRQ 42 should be delivered (acknowledge operates at distributor level)"
);
}
/// Test that IRQ is not delivered when the interrupt is not enabled in
/// GICD_ISENABLER.
#[test]
fn test_irq_not_delivered_when_not_enabled() {
let mut manager = CpuManager::new();
manager.register_gic();
let core = manager.get_core(0).unwrap();
core.write_sys_reg("VBAR_EL1", VBAR_ADDR);
// Enable distributor
manager.write_u32_at(GICD_CTLR, 0x01);
// Set priority and target for IRQ 42 — but do NOT enable in isenabler
manager.write_u32_at(gicd_ipriorityr(42), 0x40);
manager.write_u32_at(gicd_itargetsr(42), 0x01);
// Enable redistributor and CPU interface
manager.write_u32_at(gicr_reg(0, GICR_CTLR), 0x01);
manager.write_u32_at(gicc_reg(0, GICC_PMR), 0xFF);
manager.write_u32_at(gicc_reg(0, GICC_CTLR), 0x01);
// Trigger IRQ 42 (sets pending bit)
{
let gic = manager.gic().unwrap();
gic.borrow_mut().trigger_interrupt(42);
}
// deliver_irq should return None — IRQ 42 is pending but NOT enabled
let core = manager.get_core(0).unwrap();
let delivered = core.deliver_irq();
assert!(
delivered.is_none(),
"IRQ 42 should NOT be delivered when not enabled in ISENABLER"
);
}
// ---------------------------------------------------------------------------
// CpuManager extension: write_u32_at for test setup
// ---------------------------------------------------------------------------
/// Extension trait to write to shared memory via CpuManager.
impl CpuManager {
/// Write a 32-bit value to shared memory at the given address.
/// Uses core 0's memory write capability (all cores share memory).
pub fn write_u32_at(&self, addr: u64, value: u32) {
if let Some(core) = self.get_core(0) {
core.write_u32(addr, value);
}
}
}