From 123a9ab4e6c869dfa81f4648b0e8c9f997f6e752 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:12:46 -0700 Subject: [PATCH 01/33] bedrock: publish a PI's interrupt levels under one lock Several threads publish a PI's levels into its processors' Cause bits: the processor's own register stores, interrupt packets from device threads, the comparators' host timer, and the levels LB, NI, XB and the L1 UART reflect. Each publish read the inputs, compared them with the word and wrote the difference, looping until the two agreed, with no lock. The word ends right once the writers stop, but a publisher that sampled a level and was held before writing it could set the bit again after another thread had published its clear: one rise and fall of a pending bit reached the processor as two rises, so it could take an interrupt that no register held. Run against the old publish, the new test, which holds that late publisher with three threads, saw two rises. HACKING.md's Interrupts section asks for the simpler model: each PI changes its interrupt inputs and writes the word under one lock of its own, keeps the levels it last wrote, and writes only a change. The reflected levels stay outside that lock, since each output reads one reflected bank in a single load, so no sample mixes two instants. The inputs stay atomics, so register reads, the counter and the processor's reads of its word take no lock. A store or packet that changes an input now takes one uncontended lock, at interrupt rates, not per instruction. The test that checked that a mask store and a packet take no lock now checks the reads that still don't. --- HACKING.md | 14 +-- src/dev/bedrock/mod.rs | 26 ++---- src/dev/bedrock/pi.rs | 179 +++++++++++++++++++++++------------- src/dev/bedrock/pi/tests.rs | 25 ++++- src/dev/bedrock/tests.rs | 94 ++++++++++++++++--- 5 files changed, 233 insertions(+), 105 deletions(-) diff --git a/HACKING.md b/HACKING.md index 7f5a43ece..109c95231 100644 --- a/HACKING.md +++ b/HACKING.md @@ -375,13 +375,13 @@ consumer thread never takes `rec`. Bedrock, the Fuel's node chip, shows rule 4 at the scale of a whole chip. Each processor interface's interrupt inputs (pending banks, masks, timer and -error status) are atomics, and a store or an inbound packet changes one and -then publishes the slot's levels into Cause bits 10-14 of its CPU's interrupt -word with no lock: a writer that published a stale level re-reads the inputs -and corrects it, so the last write always matches the registers. A mask -store, an interrupt packet, a counter read, inbound DMA and PIO take no lock. -The comparators fire on host timers, as the Hub's do, not from the CPU's -batch hook. +error status) are atomics, so a register read takes no lock. A store, an +inbound packet or a comparator changes one and publishes the slot's levels +into Cause bits 10-14 of its CPU's interrupt word under one lock per +interface, which keeps the last levels it wrote, as the Interrupts section +asks. The CPU reads its word without a lock. A counter read, inbound DMA +and PIO take no lock. The comparators fire on host timers, as the Hub's do, +not from the CPU's batch hook. ## 6. The CPU diff --git a/src/dev/bedrock/mod.rs b/src/dev/bedrock/mod.rs index e8ff998f8..275a09e0d 100644 --- a/src/dev/bedrock/mod.rs +++ b/src/dev/bedrock/mod.rs @@ -191,6 +191,8 @@ impl Bedrock { register_mutex_fn("bedrock-pi1::state", self, |c| c.pi[1].state_locked()); register_mutex_fn("bedrock-pi0::timer", self, |c| c.pi[0].timer_locked()); register_mutex_fn("bedrock-pi1::timer", self, |c| c.pi[1].timer_locked()); + register_mutex_fn("bedrock-pi0::out", self, |c| c.pi[0].out_locked()); + register_mutex_fn("bedrock-pi1::out", self, |c| c.pi[1].out_locked()); register_mutex_fn("bedrock-md::state", self, |c| c.md.state_locked()); register_mutex_fn("bedrock-md::remapping", self, |c| c.md.remapping_locked()); register_mutex_fn("bedrock-io", self, |c| c.io.locked()); @@ -295,11 +297,7 @@ impl Bedrock { /// An interrupt packet to PI `pi`'s pending-modify register; whether the /// PI took it. fn interrupt(&self, pi: usize, value: u64) -> bool { - let taken = self.pi[pi].io_modify(value, 0); - if taken { - self.pi[pi].publish(&self.levels); - } - taken + self.pi[pi].io_modify(value, 0, &self.levels) } /// Widget `widget`'s DMA as a bus device: crosstalk addresses, with the /// widget's inbound permissions applied. @@ -341,9 +339,7 @@ impl Bedrock { /// PI `interface`'s host timer fired. fn expire(&self, interface: usize) { let Some(tm) = self.timer_manager() else { return }; - if self.pi[interface].expire(&self.host_timer(&tm), (self.clock)()) { - self.pi[interface].publish(&self.levels); - } + self.pi[interface].expire(&self.host_timer(&tm), (self.clock)(), &self.levels); } fn publish_all(&self) { for pi in &self.pi { @@ -395,18 +391,13 @@ impl Bedrock { let now = (self.clock)(); match (size, value) { (8, None) if a < pi::SIZE && a & 7 == 0 => { - let (data, latched) = pi.read(a, now, &self.levels); - if latched { - pi.publish(&self.levels); - } - data.map_or(BusRead64::err(), BusRead64::ok) + pi.read(a, now, &self.levels).map_or(BusRead64::err(), BusRead64::ok) } (8, Some(v)) if a < pi::SIZE && a & 7 == 0 => { - if pi.write(a, v, now) { + if pi.write(a, v, now, &self.levels) { let tm = self.timer_manager(); pi.reschedule(tm.as_deref().map(|tm| self.host_timer(tm)).as_ref(), now); } - pi.publish(&self.levels); BusRead64 { status: BUS_OK, data: 0 } } // The counter's two halves, high word first. @@ -467,10 +458,9 @@ impl Device for Bedrock { let tm = self.timer_manager(); let host = tm.as_deref().map(|tm| self.host_timer(tm)); for pi in &self.pi { - pi.stop(host.as_ref(), now); + pi.stop(host.as_ref(), now, &self.levels); } self.md.stop_clock(); - self.publish_all(); } fn is_running(&self) -> bool { self.pi[0].is_running() @@ -487,7 +477,7 @@ impl Resettable for Bedrock { let tm = self.timer_manager(); let host = tm.as_deref().map(|tm| self.host_timer(tm)); for pi in &self.pi { - pi.power_on(now); + pi.power_on(now, &self.levels); pi.reschedule(host.as_ref(), now); } self.md.power_on(); diff --git a/src/dev/bedrock/pi.rs b/src/dev/bedrock/pi.rs index d84e912ce..48e821793 100644 --- a/src/dev/bedrock/pi.rs +++ b/src/dev/bedrock/pi.rs @@ -7,9 +7,10 @@ //! Each slot drives IP2..IP6 of its processor: bank 0 under the slot's bank //! 0 mask, bank 1 under its bank 1 mask, its RT comparator, the profile //! timer and the error summary under its error cause. The inputs of those -//! five levels are atomics. A register store or an inbound packet changes -//! one and then publishes the slot's levels into Cause bits 10-14 of its -//! processor's interrupt word, without a lock; see [`Pi::publish`]. +//! five levels are atomics, which reads take without a lock. A register +//! store, an inbound packet or a comparator changes one and publishes the +//! slot's levels into Cause bits 10-14 of its processor's interrupt word +//! under one lock, the PI's `out`; see [`Pi::publish`]. //! //! # What is approximated here //! @@ -258,6 +259,10 @@ pub(super) struct Pi { error_cause: [AtomicU64; 2], /// Each slot's processor's interrupt word, null for an empty slot. words: [AtomicPtr; 2], + /// Each slot's IP2..IP6 as last written to its word. Held across every + /// change of an interrupt input and the publish after it, so a slot's + /// levels reach the processor in the order the inputs changed. + out: Mutex<[u64; 2]>, counter: Counter, /// The earliest deadline in host nanoseconds; `u64::MAX` while stopped. next_due: AtomicU64, @@ -281,6 +286,7 @@ impl Pi { error_summary: AtomicU64::new(0), error_cause: Default::default(), words: Default::default(), + out: Mutex::new([0; 2]), counter: Counter::new(now), next_due: AtomicU64::new(RUNNING), timer: Mutex::new(Timer { rt_compare: [0; 2], profile_compare: 0, rt_due: [0; 2], profile_due: 0, id: None }), @@ -301,9 +307,16 @@ impl Pi { pub(super) fn timer_locked(&self) -> bool { self.timer.is_locked() } + pub(super) fn out_locked(&self) -> bool { + self.out.is_locked() + } /// Where slot `slot`'s IP2..IP6 go. pub(super) fn set_word(&self, slot: usize, word: *const AtomicU64) { + let mut out = self.out.lock(); self.words[slot].store(word as *mut AtomicU64, SeqCst); + // SAFETY: `Bedrock::set_cpu_interrupts`' contract: the word + // outlives the chip. + out[slot] = unsafe { word.as_ref() }.map_or(0, |w| (w.load(SeqCst) >> CAUSE_IP2) & 0x1f); } /// Whether a processor sits in `slot` of this interface: only PI0 slot A @@ -345,17 +358,21 @@ impl Pi { | u64::from(self.error_summary.load(SeqCst) & self.error_cause[slot].load(SeqCst) != 0) << 4 } - /// Bring each processor's IP2..IP6 to what the registers say. Every - /// input change is a sequentially consistent store or read-modify-write - /// followed by a publish, so no lock is needed: take the last write to a - /// word. Its writer read the inputs again after it, and would have - /// written again had any change before that read left the word stale. A - /// change after that read is followed by its own publish, which would - /// make a later write. So once writers stop, the word matches the - /// registers. A publish that changes nothing reads the inputs and the - /// word and stores nothing. Only the PI writes Cause bits 10-14. + /// Bring each processor's IP2..IP6 to what the registers say, after a + /// change outside the PI: a reflected level or a new word. pub(super) fn publish(&self, levels: &Levels) { - for slot in 0..2 { + self.drive(&mut self.out.lock(), levels); + } + + /// Write each slot's IP2..IP6 where they differ from what was last + /// written, with `out` held. Every input but the reflected levels + /// changes only under `out`, and each output bit reads one reflected + /// bank at most, in a single load, so each level written is one the + /// registers held at some instant and the writes follow the changes in + /// order: no level rises that the registers never raised. Only the PI + /// writes Cause bits 10-14, so `out` is what the word holds there. + fn drive(&self, out: &mut [u64; 2], levels: &Levels) { + for (slot, have) in out.iter_mut().enumerate() { let p = self.words[slot].load(Acquire); if p.is_null() { continue; @@ -363,21 +380,18 @@ impl Pi { // SAFETY: `Bedrock::set_cpu_interrupts`' contract: the word // outlives the chip. let word = unsafe { &*p }; - loop { - let want = self.outputs(slot, levels); - let have = (word.load(SeqCst) >> CAUSE_IP2) & 0x1f; - if want == have { - break; - } - let (set, clear) = (want & !have, have & !want); - if set != 0 { - word.fetch_or(set << CAUSE_IP2, SeqCst); - #[cfg(feature = "idle-pause")] - crate::cpu::idle_park::wake(); - } - if clear != 0 { - word.fetch_and(!(clear << CAUSE_IP2), SeqCst); - } + let want = self.outputs(slot, levels); + #[cfg(test)] + tests::sampled(want); + let (set, clear) = (want & !*have, *have & !want); + *have = want; + if set != 0 { + word.fetch_or(set << CAUSE_IP2, SeqCst); + #[cfg(feature = "idle-pause")] + crate::cpu::idle_park::wake(); + } + if clear != 0 { + word.fetch_and(!(clear << CAUSE_IP2), SeqCst); } } } @@ -399,13 +413,16 @@ impl Pi { } /// An interrupt packet from region `region` to the pending-modify - /// register. Refused, and false, unless the region may send one. - pub(super) fn io_modify(&self, value: u64, region: u8) -> bool { + /// register, published. Refused, and false, unless the region may send + /// one. + pub(super) fn io_modify(&self, value: u64, region: u8, levels: &Levels) -> bool { if region >= 64 || self.region.load(SeqCst) & (1 << region) == 0 { self.note(reg::PENDING_MODIFY, true); return false; } + let mut out = self.out.lock(); self.modify(value); + self.drive(&mut out, levels); true } @@ -464,21 +481,36 @@ impl Pi { latched } - /// Latch what is due at `now`: one load when nothing is. Whether a - /// comparator latched, so the caller publishes. - pub(super) fn sync(&self, now: u64) -> bool { + /// Latch and publish what is due at `now`: one load, and no lock, when + /// nothing is. + fn sync(&self, now: u64, levels: &Levels) { if now < self.next_due.load(SeqCst) { - return false; + return; + } + let mut out = self.out.lock(); + if self.latch(&mut self.timer.lock(), now) { + self.drive(&mut out, levels); } - self.latch(&mut self.timer.lock(), now) } - /// The host timer fired: latch what is due and arm the next deadline. - pub(super) fn expire(&self, host: &HostTimer, now: u64) -> bool { + /// Latch what is due at `now`, with `out` held. + fn catch_up(&self, now: u64) { + if now >= self.next_due.load(SeqCst) { + self.latch(&mut self.timer.lock(), now); + } + } + + /// The host timer fired: latch and publish what is due and arm the next + /// deadline. + pub(super) fn expire(&self, host: &HostTimer, now: u64, levels: &Levels) { + let mut out = self.out.lock(); let mut t = self.timer.lock(); let latched = self.latch(&mut t, now); self.arm_host(&mut t, host, now); - latched + drop(t); + if latched { + self.drive(&mut out, levels); + } } /// (Re)arm the host timer for the earliest deadline, under the timer @@ -523,8 +555,10 @@ impl Pi { } } - /// The machine stopped at `now`: latch what is due, then freeze the counter. - pub(super) fn stop(&self, host: Option<&HostTimer>, now: u64) -> bool { + /// The machine stopped at `now`: latch and publish what is due, then + /// freeze the counter. + pub(super) fn stop(&self, host: Option<&HostTimer>, now: u64, levels: &Levels) { + let mut out = self.out.lock(); let mut t = self.timer.lock(); let latched = self.latch(&mut t, now); let mut o = self.counter.read(); @@ -536,7 +570,10 @@ impl Pi { if let (Some(host), Some(id)) = (host, t.id.take()) { host.tm.remove(id); } - latched + drop(t); + if latched { + self.drive(&mut out, levels); + } } pub(super) fn is_running(&self) -> bool { @@ -549,11 +586,9 @@ impl Pi { u64::from(self.interface) << 1 } - /// A doubleword read at `off`, and whether a comparator latched on the - /// way, so the caller publishes. `None` for a register the model does - /// not have. - pub(super) fn read(&self, off: u64, now: u64, levels: &Levels) -> (Option, bool) { - let mut latched = false; + /// A doubleword read at `off`, publishing a comparator that latched on + /// the way. `None` for a register the model does not have. + pub(super) fn read(&self, off: u64, now: u64, levels: &Levels) -> Option { let value = match off { reg::INTERFACE_ID => self.cpu_number(), reg::INTERFACE_SLOT_A => u64::from(self.started(0)), @@ -571,11 +606,11 @@ impl Pi { reg::RT_COMPARE_A | reg::RT_COMPARE_B => self.timer.lock().rt_compare[((off - reg::RT_COMPARE_A) / 8) as usize], reg::PROFILE_COMPARE => self.timer.lock().profile_compare, reg::RT_STATUS_A | reg::RT_STATUS_B => { - latched = self.sync(now); + self.sync(now, levels); self.rt_status[((off - reg::RT_STATUS_A) / 8) as usize].load(SeqCst) } reg::PROFILE_STATUS_A | reg::PROFILE_STATUS_B => { - latched = self.sync(now); + self.sync(now, levels); self.profile_status[((off - reg::PROFILE_STATUS_A) / 8) as usize].load(SeqCst) } reg::RT_ENABLE_A | reg::RT_ENABLE_B => self.rt_enable[((off - reg::RT_ENABLE_A) / 8) as usize].load(SeqCst), @@ -595,15 +630,38 @@ impl Pi { _ if stored(off) => self.state.lock().w[off as usize / 8], _ => { self.note(off, false); - return (None, false); + return None; } }; - (Some(value), latched) + Some(value) } /// A doubleword store at `off`. Whether the comparators or the counter - /// moved, so the caller re-arms the host timer. The caller publishes. - pub(super) fn write(&self, off: u64, value: u64, now: u64) -> bool { + /// moved, so the caller re-arms the host timer. A store that can change + /// an interrupt input publishes under `out`. + pub(super) fn write(&self, off: u64, value: u64, now: u64, levels: &Levels) -> bool { + match off { + reg::GRAPHICS_BASE..reg::GRAPHICS_END if off % 8 == 0 => { + let n = ((off - reg::GRAPHICS_BASE) / 8) as u32; + debug_assert!(n < PI_GFX_REGS); + self.state.lock().gfx.write(n, value); + false + } + reg::CREDIT_MODE => { + self.state.lock().credit_mode = value & 1; + false + } + _ => { + let mut out = self.out.lock(); + let moved = self.store(off, value, now); + self.drive(&mut out, levels); + moved + } + } + } + + /// Any other store, with `out` held. + fn store(&self, off: u64, value: u64, now: u64) -> bool { let index = |base: u64| ((off - base) / 8) as usize; match off { reg::OFFSET_0010 => self.region.store(value, SeqCst), @@ -624,11 +682,11 @@ impl Pi { reg::OFFSET_00E8 => self.request_enable.store(value, SeqCst), reg::RT_STATUS_A | reg::RT_STATUS_B => { // Latch a deadline that has passed first, so the clear takes it. - self.sync(now); + self.catch_up(now); self.rt_status[index(reg::RT_STATUS_A)].store(0, SeqCst); } reg::PROFILE_STATUS_A | reg::PROFILE_STATUS_B => { - self.sync(now); + self.catch_up(now); self.profile_status[index(reg::PROFILE_STATUS_A)].store(0, SeqCst); } reg::RT_ENABLE_A | reg::RT_ENABLE_B | reg::PROFILE_ENABLE_A | reg::PROFILE_ENABLE_B => { @@ -684,12 +742,6 @@ impl Pi { self.set_next_due(&t, &o); return true; } - reg::GRAPHICS_BASE..reg::GRAPHICS_END if off % 8 == 0 => { - let n = ((off - reg::GRAPHICS_BASE) / 8) as u32; - debug_assert!(n < PI_GFX_REGS); - self.state.lock().gfx.write(n, value); - } - reg::CREDIT_MODE => self.state.lock().credit_mode = value & 1, _ => self.write_stored(off, value), } false @@ -760,7 +812,8 @@ impl Pi { /// The PI block's local reset at `now`. The controls and captures /// hardware keeps across it survive; the interrupt inputs, timers, /// counter and graphics registers start over. - pub(super) fn power_on(&self, now: u64) { + pub(super) fn power_on(&self, now: u64, levels: &Levels) { + let mut out = self.out.lock(); let mut s = self.state.lock(); let old = s.w; *s = State::new(); @@ -784,6 +837,8 @@ impl Pi { let time = o.time(now); self.counter.write(Origin { base: 0, epoch: time, paused: o.paused }); self.arm_all(&mut t, now); + drop(t); + self.drive(&mut out, levels); } } @@ -817,4 +872,4 @@ fn stored(off: u64) -> bool { } #[cfg(test)] -mod tests; +pub(super) mod tests; diff --git a/src/dev/bedrock/pi/tests.rs b/src/dev/bedrock/pi/tests.rs index eef4f7c4a..156f98b7f 100644 --- a/src/dev/bedrock/pi/tests.rs +++ b/src/dev/bedrock/pi/tests.rs @@ -58,7 +58,7 @@ fn the_pi_reports_interface_state_and_retains_registers_across_reset() { rig.chip.pi[0].error_summary.fetch_or(MEMORY_ERROR, Ordering::SeqCst); rig.w(reg::ERROR_SUMMARY_ALIAS, 3); rig.w(reg::RT_COMPARE_A, 0x24); - rig.chip.pi[0].power_on(0); + rig.chip.pi[0].power_on(0, &rig.chip.levels); assert_eq!(rig.r(reg::ERROR_SUMMARY), MEMORY_ERROR | 3); assert_eq!(rig.r(reg::CACHED_ALIAS), 9); assert_eq!(rig.r(reg::CPU_A_ENABLE), 1); @@ -223,8 +223,27 @@ fn arming_a_slot_with_no_processor_reaches_no_processor() { } impl Pi { - /// Hold both locks, for tests of the paths that must not take them. + /// Hold every lock, for tests of the paths that must not take them. pub(in crate::dev::bedrock) fn hold(&self) -> impl Sized + '_ { - (self.state.lock(), self.timer.lock()) + (self.out.lock(), self.state.lock(), self.timer.lock()) } } + +thread_local! { + static SAMPLED: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; +} + +/// Run `hook` on this thread each time a publish has sampled a slot's +/// IP2..IP6 and not yet written them, so a test can hold a publisher there. +pub(in crate::dev::bedrock) fn on_sample(hook: impl FnMut(u64) + 'static) { + SAMPLED.with(|h| *h.borrow_mut() = Some(Box::new(hook))); +} + +/// The publish's side of [`on_sample`]. +pub(super) fn sampled(want: u64) { + SAMPLED.with(|h| { + if let Some(hook) = h.borrow_mut().as_mut() { + hook(want); + } + }); +} diff --git a/src/dev/bedrock/tests.rs b/src/dev/bedrock/tests.rs index 48723ca41..8c600e291 100644 --- a/src/dev/bedrock/tests.rs +++ b/src/dev/bedrock/tests.rs @@ -1,5 +1,5 @@ //! Tests of the chip as a whole: what one unit's access does to another, -//! the lock-free interrupt publish, and the paths that take no lock. +//! the interrupt publish, and the paths that take no lock. use super::reg::{io, lb, md, pi, LB_BASE, MD_BASE, PI1_BASE}; use super::*; @@ -20,9 +20,8 @@ fn the_uart_line_reaches_both_interfaces() { } /// Pending-modify stores, mask stores and a reflected level race on four -/// threads. Each publish is lock-free; once every writer has stopped, the -/// processor's word matches what the registers say, whatever the -/// interleaving. +/// threads. Once every writer has stopped, the processor's word matches +/// what the registers say, whatever the interleaving. #[test] fn racing_stores_leave_the_word_matching_the_registers() { const ROUNDS: u64 = 20_000; @@ -56,6 +55,69 @@ fn racing_stores_leave_the_word_matching_the_registers() { assert_eq!(rig.cause(), 1); } +/// One rise and one fall of a pending level, published by three threads in +/// the worst order: A sets the level and is held after sampling it, C +/// publishes the same rise, then B clears the level and publishes the fall, +/// all before A writes what it sampled. The processor must see the one rise +/// only: A's late sample must not bring the bit back after B's clear. +#[test] +fn a_late_publisher_brings_no_level_back() { + use std::sync::mpsc::channel; + use std::time::Duration; + let rig = Arc::new(Rig::new()); + rig.w(pi::OFFSET_00A8, 1 << 9); + // IP2 as each publisher saw it, in order. + let seen = Arc::new(parking_lot::Mutex::new(vec![rig.cause() & 1])); + let watch = |rig: &Arc, seen: &Arc>>| { + let (rig, seen) = (rig.clone(), seen.clone()); + move || seen.lock().push(rig.cause() & 1) + }; + let (held, is_held) = channel(); + let (release, released) = channel::<()>(); + let a = { + let (rig, log) = (rig.clone(), watch(&rig, &seen)); + std::thread::spawn(move || { + let mut first = true; + super::pi::tests::on_sample(move |want| { + log(); + if first && want & 1 != 0 { + first = false; + held.send(()).unwrap(); + // A publisher that holds the PI's lock here keeps B + // out until this gives up. + let _ = released.recv_timeout(Duration::from_millis(300)); + } + }); + rig.w(pi::PENDING_MODIFY, 0x109); + }) + }; + is_held.recv().unwrap(); + let c = { + let (rig, log) = (rig.clone(), watch(&rig, &seen)); + std::thread::spawn(move || { + super::pi::tests::on_sample(move |_| log()); + rig.w(pi::OFFSET_00A8, 1 << 9); + }) + }; + c.join().unwrap(); + let b = { + let (rig, log) = (rig.clone(), watch(&rig, &seen)); + std::thread::spawn(move || { + let mut after = log.clone(); + super::pi::tests::on_sample(move |_| log()); + rig.w(pi::PENDING_MODIFY, 0x009); + after(); + let _ = release.send(()); + }) + }; + b.join().unwrap(); + a.join().unwrap(); + let mut seen = seen.lock().clone(); + seen.push(rig.cause() & 1); + let rises = seen.windows(2).filter(|w| w == &[0, 1]).count(); + assert_eq!((rises, rig.cause()), (1, 0), "IP2 as published: {seen:?}"); +} + /// Run `work` on another thread while this one holds every lock of the /// chip; it must finish anyway. fn without_locks(rig: &Arc, work: impl FnOnce(&Rig) + Send + 'static) { @@ -81,17 +143,18 @@ fn without_locks(rig: &Arc, work: impl FnOnce(&Rig) + Send + 'static) { assert!(result.is_ok(), "the path waited for a lock"); } -/// The PI paths a running guest takes most take no lock: a mask store, a -/// pending-modify store and a counter read, each publishing as it goes. +/// The PI reads a running guest makes most take no lock: the pending +/// status, a mask and the counter. Stores that change an interrupt input +/// publish under the PI's lock. #[test] -fn the_pi_hot_paths_take_no_lock() { +fn the_pi_hot_reads_take_no_lock() { let rig = Arc::new(Rig::new()); + rig.w(pi::OFFSET_00A8, 1 << 9); + rig.w(pi::PENDING_MODIFY, 0x109); without_locks(&rig, |rig| { - rig.w(pi::OFFSET_00A8, 1 << 9); - rig.w(pi::PENDING_MODIFY, 0x109); + assert_eq!(rig.r(pi::PENDING_STATUS_A), 1 << 9); + assert_eq!(rig.r(pi::OFFSET_00A8), 1 << 9); assert_eq!(rig.cause(), 1); - rig.w(pi::PENDING_MODIFY, 0x009); - assert_eq!(rig.cause(), 0); rig.set_time(8000); assert_eq!(rig.r(pi::COUNTER), 10); assert_eq!(rig.regs.read32(pi::COUNTER + 4).data, 10); @@ -232,10 +295,11 @@ fn a_reflected_level_follows_its_register_under_racing_stores() { } } -/// The crossbar's paths take no lock either: an inbound interrupt packet, -/// an 8-byte inbound memory write and read, and PIO to a widget. +/// The crossbar's data paths take no lock either: an 8-byte inbound memory +/// write and read, and PIO to a widget. An inbound interrupt packet +/// publishes under the PI's lock. #[test] -fn the_inbound_and_pio_paths_take_no_lock() { +fn the_inbound_dma_and_pio_paths_take_no_lock() { struct Stub; impl BusDevice for Stub { fn read64(&self, _: u64) -> BusRead64 { @@ -246,8 +310,8 @@ fn the_inbound_and_pio_paths_take_no_lock() { rig.w(MD_BASE + md::CONFIG, 0x11); rig.chip.connect_widget(8, Arc::new(Stub)); rig.w(pi::OFFSET_00A8, 1 << 9); + assert_eq!(rig.chip.inbound_write(15, Some(0), packet_address(0, 0), 8, 0x109), BUS_OK); without_locks(&rig, |rig| { - assert_eq!(rig.chip.inbound_write(15, Some(0), packet_address(0, 0), 8, 0x109), BUS_OK); assert_eq!(rig.cause(), 1); assert_eq!(rig.chip.inbound_write(14, None, 0x1000, 8, 0x1234), BUS_OK); assert_eq!(rig.chip.inbound_read(14, None, 0x1000, 8).data, 0x1234); From aa38f7378f62037d964a56f5e6c60c07d1149a15 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:29:05 -0700 Subject: [PATCH 02/33] ohci: drive the interrupt line under the state lock The controller read its interrupt level under the state lock, let the lock go, drove the line, then took the lock again and drove once more if the level had moved. With three threads that can raise the line twice for one interrupt: a thread that read the level high and was held before driving it lands its high after another thread has already driven the rise and a third has acknowledged and driven the fall. A host bridge sends a set packet on every rise, so the processor gets an interrupt for a status the guest already cleared. The new test holds that late driver; against the old code it saw two rises. HACKING.md's Interrupts section asks each controller to drive its line under its own lock. The state now keeps the level last driven and the line is driven only on a change, with the lock held, so the levels reach the line in the order the state changed. Nothing above the controller calls back into it: the PCI interrupt combiner and the bridges behind it only send packets up. The old test of a parent that acknowledges the interrupt from inside the line call goes, since that parent is the one this ordering forbids, and no machine has it. --- src/dev/ohci.rs | 18 ++++++---- src/dev/ohci/tests.rs | 80 ++++++++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/dev/ohci.rs b/src/dev/ohci.rs index 214b5d3d3..93b3b770f 100644 --- a/src/dev/ohci.rs +++ b/src/dev/ohci.rs @@ -79,6 +79,9 @@ struct State { now_ms: u64, active_fi: u32, bit_credit: u64, + /// The level last driven onto the line; `None` until the first drive + /// after a reset, which always reaches the line. + out: Option, } impl State { fn new() -> Self { @@ -97,6 +100,7 @@ impl State { now_ms: 0, active_fi: 0x2edf, bit_credit: 0, + out: None, } } fn soft_reset(&mut self) { @@ -232,17 +236,17 @@ impl Ohci { }; self.advance_frames(elapsed); } + /// Drive the line to the level the state gives, under the state lock, + /// when it differs from the level last driven. The lock orders the + /// levels as the state changed; the parent never calls back down. fn drive_irq(&self) { - loop { - let level = self.state.lock().irq(); + let mut s = self.state.lock(); + let level = s.irq(); + if s.out != Some(level) { + s.out = Some(level); if let Some(line) = self.pci.irq() { line.set(level); } - // A callback may acknowledge the interrupt or another writer may - // change it while delivery is outside the state lock. - if self.state.lock().irq() == level { - break; - } } } #[cfg(test)] diff --git a/src/dev/ohci/tests.rs b/src/dev/ohci/tests.rs index 152b7da01..318ab68b7 100644 --- a/src/dev/ohci/tests.rs +++ b/src/dev/ohci/tests.rs @@ -472,33 +472,73 @@ fn operational_entry_sets_overflow_and_loads_the_interval_toggle() { } } -/// An interrupt handler can acknowledge a line while the controller raises it. +/// One rise and one fall of the interrupt, driven by three threads in the +/// worst order: A raises it and is held before its level reaches the line, +/// C drives the same rise, then B acknowledges it and drives the fall, all +/// before A's level lands. The line must see the one rise only: A's late +/// level must not raise it again after B's acknowledgement. #[test] -fn reentrant_irq_acknowledgement_leaves_final_line_clear() { - struct Ack { - hc: Weak, - level: AtomicBool, - once: AtomicBool, +fn a_late_driver_raises_the_line_once() { + use std::sync::mpsc::{channel, Receiver, Sender}; + use std::time::Duration; + struct Line { + levels: Mutex>, + held: Mutex>>, + release: Mutex>>, } - impl crate::irq::InterruptController for Ack { + impl crate::irq::InterruptController for Line { fn set_line(&self, _: u32, level: bool) { - if level && !self.once.swap(true, Ordering::SeqCst) { - self.hc.upgrade().unwrap().bar_write(0, 0xc, RHSC, 4); + if level && std::thread::current().name() == Some("a") { + if let Some(held) = self.held.lock().take() { + held.send(()).unwrap(); + // A driver that holds the controller's lock here keeps + // B out until this gives up. + let release = self.release.lock().take().unwrap(); + let _ = release.recv_timeout(Duration::from_millis(300)); + } } - self.level.store(level, Ordering::SeqCst); + self.levels.lock().push(level); } } - let h = Ohci::new(); - let ack = Arc::new(Ack { - hc: Arc::downgrade(&h), - level: AtomicBool::new(false), - once: AtomicBool::new(false), + let (held, is_held) = channel(); + let (release, released) = channel(); + let line = Arc::new(Line { + levels: Mutex::new(vec![]), + held: Mutex::new(Some(held)), + release: Mutex::new(Some(released)), }); - h.state().connect_irq(IrqLine::new(ack.clone(), 0)); - h.attached(); - h.bar_write(0, 0x10, 0x80000040, 4); - h.attach(0, Function::new(UsbOutcome::Nak)).unwrap(); - assert!(!ack.level.load(Ordering::SeqCst)); + let h = Ohci::new(); + h.state().connect_irq(IrqLine::new(line.clone(), 0)); + // Interrupts on, for the ownership change alone. + h.bar_write(0, 0x10, 0x8000_0000 | 1 << 30, 4); + let spawn = |name: &str, work: Box| { + std::thread::Builder::new().name(name.into()).spawn(work).unwrap() + }; + let a = spawn("a", Box::new({ + let h = h.clone(); + move || h.bar_write(0, 8, 8, 4) + })); + is_held.recv().unwrap(); + spawn("c", Box::new({ + let h = h.clone(); + move || h.attached() + })) + .join() + .unwrap(); + spawn("b", Box::new({ + let h = h.clone(); + move || { + h.bar_write(0, 0xc, 1 << 30, 4); + let _ = release.send(()); + } + })) + .join() + .unwrap(); + a.join().unwrap(); + let levels = line.levels.lock().clone(); + let rises = std::iter::once(false).chain(levels.iter().copied()).collect::>() + .windows(2).filter(|w| !w[0] && w[1]).count(); + assert_eq!((rises, levels.last().copied()), (1, Some(false)), "line levels: {levels:?}"); } /// The guest sees the documented write masks, HCR exemptions, and ownership bit. From 20d32e8856144455dfb88858da764d5473752302 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:28:02 -0700 Subject: [PATCH 03/33] ip34: derive the board word from the processor The PROM picks a CPU speed-table row from the board word's low nibble and then checks the flash record's clocks, mode word and machine type against that row. The word was a hard-coded 0x4004, which only agrees with the 500 MHz, 2 MB part by accident: a different clock behind the same nibble would print "CPU configuration values mismatch". Hold the PROM's table (500 MHz/2 MB, 600/4, 700/4, 800/4, 900/8) in one place and look the word up from the part's clock and cache size, failing the build when the part has no row. A test reads the word through the processor's bus and pins the nibble to the part and to the flash record's clock, and checks each table row and the nibbles the PROM rejects. Behaviour is unchanged for the one processor the Fuel offers. --- src/ip34_machine.rs | 60 ++++++++++++++++++++++++++++++++++++++++----- src/ip35/tests.rs | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/ip34_machine.rs b/src/ip34_machine.rs index d14650d6d..72c96ef93 100644 --- a/src/ip34_machine.rs +++ b/src/ip34_machine.rs @@ -436,12 +436,60 @@ pub const CPUS: &[&Part] = &[&parts::R14000_500]; /// type once here. pub(crate) type FuelCpu = MipsExecutor; -/// The board-id word. Bits 15:12 are the Fuel's product code, 4, as OpenBSD -/// reads them (IP35_FUEL, sys/arch/sgi/sgi/ip27.h). The low bits are chosen -/// to match what PROM 6.210 expects, not read from a board: with them and the -/// flash record's machine type 2 the PROM's CPU configuration check passes, -/// as on a real Fuel. A real board's bits 11:0 are unknown. -const FUEL_BOARD_ID: u64 = 0x4004; +/// A row of the PROM's CPU speed table, found by the board-id word's low +/// nibble when its class (bits 15:12) is 4, the Fuel's. The PROM prints +/// "Unknown PIMM_PSC type" for a nibble with no row (0, 5, f) and falls back +/// to zeros. The hub clock is 200 MHz in every row (established, from the +/// PROM's code). That the nibble is the processor module's own code is +/// inferred; the rows say nothing of what else the word holds. +pub(crate) struct SpeedRow { + pub(crate) nibble: u64, + pub(crate) mhz: u32, + pub(crate) l2_kb: u32, +} + +/// Every row of the PROM 6.210 table (established). Two nibbles share 500 MHz +/// (4 and 6) and two 600 MHz (7 and 8); the first listed is the one used. +/// The 700 MHz row is the R16000. +pub(crate) const SPEED_ROWS: [SpeedRow; 7] = [ + SpeedRow { nibble: 0x4, mhz: 500, l2_kb: 2048 }, + SpeedRow { nibble: 0x6, mhz: 500, l2_kb: 2048 }, + SpeedRow { nibble: 0x7, mhz: 600, l2_kb: 4096 }, + SpeedRow { nibble: 0x8, mhz: 600, l2_kb: 4096 }, + SpeedRow { nibble: 0x3, mhz: 700, l2_kb: 4096 }, + SpeedRow { nibble: 0xc, mhz: 800, l2_kb: 4096 }, + SpeedRow { nibble: 0x2, mhz: 900, l2_kb: 8192 }, +]; + +/// The class bits 15:12 of the board-id word for a Fuel. The PROM masks them +/// and compares 0x2000, 0x4000, 0x8000 and 0xc000; the kernel compares 4, 8 +/// and c (established). OpenBSD names 4 IP35_FUEL. +const FUEL_BOARD_CLASS: u64 = 0x4000; + +/// The board-id word of a Fuel built with `part`, or `None` when the PROM's +/// speed table has no row for its clock and secondary cache. The PROM checks +/// the flash record's mode word, clocks, timing bytes and machine type against +/// this row, so the word has to follow the processor. +/// +/// Bits 11:4 and 63:16 of a real board's word are unknown; they read zero +/// here and the PROM does not look at them. +pub(crate) const fn board_word(part: &Part) -> Option { + let mut i = 0; + while i < SPEED_ROWS.len() { + let row = &SPEED_ROWS[i]; + if row.mhz == part.mhz && row.l2_kb == part.l2_kb { + return Some(FUEL_BOARD_CLASS | row.nibble); + } + i += 1; + } + None +} + +/// The word for the Fuel's only processor, [`CPUS`]`[0]`. +pub(crate) const FUEL_BOARD_ID: u64 = match board_word(&parts::R14000_500) { + Some(word) => word, + None => panic!("the Fuel's processor has no row in the PROM's speed table"), +}; /// Two 256 MiB banks, locational banks 0 and 1. const FUEL_BANKS_MIB: [u32; 2] = [256, 256]; diff --git a/src/ip35/tests.rs b/src/ip35/tests.rs index 9c96dd964..037a1be2c 100644 --- a/src/ip35/tests.rs +++ b/src/ip35/tests.rs @@ -105,6 +105,51 @@ fn the_fuel_runs_the_r14000_at_revision_2_3() { .expect("thread panicked"); } +/// The board-id word's low nibble selects the PROM's speed-table row, and the +/// flash record has to agree with that row's clock; the PROM reports a +/// mismatch otherwise. Read through the processor's bus at the local +/// `0x9000000014000000`. +#[test] +fn the_board_word_names_the_speed_row_of_the_fuels_processor() { + use crate::cpu::parts; + use crate::ip34_machine::{board_word, CPUS, FUEL_BOARD_ID, SPEED_ROWS}; + let board = crate::ip34_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + let word = board.bus.read64(0x1400_0000); + assert_eq!(word.status, BUS_OK); + assert_eq!(word.data, FUEL_BOARD_ID); + for part in CPUS { + assert_eq!(board_word(part), Some(word.data), "{}", part.name()); + let row = SPEED_ROWS.iter().find(|r| r.nibble == word.data & 0xf).expect("a row for the nibble"); + assert_eq!((row.mhz, row.l2_kb), (part.mhz, part.l2_kb)); + assert_eq!(u64::from(crate::dev::bedrock_flash::FirmwareRecord::FUEL.cpu_clock_hz), u64::from(part.mhz) * 1_000_000); + } + // Class 4 is the Fuel; the nibble 4 is the 500 MHz, 2 MB module. + assert_eq!(word.data >> 12 & 0xf, 4); + assert_eq!(word.data, 0x4004); +} + +/// The PROM's table, row by row (read from the PROM's code), and the nibbles +/// with no row. +#[test] +fn the_speed_table_maps_each_established_row_and_refuses_the_rest() { + use crate::cpu::parts::{Part, R14000_500}; + use crate::ip34_machine::{board_word, SPEED_ROWS}; + let part = |mhz, l2_kb| Part { mhz, l2_kb, ..R14000_500 }; + let rows = [(500, 2048, 0x4004), (600, 4096, 0x4007), (700, 4096, 0x4003), (800, 4096, 0x400c), (900, 8192, 0x4002)]; + for (mhz, l2_kb, word) in rows { + assert_eq!(board_word(&part(mhz, l2_kb)), Some(word), "{mhz} MHz, {l2_kb} KB"); + } + // A clock with the wrong cache, or a clock off the table, has no row. + assert_eq!(board_word(&part(500, 4096)), None); + assert_eq!(board_word(&part(600, 2048)), None); + assert_eq!(board_word(&part(400, 2048)), None); + // The nibbles the PROM reports as "Unknown PIMM_PSC type". Others are + // simply not in the table here: the PROM's table was read, not these. + for nibble in [0x0, 0x5, 0xf] { + assert!(SPEED_ROWS.iter().all(|r| r.nibble != nibble), "{nibble:#x}"); + } +} + #[test] fn narrow_stores_carry_the_register_to_devices_that_ask_for_it() { let mut bus = Ip35Bus::new(Physical::new(Vec::new())); From 27789a81846b53231fe2a414c830de739655f311 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:27:49 -0700 Subject: [PATCH 04/33] bedrock_flash: mark which record fields the PROM establishes The boot-flash record's layout and the fields the PROM's CPU configuration check compares were left as "chosen" or described only in part. Document the whole 80-byte record, that machine type 2 holds for every class-4 speed row, and that the SN11 name and the 500 MHz mode word are not read from a board (a real 800 MHz Fuel's word was 0x549baf85). --- src/dev/bedrock_flash.rs | 44 +++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/dev/bedrock_flash.rs b/src/dev/bedrock_flash.rs index 2388c363b..32b8f84b7 100644 --- a/src/dev/bedrock_flash.rs +++ b/src/dev/bedrock_flash.rs @@ -33,29 +33,45 @@ fn sum(bytes: &[u8]) -> u8 { /// The fields of the PROM's configuration block that depend on the board. /// -/// The PROM reads them from flash before it touches the processor, so they -/// must agree with the CPU the machine builds: a mismatch in the boot-mode -/// word sends it to its error routine before it initialises GP. The values -/// are not a measured flash dump; each record's doc says where they come -/// from. +/// The 80-byte record at flash 0x60 holds, in order: +0x00 0x15, +0x04 the +/// CPU mode word, +0x08 "ip27conf", +0x10 CPU Hz, +0x18 bus Hz, +0x20 +/// 0x1312d0, +0x28 1, +0x2c flash timing 0x10, +0x30 machine type, +0x34 0, +/// +0x3c write timing 0x10 (established from the PROM). "CPU configuration +/// values mismatch" compares +0x00, 0x04, 0x08, 0x10, 0x18, 0x28, 0x2c, 0x30 +/// and 0x3c against the board EEPROM's record, then checks the speed table, +/// so the machine type, mode word, both clocks and the timing bytes all have +/// to agree with the row the board-id word selects. This struct sets the +/// mode word, CPU Hz and machine type; the PROM image carries the rest, the +/// 200 MHz hub clock among them (the same in every speed row). +/// +/// The PROM reads the record before it touches the processor, so it must +/// agree with the CPU the machine builds: a mismatch in the boot-mode word +/// sends it to its error routine before it initialises GP. The values are +/// not a measured flash dump of a 500 MHz Fuel; each field's doc says how +/// far it is known. A real 800 MHz Fuel's mode word was 0x549baf85. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FirmwareRecord { /// The CPU boot-mode word the PROM compares with the processor's mode. /// Its bits 24:0 are the mode inputs the processor latches into CP0 - /// Config at reset (the R10000 family's reset-mode inputs). + /// Config at reset (the R10000 family's reset-mode inputs). The 500 MHz + /// value is chosen: this PROM accepts it with board nibble 4, but it was + /// not read from a 500 MHz Fuel. The 800 MHz row's real value, 0x549baf85, + /// differs from it in bits 16 and 10:9. pub cpu_mode_word: u32, - /// The processor clock the PROM reports and times against, in Hz. + /// The processor clock the PROM reports and times against, in Hz. It is + /// the clock of the speed-table row the board-id word selects. pub cpu_clock_hz: u32, /// The record's word at +0x30, which the PROM's start-up check compares /// with a machine class of its own. When the node board's L1 record /// carries a CPU configuration, the PROM derives that class from the - /// board word's top nibble (0x4 gives 2, 0x8 gives 1, any other 0; read - /// from the stock PROM 6.210's code, established for that PROM). The - /// Fuel's record is 2, SN11: a Fuel's PROM flash session takes 2 from - /// "(0)SN1 (1)SN10 (2)SN11 (3)SN12" and its IRIX logs "Selecting SN11" - /// (just.graphica.com.au/tips/sgi-o350-chimera-notes). With 1, PROM - /// 6.210 prints "CPU configuration values mismatch", which a real Fuel's - /// console does not. + /// board word's top nibble (0x4 gives 2, 0x8 gives 1, any other 0). + /// Established from the stock PROM 6.210's code: the class is 2 for every + /// row of the class-0x4000 speed table, so the Fuel's record is 2. With 1, + /// PROM 6.210 prints "CPU configuration values mismatch", which a real + /// Fuel's console does not. The name SN11 for type 2 is inferred from a + /// public flash transcript only: a Fuel's PROM flash session takes + /// 2 from "(0)SN1 (1)SN10 (2)SN11 (3)SN12" and its IRIX logs "Selecting + /// SN11" (just.graphica.com.au/tips/sgi-o350-chimera-notes). pub machine_type: u32, } From d40023f30bf861e482932225d3c9572dc7ebc02f Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:27:49 -0700 Subject: [PATCH 05/33] bedrock: say what is known of the uncabled NI port status Only bit 1 of the port status is read, as link-up, by the PROM and IRIX; nothing reads bit 0 alone. Record that as established, and that bit 0 and bit 2 on a real uncabled Fuel stay unknown, so the next reader does not take the chosen bit 0 for a measurement. --- src/dev/bedrock/ni.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/dev/bedrock/ni.rs b/src/dev/bedrock/ni.rs index d124f9d32..a93c48b34 100644 --- a/src/dev/bedrock/ni.rs +++ b/src/dev/bedrock/ni.rs @@ -19,15 +19,22 @@ use parking_lot::Mutex; /// Length of the block-relative register aperture. pub(super) const SIZE: u64 = 0x2008; -/// Port status bits 1:0 of a node with no cable. Bit 1, link up, is clear: -/// real Fuels' PROM 6.210 consoles print "Local hub NUMAlink is down" +/// Port status of a node with no cable. +/// +/// Established from the PROM's and IRIX's code: only bit 1 is read, as +/// link-up, and nothing reads bit 0 alone. With no peer the PROM polls bit 1, +/// prints "Local hub NUMAlink is down", records code 0x9c and carries on, so +/// bit 1 is clear here. Real Fuels' consoles show the same line /// (comp.sys.sgi.admin, Atro Tossavainen 2005-04-13, Tristram Scott -/// 2007-07-18). Bit 0 is chosen to keep IRIX quiet and is unconfirmed: with -/// bits 1:0 at 0 and reset enable 0, the IRIX 6.5.30 bus-error handler prints +/// 2007-07-18). +/// +/// Unknown: bits 0 and 2 and the word as a whole on a real uncabled Fuel. +/// Bit 0 is our choice, kept because the IRIX 6.5.30 bus-error handler prints /// "Detected a link reset due to other partition reset" on every faulting -/// widget probe, and the 2005 Fuel's IRIX took a PIO read error with no such -/// line. Which nonzero value hardware reads is unknown; a PROM-monitor read -/// of this register on an uncabled node would settle it. +/// widget probe when this word and the reset-enable register are both zero, +/// and the 2005 Fuel's IRIX took a PIO read error with no such line. The +/// choice only has to keep the word nonzero with bit 1 clear. A read of this +/// register on an uncabled node would settle the real value. const PORT_STATUS_UNCABLED: u64 = 1; pub mod reg { From b2fa63af918751b21207f51e0b532526d39e0b7b Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:27:49 -0700 Subject: [PATCH 06/33] bridge: pin the XBridge table's upper half IRIX's driver treats address translation entries 512 to 1023 like 0 to 511 and zeroes them all at attach. Test that the head stores and reads back every entry, including the upper half, and that a reset zeroes the whole table. --- src/dev/bridge_tests.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/dev/bridge_tests.rs b/src/dev/bridge_tests.rs index c01a15070..af343b2b2 100644 --- a/src/dev/bridge_tests.rs +++ b/src/dev/bridge_tests.rs @@ -1954,6 +1954,24 @@ fn an_xbridge_head_identifies_itself_and_keeps_its_speed_strap() { } } +/// IRIX's driver treats all 1024 entries like the first 512 and zeroes them at +/// attach (established from the driver's code); the head keeps every one, so +/// the upper half stores and reads back like the lower, and a reset zeroes +/// the whole table. +#[test] +fn every_entry_of_an_xbridge_table_stores_and_a_reset_zeroes_them_all() { + let h = Head::new(); + for i in [0u64, 511, 512, 1023] { + h.win.write64(u64::from(ATE_BASE) + i * 8, 0x1234_0000_0000 | i); + assert_eq!(h.win.read32(u64::from(XBRIDGE_ATE_LOW_BASE) + i * 8 + 4).data, i as u32, "entry {i}"); + } + h.bridge.reset(); + for i in 0..1024u64 { + assert_eq!(h.win.read32(u64::from(XBRIDGE_ATE_LOW_BASE) + i * 8 + 4).data, 0, "entry {i} low"); + assert_eq!(h.win.read32(u64::from(ATE_BASE) + i * 8 + 4).data, 0, "entry {i} high"); + } +} + /// The head's table holds 1024 entries, written whole and read a half at a /// time; 512 of them map a window that repeats with bytes swapped at bit /// 29, and an entry's own swap bit swaps too. A transfer through an entry From b318cf8bc3fda8f69183d022c2f4d40b2a49c0d3 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 17:27:49 -0700 Subject: [PATCH 07/33] docs: mark the Fuel's board word, flash record and NI status as established or chosen The limits section named the board word's low bits and NI status bit 0 as chosen. Say which parts of each are read from PROM 6.210 and IRIX behaviour, which are inferred, and what is unknown, and describe the flash record and the XBridge table the same way. --- docs/fuel-ip34.md | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/fuel-ip34.md b/docs/fuel-ip34.md index d853ff6df..3d204fae7 100644 --- a/docs/fuel-ip34.md +++ b/docs/fuel-ip34.md @@ -81,11 +81,37 @@ approximations are in ## Limits - Machine snapshots are unsupported. -- Two values are chosen to match what the guests expect, not read from a - board: the board-id word's bits 11:0 (`0x4004`; bits 15:12 are the - Fuel's product code as OpenBSD reads it), which PROM 6.210's CPU - configuration check accepts, and NI port status bit 0, set so IRIX - 6.5.30 prints no link-reset notice on a faulting widget probe. +- The board-id word (`0x4004`) and NI port status are partly chosen, not + read from a board. For the board word, the PROM masks bits 15:12 as the + class and compares 0x2000, 0x4000, 0x8000 and 0xc000, and the kernel + compares 4, 8 and c (established); 4 is the Fuel. For class 4 the low + nibble selects a row of the PROM's CPU speed table (established): 4 or 6 + is 500 MHz with 2 MB of secondary cache, 7 or 8 is 600 MHz with 4 MB, 3 is + 700 MHz (R16000) with 4 MB, c is 800 MHz with 4 MB and 2 is 900 MHz with + 8 MB; the hub clock is 200 MHz in every row. Nibbles 0, 5 and f have no + row, and the PROM prints "Unknown PIMM_PSC type" for them. That the nibble + is the processor module's code is inferred. The emulator derives it from + the processor's clock and cache, so the board is 4 for the Fuel's 500 MHz + R14000A, and a test pins the mapping. Bits 11:4 and 63:16 of a real + board's word are unknown and read zero. Only bit 1 of NI port status is + read by the PROM and IRIX, as link-up (established): with no peer the PROM + prints "Local hub NUMAlink is down", records code 0x9c and continues, and + the model reads bit 1 clear. Bits 0 and 2 on a real uncabled Fuel are + unknown; bit 0 is set because without it IRIX 6.5.30 prints a link-reset + notice on every faulting widget probe. +- The boot flash's 80-byte record at flash 0x60 holds the CPU mode word, CPU + and bus clocks, the machine type and the flash timing bytes. The PROM + compares them with the board's EEPROM record and then the speed table, and + prints "CPU configuration values mismatch" if any disagree, so the record + follows the board word's row (established). The machine type is 2 for every + class-4 row (established); the name SN11 for it comes from a public flash + transcript only (inferred). The 500 MHz mode word `0x549aa985` is chosen: + the PROM accepts it, but it was not read from a 500 MHz Fuel (a real + 800 MHz Fuel's was `0x549baf85`). +- The XBridge's 1024-entry address translation table is stored whole and + zero at reset. IRIX's driver treats entries 512 to 1023 like 0 to 511 and + zeroes them at attach (established); the model still faults DMA through entries + 512 and up, a limit the driver's treatment does not settle (open). - The Hub-special head windows read zero and acknowledge writes with no side effect. PROM 6.210 writes the 64-bit word `0x2000` at offsets `0x0` and `0x40` before it finds DRAM; what those registers do is From 3aa8c50da56a214b4684f492488469d7d4598e15 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 6 Nov 2026 13:59:00 -0700 Subject: [PATCH 08/33] cpu: add the R16000 parts The Tezro ships with R16000s at 700, 800, 900 and 1000 MHz, and the part table carried no R16000. Add the family and one Part per shipped clock: r16000_700 (rev 2.1, which the _EARLY const marks so the Fuel's later rev 2.2 700 MHz part keeps the plain name), r16000_800 (2.2), r16000_900 and r16000_1000 (both 3.0), with 4, 4, 8 and 16 MB of secondary cache. hinv prints an R16000's major revision two below the PRId register's major, as the Linux and OpenBSD CPU probes and PROM 6.210's printers do. Part::prid undoes that in one family match, so 2.1, 2.2 and 3.0 read 0x0f41, 0x0f42 and 0x0f50. Only 0x0f50 on a 1 GHz part is on record raw (a PROM banner beside hinv's 3.0); the others follow the rule. The secondary-cache size is a parameter of the shadow cache type, so the executor, the translator and the cache know it when they are built. The three sizes are three aliases of one shadow cache (Config SS codes 3, 4 and 5), and with_cpu! picks the alias from the part's l2_kb once at machine construction, so nothing is read or branched on at run time. The R16000 shares the R10000 family's programming model, as a model choice, since no manual for it was retained. Its FIR is 0x0900, the one R14000 value on record, also a model choice. No machine lists these parts yet. Tests pin the PRIds, run each alias through the interpreter, the Config, TLB-write and LWR checks and the jitv2 equivalence check, and pin the type with_cpu! builds per part. The cached-view line tests run on the three R16000 types too: a primary data line and a secondary line hold their page in the CPU view, Index Store Data fills it, and dropping the lines ends it. --- CHANGELOG.md | 1 + src/cpu/build.rs | 21 ++++++++++- src/cpu/jitv2/equiv_test.rs | 16 ++++++++ src/cpu/mips_cache_shadow.rs | 71 ++++++++++++++++++++++++++++++++++++ src/cpu/mips_cache_v2.rs | 3 +- src/cpu/mips_exec_test.rs | 12 ++++++ src/cpu/parts.rs | 44 +++++++++++++++++++--- 7 files changed, 161 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96858c824..97f4059af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,7 @@ hashes are given where a change is easiest to understand by reading the commit. - The R10000 family has 44-bit virtual and 40-bit physical addresses, FrameMask and a 28-bit EntryLo PFN, picks the TLB refill vector by address space, invalidates overlapped TLB writes (Status.TS), and completes uncached stores nothing answers. Partial LWR keeps the upper word, and the cache diagnostics (Index Store Data, secondary tag mask, Config size) work. - `cpu` takes the parts each machine shipped with, such as `r12000_360` on the Octane, or a family name for the machine's own. The Indy and Indigo2 R4400 is now rev 5.0 at 150 MHz, the O2 R10000 rev 3.4 at 250 MHz, and an Indy no longer takes an R10000 (the R10000 Indigo2 is the IP28). The R12000 and R14000 are new. +- The R16000 is a family of its own, with four parts for the Tezro (`r16000_700` rev 2.1, `r16000_800` rev 2.2, `r16000_900` and `r16000_1000` rev 3.0) and a 4, 8 or 16 MB secondary cache picked once when a machine is built, so no run-time field or branch is added. `hinv` prints an R16000's major revision two below the PRId register's, and the part table undoes that in one place. The FIR is 0x0900, the one R14000 value on record; a model choice. No machine takes them yet. ### Onyx2 diff --git a/src/cpu/build.rs b/src/cpu/build.rs index 7a8e1f0e9..b1956a73d 100644 --- a/src/cpu/build.rs +++ b/src/cpu/build.rs @@ -7,7 +7,8 @@ /// construction so the hot path carries no per-model branch. `$shadow` picks /// the shadow caches for the R4400 and R5000, which also have the functional /// ones (`CacheModel`). `$o2` selects the O2's R10000 and R12000 types, whose -/// secondary caches have 64-byte lines where the others have 128. +/// secondary caches have 64-byte lines where the others have 128. The R16000's +/// secondary cache size is a type parameter, so `l2_kb` picks the type here, once. #[macro_export] macro_rules! with_cpu { ($part:expr, $shadow:expr, $o2:expr, $C:ident => $body:expr) => {{ @@ -23,6 +24,24 @@ macro_rules! with_cpu { (F::R10000, _, false) => { type $C = sh::R10000ShadowCache; $body } (F::R12000 | F::R14000, _, true) => { type $C = sh::R12000O2ShadowCache; $body } (F::R12000 | F::R14000, _, false) => { type $C = sh::R12000ShadowCache; $body } + (F::R16000, _, _) if $part.l2_kb == 8192 => { type $C = sh::R16000Scache8ShadowCache; $body } + (F::R16000, _, _) if $part.l2_kb == 16384 => { type $C = sh::R16000Scache16ShadowCache; $body } + (F::R16000, _, _) => { type $C = sh::R16000ShadowCache; $body } } }}; } + +#[cfg(test)] +mod tests { + use crate::cpu::mips_cache_v2::{CpuModel, MipsCache}; + use crate::cpu::parts::{R16000_1000, R16000_700_EARLY, R16000_800, R16000_900}; + + /// `l2_kb` picks the R16000 type: 4, 8 or 16 MiB of secondary cache. + #[test] + fn an_r16000_part_gets_the_cache_type_of_its_secondary_size() { + for (part, mib) in [(&R16000_700_EARLY, 4), (&R16000_800, 4), (&R16000_900, 8), (&R16000_1000, 16)] { + let (name, bytes) = crate::with_cpu!(part, true, false, C => (::NAME, ::L2_SIZE)); + assert_eq!((name, bytes >> 20), ("R16000", mib), "{}", part.name()); + } + } +} diff --git a/src/cpu/jitv2/equiv_test.rs b/src/cpu/jitv2/equiv_test.rs index 6e808364a..36b78f47a 100644 --- a/src/cpu/jitv2/equiv_test.rs +++ b/src/cpu/jitv2/equiv_test.rs @@ -4544,6 +4544,22 @@ mod tests { } } + /// The R16000 types take the R10000 family's LWR path, whatever their + /// secondary cache. + #[test] + fn jit_lwr_matches_the_interpreter_on_the_r16000() { + use crate::cpu::mips_cache_shadow::{R16000Scache16ShadowCache, R16000Scache8ShadowCache, R16000ShadowCache}; + use crate::cpu::mips_core::STATUS_KX; + fn check>>() { + let (interp, jit) = lwr_both::(STATUS_KX, 1); + assert_eq!(jit, interp, "{}", C::NAME); + assert_eq!(interp.gpr[2] >> 32, 0x1122_3344, "{} keeps the upper word", C::NAME); + } + check::(); + check::(); + check::(); + } + /// One instruction at 0x1000, interpreted and compiled, on CPU model `C` /// after `setup`. The JIT learns the model's choices from /// `install_jit_hooks`, as in production. diff --git a/src/cpu/mips_cache_shadow.rs b/src/cpu/mips_cache_shadow.rs index 584f761f5..9ef002f5b 100644 --- a/src/cpu/mips_cache_shadow.rs +++ b/src/cpu/mips_cache_shadow.rs @@ -610,6 +610,7 @@ impl< model::R4400 => "R4400", model::R5000 => "R5000", model::R12000 => "R12000", + model::R16000 => "R16000", _ => "shadow", }; const CACHE_KIND: &'static str = "shadow"; @@ -1055,6 +1056,27 @@ pub type R12000ShadowCache = pub type R12000Module4MShadowCache = ShadowCache<32768, 64, 32768, 32, 4194304, 128, true, 0x0000_0e35, 0x0000_0900, 64, { model::R12000 }>; +/// The R16000 with a 4 MB secondary cache, the Tezro's 700 and 800 MHz parts: +/// the R12000's geometry and programming model with the larger secondary (SS +/// code 3 in Config) and 128-byte secondary lines (SB: a model choice). The +/// PRId here is a placeholder: the part's own, `Part::prid`, is set at build +/// time, as for the R14000. FIR 0x0900, the one R14000 value on record; a +/// model choice: no R16000 FIR was found in the retained evidence. +pub type R16000ShadowCache = + ShadowCache<32768, 64, 32768, 32, 4194304, 128, true, 0x0000_0f41, 0x0000_0900, 64, { model::R16000 }>; + +/// The R16000 with an 8 MB secondary cache, the 900 MHz part's: the 4 MB type +/// with twice the secondary (SS code 4). The size is a type parameter so the +/// executor, the translator and the cache know it when they are built, and no +/// run-time field is read to find it. +pub type R16000Scache8ShadowCache = + ShadowCache<32768, 64, 32768, 32, 8388608, 128, true, 0x0000_0f50, 0x0000_0900, 64, { model::R16000 }>; + +/// The R16000 with a 16 MB secondary cache, the 1 GHz part's (SS code 5); +/// otherwise the 4 MB type. +pub type R16000Scache16ShadowCache = + ShadowCache<32768, 64, 32768, 32, 16777216, 128, true, 0x0000_0f50, 0x0000_0900, 64, { model::R16000 }>; + /// R12000 3.5 as the O2 has it: the Octane part with 64-byte secondary lines /// (SB clear). With 128-byte lines the O2 PROM panics before its banner /// (write TLB miss at 0x0); 64 boots. IRIX on the O2 never reads SB and sizes @@ -1294,6 +1316,20 @@ mod tests { } assert_eq!(run::(), 0); assert_eq!(run::(), 0x1_2345_6780); + assert_eq!(run::(), 0, "the R16000 follows the R12000 and R14000"); + } + + /// The three R16000 types differ in the secondary cache alone: size, the + /// SS code Config reads from it, and the placeholder PRId each starts + /// with; the PRId a part really presents is `Part::prid`'s. + #[test] + fn the_r16000_types_differ_only_in_the_secondary_cache() { + fn id() -> (u32, u32, usize, usize, &'static str, u32, u32, bool) { + (C::PRID, C::FIR, C::L2_SIZE, C::L2_LINE, C::NAME, C::VA_BITS, C::PA_BITS, C::R10K_CACHE_OPS) + } + assert_eq!(id::(), (0xf41, 0x900, 4 << 20, 128, "R16000", 44, 40, true)); + assert_eq!(id::(), (0xf50, 0x900, 8 << 20, 128, "R16000", 44, 40, true)); + assert_eq!(id::(), (0xf50, 0x900, 16 << 20, 128, "R16000", 44, 40, true)); } /// Geometry is what CP0 Config is built from, so it must be the real @@ -1634,6 +1670,41 @@ mod tests { assert_eq!(c.read::<4>(0, 0x6000).data, 0x600d); } + /// The R16000 parts keep the R10000 family's index operations: a valid + /// primary data line or secondary line holds its page in the CPU's view, + /// with 4, 8 and 16 MiB secondary caches, and Index Store Data fills it. + #[test] + fn the_r16000_types_hold_lines_in_the_view() { + fn run>>() { + let (space, mut banks) = PpMemSpace::with_bank_sizes(&[64]).unwrap(); + let space = Box::new(space); + let bank = Arc::new(banks.remove(0)); + let c = Box::new(C::from(bank.clone() as Arc)); + let v = space.add_cached_view(&[c.tcache_bitmap_ptr()]).unwrap(); + unsafe { + c.set_tcache_window(space.view_base(v)); + c.set_cached_view(&*space, v); + } + space.map_bank(0, 0, 64 << 20, 64 << 20).unwrap(); + bank.write32(0x2004, 0x5555_5555); + c.cache_op(C_R10K_ISD | CACH_PD, 0x8000_0004, 0xdead_beef); + c.cache_op(C_IST | CACH_PD, 0x8000_0000, pd_dirty(0x2000)); + assert_eq!(space.held_line_pages(v), vec![0x2000]); + assert_eq!(c.read::<4>(0, 0x2004).data, 0xdead_beef); + assert_eq!(bank.read32(0x2004).data, 0x5555_5555, "memory never saw the line"); + c.cache_op(C_R10K_ISD | CACH_SD, 0x8000_3008, 0x1111_2222_3333_4444); + c.cache_op(C_IST | CACH_SD, 0x8000_3000, sd_tag(0x3000, 2)); + assert_eq!(space.held_line_pages(v), vec![0x2000, 0x3000]); + assert_eq!(c.read::<4>(0, 0x3008).data, 0x3333_4444, "TagLo at the lower address"); + c.cache_op(C_IST | CACH_PD, 0x8000_0000, 0); + c.cache_op(C_IST | CACH_SD, 0x8000_3000, 0); + assert!(space.held_line_pages(v).is_empty()); + } + run::(); + run::(); + run::(); + } + /// Power-on drops every held page. A snapshot taken while lines hold /// pages is refused at restore. #[test] diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index e36fcd915..1c3488bdf 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -1516,11 +1516,12 @@ pub mod model { pub const R5000: u8 = 1; pub const R10000: u8 = 2; pub const R12000: u8 = 3; + pub const R16000: u8 = 4; /// The R10000 family: the R10000 and the parts that follow it, which share /// one programming model. pub const fn r10k_family(model: u8) -> bool { - matches!(model, R10000 | R12000) + matches!(model, R10000 | R12000 | R16000) } } diff --git a/src/cpu/mips_exec_test.rs b/src/cpu/mips_exec_test.rs index 9f22fd12b..9c938e5c7 100644 --- a/src/cpu/mips_exec_test.rs +++ b/src/cpu/mips_exec_test.rs @@ -673,6 +673,9 @@ mod tests { check::(); check::(); check::(); + check::(); + check::(); + check::(); } /// JIT store callouts carry the same source register as the interpreter. @@ -7614,6 +7617,11 @@ mod tests { assert_eq!(entry::(0xFFFF_C000), (0xC000, 0x0300_4000, (3 << 62) | (lo & !(3 << 32)))); assert_eq!(entry::(0), (0, 0xC0_0300_4000, (3 << 62) | lo)); assert_eq!(entry::(0xFFFF_C000), (0, 0x0300_4000, (lo & 0x3FFF_FFFF) | 0xC0)); + // The R16000 types share the R10000 family's TLB. + let masked = (3 << 62) | (lo & !(3 << 32)); + assert_eq!(entry::(0xFFFF_C000), (0xC000, 0x0300_4000, masked)); + assert_eq!(entry::(0xFFFF_C000), (0xC000, 0x0300_4000, masked)); + assert_eq!(entry::(0xFFFF_C000), (0xC000, 0x0300_4000, masked)); }); } @@ -7774,6 +7782,10 @@ mod tests { assert_eq!(ss_sb::(), (1, 1)); assert_eq!(ss_sb::(), (2, 1)); assert_eq!(ss_sb::(), (2, 0)); + // 4, 8 and 16 MB with 128-byte lines: SS codes 3, 4 and 5. + assert_eq!(ss_sb::(), (3, 1)); + assert_eq!(ss_sb::(), (4, 1)); + assert_eq!(ss_sb::(), (5, 1)); }); } } diff --git a/src/cpu/parts.rs b/src/cpu/parts.rs index b3d0479e3..2271e7173 100644 --- a/src/cpu/parts.rs +++ b/src/cpu/parts.rs @@ -14,6 +14,7 @@ pub enum Family { R10000 = 10000, R12000 = 12000, R14000 = 14000, + R16000 = 16000, } impl Family { @@ -24,7 +25,8 @@ impl Family { Self::R5000 => 0x23, Self::R10000 => 0x09, Self::R12000 => 0x0e, - Self::R14000 => 0x0f, + // The R16000 reuses the R14000's implementation byte. + Self::R14000 | Self::R16000 => 0x0f, } } } @@ -33,7 +35,8 @@ impl Family { pub struct Part { pub family: Family, pub mhz: u32, - /// Major and minor revision, as `hinv` prints them ("Revision: 2.7"). + /// Major and minor revision, as `hinv` prints them ("Revision: 2.7"). For + /// an R16000 the PRId's major is this major plus 2 (see [`Part::prid`]). pub rev: (u8, u8), /// Secondary cache in KB. The R5000 reads 0: its cache is not modelled. pub l2_kb: u32, @@ -56,17 +59,34 @@ pub const R14000_600: Part = Part { family: Family::R14000, mhz: 600, rev: (2, 4 /// `hinv` dumps show it. pub const R14000_500: Part = Part { mhz: 500, rev: (2, 3), ..R14000_600 }; +/// The Tezro's and Origin 350's earliest R16000: rev 2.1, 4 MB secondary cache. +/// A 700 MHz R16000 also shipped as rev 2.2, in the Fuel, so this one takes +/// the `_EARLY` suffix and the plain `R16000_700` stays free for that part +/// when the Fuel takes it. The configuration name `r16000_700` is this part. +pub const R16000_700_EARLY: Part = Part { family: Family::R16000, mhz: 700, rev: (2, 1), l2_kb: 4096 }; +pub const R16000_800: Part = Part { mhz: 800, rev: (2, 2), ..R16000_700_EARLY }; +pub const R16000_900: Part = Part { mhz: 900, rev: (3, 0), l2_kb: 8192, ..R16000_700_EARLY }; +pub const R16000_1000: Part = Part { mhz: 1000, rev: (3, 0), l2_kb: 16384, ..R16000_700_EARLY }; + /// Every part, for the bare-metal bench rig, which takes any. A name matches /// the first listed, so the rig runs the later silicon of a clock that has two. pub const ALL: &[&Part] = &[ &R4400_150, &R5000_180_EARLY, &R5000_180, &R10000_195, &R10000_195_EARLY, &R10000_250, - &R10000_180, &R12000_270, &R12000_360, &R12000_400, &R14000_600, + &R10000_180, &R12000_270, &R12000_360, &R12000_400, &R14000_600, &R16000_700_EARLY, &R16000_800, + &R16000_900, &R16000_1000, ]; impl Part { - /// Implementation, then major and minor revision. + /// Implementation, then major and minor revision, as the PRId register + /// holds them. `rev` is what `hinv` prints, and for an R16000 that is two + /// below the register's major: the Linux and OpenBSD CPU probes both + /// display an R16000's revision that way (`rev.0 + 2` here undoes it). pub const fn prid(&self) -> u32 { - self.family.implementation() << 8 | (self.rev.0 as u32) << 4 | self.rev.1 as u32 + let major = match self.family { + Family::R16000 => self.rev.0 + 2, + _ => self.rev.0, + }; + self.family.implementation() << 8 | (major as u32) << 4 | self.rev.1 as u32 } /// CP0 Count ticks at half the pipeline clock. @@ -108,4 +128,18 @@ mod tests { assert_eq!((R5000_180_EARLY.prid(), R5000_180.prid()), (0x2310, 0x2321)); assert_eq!(find(&[&R5000_180, &R10000_250, &R12000_400], "r10000"), Some(&R10000_250)); } + + /// An R16000 prints a major two below the register's: the Tezro's 2.1, + /// 2.2 and 3.0 are raw 0x41, 0x42 and 0x50, and the R14000 beside it in + /// the table keeps its own major. + #[test] + fn an_r16000_register_major_is_the_displayed_one_plus_two() { + assert_eq!( + (R16000_700_EARLY.prid(), R16000_800.prid(), R16000_900.prid(), R16000_1000.prid()), + (0x0f41, 0x0f42, 0x0f50, 0x0f50) + ); + assert_eq!(R14000_600.prid(), 0x0f24); + assert_eq!(find(&[&R16000_800, &R16000_1000], "r16000"), Some(&R16000_800)); + assert_eq!(find(&[&R16000_700_EARLY], "r16000_700"), Some(&R16000_700_EARLY)); + } } From 29392afac690af85897227f98bbefad6f494d7af Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 31 Oct 2026 08:26:55 -0700 Subject: [PATCH 09/33] ip35: compile the reset PROM from the PROM, not the RAM over it The IP35 PROM fetches its own code uncached at plain physical 0x1fcxxxxx. Bedrock serves that from the reset PROM, but the compile pool reads through the cached view, and once a memory bank covers the window the cached view answers RAM there. The memory test zeroes it, so the JIT compiled pages of NOPs in place of PROM routines and the PROM faulted (an instruction fetch bus error, or endless refills). Code pages are keyed by physical frame, so the cached and uncached fetches of that address were one page. The Fuel's 256 MiB banks never reach the window; larger banks do. Make them two addresses, the way the board decodes them. An uncached instruction fetch of the reset window is now presented to the bus at the same PROM at an address of its own (MipsExecutor::set_uncached_fetch_alias, called by the R16000 boards; RESET_WINDOW_FETCH, above every RAM slot). The JIT stays as it is: it keys, reads and tracks that page by address like any other, the flash device's generation counter invalidates it, and a store to RAM over the window never does. The alias wraps the translation the CPU installs when it is set, so a cached fetch, a data access and every other machine's fetch path are untouched. The board gives the executor a code bus (the cached view plus that window) through set_cached_bus. Only the R16000 model takes an alias (CpuModel::FETCH_ALIAS), so the translation update of every other model is as it was. An unconditional check there cost the R14000 interpreter host instructions. Counted with perf stat -e instructions:u \ iris-bench run --quick --deterministic --cpu r14000_600 the benchmark took 369.438 G with it against 367.938 G without it, 0.41% more. The evidence is partial. Which space a request reaches follows the processor's uncached attribute (ip35-address-map.md, processor-side address spaces, established), so an uncached and a cached access of one address are different requests. What attribute 0 returns at 0x1fc00000 is not established: no public source says how the reset request reaches flash, and the cached side there is open too. The split rests on what PROM 6.210's memory test needs, and the code and CHANGELOG say so. Tests: an uncached reset-vector fetch compiles the PROM's ADDIU and the cached frame compiles RAM's boundary word, and a flash write invalidates only the PROM's page; a cached fetch and an unaliased executor key the frame they name; uncached attributes 1 to 3 and addresses past the end of memory get no code page. --- CHANGELOG.md | 7 + HACKING.md | 11 ++ src/cpu/mips_cache_shadow.rs | 1 + src/cpu/mips_cache_v2.rs | 5 + src/cpu/mips_exec.rs | 76 +++++++++- src/ip34_machine.rs | 35 ++++- src/ip35.rs | 63 +++++++- src/ip35/node.rs | 22 ++- src/ip35/tests.rs | 268 +++++++++++++++++++++++++++++++++++ 9 files changed, 480 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f4059af..58202569c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,13 @@ hashes are given where a change is easiest to understand by reading the commit. - **The Fuel takes a VPro board**: `[graphics] board = "v12"` or `"v10"` fits the Octane's VPro model at crossbar widget 13. IRIX 6.5.30 with the VPro driver brings the X login screen up on it. +- **jitv2 compiles the reset PROM from the PROM.** The PROM runs from the reset + window uncached; once memory covers that window the cached view returns RAM + there, and the compile pool used to compile RAM's bytes in place of the + PROM's. The board now presents an uncached instruction fetch of that window at + an address of the PROM's own (`MipsExecutor::uncached_fetch_alias`), so its + code page is keyed, compiled and invalidated by address like any other. The + other machines are unchanged. ### Benchmark and test suites: one pinned compiler diff --git a/HACKING.md b/HACKING.md index 109c95231..bc8640087 100644 --- a/HACKING.md +++ b/HACKING.md @@ -776,6 +776,17 @@ code into host code with Cranelift. The concepts: pfn map publishes a slot after its claim, and inline stores bump generations atomically. Compiled code is freed only by a full flush, and every flush parks all CPUs first (`jitv2_flush_from_here`, `run_leader_flush`). +- **Uncached fetches the board decodes apart.** A page is keyed by the + physical address of the fetch. The IP35 reset PROM answers an uncached fetch + at 0x1fc00000 whatever RAM sits behind the cached view there, so a board whose + banks reach that window presents the fetch at an address of its own + (`uncached_fetch_alias`, which only the R16000 model takes) and the JIT sees + two different frames. Compiles read + that frame through the executor's cached bus (`set_cached_bus`, given + `Ip35Bus::code_view`), and its generation counter is the PROM device's, so a + flash command invalidates it and a store to RAM never does. The alias wraps the + translation (`update_translate_fn` installs the wrapper when one is set), so + the other machines' fetch paths are the ones they always had. - Compiled pages can optionally be cached on disk across runs (`[jitv2] cache`, [docs/jitv2-persistent-cache.md](docs/jitv2-persistent-cache.md)). diff --git a/src/cpu/mips_cache_shadow.rs b/src/cpu/mips_cache_shadow.rs index 9ef002f5b..d5d8198fb 100644 --- a/src/cpu/mips_cache_shadow.rs +++ b/src/cpu/mips_cache_shadow.rs @@ -616,6 +616,7 @@ impl< const CACHE_KIND: &'static str = "shadow"; const R10K_CACHE_OPS: bool = Self::R10K; const CVT_L_51_BITS: bool = MODEL == model::R10000; + const FETCH_ALIAS: bool = MODEL == model::R16000; } impl< diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index 1c3488bdf..c5489ff92 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -686,6 +686,11 @@ pub trait CpuModel: MipsCache { /// to match. An uncached SC succeeding is a model choice. Loads and fetches /// still fault. const POSTS_UNCACHED_STORES: bool = Self::R10K_FAMILY; + /// Whether a board may present an uncached instruction fetch of a window at + /// another address (`MipsExecutor::set_uncached_fetch_alias`). Off except + /// for the parts of the boards that need it, so the translation paths of + /// every other model carry no check for it. + const FETCH_ALIAS: bool = false; /// Bits `Index_Store_Tag` keeps for the secondary cache: all of them, except /// on the R10000 family, whose reserved tag bits read as zero (R10000 UM /// p. 270, figure 11-31). Kept are MRU (bit 63), the physical tag (TagHi 3:0 diff --git a/src/cpu/mips_exec.rs b/src/cpu/mips_exec.rs index dc77c4447..9b600d2e1 100644 --- a/src/cpu/mips_exec.rs +++ b/src/cpu/mips_exec.rs @@ -1311,6 +1311,15 @@ pub struct MipsExecutor { /// reach EntryLo and come out as physical address bits 32 and up. All /// ones (no fold) by default; each machine sets its own. pub phys_mask: u64, + /// A window of physical addresses the board decodes differently for an + /// uncached instruction fetch, `(base, size, target)`: the fetch is + /// presented to the bus at `target` plus the offset into the window. The + /// Fuel's reset PROM answers uncached fetches at 0x1fc00000 whatever the + /// cached view holds there, and gives that device an address of its own, + /// so the code page for it is keyed, read and invalidated by address like + /// any other. None on the other machines, whose uncached and cached + /// instruction fetches reach the same bytes. + uncached_fetch_alias: Option>>, /// Where the machine has memory, from [`MipsCpuConfig::memory`]. pub memory: Arc, /// Frames the JIT's page table has, the end of `memory` in pages. Kept @@ -1434,6 +1443,31 @@ fn translate_64_user(e: &mut MipsExecutor, va: u64, at e.translate_64bit_impl::(va, at) } +/// A window whose uncached instruction fetch the board decodes apart, and the +/// translation it wraps (`MipsExecutor::set_uncached_fetch_alias`). +struct FetchAlias { + base: u64, + size: u64, + target: u64, + inner: fn(&mut MipsExecutor, u64, AccessType) -> TranslateResult, +} + +/// The translation wrappers above for a board that decodes an uncached +/// instruction fetch apart. Only a fetch that comes out uncached is changed. +fn translate_fetch_aliased(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { + let inner = e.uncached_fetch_alias.as_ref().map_or(translate_32_kernel:: as fn(&mut MipsExecutor, u64, AccessType) -> TranslateResult, |a| a.inner); + let mut result = inner(e, va, at); + if at == AccessType::Fetch && !result.is_exception() && !result.is_cached() { + if let Some(alias) = e.uncached_fetch_alias.as_ref() { + let offset = result.phys.wrapping_sub(alias.base); + if offset < alias.size { + result.phys = alias.target + offset; + } + } + } + result +} + /// Free-standing trampoline for the CP0 Status callback installed by `install_status_cb`. /// Rust does not allow `Self` inside a nested fn, so the generic trampoline lives here. // Safety: the executor's raw pointers point into allocations owned by @@ -2936,6 +2970,7 @@ impl MipsExecutor { batch_hook: None, phys_alias: None, phys_mask: u64::MAX, + uncached_fetch_alias: None, memory: cfg.memory.clone(), #[cfg(feature = "jitv2")] jitv2_frames: crate::cpu::jitv2::jitv2::frames_for(&cfg.memory), @@ -3540,6 +3575,12 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", paddr, self.memory)) } + /// The code page the last fetch landed on, or null. For tests. + #[cfg(all(test, feature = "jitv2"))] + pub fn jitv2_current_page(&self) -> *mut crate::cpu::jitv2::PhysicalCodePage { + self.pcp + } + #[cfg(feature = "jitv2")] fn jitv2_share(&mut self, jit: Arc>, world: Arc) { { @@ -3587,7 +3628,9 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", #[inline(always)] fn jitv2_track_pcp(&mut self, phys_addr: u64) { // Code above the JIT's pfn map gets no page and runs in the - // interpreter (`step_jit` checks for the null page). + // interpreter (`step_jit` checks for the null page). That includes + // uncached attributes 1 to 3: their bits 58:57 sit in `phys_addr`, + // so the frame range check rejects them. let Some(pfn) = crate::cpu::jitv2::jitv2::pfn_of(phys_addr, self.jitv2_frames) else { self.clear_pcp(); return; @@ -3730,6 +3773,37 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", // A partial LWR keeps the upper word below this offset: 3 in 64-bit mode // on the R10000 family, else 0. self.core.lwr_upper_below = if C::PARTIAL_LWR_KEEPS_UPPER { wide } else { 0 }; + // A board that decodes an uncached instruction fetch apart wraps the + // translation itself, so the fetch paths of the other machines stay + // as they are. + if C::FETCH_ALIAS && self.uncached_fetch_alias.is_some() { + self.wrap_translate_fn(translate_fn); + } + } + + /// `update_translate_fn`'s choice, wrapped for `uncached_fetch_alias`. + /// Out of line so the update itself stays as small as it was; the wrapper + /// reaches the choice through a pointer so that the translation bodies keep + /// the one caller they inline into. + #[cold] + #[inline(never)] + fn wrap_translate_fn(&mut self, inner: fn(&mut Self, u64, AccessType) -> TranslateResult) { + if let Some(alias) = self.uncached_fetch_alias.as_mut() { + alias.inner = inner; + } + self.translate_fn = translate_fetch_aliased::; + } + + /// Present an uncached instruction fetch of the window `base..base+size` + /// to the bus at `target` plus the offset (see [`Self::uncached_fetch_alias`]). + pub fn set_uncached_fetch_alias(&mut self, base: u64, size: u64, target: u64) { + assert!(C::FETCH_ALIAS, "{} has no uncached fetch alias", C::NAME); + // Never called: `update_translate_fn` replaces it below. + fn placeholder(_: &mut MipsExecutor, _: u64, _: AccessType) -> TranslateResult { + unreachable!("the translation wrapper is installed before it is used") + } + self.uncached_fetch_alias = Some(Box::new(FetchAlias { base, size, target, inner: placeholder:: })); + self.update_translate_fn(); } /// Re-derive `fpr_read_d/write_d/read_l/write_l` from STATUS_FR. diff --git a/src/ip34_machine.rs b/src/ip34_machine.rs index 72c96ef93..767d01885 100644 --- a/src/ip34_machine.rs +++ b/src/ip34_machine.rs @@ -131,9 +131,9 @@ impl FuelMachine { // After the windows above: each compile worker takes the cache's // geometry and the CPU model's policies once, as it starts, and // without the windows the geometry has no inline load or store path. - // Workers read plain physical code addresses through the CPU's cached - // bus, which serves them; the uncached views would decode them as Hub - // registers. + // Workers read plain physical code addresses through the CPU's code + // bus: the cached bus, plus the reset PROM's uncached-fetch window. + // The uncached views would decode plain addresses as Hub registers. #[cfg(feature = "jitv2")] { let cpu_device: Arc = cpu.clone().as_device(); @@ -148,7 +148,7 @@ impl FuelMachine { #[cfg(not(feature = "jitv2_lockstep"))] { let stats = g.stats.clone(); - g.compile_queue.start(board.cached_bus(), stats); + g.compile_queue.start(board.code_bus(), stats); } // The compile pool keeps only a weak handle on the CPU. std::mem::forget(cpu_device); @@ -519,6 +519,10 @@ pub(crate) struct FuelBoard { /// Cacheable memory at plain physical addresses: what the cache fills /// from, and the one bus the JIT and its compile pool read code through. cached: Arc, + /// What the JIT compiles code through: `cached`, and the reset PROM's + /// uncached-fetch window ([`Ip35Bus::code_view`]). + #[cfg(feature = "jitv2")] + code: Arc, pub bedrock: Arc, pub l1: Arc, pub flash: Arc, @@ -612,7 +616,17 @@ impl FuelBoard { board_id: FUEL_BOARD_ID, l1: l1_config, })?; - let Node { bus, bedrock, l1, flash, cached, events, receiver } = node; + let Node { + bus, + bedrock, + l1, + flash, + cached, + #[cfg(feature = "jitv2")] + code, + events, + receiver, + } = node; #[cfg(test)] let irq = Arc::new(PendingIrq::default()); #[cfg(test)] @@ -693,6 +707,8 @@ impl FuelBoard { Ok(Self { bus, cached, + #[cfg(feature = "jitv2")] + code, bedrock, l1, flash, @@ -720,9 +736,18 @@ impl FuelBoard { self.cached.clone() } + /// The bus the JIT compiles code through; see [`Ip35Bus::code_view`]. + #[cfg(feature = "jitv2")] + pub fn code_bus(&self) -> Arc { + self.code.clone() + } + pub fn build_cpu(&self, part: &Part) -> Box { let mut cpu = build_cpu(self.bus.clone(), part); cpu.cache = R12000ShadowCache::new(self.cached_bus()); + #[cfg(feature = "jitv2")] + cpu.set_cached_bus(self.code_bus()); + #[cfg(not(feature = "jitv2"))] cpu.set_cached_bus(self.cached_bus()); cpu } diff --git a/src/ip35.rs b/src/ip35.rs index d890212dc..e99018052 100644 --- a/src/ip35.rs +++ b/src/ip35.rs @@ -23,6 +23,25 @@ use std::sync::Arc; pub(crate) const CPU_PHYS_MASK: u64 = ((1u64 << 40) - 1) | (3u64 << 57); const PA_MASK: u64 = (1u64 << 40) - 1; +/// The reset PROM's compatibility window: the processor's reset vector and +/// the PROM's own code are at 0x1fc00000, fetched uncached. +pub(crate) const RESET_WINDOW: u64 = 0x1fc0_0000; +pub(crate) const RESET_WINDOW_SIZE: u64 = 0x40_0000; +/// Where an uncached instruction fetch of the reset window is presented to +/// the bus (`MipsExecutor::uncached_fetch_alias`): the same PROM at an +/// address of its own, above every RAM slot and below the 16 GiB the JIT's +/// frame map covers. +/// +/// The split is inferred. The hardware selects a space by the processor's +/// uncached attribute (established: ip35-address-map.md, "Processor-side +/// address spaces"), so an uncached attribute-0 access and a cached one are +/// different requests to Bedrock. What attribute 0 returns at 0x1fc00000 is +/// not established: no public source says how the reset request reaches flash +/// (ip35-address-map.md, reset vector), and the cached side is likewise open. +/// The reset PROM answering uncached fetches while cached accesses reach RAM +/// is what PROM 6.210's memory test needs, and that is all this decode rests on. +pub(crate) const RESET_WINDOW_FETCH: u64 = 0x2_0000_0000; + /// Where a Bedrock node has memory: each 1 GiB software slot `extent` says /// can hold RAM, as far as its largest bank reaches, and the reset PROM. /// Which bank lands in which slot of a group of four depends on how the PROM @@ -34,7 +53,7 @@ pub fn memory_map(extent: &RamExtent) -> crate::memory_map::MemoryMap { use crate::dev::bedrock::{BANK_SPAN, SLOTS}; use crate::memory_map::{MemKind, MemRange, MemoryMap}; const NAMES: [&str; SLOTS] = ["ram slot 0", "ram slot 1", "ram slot 2", "ram slot 3"]; - const PROM: (u64, u64) = (0x1fc0_0000, 0x2000_0000); + const PROM: (u64, u64) = (RESET_WINDOW, RESET_WINDOW + RESET_WINDOW_SIZE); let mut ranges = vec![MemRange::new("prom", MemKind::Rom, PROM.0, PROM.1)]; for (slot, &name) in NAMES.iter().enumerate() { let (base, end) = (slot as u64 * BANK_SPAN, slot as u64 * BANK_SPAN + extent.prefix(slot)); @@ -50,6 +69,7 @@ pub fn memory_map(extent: &RamExtent) -> crate::memory_map::MemoryMap { ranges.push(MemRange::new(name, MemKind::Ram, base, end)); } } + ranges.push(MemRange::new("prom (uncached fetch)", MemKind::Rom, RESET_WINDOW_FETCH, RESET_WINDOW_FETCH + RESET_WINDOW_SIZE)); MemoryMap::new(ranges) } @@ -242,6 +262,13 @@ impl Ip35Bus { let pa = plain_address(a, n)?; Some((&self.views[(a >> 57) as usize], pa)) } + /// The bus the JIT compiles code through: the cached view, except the + /// reset PROM's uncached-fetch window ([`RESET_WINDOW_FETCH`]), which only + /// the uncached view holds. + #[cfg(feature = "jitv2")] + pub fn code_view(self: &Arc, cached: Arc) -> Arc { + Arc::new(CodeViews { cached, uncached: self.clone() }) + } /// Cached RAM, with `plain_memory` answering wherever `extent` says /// fitted RAM can be. pub fn cached_view(&self, plain_memory: Arc, extent: RamExtent) -> Arc { @@ -259,6 +286,29 @@ fn plain_address(a: u64, n: u64) -> Option { let pa = a & PA_MASK; (pa.checked_add(n)? <= PA_MASK + 1).then_some(pa) } +/// Whether `a` is in the reset PROM's uncached-fetch window. +fn in_fetch_window(a: u64) -> bool { + a.wrapping_sub(RESET_WINDOW_FETCH) < RESET_WINDOW_SIZE +} +/// See [`Ip35Bus::code_view`]. Compiles read instruction words only. +#[cfg(feature = "jitv2")] +struct CodeViews { + cached: Arc, + uncached: Arc, +} +#[cfg(feature = "jitv2")] +impl BusDevice for CodeViews { + fn read32(&self, a: u64) -> BusRead32 { + if in_fetch_window(a) { + self.uncached.read32(a) + } else { + self.cached.read32(a) + } + } + fn gen_ptr(&self, a: u64) -> *const AtomicU64 { + if in_fetch_window(a) { self.uncached.gen_ptr(a) } else { self.cached.gen_ptr(a) } + } +} struct CachedPhysical { phys: Arc, plain_memory: Arc, @@ -302,6 +352,17 @@ macro_rules! processor_access { }; } impl BusDevice for Ip35Bus { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + // Only the reset PROM's page asks this bus: its generation is the PROM + // device's, so a flash command invalidates it and a store to RAM never + // does. The counter covers the whole flash, so any flash write + // invalidates every PROM page (over-invalidation, safe). + if in_fetch_window(address) { + return self.views[0].gen_ptr(address); + } + std::ptr::null() + } fn write64_masked(&self, a: u64, val: u64, mask: u64) -> u32 { if a & 7 != 0 { return BUS_ERR; diff --git a/src/ip35/node.rs b/src/ip35/node.rs index 8f0b35bba..9eba894d6 100644 --- a/src/ip35/node.rs +++ b/src/ip35/node.rs @@ -51,6 +51,10 @@ pub(crate) struct Node { /// Cacheable memory at plain physical addresses: what a cache fills /// from, and the one bus the JIT and its compile pool read code through. pub cached: Arc, + /// What the JIT compiles code through: `cached`, plus the reset PROM's + /// uncached-fetch window ([`Ip35Bus::code_view`]). + #[cfg(feature = "jitv2")] + pub code: Arc, /// The node's resets and power events, for the system controller. pub events: Sender, pub receiver: Receiver, @@ -94,6 +98,9 @@ impl Node { } // Reset execution always sees stored bytes, independently of commands. bus.map(0, 0x1fc0_0000, 0x40_0000, prom.clone()); + // The same PROM where an uncached instruction fetch of that window is + // presented (`MipsExecutor::uncached_fetch_alias`). + bus.map(0, super::RESET_WINDOW_FETCH, super::RESET_WINDOW_SIZE, prom.clone()); if !payload.is_empty() { map_device(&phys, 0x1fc0_0000, payload.len() as u64, prom.clone(), &format!("{name} cached reset PROM")); } @@ -118,8 +125,21 @@ impl Node { bus.map(2, 0, 1 << 33, bedrock.fetchop_bus()); let bus = Arc::new(bus); let cached = bus.cached_view(bedrock.dma_view(), bedrock.ram_extent()); - Ok(Self { bus, bedrock, l1, flash, cached, events, receiver }) + #[cfg(feature = "jitv2")] + let code = bus.code_view(cached.clone()); + Ok(Self { + bus, + bedrock, + l1, + flash, + cached, + #[cfg(feature = "jitv2")] + code, + events, + receiver, + }) } + } /// Give a node's memory and interrupts to its processors, CPU 0 first: a diff --git a/src/ip35/tests.rs b/src/ip35/tests.rs index 037a1be2c..a8d617b48 100644 --- a/src/ip35/tests.rs +++ b/src/ip35/tests.rs @@ -557,6 +557,9 @@ fn the_cpu_reads_code_through_the_cached_view_not_the_uncached_connection() { fn cpu_cached_bus() { let board = crate::ip34_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); let cpu = board.build_cpu(crate::ip34_machine::CPUS[0]); + #[cfg(feature = "jitv2")] + let cached = board.code_bus(); + #[cfg(not(feature = "jitv2"))] let cached = board.cached_bus(); assert!(Arc::ptr_eq(cpu.cached_bus(), &cached)); let sysad: Arc = board.bus.clone(); @@ -746,3 +749,268 @@ fn processor_interface_graphics_registers_keep_their_writes() { board.bedrock.power_on(); assert_eq!(pi_regs(&board, 0 as u64).read64(0x308).data, 0); } + +/// The reset PROM fetches uncached at the same plain address that cached RAM +/// answers once memory covers its window. The board presents that fetch at the +/// PROM's own address (`uncached_fetch_alias`), so the JIT keys the page by +/// address and compiles the PROM's bytes; it used to read the cached view and +/// compile the RAM's bytes in place of the PROM's. +#[cfg(feature = "jitv2")] +#[test] +fn uncached_fetch_compiles_the_prom_not_the_ram_over_it() { + // The executor and the code generator are large; the default test stack is not. + std::thread::Builder::new() + .stack_size(64 << 20) + .spawn(uncached_fetch_compiles_the_prom_body) + .unwrap() + .join() + .expect("thread panicked"); +} + +#[cfg(feature = "jitv2")] +fn uncached_fetch_compiles_the_prom_body() { + use crate::cpu::jitv2::jitv2::CompileRequest; + use crate::cpu::mips_isa::{JIT_REGION_BOUNDARY_SENTINEL, OP_ADDIU}; + + /// Every word the same; a region-boundary word compiles to nothing. + struct Words { + word: u32, + generation: AtomicU64, + } + impl Words { + fn new(word: u32) -> Arc { + Arc::new(Self { word, generation: AtomicU64::new(0) }) + } + } + impl BusDevice for Words { + fn read32(&self, _: u64) -> BusRead32 { + BusRead32::ok(self.word) + } + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::ok((self.word as u64) << 32 | self.word as u64) + } + fn gen_ptr(&self, _: u64) -> *const AtomicU64 { + &self.generation + } + } + + let reset_vector = 0x1fc0_0000u64; + let addiu_r1 = (OP_ADDIU << 26) | (1 << 16) | 1; + let rom = Words::new(addiu_r1); + let ram = Words::new(JIT_REGION_BOUNDARY_SENTINEL); + let physical = Physical::new(Vec::new()); + let mut bus = Ip35Bus::new(physical.clone()); + map_device(&physical, reset_vector, 0x1000, ram.clone(), "RAM over the reset window"); + bus.map(0, reset_vector, 0x1000, rom.clone()); + bus.map(0, RESET_WINDOW_FETCH, 0x1000, rom.clone()); + let bus = Arc::new(bus); + let code_bus = bus.code_view(bus.cached_view(ram.clone(), RamExtent::new(&[1024]))); + // The two views really disagree here. + assert_eq!(bus.read32(reset_vector).data, addiu_r1); + assert_eq!(bus.cached_view(ram.clone(), RamExtent::new(&[1024])).read32(reset_vector).data, JIT_REGION_BOUNDARY_SENTINEL); + + let mut cpu = alias_cpu(bus.clone()); + alias_the_reset_window(&mut cpu); + cpu.set_cached_bus(code_bus.clone()); + // The reset vector, KSEG1: uncached, plain physical address 0x1fc00000. + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = cpu.jitv2_current_page(); + assert!(!page.is_null(), "the uncached fetch must land on a code page"); + let page = unsafe { &mut *page }; + assert_eq!(page.pfn as u64, RESET_WINDOW_FETCH / 4096, "an uncached fetch of the reset window keys the PROM's own frame"); + + page.mark_requested(0); + let req = CompileRequest { page: page as *mut _, compiled_for_fr1: true }; + let mut analyzer = crate::cpu::jitv2::analyzer::Analyzer::new(); + let mut codegen = crate::cpu::jitv2::codegen::Codegen::new(); + #[cfg(feature = "developer")] + crate::cpu::jitv2::comp::handle_request(&req, &cpu.cached_bus(), &mut analyzer, &mut codegen, &crate::cpu::jitv2::JitStats::default()); + #[cfg(not(feature = "developer"))] + crate::cpu::jitv2::comp::handle_request(&req, &cpu.cached_bus(), &mut analyzer, &mut codegen); + assert!(page.is_runnable(0), "the page must compile the PROM's ADDIU, not the RAM's boundary word"); + + // The cached view of the same frame is another page, with RAM's counter and bytes. + let jitv2 = cpu.jitv2.clone(); + let mut jit = jitv2.lock(); + let slot = jit.page_for(0x1fc00, 0x1fc0_0000, bus.as_ref(), false).unwrap(); + let cached = unsafe { &mut *jit.page_ptr(slot) }; + assert_ne!(cached as *mut _, page as *mut _); + cached.mark_requested(0); + let req = CompileRequest { page: cached as *mut _, compiled_for_fr1: true }; + #[cfg(feature = "developer")] + crate::cpu::jitv2::comp::handle_request(&req, &code_bus, &mut analyzer, &mut codegen, &crate::cpu::jitv2::JitStats::default()); + #[cfg(not(feature = "developer"))] + crate::cpu::jitv2::comp::handle_request(&req, &code_bus, &mut analyzer, &mut codegen); + assert!(!cached.is_runnable(0), "the cached page compiles RAM's boundary word to nothing"); + + // A write through one view invalidates only the pages that view's bytes feed. + ram.generation.fetch_add(1, std::sync::atomic::Ordering::Release); + assert!(page.is_runnable(0), "a cached-view write does not change the PROM's bytes"); + rom.generation.fetch_add(1, std::sync::atomic::Ordering::Release); + assert!(!page.is_runnable(0), "a write to the PROM's bytes invalidates its code"); +} + +/// The R16000 is the part that takes an uncached fetch alias. +#[cfg(feature = "jitv2")] +type AliasCpu = crate::cpu::mips_exec::MipsExecutor; + +#[cfg(feature = "jitv2")] +fn alias_cpu(bus: Arc) -> Box { + use crate::cpu::mips_exec::{MipsCpuConfig, MipsExecutor}; + use crate::cpu::mips_tlb::MipsTlb; + let cfg = MipsCpuConfig::for_model::(Arc::new(memory_map(&RamExtent::new(&[1024])))) + .with_prid(crate::cpu::parts::R16000_700_EARLY.prid()); + let mut cpu = Box::new(MipsExecutor::new(bus as Arc, MipsTlb::new(cfg.tlb_entries), &cfg)); + cpu.phys_mask = CPU_PHYS_MASK; + // KSEG0 cached, as the PROM's boot mode leaves it. + cpu.core.cp0_config = (cpu.core.cp0_config & !7) | 3; + cpu +} + +#[cfg(feature = "jitv2")] +fn alias_the_reset_window(cpu: &mut AliasCpu) { + cpu.set_uncached_fetch_alias(RESET_WINDOW, RESET_WINDOW_SIZE, RESET_WINDOW_FETCH); +} + +#[cfg(feature = "jitv2")] +mod code_views { + use super::*; + use crate::cpu::jitv2::jitv2::CompileRequest; + use crate::cpu::mips_isa::{JIT_REGION_BOUNDARY_SENTINEL, OP_ADDIU}; + + struct Words(u32, AtomicU64); + impl BusDevice for Words { + fn read32(&self, _: u64) -> BusRead32 { + BusRead32::ok(self.0) + } + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::ok((self.0 as u64) << 32 | self.0 as u64) + } + fn gen_ptr(&self, _: u64) -> *const AtomicU64 { + &self.1 + } + } + + const RESET: u64 = 0x1fc0_0000; + const ADDIU_R1: u32 = (OP_ADDIU << 26) | (1 << 16) | 1; + + /// A bus whose uncached attribute-0 view of the reset frame holds an ADDIU + /// and whose cached view holds a region boundary, as RAM over the PROM does. + fn bus_and_cpu(aliased: bool) -> (Arc, Box) { + let rom = Arc::new(Words(ADDIU_R1, AtomicU64::new(0))); + let ram = Arc::new(Words(JIT_REGION_BOUNDARY_SENTINEL, AtomicU64::new(0))); + let physical = Physical::new(Vec::new()); + let mut bus = Ip35Bus::new(physical.clone()); + map_device(&physical, RESET, 0x1000, ram.clone(), "RAM over the reset window"); + bus.map(0, RESET, 0x1000, rom.clone()); + // Code that is fetchable in the other views and past the end of memory, so only + // the JIT's own range check keeps it out of a code page. + for attribute in 1..4 { + bus.map(attribute, RESET, 0x1000, rom.clone()); + } + bus.map(0, RESET_WINDOW_FETCH, 0x1000, rom.clone()); + bus.map(0, 0x3_0000_0000, 0x1000, rom); + let bus = Arc::new(bus); + let mut cpu = alias_cpu(bus.clone()); + if aliased { + let cached = bus.cached_view(ram, RamExtent::new(&[1024])); + alias_the_reset_window(&mut cpu); + cpu.set_cached_bus(bus.code_view(cached)); + } + (bus, cpu) + } + + /// The executor and the code generator are large; the default test stack is not. + fn big_stack(body: fn()) { + std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join().expect("thread panicked"); + } + + fn wait_runnable(page: &crate::cpu::jitv2::PhysicalCodePage) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while std::time::Instant::now() < deadline { + if page.is_runnable(0) { return true; } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + false + } + + /// After a page-pool flush the restarted compile pool must still read the + /// PROM's page through the compile bus. Restarted on the CPU's own bus it + /// rejects the view address and the page never compiles. + #[test] + fn a_pool_restart_after_a_flush_keeps_the_compile_bus() { + big_stack(|| { + let (_bus, mut cpu) = bus_and_cpu(true); + { + let jit = cpu.jitv2.clone(); + let mut g = jit.lock(); + let stats = g.stats.clone(); + g.compile_queue.set_thread_count(1); + g.compile_queue.start(cpu.cached_bus().clone(), stats); + } + cpu.jitv2_flush_from_here(false); + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &mut *cpu.jitv2_current_page() }; + assert_eq!(page.pfn as u64, RESET_WINDOW_FETCH / 4096); + page.mark_requested(0); + let jit = cpu.jitv2.clone(); + let g = jit.lock(); + let stats = g.stats.clone(); + assert!(g.compile_queue.send(CompileRequest { page: page as *mut _, compiled_for_fr1: true }, &stats)); + drop(g); + assert!(wait_runnable(page), "the restarted pool must compile the PROM's page"); + jit.lock().compile_queue.stop(); + }); + } + + /// With no alias an uncached fetch keys the frame it names, as on every + /// other machine; and with it a cached fetch of the same window still does. + #[test] + fn a_cached_fetch_and_an_unaliased_executor_key_the_frame_they_name() { + big_stack(|| { + let (_bus, mut cpu) = bus_and_cpu(false); + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &*cpu.jitv2_current_page() }; + assert_eq!(page.pfn, 0x1fc00); + + let (_bus, mut cpu) = bus_and_cpu(true); + // KSEG0: cached, the same physical address. + cpu.core.pc = 0xffff_ffff_9fc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &*cpu.jitv2_current_page() }; + assert_eq!(page.pfn, 0x1fc00, "a cached fetch is not presented at the PROM's frame"); + }); + } + + /// Uncached attributes 1 to 3 and anything past the end of memory stay in the interpreter. + #[test] + fn attributes_one_to_three_and_high_addresses_get_no_code_page() { + big_stack(|| { + for pc in [ + 0x9000_0000_0000_0000u64 | RESET, // attribute 0: a page + 0x9200_0000_0000_0000u64 | RESET, // attribute 1 + 0x9400_0000_0000_0000u64 | RESET, // attribute 2 + 0x9600_0000_0000_0000u64 | RESET, // attribute 3 + 0x9000_0003_0000_0000u64, // past the end of memory, attribute 0 + ] { + // A fresh executor each time: a tracked page would survive a + // fetch that wrongly kept the same frame. + let (_bus, mut cpu) = bus_and_cpu(true); + // KX, so XKPHYS addresses translate. + let status = cpu.core.cp0_status | 0x80; + cpu.set_cp0_status(status); + cpu.core.pc = pc; + let _ = cpu.step_int(); + let page = cpu.jitv2_current_page(); + if pc == 0x9000_0000_0000_0000u64 | RESET { + assert!(!page.is_null(), "attribute 0 below 4 GiB is compiled"); + } else { + assert!(page.is_null(), "{pc:#x} must not get a code page"); + } + } + }); + } +} From eabfedb17e7f9fc47ac49df63e8a7efeb55ebb07 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sun, 8 Nov 2026 12:15:00 -0700 Subject: [PATCH 10/33] cpu: keep valid primary instruction lines in the CPU's cached view The IP35 PROM, 6.210, copies itself to memory from code it stages in primary instruction lines with Index Store Tag and Index Store Data (I) and runs through cached xkphys. On the Tezro, memory at those addresses holds something else once it is configured: the first bank is 1 GiB and covers the PROM's 0x1fc00000. Without the lines the Tezro's PROM hangs on iris at "Copying PROM code to memory ...", running zeros from the mapped copy (measured on the model). The Fuel reaches the IRIX login without them on the model; its two 256 MiB banks leave those addresses unbacked, which is the inferred difference. A valid primary instruction line now holds its page in the CPU's view like a data line, with each word written there as the instruction it predecodes. A primary instruction word is 36 bits of predecoded instruction (NEC VR10000 user's manual p. 288). Neither manual gives the layout, so it is inferred from the words PROM 6.210 stores, and only its eleven sampled classes decode. A word of any other class leaves memory's word in the page, where hardware would run it. Index Store Data (I) into a held line writes through. The PROM gets the predecoded form of other code from the hardware: it runs a secondary line it staged, which refills a primary line, and reads the words back with Index Load Data (I) to stage them itself (measured on the model). The model does not refill on a fetch, so Index Load Data (I) in a set a held secondary line maps to now answers with that line's words predecoded, as if it had been refilled, until Index Store Data (I) writes the set. A guest's own pattern in the set reads back as written (MIPS R10000 user's manual pp. 201-202). An instruction outside the eleven classes comes back in a form only this model reads, bits 35:32 set over the instruction, which takes predecode classes 0x78-0x7f. Logging every page held in a boot to the IRIX login, the IP28 and the Origin 200 hold no page through an instruction line, so they hold what they held before. Nothing on a fetch, load or store, and nothing in the decode cache, changes. A page held only for code also answers cached loads with the code's bytes, where hardware would read memory. --- CHANGELOG.md | 1 + .../cpu/cache-lines-live-in-the-cpus-view.md | 16 ++ src/cpu/mips_cache_shadow.rs | 221 +++++++++++++++++- 3 files changed, 235 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58202569c..fd64d0693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,7 @@ hashes are given where a change is easiest to understand by reading the commit. ### R10000-family cache lines as memory - A valid primary data or secondary cache line now holds its page in that CPU's own copy of ppmem's window, so a PROM that keeps its stack in cache lines reads and writes the lines with cached accesses while the uncached path, DMA and the other CPUs see memory. Index Store Data writes through to such a line. `l2 probe 0` lists the pages held. Nothing changes on a load, store or fetch. A valid R5000 primary data line does the same, for the O2's POST stack. +- Primary instruction lines hold their pages too, each word written there as the instruction it predecodes, for the eleven predecode classes the model knows. Index Load Data (I) where a held secondary line would refill the set returns that line's words, predecoded, until Index Store Data (I) writes the set. Only the Tezro's PROM uses them. - jitv2 does not compile a page any CPU's cache holds lines in: that CPU runs it from its own view in the interpreter, and every other page stays compiled. - The O2 keeps POST's stack in those lines. CRIME's side page for the uncached path and the tag-store hook are gone, and an O2 snapshot an older iris took during POST is refused. diff --git a/clanker/cpu/cache-lines-live-in-the-cpus-view.md b/clanker/cpu/cache-lines-live-in-the-cpus-view.md index 84072982d..8f74e12d1 100644 --- a/clanker/cpu/cache-lines-live-in-the-cpus-view.md +++ b/clanker/cpu/cache-lines-live-in-the-cpus-view.md @@ -84,6 +84,22 @@ that device's counter instead, so the cache puts the same mark there when a page becomes held and takes it off with the page's last line (`mark_foreign`). That work is at CACHE instructions only; a load, store or fetch is unchanged. +## Primary instruction lines + +Only the Tezro's PROM uses these. A valid primary instruction line +writes each 36-bit predecoded word (p. 288) into its page as the instruction it +predecodes, through an eleven-class table inferred from PROM 6.210's words +(`l1i_decode`). A word of another class leaves memory's word, where hardware would +run it. A page held only for code also answers cached loads with code. + +The model never refills on a fetch. The PROM runs a staged secondary line +and reads the primary words back with Index Load Data (I) to stage them itself, so +Index Load Data (I) in a set a held secondary line maps to answers with that line's +words predecoded (`refilled_word`), until Index Store Data (I) writes the set. When +two held lines map one set, the later wins. An instruction no class holds comes back +in the model's own form (bits 35:32 set, which takes classes 0x78-0x7f), which +hardware would not produce. Index Load Tag (I) reads the stored tag. + ## Seeing it `l2 probe 0` in the monitor prints how many lines hold pages and which. Every diff --git a/src/cpu/mips_cache_shadow.rs b/src/cpu/mips_cache_shadow.rs index d5d8198fb..e548f6f8f 100644 --- a/src/cpu/mips_cache_shadow.rs +++ b/src/cpu/mips_cache_shadow.rs @@ -109,7 +109,65 @@ const MRU_BIT: u64 = 1 << 63; const PD_STATE_SHIFT: u32 = 6; const SD_STATE_SHIFT: u32 = 10; +/// The primary instruction state, TagLo[6] (NEC U10278EJ3V0UM p. 285). +const PI_VALID: u64 = 1 << 6; + +/// A primary instruction word is the instruction predecoded, 36 bits (NEC +/// U10278EJ3V0UM p. 288). Neither manual gives the layout. This one is +/// inferred from the words PROM 6.210 stores, `p` the word and `f` the +/// instruction: p[25:20] = f[5:0], p[19:15] = f[10:6], p[14:10] = f[25:21], +/// p[28:26] = f[28:26], p[35:29] a class that also gives f[31:29], and p[4:0] +/// and p[9:5] the rt and rd fields in the order the class gives. Only the +/// sampled classes decode, and every row is inferred. `addiu $4,$0,2` +/// (0x24040002) is 0x3_4420_0004. +/// +/// Rows: class, f[31:29], whether p[4:0] is rt, the f[28:26] values as a bit +/// mask, and the function fields of the SPECIAL members. +const L1I_CLASSES: &[(u8, u32, bool, u8, &[u8])] = &[ + (0x00, 0, false, 1 << 0, &[0x00]), // nop + (0x08, 0, false, 1 << 0, &[0x38, 0x3c]), // dsll, dsll32 + (0x18, 0, false, 1 << 0, &[0x25]), // or + (0x1a, 1, true, 0b1010_0011, &[]), // addi, addiu, ori, lui + (0x1e, 3, true, 1 << 1, &[]), // daddiu + (0x35, 5, false, 1 << 7, &[]), // cache + (0x36, 6, true, 1 << 7, &[]), // ld + (0x3f, 2, false, 1 << 0, &[]), // mtc0 + (0x40, 0, false, 1 << 3, &[]), // jal + (0x41, 0, false, 0b101, &[0x08]), // jr, j + (0x71, 0, false, 1 << 1, &[]), // REGIMM +]; + +/// The model's own form, bits 35:32 set over an instruction no class holds. +/// Hardware would never produce it. It takes classes 0x78-0x7f, so a real +/// word of those classes would be read as raw. +const L1I_RAW: u64 = 0xf << 32; + +/// The instruction a predecoded word encodes, or the class that is refused. +fn l1i_decode(p: u64) -> Result { + if p >> 32 == L1I_RAW >> 32 { + return Ok(p as u32); + } + let (class, op, funct) = ((p >> 29) as u8 & 0x7f, (p >> 26) as u32 & 7, (p >> 20) as u8 & 0x3f); + let &(_, hi3, rt_low, ops, functs) = L1I_CLASSES.iter().find(|c| c.0 == class).ok_or(class)?; + if ops >> op & 1 == 0 || (op == 0 && !functs.is_empty() && !functs.contains(&funct)) || (class == 0 && p != 0) { + return Err(class); + } + let (low, high) = ((p & 0x1f) as u32, (p >> 5) as u32 & 0x1f); + let (rt, rd) = if rt_low { (low, high) } else { (high, low) }; + Ok(hi3 << 29 | op << 26 | ((p >> 10) as u32 & 0x1f) << 21 | rt << 16 | rd << 11 | ((p >> 15) as u32 & 0x1f) << 6 | funct as u32) +} +/// The predecoded word for `f`, or the raw form where no class holds it. +fn l1i_encode(f: u32) -> u64 { + let w = f as u64; + let (rt, rd) = (w >> 16 & 0x1f, w >> 11 & 0x1f); + L1I_CLASSES.iter().find_map(|&(class, _, rt_low, _, _)| { + let (low, high) = if rt_low { (rt, rd) } else { (rd, rt) }; + let p = (class as u64) << 29 | (w >> 26 & 7) << 26 | (w & 0x3f) << 20 | (w >> 6 & 0x1f) << 15 + | (w >> 21 & 0x1f) << 10 | high << 5 | low; + (l1i_decode(p) == Ok(f)).then_some(p) + }).unwrap_or(L1I_RAW | w) +} /// R10000 only: ways per set, as CACHE index operations address them. Bit 0 /// of the index selects the way; the set starts above the line offset. The @@ -186,6 +244,10 @@ pub struct ShadowCache< /// counters carrying their `GEN_HELD`: see [`Self::mark_foreign`]. #[cfg(feature = "jitv2")] foreign: UnsafeCell>, + /// For each primary instruction set, the 64-byte half of a held + /// secondary line that a refill there would load (`refilled_word`), or + /// `u64::MAX`. + refill: UnsafeCell>, } // Safety: the CPU thread is the only accessor, as for every other cache model @@ -246,6 +308,7 @@ impl< view: UnsafeCell::new(0), #[cfg(feature = "jitv2")] foreign: UnsafeCell::new(Vec::new()), + refill: UnsafeCell::new(vec![u64::MAX; Self::PI_SETS].into_boxed_slice()), } } @@ -376,10 +439,19 @@ impl< if s.held.get(self.tag_slot(sel, virt_addr)) == Some(&true) { self.store_into_held_line(sel, virt_addr, phys_addr); } + if sel == CACH_PI { + // The set now holds what the guest wrote, not a refill. + if let Some(r) = unsafe { &mut *self.refill.get() }.get_mut((virt_addr as usize / IC_LINE.max(1)) % Self::PI_SETS.max(1)) { + *r = u64::MAX; + } + } } // ---- valid lines hold pages in the CPU's cached view ---- + /// Primary instruction sets, as Index Load Data (I) addresses them. + const PI_SETS: usize = if IC_LINE == 0 { 0 } else { IC_SIZE / WAYS / IC_LINE }; + fn view_space(&self) -> Option<&PpMemSpace> { unsafe { (*self.view_space.get()).as_ref() } } @@ -400,6 +472,9 @@ impl< CACH_PD if DC_LINE != 0 && (tag >> PD_STATE_SHIFT) & 3 != 0 => { Some((pa_hi | ((tag >> 8) & 0xff_ffff) << 12 | (va & 0xfff & !(DC_LINE as u64 - 1)), DC_LINE)) } + CACH_PI if IC_LINE != 0 && tag & PI_VALID != 0 => { + Some((pa_hi | ((tag >> 8) & 0xff_ffff) << 12 | (va & 0xfff & !(IC_LINE as u64 - 1)), IC_LINE)) + } CACH_SD if L2_LINE != 0 && (tag >> SD_STATE_SHIFT) & 3 != 0 => { Some((pa_hi | ((tag >> 14) & 0x3_ffff) << 18 | (va & 0x3_ffff & !(L2_LINE as u64 - 1)), L2_LINE)) } @@ -474,6 +549,7 @@ impl< let word = s.data[self.data_slot(sel, s.data.len(), first + (unit * k) as u64 + way)]; let at = pa + (unit * k) as u64; match sel { + CACH_PI => self.put_instruction(at, word), CACH_PD => unsafe { *(base.add(at as usize) as *mut u32) = word as u32 }, // TagHi is the doubleword's high word and TagLo its low word // (MIPS R10000 UM p. 201; NEC VR10000 UM p. 289, Fig. 13-33). @@ -487,6 +563,9 @@ impl< _ => unsafe { *(base.add(at as usize) as *mut u64) = word }, } } + if sel == CACH_SD { + self.set_refill(s.tags[idx], pa, len, true); + } } /// The line in slot `idx` of `sel` leaves, unwritten, as Index Store Tag @@ -495,6 +574,9 @@ impl< fn line_leaves(&self, sp: &PpMemSpace, sel: u32, idx: usize, (pa, len): (u64, usize)) { let s = self.shadow(sel); s.held[idx] = false; + if sel == CACH_SD { + self.set_refill(s.tags[idx], pa, len, false); + } let gone = sp.release_line_page(self.view_no(), pa); if gone { self.mark_foreign(sp, pa, false); @@ -547,24 +629,79 @@ impl< let at = pa + (va & (len as u64 - 1) & !(unit - 1)); let base = unsafe { *self.tc_base.get() }; match sel { + CACH_PI => self.put_instruction(at, value & 0xf_ffff_ffff), CACH_PD => unsafe { *(base.add(at as usize) as *mut u32) = value as u32 }, _ => unsafe { *(base.add(at as usize) as *mut u64) = value; *(base.add((at ^ 8) as usize) as *mut u64) = 0; }, } + if sel == CACH_SD { + self.set_refill(self.shadow(sel).tags[idx], pa, len, true); + } + } + + /// A primary instruction word goes into its line's page as the + /// instruction it predecodes, so a fetch runs it. A word of a class the + /// model does not know leaves the page as it was, where hardware would + /// run it. + fn put_instruction(&self, at: u64, word: u64) { + match l1i_decode(word) { + Ok(f) => unsafe { *((*self.tc_base.get()).add(at as usize) as *mut u32) = f }, + Err(class) => crate::dlog!(LogModule::L2c, + "shadow: primary instruction word {word:#011x} at {at:#x}: predecode class {class:#04x} unknown, memory's word runs"), + } + } + + /// A held secondary line `[pa, pa + len)` with tag `tag` is what a primary + /// instruction refill in each set it maps to would load, or stops being. + /// The set is the line's virtual index, TagLo[8:7] (VA[13:12], NEC + /// U10278EJ3V0UM p. 287), over PA[11:6]. Two lines for one set: the + /// later wins. + fn set_refill(&self, tag: u64, pa: u64, len: usize, held: bool) { + let refill = unsafe { &mut *self.refill.get() }; + if !held { + // By address: the tag may already name another line. + refill.iter_mut().filter(|r| (pa..pa + len as u64).contains(*r)).for_each(|r| *r = u64::MAX); + return; + } + for half in (pa..pa + len as u64).step_by(IC_LINE.max(1)) { + if let Some(r) = refill.get_mut(((tag >> 7 & 3) << 12 | (half & 0xfff)) as usize / IC_LINE) { + *r = half; + } + } + } + + /// Index Load Data (I) in a set a held secondary line maps to, which + /// Index Store Data (I) has not written since and where no held primary + /// line is: the word a refill from that + /// line would have left, predecoded. The model never refills on a fetch, + /// so this assumes the line was refilled, whichever way the operand + /// names. PROM 6.210, the IP35 machines' PROM, runs a staged secondary + /// line and reads the primary words back to stage them itself. Index + /// Load Tag (I) still + /// reads the stored tag. + #[cold] + #[inline(never)] + fn refilled_word(&self, va: u64) -> Option { + let half = *unsafe { &*self.refill.get() }.get((va as usize / IC_LINE.max(1)) % Self::PI_SETS.max(1))?; + if half == u64::MAX || self.shadow(CACH_PI).held[self.tag_slot(CACH_PI, va)] { + return None; + } + let at = half + (va & (IC_LINE as u64 - 1) & !3); + Some(l1i_encode(unsafe { *((*self.tc_base.get()).add(at as usize) as *const u32) })) } /// How many lines hold pages. fn held_count(&self) -> usize { - [CACH_PD, CACH_SD].iter().map(|&sel| self.shadow(sel).held.iter().filter(|&&h| h).count()).sum() + [CACH_PI, CACH_PD, CACH_SD].iter().map(|&sel| self.shadow(sel).held.iter().filter(|&&h| h).count()).sum() } /// Every held line goes, with no writeback: power-on, or a snapshot /// restore. fn drop_held_lines(&self) { let Some(sp) = self.view_space() else { return }; - for sel in [CACH_PD, CACH_SD] { + for sel in [CACH_PI, CACH_PD, CACH_SD] { for i in 0..self.shadow(sel).held.len() { if self.shadow(sel).held[i] { let line = self.line_of(sel, self.shadow(sel).tags[i], self.slot_va(sel, i)).expect("a held line is valid"); @@ -867,7 +1004,7 @@ impl< s.mru[set] = phys_addr & MRU_BIT != 0; let old = s.tags[idx]; s.tags[idx] = phys_addr & mask & !MRU_BIT; - if Self::R10K && matches!(sel, CACH_PD | CACH_SD) && old != s.tags[idx] { + if Self::R10K && matches!(sel, CACH_PI | CACH_PD | CACH_SD) && old != s.tags[idx] { self.tag_changes(sel, idx, virt_addr, old, s.tags[idx]); } } @@ -904,6 +1041,11 @@ impl< crate::dlog!(LogModule::L2c, "shadow: -> ILD slot={slot} data={:#018x} ecc={:#x}", s.data[slot], s.ecc[slot]); } + if sel == CACH_PI { + if let Some(p) = self.refilled_word(virt_addr) { + return p; + } + } s.data[slot] } } @@ -1719,4 +1861,77 @@ mod tests { assert!(c.debug_probe("l2", 0, 0).contains("0 valid lines hold 0 pages")); assert!(c.load_cache_state(&c.save_cache_state()).is_ok()); } + + // ---- primary instruction lines ---- + + /// Every class of the table, encoded and decoded back, one field moved + /// at a time for the example word, and what the table refuses. + #[test] + fn predecoded_words_round_trip() { + let p = 0x3_4420_0004; // addiu $4,$0,2 + for (q, f) in [(p, 0x2404_0002), (p | 0x1f << 5, 0x2404_f802), (p | 0x1f << 10, 0x27e4_0002), (p | 0x1f << 15, 0x2404_07c2)] { + assert_eq!(l1i_decode(q), Ok(f), "{q:#x}"); + } + for (class, f) in [(0x08, 0x0009_20f8), (0x08, 0x0009_20fc), (0x18, 0x0109_5025), (0x1a, 0x2084_0010), + (0x1a, 0x3484_0010), (0x1a, 0x3c04_8000), (0x1e, 0x6484_0010), (0x35, 0xbd04_0040), + (0x36, 0xdd04_0008), (0x3f, 0x4084_6000), (0x40, 0x0c00_1234), (0x41, 0x03e0_0008), + (0x41, 0x0800_1234), (0x71, 0x0481_0004), (0, 0)] { + assert_eq!((l1i_encode(f) >> 29, l1i_decode(l1i_encode(f))), (class, Ok(f)), "{f:#x}"); + } + for f in [0x0009_20c0, 0x2884_0010, 0x4422_3000] { // sll, slti, mtc1: no class + assert_eq!((l1i_encode(f), l1i_decode(L1I_RAW | f as u64)), (L1I_RAW | f as u64, Ok(f))); + } + assert_eq!((l1i_decode(1), l1i_decode(0x2a << 29)), (Err(0), Err(0x2a))); + } + + /// TagLo of a valid primary instruction line for `pa`'s page. + fn pi_valid(pa: u64) -> u64 { (pa >> 12) << 8 | PI_VALID } + + /// A valid primary instruction line puts the instructions its words + /// predecode into the page, leaving memory's word for one it cannot + /// decode, and Index Store Data (I) writes through. + #[test] + fn a_primary_instruction_line_puts_its_instructions_in_the_page() { + let (c, space, bank) = windowed(); + bank.write32(0x9008, 0x1111_1111); + let va = 0x8000_9001u64; // way 1 + c.cache_op(C_R10K_ISD | CACH_PI, va, 0x3_4420_0004); + c.cache_op(C_R10K_ISD | CACH_PI, va + 8, 0x2a << 29); + c.cache_op(C_IST | CACH_PI, va, pi_valid(0x9000)); + assert_eq!(space.held_line_pages(0), vec![0x9000]); + assert_eq!([0x9000, 0x9008].map(|a| c.read::<4>(0, a).data), [0x2404_0002, 0x1111_1111]); + c.cache_op(C_R10K_ISD | CACH_PI, va + 4, l1i_encode(0x03e0_0008)); + assert_eq!(c.read::<4>(0, 0x9004).data, 0x03e0_0008); + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, va, 0), 0x3_4420_0004, "Index Load Data (I) reads the array"); + assert_eq!(bank.read32(0x9000).data, 0, "memory never saw it"); + c.cache_op(C_IST | CACH_PI, va, 0); + assert!(space.held_line_pages(0).is_empty()); + } + + /// A held secondary line answers Index Load Data (I) in the primary sets + /// it maps to, as the predecoded words a refill would leave, until Index + /// Store Data (I) writes the set or the line goes. A held primary line + /// answers for itself. + #[test] + fn index_load_data_i_reads_a_refill_until_the_guest_writes_the_set() { + let (c, _space, _bank) = windowed(); + c.cache_op(C_IST | CACH_SD, 0x8000_4f00, sd_tag(0x4f00, 2) | 1 << 7); // VA[13:12] = 1 + c.cache_op(C_R10K_ISD | CACH_SD, 0x8000_4f10, 0x0140_0008); // jr $10 at 0x4f10 + let set = |off: u64| 0x8000_1f00 | off; + for va in [set(0x10), set(0x10) | 1] { + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, va, 0), l1i_encode(0x0140_0008), "either way"); + } + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, 0x8000_0f10, 0), 0, "another set: the array"); + c.cache_op(C_R10K_ISD | CACH_PI, set(0x10), 0x5_5555_5555); + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, set(0x10), 0), 0x5_5555_5555, "the guest's pattern"); + c.cache_op(C_R10K_ISD | CACH_SD, 0x8000_4f10, 0x0140_0008); + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, set(0x10), 0), l1i_encode(0x0140_0008), "restaged: a refill again"); + c.cache_op(C_R10K_ISD | CACH_PI, set(0x10), 0x3_4420_0004); + c.cache_op(C_IST | CACH_PI, set(0x10), pi_valid(0x1_4000)); + c.cache_op(C_R10K_ISD | CACH_SD, 0x8000_4f10, 0x0140_0008); + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, set(0x10), 0), 0x3_4420_0004, "a held primary line answers for itself"); + c.cache_op(C_IST | CACH_PI, set(0x10), 0); + c.cache_op(C_IST | CACH_SD, 0x8000_4f00, 0); + assert_eq!(c.cache_op(C_R10K_ILD | CACH_PI, set(0x10), 0), 0x3_4420_0004, "gone with the line: the array"); + } } From 3f3a0c58fd41b54417fbed59425248753b733ae5 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 07:52:10 -0700 Subject: [PATCH 11/33] bedrock: decode eight banks, four of them above 4 GiB The memory model decoded four banks in four 1 GiB software slots, all inside the 4 GiB window ppmem maps for the processor's direct access. That is the Fuel's board, with two 256 MiB banks. A Tezro fits up to eight banks; its observed 4 GiB population is 1024, 1024 and four 512 MiB banks, and banks 4 and 5 then sit at 4 to 6 GiB. The controller now decodes eight slots. The bank-select swizzle permutes slots within groups of four, so the RAM extent a board's cached view and processor memory map use reaches, in each slot, as far as the largest bank of its group. A node with banks above 4 GiB asks ppmem for its 16 GiB window, as the Octane's HEART does past 4 GiB, and each processor takes that window's region size, so every bank stays on the direct path. The generation table covers the slots up to the last that can hold RAM, so the Fuel's table is as large as before. The configuration word is four byte lanes. Each lane enables its two banks separately but holds one size code for both: software bank i is bank i & 1 of lane i >> 1, as the stock IP35 PROM's decode of the word reads it (established for PROM 6.210 and 6.211). A board therefore cannot fit a lane's two banks at different sizes, and Memory refuses such a fitting rather than decode it more loosely than the chip. A lane with only its first bank fitted stays allowed: the enables are separate. The Fuel is unchanged: its two banks keep the 4 GiB window, which a test checks. Tests follow the qtest memory-banks, memory-size-boundaries, memory-selector-high-bank, selftest-bank-remap and origin300-selftest-fill cases in tests/qtest/sn1-bedrock-md-test.c, and cover a compare-and-swap, a code page's counter above 4 GiB and the shared size of a lane. Ported from hw/misc/sgi-bedrock-md.c in jamesbraid/qemu-sgi-origami. --- src/dev/bedrock/md.rs | 165 +++++++++++++++++++++++++++++++++++++++-- src/dev/bedrock/mem.rs | 55 ++++++++++---- src/ip35.rs | 5 +- src/ip35/node.rs | 1 + 4 files changed, 202 insertions(+), 24 deletions(-) diff --git a/src/dev/bedrock/md.rs b/src/dev/bedrock/md.rs index 3b9cafd0b..009681343 100644 --- a/src/dev/bedrock/md.rs +++ b/src/dev/bedrock/md.rs @@ -816,6 +816,15 @@ mod tests { assert_eq!(md.md_bus().write64(8, 0x33), BUS_OK); md } + /// The Tezro's default 4 GiB: two 1 GiB banks and four of 512 MiB. + const TEZRO_BANKS: [u32; 6] = [1024, 1024, 512, 512, 512, 512]; + /// Banks 0 and 1 at size code 5 (1 GiB), the other four at code 4 (512 MiB). + const TEZRO_CONFIG: u64 = 0x53 | (0x43 << 8) | (0x43 << 16); + fn tezro() -> Arc { + let md = TestMd::new(&TEZRO_BANKS).unwrap(); + assert_eq!(md.md_bus().write64(reg::CONFIG, TEZRO_CONFIG), BUS_OK); + md + } /// Cached, uncached and DMA memory take no MD lock: each works while /// both are held. An address no enabled bank holds answers a bus error @@ -920,20 +929,35 @@ mod tests { /// The board names what it fits; nothing else is accepted. A bank is a /// whole number of direct-window regions and at most the largest size - /// code names, and the controller decodes four. + /// code names, and the controller decodes eight. #[test] fn a_board_may_only_fit_banks_the_controller_can_decode() { assert!(TestMd::new(&FUEL_BANKS).is_ok()); - assert!(TestMd::new(&[1024; 4]).is_ok()); - for banks in [&[][..], &[0, 256], &[2048], &[100], &[64; 5]] { + assert!(TestMd::new(&TEZRO_BANKS).is_ok()); + assert!(TestMd::new(&[1024; 8]).is_ok()); + for banks in [&[][..], &[0, 256], &[2048], &[100], &[64; 9]] { assert!(TestMd::new(banks).is_err(), "{banks:?}"); } } - /// Where RAM can be, for a board's cached view: the swizzle permutes the - /// four slots, so every slot reaches as far as the largest bank. + /// A byte lane of the configuration word holds one size code for both + /// of its banks, so a board cannot fit them at different sizes; a lane + /// with one bank fitted is still a board's to choose. #[test] - fn the_ram_extent_reaches_as_far_as_the_largest_bank_in_every_slot() { + fn the_two_banks_of_a_lane_share_one_size() { + for banks in [&[1024, 512][..], &[512, 512, 1024, 512], &[1024, 1024, 512, 512, 512, 1024]] { + assert!(TestMd::new(banks).err().is_some_and(|e| e.contains("share one size")), "{banks:?}"); + } + for banks in [&[512][..], &[1024, 1024, 512], &[512, 512, 1024, 1024]] { + assert!(TestMd::new(banks).is_ok(), "{banks:?}"); + } + } + + /// Where RAM can be, for a board's cached view: the swizzle permutes slots + /// within groups of four, so a slot reaches as far as the largest bank of + /// its group. + #[test] + fn the_ram_extent_reaches_as_far_as_the_largest_bank_of_a_slot_group() { let extent = RamExtent::new(&FUEL_BANKS); for slot in 0..4 { assert!(extent.contains(slot * BANK_SPAN + FITTED_BANK - 8), "slot {slot}"); @@ -944,6 +968,135 @@ mod tests { assert!(mixed.contains(3 * BANK_SPAN + (512 << 20) - 8), "slot 3 may hold the 512 MiB bank"); assert!(!mixed.contains(512 << 20)); assert_eq!(extent, fuel().ram_extent()); + let tezro = RamExtent::new(&TEZRO_BANKS); + assert!(tezro.contains(3 * BANK_SPAN + BANK_SPAN - 8), "slot 3 may hold a 1 GiB bank"); + assert!(tezro.contains(5 * BANK_SPAN + (512 << 20) - 8)); + assert!(!tezro.contains(5 * BANK_SPAN + (512 << 20))); + assert!(!tezro.contains(8 * BANK_SPAN)); + assert_eq!(tezro, TestMd::new(&TEZRO_BANKS).unwrap().ram_extent()); + } + + /// Each Tezro bank answers in its own 1 GiB slot at its own size: a + /// 1 GiB bank fills its slot, a 512 MiB bank stops halfway, and the + /// slots above bank 5 answer nothing. Banks 4 and 5 sit above 4 GiB, + /// inside the node's 16 GiB direct window. Follows the qtest memory-banks and + /// memory-size-boundaries cases in tests/qtest/sn1-bedrock-md-test.c. + #[test] + fn every_tezro_bank_answers_at_its_own_size_including_above_4_gib() { + use crate::ppmem::MappedMemory; + let md = tezro(); + let bus = md.physical(); + let sizes = [1024u64, 1024, 512, 512, 512, 512]; + for (slot, mib) in sizes.iter().enumerate() { + let base = slot as u64 * BANK_SPAN; + let size = mib << 20; + assert_eq!(bus.write64(base, 0x1111 * (slot as u64 + 1)), BUS_OK, "slot {slot}"); + assert_eq!(bus.write64(base + size - 8, 0xe0 + slot as u64), BUS_OK, "slot {slot} top"); + if size < BANK_SPAN { + assert_eq!(bus.read64(base + size).status, BUS_ERR, "slot {slot} past its bank"); + } + } + for slot in 0..6u64 { + assert_eq!(bus.read64(slot * BANK_SPAN).data, 0x1111 * (slot + 1), "slot {slot}"); + let top = (sizes[slot as usize] << 20) - 8; + assert_eq!(bus.read64(slot * BANK_SPAN + top).data, 0xe0 + slot, "slot {slot} top"); + } + for slot in [6, 7] { + assert_eq!(bus.read64(slot * BANK_SPAN).status, BUS_ERR, "slot {slot} holds no bank"); + } + assert_eq!(bus.banks()[4].read64(0).data, 0x1111 * 5, "bank 4 holds what slot 4 wrote"); + assert_eq!(bus.banks()[5].read64(0).data, 0x1111 * 6); + // The 16 GiB window maps every bank, in 256 MiB regions. + let space = bus.ppmem_space().expect("a window"); + assert_eq!(space.window_size(), crate::ppmem::LARGE_WINDOW_SIZE); + assert_eq!(space.mapped_bitmap(), 0xf | (0xf << 4) | (0x3 << 8) | (0x3 << 12) | (0x3 << 16) | (0x3 << 20)); + assert_eq!(md.dma_view().read64(5 * BANK_SPAN).data, 0x1111 * 6, "DMA reaches it too"); + assert_eq!(md.uncached_view().read64(4 * BANK_SPAN).data, 0x1111 * 5, "and uncached"); + } + + /// A node whose banks all lie below 4 GiB, as the Fuel's do, keeps the + /// 4 GiB window and its 64 MiB regions. + #[test] + fn a_node_below_4_gib_keeps_the_4_gib_window() { + use crate::ppmem::MappedMemory; + let md = configured(); + let space = md.physical().ppmem_space().expect("a window"); + assert_eq!(space.window_size(), crate::ppmem::WINDOW_SIZE); + assert_eq!(space.mapped_bitmap(), 0xf | (0xf << 16)); + } + + /// A processor's SC to a bank above 4 GiB swaps through the DMA view in one step and leaves a failed compare + /// alone; a successful one advances the page's logical counter. + #[test] + fn a_compare_and_swap_above_4_gib_is_one_step() { + let md = tezro(); + let view = md.dma_view(); + let at = 5 * BANK_SPAN + 0x40; + assert_eq!(view.write64(at, 7), BUS_OK); + #[cfg(feature = "jitv2")] + let before = unsafe { (*view.gen_ptr(at)).load(Ordering::Relaxed) }; + assert_eq!(view.cas64(at, 6, 9), Some(false)); + assert_eq!(view.read64(at).data, 7); + assert_eq!(view.cas64(at, 7, 9), Some(true)); + assert_eq!(view.read64(at).data, 9); + assert_eq!(view.cas32(at + 4, 9, 3), Some(true)); + assert_eq!(view.read64(at).data, 3); + #[cfg(feature = "jitv2")] + assert!(unsafe { (*view.gen_ptr(at)).load(Ordering::Relaxed) } > before); + assert_eq!(view.cas64(7 * BANK_SPAN, 0, 1), None, "no bank there"); + } + + /// Selector 1 swaps the two banks of each pair, so the 512 MiB banks 4 and 5 + /// trade places above 4 GiB. Follows memory-selector-high-bank. + #[test] + fn the_bank_select_swaps_banks_above_4_gib() { + let md = tezro(); + let bus = md.physical(); + assert_eq!(bus.write64(4 * BANK_SPAN + 0x28, 0xfedc_ba98_7654_3210), BUS_OK); + assert_eq!(bus.read64(5 * BANK_SPAN + 0x28).data, 0); + assert_eq!(md.md_bus().write64(reg::CONFIG, TEZRO_CONFIG | (1 << 32)), BUS_OK); + assert_eq!(bus.read64(5 * BANK_SPAN + 0x28).data, 0xfedc_ba98_7654_3210); + assert_eq!(bus.read64(4 * BANK_SPAN + 0x28).data, 0); + } + + /// A fitting that covers the bank under test zero-fills it, whichever slot + /// the bank is in; a bank that is not fitted fails. Follows the + /// selftest-bank-remap and origin300-selftest-fill cases. + #[test] + fn selftest_fills_a_tezro_bank_above_4_gib_and_fails_a_missing_bank() { + let md = TestMd::new(&TEZRO_BANKS).unwrap(); + // Every pair at size code 1 (64 MiB): small enough to fill in a test. + let regs = md.md_bus(); + regs.write64(reg::CONFIG, 0x13 | (0x13 << 8) | (0x13 << 16) | (0x13 << 24)); + let bus = md.physical(); + bus.write64(4 * BANK_SPAN + 0x40, 0x5555); + regs.write64(reg::SELFTEST_DATA, 0x80); + regs.write64(reg::SELFTEST_COMMAND, (4 << 4) | 1); + md.advance_ns(1000); + assert_eq!(regs.read64(reg::SELFTEST_STATUS).data, 3, "bank 4 is fitted"); + assert_eq!(bus.read64(4 * BANK_SPAN + 0x40).data, 0, "and zeroed"); + regs.write64(reg::SELFTEST_COMMAND, (6 << 4) | 1); + md.advance_ns(1000); + assert_eq!(regs.read64(reg::SELFTEST_STATUS).data, 2, "bank 6 is not fitted"); + } + + /// Code pages above 4 GiB have stable logical generation counters like + /// the rest, and a write bumps them. + #[cfg(feature = "jitv2")] + #[test] + fn code_pages_above_4_gib_keep_a_logical_generation() { + let md = tezro(); + let above = 4 * BANK_SPAN + 0x2000; + let pointer = md.dma_view().gen_ptr(above); + assert!(!pointer.is_null()); + assert_ne!(pointer, md.dma_view().gen_ptr(5 * BANK_SPAN + 0x2000)); + assert!(md.dma_view().gen_ptr(8 * BANK_SPAN).is_null(), "past the node's 8 GiB"); + assert!(md.dma_view().gen_ptr(4 * BANK_SPAN + (512 << 20)).is_null(), "past the largest bank there"); + let before = unsafe { (*pointer).load(Ordering::Relaxed) }; + assert_eq!(md.dma_view().write64(above, 1), BUS_OK); + assert!(unsafe { (*pointer).load(Ordering::Relaxed) } > before); + md.md_bus().write64(reg::CONFIG, TEZRO_CONFIG | (1 << 32)); + assert_eq!(md.dma_view().gen_ptr(above), pointer, "a bank swap keeps the counter"); } /// Guest density settings remap fitted RAM, and reset restores the default map. diff --git a/src/dev/bedrock/mem.rs b/src/dev/bedrock/mem.rs index 4f074aa11..5ea3cc137 100644 --- a/src/dev/bedrock/mem.rs +++ b/src/dev/bedrock/mem.rs @@ -2,9 +2,11 @@ //! decode selected by Bedrock's memory configuration register. //! //! The board names the fitted banks, locational bank 0 upward, one size in -//! MiB each: two 256 MiB banks on the Fuel. Each bank answers in its own -//! 1 GiB software slot, all four inside the 4 GiB window ppmem maps for the -//! CPU's direct access. +//! MiB each: two 256 MiB banks on the Fuel, up to eight on the Tezro. Each +//! bank answers in its own 1 GiB software slot, so banks 4 to 7 sit at 4 to +//! 8 GiB. A node with banks there asks ppmem for its 16 GiB window, as the +//! Octane's HEART does past 4 GiB, so every bank is in the CPU's direct +//! window. //! //! Every access reaches a bank through Physical's bank views or the bank //! itself, without a lock. Remapping runs on the CPU thread, or with the CPU @@ -18,35 +20,42 @@ use crate::physical::{Physical, RamBank}; use crate::traits::*; use crate::ppmem::devmap::SLOT_SHIFT; -use crate::ppmem::{MappedMemory, BITMAP_SHIFT}; +use crate::ppmem::{MappedMemory, BITMAP_SHIFT, LARGE_WINDOW_SIZE, WINDOW_SIZE}; use parking_lot::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; pub const BANK_SPAN: u64 = 1 << 30; -/// Software slots of [`BANK_SPAN`]: the bank-select swizzle permutes the -/// four of them. -pub const SLOTS: usize = 4; +/// Software slots of [`BANK_SPAN`] in the node's 8 GiB. +pub const SLOTS: usize = 8; /// The most memory a bank can hold: what the largest size code names. const MAX_BANK_MIB: u32 = 1024; /// Banks are mapped into the direct window in whole regions of this size. const REGION_MIB: u32 = 1 << (BITMAP_SHIFT - 20); /// Where fitted RAM can answer: for each software slot, the most any bank the -/// bank-select swizzle can place there holds. The swizzle permutes the four -/// slots, so any slot may hold the largest bank. +/// bank-select swizzle can place there holds. The swizzle permutes slots +/// within groups of four, so a slot may hold any bank of its group. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RamExtent([u64; SLOTS]); impl RamExtent { /// `banks` are the fitted sizes in MiB from locational bank 0. pub fn new(banks: &[u32]) -> Self { - Self([banks.iter().map(|&mib| u64::from(mib) << 20).max().unwrap_or(0); SLOTS]) + let fitted = |bank: usize| banks.get(bank).map_or(0, |&mib| u64::from(mib) << 20); + Self(std::array::from_fn(|slot| { + (0..4).map(|group_slot| fitted(slot / 4 * 4 + group_slot)).max().unwrap_or(0) + })) } /// Whether `address` can be fitted RAM under some bank-select setting. pub fn contains(&self, address: u64) -> bool { let slot = address / BANK_SPAN; slot < SLOTS as u64 && address % BANK_SPAN < self.0[slot as usize] } + /// Slots from 0 through the last that can hold RAM. + #[cfg(feature = "jitv2")] + fn slots(&self) -> usize { + self.0.iter().rposition(|&size| size != 0).map_or(0, |last| last + 1) + } /// The most RAM software slot `slot` can hold, in bytes. pub fn prefix(&self, slot: usize) -> u64 { self.0[slot] } } @@ -68,9 +77,9 @@ impl BusDevice for NoMemory {} pub(crate) struct Generations(Box<[AtomicU64]>); #[cfg(feature = "jitv2")] impl Generations { - /// One counter per 4 KiB page of every slot that can hold RAM. + /// One counter per 4 KiB page, up to the last slot that can hold RAM. fn new(extent: &RamExtent) -> Self { - let pages = if extent.prefix(0) == 0 { 0 } else { SLOTS << (BANK_SPAN.trailing_zeros() - 12) }; + let pages = extent.slots() << (BANK_SPAN.trailing_zeros() - 12); Self((0..pages).map(|_| AtomicU64::new(0)).collect::>().into_boxed_slice()) } pub(crate) fn base(&self) -> *mut AtomicU64 { @@ -85,10 +94,10 @@ impl Generations { } } fn bump_all(&self, extent: &RamExtent) { - for slot in 0..SLOTS { + for slot in 0..extent.slots() { let first = slot * (BANK_SPAN >> 12) as usize; let pages = (extent.prefix(slot) >> 12) as usize; - for counter in self.0[first..].iter().take(pages) { + for counter in &self.0[first..first + pages] { counter.fetch_add(1, Ordering::Relaxed); } } @@ -111,7 +120,12 @@ pub struct Memory { impl Memory { /// Memory with `banks` fitted, locational bank 0 upward, each a size in /// MiB: at most [`SLOTS`] banks, none empty or over 1 GiB (the largest - /// size code), each a whole number of direct-window regions. + /// size code), each a whole number of direct-window regions, and banks + /// 2k and 2k+1 the same size where both are fitted. The configuration + /// word is four byte lanes, each enabling its two banks separately and + /// holding one size code for both, so the hardware cannot size a lane's + /// banks apart (established from the stock IP35 PROM's decode of that + /// word). pub fn new(banks: &[u32]) -> Result { if banks.is_empty() || banks.len() > SLOTS { return Err(format!("memory: {} banks; fit 1 to {SLOTS}", banks.len())); @@ -122,7 +136,16 @@ impl Memory { return Err(format!( "memory: bank {bank} is {mib} MiB; a bank is a multiple of {REGION_MIB} MiB up to {MAX_BANK_MIB} MiB")); } - let physical = Physical::new(banks.iter().map(|&mib| RamBank::new(mib as usize)).collect()); + // The configuration word has one size code per byte lane, shared by + // the lane's two banks, so the two cannot differ. + if let Some(lane) = banks.chunks(2).position(|pair| pair.len() == 2 && pair[0] != pair[1]) { + return Err(format!( + "memory: banks {} and {} are {} and {} MiB; the two banks of a lane share one size", + 2 * lane, 2 * lane + 1, banks[2 * lane], banks[2 * lane + 1])); + } + // Bank n starts at n GiB; past bank 3 the node runs over 4 GiB. + let window = if banks.len() as u64 * BANK_SPAN > WINDOW_SIZE { LARGE_WINDOW_SIZE } else { WINDOW_SIZE }; + let physical = Physical::with_window(banks.iter().map(|&mib| RamBank::new(mib as usize)).collect(), window); physical.set_default(Arc::new(NoMemory), "Bedrock unconfigured memory"); Ok(Self { physical, fitted: banks.iter().map(|&mib| u64::from(mib) << 20).collect(), config: AtomicU64::new(0), runs: Mutex::new(Vec::new()), diff --git a/src/ip35.rs b/src/ip35.rs index e99018052..e942bec5d 100644 --- a/src/ip35.rs +++ b/src/ip35.rs @@ -46,13 +46,14 @@ pub(crate) const RESET_WINDOW_FETCH: u64 = 0x2_0000_0000; /// can hold RAM, as far as its largest bank reaches, and the reset PROM. /// Which bank lands in which slot of a group of four depends on how the PROM /// programs the memory controller; the Fuel's two 256 MiB banks can be in any -/// of the first four. Uncached fetches of the PROM's own code land in its +/// of the first four, and a Tezro's banks 4 and 5 in any of the next four. Uncached fetches of the PROM's own code land in its /// range. A slot 0 larger than 508 MiB reaches the PROM's range, which the /// node maps over it, so its RAM is listed on either side. pub fn memory_map(extent: &RamExtent) -> crate::memory_map::MemoryMap { use crate::dev::bedrock::{BANK_SPAN, SLOTS}; use crate::memory_map::{MemKind, MemRange, MemoryMap}; - const NAMES: [&str; SLOTS] = ["ram slot 0", "ram slot 1", "ram slot 2", "ram slot 3"]; + const NAMES: [&str; SLOTS] = + ["ram slot 0", "ram slot 1", "ram slot 2", "ram slot 3", "ram slot 4", "ram slot 5", "ram slot 6", "ram slot 7"]; const PROM: (u64, u64) = (RESET_WINDOW, RESET_WINDOW + RESET_WINDOW_SIZE); let mut ranges = vec![MemRange::new("prom", MemKind::Rom, PROM.0, PROM.1)]; for (slot, &name) in NAMES.iter().enumerate() { diff --git a/src/ip35/node.rs b/src/ip35/node.rs index 9eba894d6..00178c277 100644 --- a/src/ip35/node.rs +++ b/src/ip35/node.rs @@ -171,6 +171,7 @@ pub(crate) fn connect_cpus( unsafe { cpu.set_tcache_window(space.view_base(v)); cpu.set_cached_view(space, v); + cpu.set_tcache_shift(crate::ppmem::window_shift(space.window_size())); #[cfg(feature = "jitv2")] cpu.set_tcache_gen_window(bedrock.generation_base()); } From 48232ca1a029a9574bbeb182d918932bd3c16b39 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 07:56:30 -0700 Subject: [PATCH 12/33] bedrock_flash: author a Tezro's configuration block The flash's firmware record held the Fuel's values alone: boot mode word 0x549aa985, 500 MHz and machine type 2 (SN11), written over the payload's own configuration block whenever a store is formatted or opened. A Tezro needs the word of its own chip and a Config whose mode bits agree. On a mismatch the PROM's early check calls a routine that clears the processor's presence and enable bits and returns into the normal start-up, but it runs before GP is set: what it does on silicon is unknown, and on the model the PROM stops in it. There is a record for each Tezro chip, 700, 800, 900 and 1000 MHz. The first three mode words are rows of the stock IP35 PROM's CPU speed table (0x589bad85, 0x549baf85 and 0x569cb185). The table stops at 900 MHz, so the 1 GHz word 0x4aedb385 is built from the formulas the PROM's own configuration editor uses for the other rows and from the cache clock and tap its inventories print; it is approximated, and the doc comment says how each field was derived. The reset Config still comes from the mode word's low 25 bits, so the flash and the CPU read one source. A record now carries a header policy. The Fuel's stays refreshed: its fields are rewritten over the block, keeping the block's byte sum, on every format and open. A Tezro's is authored: a format writes a whole 0x50-byte block in the editor's layout, with the compensation byte the editor would choose so the new block sums to what the old one did and the image keeps summing to zero, and an open keeps the stored block, so a flash update the PROM wrote survives. The word at +0x30 is a machine class, not a CPU type. With a CPU configuration in the node board's L1 record, the PROM's start-up check derives its own class from the board word's top nibble (0x4 gives 2, 0x8 gives 1, any other 0; established for PROM 6.210 and 6.211) and prints "CPU configuration values mismatch" when the flash disagrees. An IP53 or IP59 board's class is not 4, 8 or 0xc: Tezro and Origin 350 PROM banners lack the IP34 and IP45 mode lines and print the brick-type banner, which the PROM reaches only for other classes (established). So a Tezro's flash holds 0, and the Tezro records author 0, as QEMU's Tezro does; that a real Tezro flash holds 0 is inferred, since no flash has been read and two real captures show no mismatch notice. The speed table's rows carry 2 because the PROM reaches them only through class 0x4000: they are the Fuel's R16000 options, whose tuples match Tezro boards because one tuple means one set of mode bits. --- src/dev/bedrock_flash.rs | 312 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 298 insertions(+), 14 deletions(-) diff --git a/src/dev/bedrock_flash.rs b/src/dev/bedrock_flash.rs index 32b8f84b7..ebb8a699f 100644 --- a/src/dev/bedrock_flash.rs +++ b/src/dev/bedrock_flash.rs @@ -38,17 +38,23 @@ fn sum(bytes: &[u8]) -> u8 { /// 0x1312d0, +0x28 1, +0x2c flash timing 0x10, +0x30 machine type, +0x34 0, /// +0x3c write timing 0x10 (established from the PROM). "CPU configuration /// values mismatch" compares +0x00, 0x04, 0x08, 0x10, 0x18, 0x28, 0x2c, 0x30 -/// and 0x3c against the board EEPROM's record, then checks the speed table, -/// so the machine type, mode word, both clocks and the timing bytes all have -/// to agree with the row the board-id word selects. This struct sets the -/// mode word, CPU Hz and machine type; the PROM image carries the rest, the -/// 200 MHz hub clock among them (the same in every speed row). +/// and 0x3c against a reference record the PROM builds from the board +/// EEPROM's record, or, when it has none, from the speed-table row the +/// board-id word selects. Either way the machine type in the reference is the +/// one the board word's class gives. This struct sets the mode word, CPU Hz +/// and machine type; the PROM image carries the rest, the 200 MHz hub clock +/// among them (the same in every speed row). /// /// The PROM reads the record before it touches the processor, so it must /// agree with the CPU the machine builds: a mismatch in the boot-mode word /// sends it to its error routine before it initialises GP. The values are -/// not a measured flash dump of a 500 MHz Fuel; each field's doc says how -/// far it is known. A real 800 MHz Fuel's mode word was 0x549baf85. +/// not a measured flash dump: each field's doc says how far it is known. The +/// Fuel's word selects its row by class 0x4000 and the processor's nibble. +/// The Tezro's records reuse the mode words of speed-table rows with the +/// same clock, cache size and tap, but a Tezro's board word is outside that +/// class, so the PROM checks them against the node board's record and the +/// class's machine type, not against a row. A real 800 MHz Fuel's mode word +/// was 0x549baf85. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FirmwareRecord { /// The CPU boot-mode word the PROM compares with the processor's mode. @@ -58,8 +64,9 @@ pub struct FirmwareRecord { /// not read from a 500 MHz Fuel. The 800 MHz row's real value, 0x549baf85, /// differs from it in bits 16 and 10:9. pub cpu_mode_word: u32, - /// The processor clock the PROM reports and times against, in Hz. It is - /// the clock of the speed-table row the board-id word selects. + /// The processor clock the PROM reports and times against, in Hz. On the + /// Fuel it is the clock of the speed-table row the board-id word selects; + /// on the Tezro, the clock of the fitted part. pub cpu_clock_hz: u32, /// The record's word at +0x30, which the PROM's start-up check compares /// with a machine class of its own. When the node board's L1 record @@ -73,6 +80,23 @@ pub struct FirmwareRecord { /// 2 from "(0)SN1 (1)SN10 (2)SN11 (3)SN12" and its IRIX logs "Selecting /// SN11" (just.graphica.com.au/tips/sgi-o350-chimera-notes). pub machine_type: u32, + /// How the record reaches the configuration block. + pub header: Header, +} + +/// How a board's record is put into the configuration block at 0x60. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Header { + /// Rewrite the board's fields over the payload's own block, keeping its + /// byte sum, whenever a store is formatted or opened. QEMU configures a + /// Fuel's flash this way. + Refreshed, + /// Replace the whole block with the one authored for the board's flash + /// seed, once, when a store is formatted, with the compensation byte the + /// PROM's editor would choose (the block's byte sum is the replaced + /// block's, so the image still sums to zero). An opened store keeps the + /// block it holds, so a flash update the PROM wrote survives. + Authored, } impl FirmwareRecord { @@ -81,7 +105,69 @@ impl FirmwareRecord { cpu_mode_word: 0x549a_a985, cpu_clock_hz: 500_000_000, machine_type: 2, + header: Header::Refreshed, + }; + /// The Tezro's 700 MHz R16000 with a 4 MiB secondary cache. The mode word + /// is the row of the stock IP35 PROM's speed table for 700 MHz, 4 MiB and + /// cache tap 0xc (established), and its low 25 bits are the Config word the + /// Tezro board resets this chip with. The machine type is 0, the class the + /// PROM derives from the Tezro's board word 0x2001, so that the block + /// agrees with the CPU configuration the node board's L1 record carries + /// (see `ip53_machine`). The speed-table row itself carries 2, the class + /// of a board word in 0x4000, as the Fuel's is: copying it made the PROM + /// print "CPU configuration values mismatch" at every start. The PROM + /// derives the machine type from the board word's class: 2 for class 4, + /// 1 for class 8, 0 for any other (established for PROM 6.210 and 6.211). + /// A real IP53 or IP59 board's class is not 4, 8 or 0xc (established: + /// Tezro and Origin 350 PROM banners print neither the IP34 nor the IP45 + /// mode line and do print the brick-type banner, which the PROM reaches + /// only for other classes), so its flash must hold 0; that a real Tezro + /// flash holds 0 is inferred (no mismatch notice in two real captures; no + /// flash has been read). The speed-table rows that carry 2 are reached + /// only through class 4: they are the Fuel's R16000 options, whose tuples + /// match Tezro boards because the same tuple means the same mode bits. + pub const TEZRO: Self = Self { + cpu_mode_word: 0x589b_ad85, + cpu_clock_hz: 700_000_000, + machine_type: 0, + header: Header::Authored, + }; + /// The Tezro's 800 MHz R16000 with a 4 MiB secondary cache: the PROM speed + /// table's row for 800 MHz, 4 MiB and tap 0xa (established; the table has a + /// second row with tap 0xb and the same low 25 bits, and the 0xa one is + /// what 800 MHz Tezro inventories print). + pub const TEZRO_800: Self = Self { + cpu_mode_word: 0x549b_af85, + cpu_clock_hz: 800_000_000, + machine_type: 0, + header: Header::Authored, + }; + /// The Tezro's 900 MHz R16000 with an 8 MiB secondary cache: the PROM + /// speed table's row for 900 MHz (established; it is the table's only 900 + /// MHz row, tap 0xb, which a 900 MHz inventory prints). That a Tezro was + /// ever sold or flashed with this part is not established. + pub const TEZRO_900: Self = Self { + cpu_mode_word: 0x569c_b185, + cpu_clock_hz: 900_000_000, + machine_type: 0, + header: Header::Authored, + }; + /// The Tezro's 1 GHz R16000 with a 16 MiB secondary cache. The PROM's + /// speed table, whose top clock is 900 MHz, has no row for it, and no + /// capture records the word: it is computed by formula, so the record is + /// approximated. The clock-dependent fields follow the formulas the + /// table's rows decode by, and bit 23 is set as in every row of 500 MHz + /// and up. + /// EC code 9 is 2 * 1000 / 200 - 1, SC code 5 gives the printed 333 MHz + /// cache clock (2 * 1000 / (SC + 1)), SS code 5 is 16 MiB, and the printed + /// tap 0x15 is 0x5 in bits 28:25 with the fifth bit at bit 22. + pub const TEZRO_1000: Self = Self { + cpu_mode_word: 0x4aed_b385, + cpu_clock_hz: 1_000_000_000, + machine_type: 0, + header: Header::Authored, }; + /// CP0 Config at reset: the fixed 32 KiB primary cache sizes (fields 3 /// in bits 31:29 and 28:26) with the board's mode bits. pub const fn reset_config0(&self) -> u32 { @@ -92,12 +178,67 @@ impl FirmwareRecord { pub const fn reset_mode(&self) -> u32 { self.cpu_mode_word & 0x01ff_ffff } + + /// The configuration block written into an IP53 seed: the format word, + /// the board's mode word, the `ip27conf` magic, the processor and + /// 200 MHz controller clocks as 64-bit words, the real-time clock's + /// 1,250,000 Hz, the machine type, a zero compensation byte (the caller sets + /// it), an edit counter of one, and the PROM's 6.210 version fields. + /// The layout is the PROM editor's. + fn authored_block(&self) -> [u8; CONFIG_LEN] { + let mut block = [0u8; CONFIG_LEN]; + let mut put = |offset: usize, bytes: &[u8]| block[offset..offset + bytes.len()].copy_from_slice(bytes); + put(0x00, &21u32.to_be_bytes()); + put(0x04, &self.cpu_mode_word.to_be_bytes()); + put(0x08, b"ip27conf"); + put(0x10, &u64::from(self.cpu_clock_hz).to_be_bytes()); + put(0x18, &200_000_000u64.to_be_bytes()); + put(0x20, &1_250_000u64.to_be_bytes()); + for (offset, value) in [ + (0x28, 1), + (0x2c, 16), + (0x30, self.machine_type), + (0x34, 0), + (0x38, 1), + (0x3c, 16), + (0x40, 6), + (0x44, 210), + (0x48, 0), + ] { + put(offset, &u32::to_be_bytes(value)); + } + block + } } /// Where the PROM's configuration block sits in the payload, and its size. const CONFIG_BASE: usize = 0x60; const CONFIG_LEN: usize = 0x50; +/// Put the board's record into a store being formatted. +fn format_header(data: &mut [u8], record: &FirmwareRecord) { + match record.header { + Header::Refreshed => configure(data, record), + Header::Authored => { + // The compensation byte is the PROM editor's rule: the new block + // sums, byte for byte, to what the block it replaces did, so the + // image still sums to zero (the check the PROM runs on itself). + let old = sum(&data[CONFIG_BASE..CONFIG_BASE + CONFIG_LEN]); + let mut block = record.authored_block(); + block[0x37] = old.wrapping_sub(sum(&block)); + data[CONFIG_BASE..CONFIG_BASE + CONFIG_LEN].copy_from_slice(&block); + } + } +} + +/// Put the board's record into a store that was opened rather than formatted. +fn reopen_header(data: &mut [u8], record: &FirmwareRecord) { + match record.header { + Header::Refreshed => configure(data, record), + Header::Authored => {} + } +} + /// Apply the board's firmware record while retaining the configuration sum. fn configure(data: &mut [u8], record: &FirmwareRecord) { let config = &mut data[CONFIG_BASE..CONFIG_BASE + CONFIG_LEN]; @@ -181,7 +322,7 @@ impl Flash { data[label + 0x10..label + 0x14].copy_from_slice(&0x504c4f49u32.to_be_bytes()); data[label + 0x14..label + 0x18].copy_from_slice(&1u32.to_be_bytes()); data[label + 0x18..label + 0x1c].copy_from_slice(&1u32.to_be_bytes()); - configure(&mut data, &record); + format_header(&mut data, &record); Ok(Self { st: Mutex::new(State::new(data)), gen: AtomicU64::new(0), @@ -189,7 +330,8 @@ impl Flash { } /// Load an exact 16 MiB store, or seed a missing/empty one. Wrong sizes are - /// refused without writes. Reopening reapplies the firmware record. + /// refused without writes. Reopening reapplies a refreshed firmware record + /// and leaves an authored one as the store holds it. /// Completed guest programming and erasure write through to this file. pub fn with_file(payload: &[u8], path: impl AsRef, record: FirmwareRecord) -> io::Result { let mut data = Self::new(payload, record)?.st.into_inner().data; @@ -203,9 +345,9 @@ impl Flash { error } })?; - configure(&mut data, &record); - // Opening an existing store loads it before the record's fields are - // applied; an empty store was seeded from this same data. + reopen_header(&mut data, &record); + // Opening an existing store loads it before a refreshed record's + // fields are applied; an empty store was seeded from this same data. backing.write_range(0, &data)?; let flash = Self { st: Mutex::new(State { @@ -535,6 +677,148 @@ mod tests { assert_eq!(std::fs::read(&path).unwrap(), fuel, "the store holds what the machine sees"); std::fs::remove_dir_all(&dir).unwrap(); } + const TEZRO_RECORDS: [(&str, FirmwareRecord); 4] = [ + ("700", FirmwareRecord::TEZRO), + ("800", FirmwareRecord::TEZRO_800), + ("900", FirmwareRecord::TEZRO_900), + ("1000", FirmwareRecord::TEZRO_1000), + ]; + + /// The stock block the PROM ships: the 400 MHz R10000 and R12000 row, + /// machine type 0, no compensation, edit counter 0. Built field by field, not + /// copied; its byte sum is 0xac, as the PROM's own checksum rule sees it. + fn stock_block() -> [u8; CONFIG_LEN] { + let mut block = FirmwareRecord { cpu_mode_word: 0x5219_a785, cpu_clock_hz: 400_000_000, machine_type: 0, ..FirmwareRecord::TEZRO } + .authored_block(); + block[0x38..0x3c].fill(0); + assert_eq!(sum(&block), 0xac, "the stock block's byte sum"); + block + } + + /// An image whose configuration block is the stock one, as the PROM's own + /// image has it. + fn stock_image() -> Vec { + let mut image = vec![0x21; 0x200]; + image[0x60..0xb0].copy_from_slice(&stock_block()); + image + } + + /// The seed's block follows the PROM editor's layout and rule: the mode + /// word, the clock, the Bedrock and real-time clocks, machine type 0 (the + /// class the PROM derives from the Tezro's board word) and an edit + /// counter of one, and a compensation byte that makes the block sum to + /// what the stock block did, 0xac, so the image keeps summing to zero: + /// 0xac less the sum of the other bytes. The PROM's editor, copying a + /// speed-table row's class 2, computes 0xd8, 0xfd (the 800 MHz row) and + /// 0x1c for one edit of a stock-seeded image; class 0 makes each two more. + #[test] + fn the_tezro_seed_writes_the_blocks_the_proms_editor_would() { + let image = stock_image(); + for ((name, record), compensation) in TEZRO_RECORDS.iter().zip([0xdau8, 0xff, 0x1e, 0xfa]) { + let bytes = Flash::new(&image, *record).unwrap().contents(); + let block = &bytes[0x60..0xb0]; + assert_eq!(&block[0..4], &21u32.to_be_bytes(), "{name}"); + assert_eq!(&block[4..8], &record.cpu_mode_word.to_be_bytes(), "{name}"); + assert_eq!(&block[8..16], b"ip27conf", "{name}"); + assert_eq!(&block[0x10..0x18], &u64::from(record.cpu_clock_hz).to_be_bytes(), "{name}"); + assert_eq!(&block[0x18..0x20], &200_000_000u64.to_be_bytes(), "{name}: Bedrock clock"); + assert_eq!(&block[0x20..0x28], &1_250_000u64.to_be_bytes(), "{name}: real-time clock"); + assert_eq!(&block[0x30..0x34], &0u32.to_be_bytes(), "{name}: the board word's machine class"); + assert_eq!(&block[0x38..0x3c], &1u32.to_be_bytes(), "{name}: one edit"); + assert_eq!(sum(block), 0xac, "{name}: the stock block's sum"); + assert_eq!(block[0x37], compensation, "{name}: the compensation the PROM editor computes"); + assert_eq!(&bytes[..0x60], &image[..0x60], "{name}: the payload around the block is untouched"); + assert_eq!(&bytes[0xb0..image.len()], &image[0xb0..]); + } + } + + /// The compensation follows whatever block the payload carried (the whole + /// image keeps its sum), and changing any one byte of a seeded block off + /// its compensated value breaks the sum. + #[test] + fn a_tezro_compensation_byte_keeps_the_images_sum_and_any_change_breaks_it() { + // An image that sums to zero, as the PROM's own does, still sums to + // zero once a record's block is seeded into it. + let mut zero = stock_image(); + zero[0x1ff] = 0u8.wrapping_sub(sum(&zero[..0x1ff])); + assert_eq!(sum(&zero), 0); + for (name, record) in TEZRO_RECORDS { + assert_eq!(sum(&Flash::new(&zero, record).unwrap().contents()[..0x200]), 0, "{name}: the seeded image sums to zero"); + } + for payload in [stock_image(), vec![0x5a; 0x200], factory_block()] { + let before = sum(&payload[..0x200.min(payload.len())]); + for (name, record) in TEZRO_RECORDS { + let bytes = Flash::new(&payload, record).unwrap().contents(); + let end = 0x200.min(payload.len()); + assert_eq!(sum(&bytes[..end]), before, "{name}: the image's byte sum"); + // A record that differs in one field is compensated to the same sum. + let other = FirmwareRecord { cpu_mode_word: record.cpu_mode_word ^ 0x0100_0000, ..record }; + assert_eq!(sum(&Flash::new(&payload, other).unwrap().contents()[..end]), before, "{name}: another mode word"); + for at in 0x60..0xb0 { + let mut broken = bytes[..end].to_vec(); + broken[at] ^= 1; + assert_ne!(sum(&broken), before, "{name}: byte {at:#x}"); + } + } + } + } + + /// The Tezro records carry the PROM table's mode words and the Config the + /// CPU resets to carries the same low 25 bits, so the PROM's check of one + /// against the other passes; the mode words of the three chips the PROM's + /// speed table lists are its rows, and the 1 GHz one decodes by the + /// editor's formulas. + #[test] + fn the_tezro_records_carry_their_mode_words_and_the_configs_they_imply() { + // Config bits 24:0 are the mode inputs; the cache sizes above are fixed. + const CONFIG0_MODE_MASK: u32 = 0x01ff_ffff; + // (mode word, clock MHz, Bedrock MHz = 200): the PROM table's rows. + let rows = [(0x589bad85u32, 700u32, 0x6c9b_ad85u32), (0x549baf85, 800, 0x6c9b_af85), (0x569cb185, 900, 0x6c9c_b185)]; + for ((_, record), (word, mhz, config)) in TEZRO_RECORDS.iter().zip(rows) { + assert_eq!(record.cpu_mode_word, word); + assert_eq!(record.cpu_clock_hz, mhz * 1_000_000); + assert_eq!(record.reset_config0(), config); + } + let field = |word: u32, lo: u32, bits: u32| (word >> lo) & ((1 << bits) - 1); + let decoded = [(350, 4u32 << 20), (400, 4 << 20), (450, 8 << 20), (333, 16 << 20)]; + for ((name, r), (cache_mhz, cache_bytes)) in TEZRO_RECORDS.iter().zip(decoded) { + let (w, mhz) = (r.cpu_mode_word, r.cpu_clock_hz / 1_000_000); + assert_eq!(w & CONFIG0_MODE_MASK, r.reset_mode(), "{name}"); + assert_eq!((field(w, 0, 3), field(w, 7, 2), field(w, 13, 1), field(w, 15, 1)), (5, 3, 1, 1), "{name}: K0 PM SB BE"); + assert_eq!(field(w, 9, 4), 2 * mhz / 200 - 1, "{name}: EC = 2 * CPU / Bedrock - 1"); + assert_eq!(2 * mhz / (field(w, 19, 3) + 1), cache_mhz, "{name}: cache clock = 2 * CPU / (SC + 1)"); + assert_eq!(1u32 << (field(w, 16, 3) + 19), cache_bytes, "{name}: SS is 512 KiB << SS"); + } + // The tap the PROM prints: tap[3:0] in bits 28:25, tap[4] at bit 22. + let tap = |word: u32| field(word, 25, 4) | (field(word, 22, 1) << 4); + assert_eq!(TEZRO_RECORDS.map(|(_, r)| tap(r.cpu_mode_word)), [0xc, 0xa, 0xb, 0x15]); + assert_eq!(FirmwareRecord::TEZRO_1000.cpu_mode_word, 0x4aed_b385); + assert_eq!(FirmwareRecord::TEZRO_1000.reset_config0(), 0x6ced_b385); + } + + /// Reopening a store reapplies a refreshed record, so a Fuel follows the + /// machine that opens it, and leaves an authored block as stored, so a + /// block the PROM rewrote on a Tezro survives the next start. + #[test] + fn reopening_keeps_a_tezros_stored_block_and_refreshes_a_fuels() { + let dir = std::env::temp_dir().join(format!("bedrock-flash-tezro-record-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let fuel_path = dir.join("fuel.bin"); + drop(Flash::with_file(&factory_block(), &fuel_path, FirmwareRecord::TEZRO).unwrap()); + let fuel = Flash::with_file(&[], &fuel_path, FirmwareRecord::FUEL).unwrap().contents(); + assert_eq!(&fuel[0x64..0x68], &0x549aa985u32.to_be_bytes()); + assert_eq!(&fuel[0x90..0x94], &2u32.to_be_bytes(), "machine type: SN11"); + + let tezro_path = dir.join("tezro.bin"); + let seeded = Flash::with_file(&factory_block(), &tezro_path, FirmwareRecord::TEZRO).unwrap().contents(); + assert_eq!(std::fs::read(&tezro_path).unwrap(), seeded, "the seed is what the store holds"); + let mut stored = seeded.clone(); + stored[0x64..0x68].copy_from_slice(&0x1234_5678u32.to_be_bytes()); + std::fs::write(&tezro_path, &stored).unwrap(); + let reopened = Flash::with_file(&factory_block(), &tezro_path, FirmwareRecord::TEZRO).unwrap().contents(); + assert_eq!(reopened, stored); + std::fs::remove_dir_all(&dir).unwrap(); + } /// The PROM programs through one view while executable aliases must /// keep reading array bytes. #[test] From 9e26d5e3072625b19ff55d760d275b646f651d0e Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 07:44:01 -0700 Subject: [PATCH 13/33] bedrock: fit processors in any of the node's four slots A Bedrock node has two processor interfaces with two slots each, and a Tezro tower fits one, two or four processors. The chip answered as the Fuel's node, with one processor in PI0 slot A: only that slot read present and enabled, only CPU 0's interrupt word was wired, and the CPU-number register answered slot A of whichever PI was read. The board now names its processors in a Population: which of the four slots hold one, filled in the order PI0/A, PI1/A, PI0/B, PI1/B, so two processors sit one per interface. Every fitted slot reads present and starts with its enable bit set, CPU n's IP2..IP6 come from the nth fitted slot, and the CPU-number register answers the reading processor's own slice, slot in bit 0 and interface in bit 1, as QEMU's PI does from the issuing processor. A reader that is no processor reads slot A, QEMU's behaviour for an access with no requester. A processor on PI1 reads its CPU number and programs its interrupts through the local XIO register form at offset 0, as QEMU's IP35 node overlays PI1 there for every processor sitting on it (OpenBSD expects the local form to select the requester's interface). The board maps local_registers() there; with processors on PI0 alone it is the node form, as before. That the chip answers PI1 there is an approximation pending a hardware read. A processor running with others makes SC atomic through its cache's compare-and-swap, which the shadow cache does itself for RAM in its direct window and hands anything else to the bus. The IP35 cached view had no swap, so an SC there became an ordinary store; it now passes the swap to the memory controller's DMA view. The Fuel passes Population::SINGLE and reads as before. Tests follow sn1-cpu-identity-test four-processors-know-their-slots and sn1-bedrock-pi-test cpu-number-fallback and cpu-enable-lines. Ported from hw/misc/sgi-bedrock-pi.c and hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. --- src/dev/bedrock/mod.rs | 71 ++++++++++++++++++++--------- src/dev/bedrock/pi.rs | 89 +++++++++++++++++++++++++++++------- src/dev/bedrock/pi/tests.rs | 90 ++++++++++++++++++++++++++++++++----- src/dev/bedrock/tests.rs | 11 +++-- src/ip34_machine.rs | 3 +- src/ip35.rs | 9 ++++ src/ip35/node.rs | 22 ++++----- 7 files changed, 233 insertions(+), 62 deletions(-) diff --git a/src/dev/bedrock/mod.rs b/src/dev/bedrock/mod.rs index 275a09e0d..fa1bcc0c2 100644 --- a/src/dev/bedrock/mod.rs +++ b/src/dev/bedrock/mod.rs @@ -44,6 +44,7 @@ mod tests; use tests::{test_config, Rig}; pub use mem::{RamExtent, BANK_SPAN, SLOTS}; +pub use pi::Population; use crate::dev::heart::Clock; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; @@ -90,6 +91,9 @@ pub struct BedrockConfig<'a> { /// Host nanoseconds: the PIs' counter and timers run on it, and the /// other units' clocked work. pub clock: Clock, + /// The processor slots the board fits. CPU n sits in + /// `processors.slot_of(n)`; a fitted slot starts with its enable bit set. + pub processors: Population, /// The memory banks in MiB, locational bank 0 upward. pub banks_mib: &'a [u32], /// Where a node reset goes: LB's soft reset and NI's local reset each @@ -140,6 +144,7 @@ impl HostTimer<'_> { pub struct Bedrock { name: &'static str, clock: Clock, + processors: Population, pi: [Pi; 2], md: Md, io: Io, @@ -159,14 +164,14 @@ pub struct Bedrock { impl Bedrock { pub fn new(config: BedrockConfig<'_>) -> Result, String> { - let BedrockConfig { name, clock, banks_mib, events } = config; + let BedrockConfig { name, clock, processors, banks_mib, events } = config; let memory = mem::Memory::new(banks_mib)?; let now = clock(); let chip = Arc::new_cyclic(|weak: &Weak| { let md = Md::new(memory, clock.clone()); Self { name, - pi: [Pi::new(0, now), Pi::new(1, now)], + pi: [Pi::new(0, processors, now), Pi::new(1, processors, now)], dma: md.dma_view(), md, io: Io::new(), @@ -178,6 +183,7 @@ impl Bedrock { timers: OnceLock::new(), events, clock, + processors, weak: weak.clone(), } }); @@ -204,10 +210,22 @@ impl Bedrock { // The faces a board maps into its processor bus. Each is its own type, // so no access tests which face it came through. - /// The 8 MiB register window. With the node's one processor on PI0, the - /// local and node XIO forms are the same window. + /// The 8 MiB register window in its node XIO form. pub fn registers(self: &Arc) -> Arc { - Arc::new(Registers(self.clone())) + Arc::new(Registers::(self.clone())) + } + /// The register window in its local XIO form: PI0's range answers the + /// requester's own PI when the board fits processors on both PIs, as + /// QEMU's IP35 node overlays PI1 there for a processor on it (OpenBSD + /// expects the local form to select the requester's interface). That the + /// chip answers so is an approximation pending a hardware read. With + /// processors on PI0 alone it is the node form. + pub fn local_registers(self: &Arc) -> Arc { + if (0..2).any(|slot| self.processors.fitted(1, slot)) { + Arc::new(Registers::(self.clone())) + } else { + self.registers() + } } /// The II's PIO face: the processor's uncached I/O view below 4 GiB, /// decoded to a widget. @@ -307,14 +325,15 @@ impl Bedrock { // Processors. - /// Where the processor's IP2..IP6 go: Cause bits 10-14 of its interrupt - /// word (`MipsCpu::interrupts_ptr`). The node's one processor, CPU 0, - /// sits in PI0 slot A. The chip writes no other bit, and no other device - /// may write these. The word must outlive the chip. + /// Where CPU `cpu`'s IP2..IP6 go: Cause bits 10-14 of its interrupt word + /// (`MipsCpu::interrupts_ptr`), from the slot the board's population + /// gives it. The chip writes no other bit, and no other device may write + /// these. The word must outlive the chip. pub fn set_cpu_interrupts(&self, cpu: usize, word: *const AtomicU64) { - if cpu == 0 { - self.pi[0].set_word(0, word); - self.pi[0].publish(&self.levels); + if let Some((interface, slot)) = self.processors.slot_of(cpu) { + let pi = &self.pi[usize::from(interface)]; + pi.set_word(slot, word); + pi.publish(&self.levels); } } /// The PIs' comparators and profile timers fire on `tm`. Held weakly. @@ -368,15 +387,23 @@ impl Bedrock { } } + /// The PI a local-form access at offset 0 reaches: the requester's own. + /// A reader that is no processor reaches PI0. + fn own_pi(&self) -> usize { + crate::cpu::smp::on_cpu_thread() + .and_then(|cpu| self.processors.slot_of(cpu)) + .map_or(0, |(interface, _)| usize::from(interface)) + } + /// An access of `size` bytes at `offset` in the register window, a write - /// when `value` is given. What no unit answers, the padding after a - /// unit's registers included, is a bus error. - fn access(&self, offset: u64, size: u8, value: Option) -> BusRead64 { + /// when `value` is given; PI0's range reaches PI `pi0`. What no unit + /// answers, the padding after a unit's registers included, is a bus error. + fn access(&self, pi0: usize, offset: u64, size: u8, value: Option) -> BusRead64 { if !self.md.cell_permits() { return BusRead64::err(); } match offset { - reg::PI0_BASE..reg::PI1_BASE => self.pi_access(0, offset, size, value), + reg::PI0_BASE..reg::PI1_BASE => self.pi_access(pi0, offset, size, value), reg::PI1_BASE..reg::IO_BASE => self.pi_access(1, offset - reg::PI1_BASE, size, value), reg::IO_BASE..reg::MD_BASE => self.block_access(offset, size, value), reg::MD_BASE..reg::WINDOW => { @@ -489,12 +516,14 @@ impl Resettable for Bedrock { } } -/// The register window. -struct Registers(Arc); -impl Registers { +/// The register window. With `LOCAL`, PI0's range answers the requester's +/// own PI. +struct Registers(Arc); +impl Registers { #[inline] fn access(&self, offset: u64, size: u8, value: Option) -> BusRead64 { - self.0.access(offset, size, value) + let pi0 = if LOCAL { self.0.own_pi() } else { 0 }; + self.0.access(pi0, offset, size, value) } } macro_rules! face_access { @@ -508,7 +537,7 @@ macro_rules! face_access { } }; } -impl BusDevice for Registers { +impl BusDevice for Registers { face_access!(read8, write8, u8, BusRead8, 1); face_access!(read16, write16, u16, BusRead16, 2); face_access!(read32, write32, u32, BusRead32, 4); diff --git a/src/dev/bedrock/pi.rs b/src/dev/bedrock/pi.rs index 48e821793..f3883e9e0 100644 --- a/src/dev/bedrock/pi.rs +++ b/src/dev/bedrock/pi.rs @@ -1,8 +1,8 @@ //! A Bedrock processor interface: processor-slot control, pending //! interrupts, the real-time counter and its comparators, the profile timer, //! error records and the graphics flow-control registers. A node has two -//! PIs with two processor slots each, A and B. The Fuel fits one processor, -//! in PI0 slot A. +//! PIs with two processor slots each, A and B; the board names which of the +//! four slots hold a processor ([`Population`]). The Fuel fits PI0 slot A. //! //! Each slot drives IP2..IP6 of its processor: bank 0 under the slot's bank //! 0 mask, bank 1 under its bank 1 mask, its RT comparator, the profile @@ -14,10 +14,10 @@ //! //! # What is approximated here //! -//! The processor runs from reset, with its CPU-enable bit set, whatever the -//! register later holds: no cold-reset value for the bit is known and the -//! register is not known to stop a processor. A write that clears its enable -//! is stored and logged, not carried out. +//! Every fitted slot runs its processor from reset, with its CPU-enable bit +//! set, whatever the register later holds: no cold-reset value for the bit +//! is known and the register is not known to stop a processor. A write that +//! clears a running slot's enable is stored and logged, not carried out. //! //! For a slot with no processor nothing is delivered. A store that arms //! such a slot is logged under the `bedrock` module instead of being dropped @@ -27,8 +27,9 @@ //! has an NMI entry, so every NMI is logged. //! //! The CPU-number register answers the reading processor's slice: its slot -//! in bit 0 and the interface in bit 1. Every read answers slot A, the one -//! processor's. +//! in bit 0 and the interface in bit 1. A read from a thread that runs no +//! processor (a device, the monitor, a test) answers slot A; hardware always +//! has a requester. //! //! The model accepts Linux's public `SGIrules` key at offset 0x8 for //! compatibility. Linux documents the literal for Hub I/O, but the available @@ -130,6 +131,58 @@ pub mod reg { pub const GRAPHICS_END: u64 = 0x350; } +/// The processor slots a board fits, one bit per slot: bit `interface * 2 + +/// slot`, slot 0 being A. CPU 0 sits in PI0 slot A, so every population has +/// it; a board fills the others in the order PI0/A, PI1/A, PI0/B, PI1/B, so +/// two processors sit one per interface. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Population(u8); +impl Default for Population { + fn default() -> Self { + Self::SINGLE + } +} +impl Population { + /// One processor, in PI0 slot A. + pub const SINGLE: Self = Self(0b0001); + /// Slots named by `mask`. CPU 0 runs in PI0/A, so it must be fitted, and + /// only the four slots exist. + pub fn new(mask: u8) -> Result { + if mask & !0b1111 != 0 { + return Err(format!("processor mask {mask:#x} names a slot outside the node's four")); + } + if mask & 1 == 0 { + return Err(format!("processor mask {mask:#x} leaves PI0 slot A empty, where CPU 0 runs")); + } + Ok(Self(mask)) + } + /// `count` processors in the board's order: PI0/A, PI1/A, PI0/B, PI1/B. + pub fn fitted_cpus(count: usize) -> Result { + if !(1..=4).contains(&count) { + return Err(format!("{count} processors; a node fits 1 to 4")); + } + Self::new(ORDER[..count].iter().fold(0, |mask, bit| mask | (1 << bit))) + } + /// The slot (interface, 0 = A or 1 = B) CPU `cpu` sits in: the board puts + /// CPU n in the nth slot of this set in its order. + pub fn slot_of(self, cpu: usize) -> Option<(u8, usize)> { + ORDER.iter().filter(|&&bit| self.0 & (1 << bit) != 0).nth(cpu).map(|&bit| (bit / 2, usize::from(bit % 2))) + } + pub fn mask(self) -> u8 { + self.0 + } + pub fn count(self) -> usize { + self.0.count_ones() as usize + } + /// Whether a processor is fitted in `slot` (0 = A, 1 = B) of `interface`. + pub fn fitted(self, interface: u8, slot: usize) -> bool { + self.0 & (1 << (usize::from(interface) * 2 + slot)) != 0 + } +} +/// The board's population order, as slot numbers `interface * 2 + slot`: +/// PI0/A, PI1/A, PI0/B, PI1/B. +const ORDER: [u8; 4] = [0, 2, 1, 3]; + /// Bank 0 bit 4 and bank 1 bits 54, 55 and 62: levels other blocks of the /// chip drive, which both PIs reflect. Each writer sets its own bit under /// its own lock, so two writers' levels land in register order. @@ -242,6 +295,7 @@ impl State { pub(super) struct Pi { interface: u8, + processors: Population, pending: [AtomicU64; 2], /// Each slot's bank 0 and bank 1 masks: 0xa8 and 0xb0 for A, 0xb8 and /// 0xc0 for B. @@ -271,9 +325,10 @@ pub(super) struct Pi { } impl Pi { - pub(super) fn new(interface: u8, now: u64) -> Self { + pub(super) fn new(interface: u8, processors: Population, now: u64) -> Self { let pi = Self { interface, + processors, pending: Default::default(), mask: Default::default(), request: Default::default(), @@ -319,10 +374,9 @@ impl Pi { out[slot] = unsafe { word.as_ref() }.map_or(0, |w| (w.load(SeqCst) >> CAUSE_IP2) & 0x1f); } - /// Whether a processor sits in `slot` of this interface: only PI0 slot A - /// holds one. + /// Whether a processor sits in `slot` of this interface. fn started(&self, slot: usize) -> bool { - self.interface == 0 && slot == 0 + self.processors.fitted(self.interface, slot) } /// Report an access to a register the model does not implement. @@ -580,10 +634,15 @@ impl Pi { self.counter.read().paused == RUNNING } - /// The CPU-number register: the reading processor's slot in bit 0, A - /// for the one processor, and this interface in bit 1. + /// The CPU-number register: the reading processor's slot in bit 0 and + /// this interface in bit 1; slot A for a reader that is no processor + /// here. fn cpu_number(&self) -> u64 { - u64::from(self.interface) << 1 + let slot = crate::cpu::smp::on_cpu_thread() + .and_then(|cpu| self.processors.slot_of(cpu)) + .filter(|&(interface, _)| interface == self.interface) + .map_or(0, |(_, slot)| slot as u64); + slot | u64::from(self.interface) << 1 } /// A doubleword read at `off`, publishing a comparator that latched on diff --git a/src/dev/bedrock/pi/tests.rs b/src/dev/bedrock/pi/tests.rs index 156f98b7f..80ee007a4 100644 --- a/src/dev/bedrock/pi/tests.rs +++ b/src/dev/bedrock/pi/tests.rs @@ -1,6 +1,7 @@ // Cases follow tests/qtest/sn1-bedrock-pi-test.c in jamesbraid/qemu-sgi-origami: // power-on-values, local-reset, pending-reflection, real-time-comparators, -// profile-counter-wrap, cpu-number-fallback, cpu-enable-lines and nmi. +// profile-counter-wrap, cpu-number-fallback, cpu-enable-lines and nmi, and +// sn1-cpu-identity-test's four-processors-know-their-slots. use super::super::reg::PI1_BASE; use super::super::Rig; @@ -171,18 +172,83 @@ fn the_timer_manager_delivers_expirations_without_mmio_polling() { assert!(!chip.is_running()); } -/// The PROM counts processors from the present bits: the one processor -/// sits in PI0 slot A, present and enabled, and reads slice A of PI0 from -/// the CPU-number register. +/// The PROM counts processors from the present bits. A board fills PI0/A, +/// then PI1/A, then PI0/B and PI1/B, so two processors sit one per interface. #[test] -fn the_one_processor_sits_in_pi0_slot_a() { - let rig = Rig::new(); - let read = |interface: u64, off| rig.r(interface * PI1_BASE + off); - assert_eq!([read(0, reg::INTERFACE_SLOT_A), read(0, reg::INTERFACE_SLOT_B), - read(1, reg::INTERFACE_SLOT_A), read(1, reg::INTERFACE_SLOT_B)], [1, 0, 0, 0]); - assert_eq!([read(0, reg::CPU_A_ENABLE), read(0, reg::CPU_B_ENABLE), - read(1, reg::CPU_A_ENABLE), read(1, reg::CPU_B_ENABLE)], [1, 0, 0, 0], "enables"); - assert_eq!((read(0, reg::INTERFACE_ID), read(1, reg::INTERFACE_ID)), (0, 2)); +fn every_fitted_slot_reads_present_and_starts_enabled() { + for (mask, present) in [(0b0001, [1, 0, 0, 0]), (0b0101, [1, 0, 1, 0]), (0b0111, [1, 1, 1, 0]), (0b1111, [1, 1, 1, 1])] { + let rig = Rig::with(Population::new(mask).unwrap()); + let read = |interface: u64, off| rig.r(interface * PI1_BASE + off); + assert_eq!([read(0, reg::INTERFACE_SLOT_A), read(0, reg::INTERFACE_SLOT_B), + read(1, reg::INTERFACE_SLOT_A), read(1, reg::INTERFACE_SLOT_B)], present, "{mask:#06b}"); + assert_eq!([read(0, reg::CPU_A_ENABLE), read(0, reg::CPU_B_ENABLE), + read(1, reg::CPU_A_ENABLE), read(1, reg::CPU_B_ENABLE)], present, "{mask:#06b}: enables"); + assert_eq!((read(0, reg::INTERFACE_ID), read(1, reg::INTERFACE_ID)), (0, 2), "{mask:#06b}: no requester"); + } +} + +/// A population that cannot hold CPU 0, or names a fifth slot, is refused +/// rather than built; the board's order puts CPU n in the nth fitted slot, +/// and one, two and four processors fill PI0/A, then PI1/A, then the B slots. +#[test] +fn a_population_without_cpu_0_or_beyond_four_slots_is_refused() { + for mask in [0, 0b0010, 0b1100, 0b1_0001, 0xff] { + assert!(Population::new(mask).is_err(), "{mask:#x}"); + } + let four = Population::new(0b1111).unwrap(); + assert_eq!((0..4).map(|cpu| four.slot_of(cpu).unwrap()).collect::>(), [(0, 0), (1, 0), (0, 1), (1, 1)]); + assert_eq!(four.slot_of(4), None); + assert_eq!(Population::new(0b0101).unwrap().slot_of(1), Some((1, 0))); + assert_eq!(Population::SINGLE.count(), 1); + assert_eq!([1, 2, 4].map(|n| Population::fitted_cpus(n).unwrap().mask()), [0b0001, 0b0101, 0b1111]); + assert!(Population::fitted_cpus(0).is_err() && Population::fitted_cpus(5).is_err()); +} + +/// Each processor reads its own slice from the CPU-number register of its +/// interface, and a reader that is no processor reads slot A. The local +/// register form answers the requester's own PI at offset 0. +/// (sn1-cpu-identity-test four-processors-know-their-slots) +#[test] +fn four_processors_know_their_slots_and_their_own_pi() { + let rig = Arc::new(Rig::with(Population::new(0b1111).unwrap())); + assert_eq!((rig.r(reg::INTERFACE_ID), rig.r(PI1_BASE + reg::INTERFACE_ID)), (0, 2), "no issuing processor"); + let local = rig.chip.local_registers(); + let slices: Vec<(u64, u64)> = (0..4u8) + .map(|cpu| { + let (rig, local) = (rig.clone(), local.clone()); + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(cpu); + let (interface, _) = Population::new(0b1111).unwrap().slot_of(cpu as usize).unwrap(); + (rig.r(u64::from(interface) * PI1_BASE + reg::INTERFACE_ID), local.read64(reg::INTERFACE_ID).data) + }) + .join() + .unwrap() + }) + .collect(); + assert_eq!(slices, [(0, 0), (2, 2), (1, 1), (3, 3)], "slices A, C, B, D"); + // With processors on PI0 alone, the local form is the node form. + let fuel = Rig::new(); + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(0); + assert_eq!(fuel.chip.local_registers().read64(reg::INTERFACE_ID).data, 0); + }) + .join() + .unwrap(); +} + +/// Each processor's IP2..IP6 come from its own slot: an interrupt pending +/// in PI1's bank 0 under slot A's mask reaches CPU 1 and no other. +#[test] +fn each_processor_takes_the_interrupts_of_its_own_slot() { + let rig = Rig::with(Population::new(0b1111).unwrap()); + let words: Vec> = (0..4).map(|_| Arc::new(Default::default())).collect(); + for (cpu, word) in words.iter().enumerate() { + rig.chip.set_cpu_interrupts(cpu, &**word); + } + rig.w(PI1_BASE + reg::OFFSET_00A8, 1 << 9); + rig.w(PI1_BASE + reg::PENDING_MODIFY, 0x109); + let ip2 = |cpu: usize| (words[cpu].load(Ordering::SeqCst) >> 10) & 1; + assert_eq!([ip2(0), ip2(1), ip2(2), ip2(3)], [0, 1, 0, 0]); } /// Enabling a processor that is not fitted, or clearing the fitted one's diff --git a/src/dev/bedrock/tests.rs b/src/dev/bedrock/tests.rs index 8c600e291..9073c7617 100644 --- a/src/dev/bedrock/tests.rs +++ b/src/dev/bedrock/tests.rs @@ -319,9 +319,10 @@ fn the_inbound_dma_and_pio_paths_take_no_lock() { }); } -/// A test chip's configuration: one 64 MiB bank, on `clock`. +/// A test chip's configuration: the Fuel's one processor and one 64 MiB +/// bank, on `clock`. pub(super) fn test_config(clock: Clock) -> BedrockConfig<'static> { - BedrockConfig { name: "test", clock, banks_mib: &[64], events: None } + BedrockConfig { name: "test", clock, processors: Population::SINGLE, banks_mib: &[64], events: None } } /// A chip on a hand-driven clock, for the unit tests: its register window @@ -335,12 +336,16 @@ pub(super) struct Rig { pub(super) events: parking_lot::Mutex>, } impl Rig { + /// The Fuel's one processor. pub(super) fn new() -> Self { + Self::with(Population::SINGLE) + } + pub(super) fn with(processors: Population) -> Self { let now = Arc::new(AtomicU64::new(0)); let clock = now.clone(); let config = test_config(Arc::new(move || clock.load(Ordering::SeqCst))); let (events, receiver) = std::sync::mpsc::channel(); - let chip = Bedrock::new(BedrockConfig { events: Some(events), ..config }).unwrap(); + let chip = Bedrock::new(BedrockConfig { processors, events: Some(events), ..config }).unwrap(); let word = Arc::new(AtomicU64::new(0)); chip.set_cpu_interrupts(0, &*word); chip.start(); diff --git a/src/ip34_machine.rs b/src/ip34_machine.rs index 767d01885..9591daf2b 100644 --- a/src/ip34_machine.rs +++ b/src/ip34_machine.rs @@ -26,7 +26,7 @@ use crate::config::{scsi_label, FuelConsole, GraphicsBoard, MachineConfig}; use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; use crate::cpu::mips_tlb::MipsTlb; -use crate::dev::bedrock::Bedrock; +use crate::dev::bedrock::{Bedrock, Population}; use crate::dev::bedrock_flash::{FirmwareRecord, Flash}; use crate::dev::heart::{host_clock, Clock}; use crate::dev::ioc3::{Ioc3, Ioc3Config}; @@ -612,6 +612,7 @@ impl FuelBoard { clock: clock.clone(), flash_path, banks_mib: &FUEL_BANKS_MIB, + processors: Population::SINGLE, firmware: FirmwareRecord::FUEL, board_id: FUEL_BOARD_ID, l1: l1_config, diff --git a/src/ip35.rs b/src/ip35.rs index e942bec5d..5867a6710 100644 --- a/src/ip35.rs +++ b/src/ip35.rs @@ -405,6 +405,15 @@ impl BusDevice for CachedPhysical { self.memory_target(a, 8) .map(|(view, pa)| view.write_block(pa, data)).unwrap_or(BUS_ERR) } + // A processor's SC to RAM the shadow cache cannot reach directly, such + // as a bank smaller than a direct-window region: the swap the store makes atomic, + // which matters once a node runs more than one processor. + fn cas32(&self, a: u64, old: u32, new: u32) -> Option { + self.memory_target(a, 4).and_then(|(view, pa)| view.cas32(pa, old, new)) + } + fn cas64(&self, a: u64, old: u64, new: u64) -> Option { + self.memory_target(a, 8).and_then(|(view, pa)| view.cas64(pa, old, new)) + } processor_access!(read8, write8, u8, BusRead8, 1, memory_target); processor_access!(read16, write16, u16, BusRead16, 2, memory_target); processor_access!(read32, write32, u32, BusRead32, 4, memory_target); diff --git a/src/ip35/node.rs b/src/ip35/node.rs index 00178c277..d6079f84c 100644 --- a/src/ip35/node.rs +++ b/src/ip35/node.rs @@ -15,7 +15,7 @@ use super::{map_device, Ip35Bus, Window}; use crate::cpu::mips_exec::CpuDevice; -use crate::dev::bedrock::{Bedrock, BedrockConfig, BANK_SPAN, SLOTS}; +use crate::dev::bedrock::{Bedrock, BedrockConfig, Population, BANK_SPAN, SLOTS}; use crate::dev::bedrock_flash::{FirmwareRecord, Flash, PART_SIZE}; use crate::dev::heart::Clock; use crate::dev::l1::{L1Config, L1Event, L1}; @@ -36,6 +36,8 @@ pub(crate) struct NodeConfig<'a> { pub flash_path: Option<&'a Path>, /// The memory banks in MiB, locational bank 0 upward. pub banks_mib: &'a [u32], + /// The processor slots the board fits. + pub processors: Population, pub firmware: FirmwareRecord, /// The word the board reads back at the Hub-special board-id window. pub board_id: u64, @@ -62,11 +64,12 @@ pub(crate) struct Node { impl Node { pub fn build(config: NodeConfig<'_>) -> Result { - let NodeConfig { name, payload, clock, flash_path, banks_mib, firmware, board_id, l1 } = config; + let NodeConfig { name, payload, clock, flash_path, banks_mib, processors, firmware, board_id, l1 } = config; let (events, receiver) = channel(); let bedrock = Bedrock::new(BedrockConfig { name, clock: clock.clone(), + processors, banks_mib, events: Some(events.clone()), })?; @@ -80,11 +83,10 @@ impl Node { let mut bus = Ip35Bus::new(phys.clone()); bedrock.rebuild_memory(); bus.set_fallback(1, 1 << 32, bedrock.pio()); - // The local and node XIO register forms: with the one processor on - // PI0, the same window. - let registers = bedrock.registers(); - bus.map(1, 0x0100_0000, 0x80_0000, registers.clone()); - bus.map(1, 0x0180_0000, 0x80_0000, registers); + // The local and node XIO register forms: with processors on PI0 + // alone, the same window. + bus.map(1, 0x0100_0000, 0x80_0000, bedrock.local_registers()); + bus.map(1, 0x0180_0000, 0x80_0000, bedrock.registers()); // Uncached RAM reaches the banks directly, as cached RAM does; the // diagnostic views stay separate. let uncached = bedrock.uncached_view(); @@ -142,9 +144,9 @@ impl Node { } -/// Give a node's memory and interrupts to its processors, CPU 0 first: a -/// cached view of memory for every one, and CPU 0's IP2..IP6 from PI0 slot -/// A, where the node's one processor sits. The L1's power events reach +/// Give a node's memory and interrupts to its processors, CPU 0 first, each +/// in the slot the board's population gives it: a cached view of memory for +/// every one and its IP2..IP6 from its PI slot. The L1's power events reach /// the system controller on `events`, and its soft reset every processor. pub(crate) fn connect_cpus( name: &'static str, From af5ba27ac7fc7db8d1ce09a7329de0b57e383142 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 04:05:39 -0700 Subject: [PATCH 14/33] bedrock: order the PROM's reset barrier on LB scratch 1 Every processor of an IP35 node clears LB scratch 1 on its reset path, then the boot master stores its release there and the others poll for it. On hardware every clear lands first because the processors leave reset on one clock edge (inferred: they share the node's reset; no timing specification is retained). In the emulator each processor is a host thread, so on a multiprocessor board a late clear can land on the release and the PROM counts that processor as absent. Four-processor Tezro boots log such late clears. The LB now orders those stores as its own state, under its lock, with no call out of the chip. A processor's first store after the block's reset that arrives once another processor's second store (the release) has landed does not land, and that processor reads the release until it stores again. The rule depends only on who stored in what order. An access no processor makes is plain storage. Each processor's first store after the release, the PROM's presence join, is recorded in an atomic mask that Bedrock::scratch1_joined reads, for a board that needs to know who has joined. This is an approximated model rule with no hardware counterpart: hardware drops no store and answers every processor alike. Its limitations, also listed in the code: no ordering for a second PROM run after an L1 soft reset, which restarts the processors without an LB reset; a processor that stalls past the others' joins reads them; a late clear after a peer node's store is dropped. The Fuel's one processor never stores after its own release first, so its sequence is unchanged. The discriminating test, the_logged_late_prom_sequence_keeps_the_release, replays the stores of a four-processor Tezro boot in which two processors cleared after the release: with the rule the release survives and every processor joins; without it the late clears erase the release. --- src/dev/bedrock/lb.rs | 322 +++++++++++++++++++++++++++++++++++++++-- src/dev/bedrock/mod.rs | 10 +- 2 files changed, 315 insertions(+), 17 deletions(-) diff --git a/src/dev/bedrock/lb.rs b/src/dev/bedrock/lb.rs index 1dad35c39..9f8ede2dc 100644 --- a/src/dev/bedrock/lb.rs +++ b/src/dev/bedrock/lb.rs @@ -8,12 +8,20 @@ //! The slot is decoded immediately and completes after one microsecond of //! machine time; no 1-wire part answers, so a reset pulse and a short write //! slot read back as presence-free and as a one. Permission registers retain -//! guest writes, but the register path carries no requester identity, so it -//! cannot enforce those permissions. +//! guest writes, but the register path carries no requester identity except +//! the calling CPU thread's, which only the scratch-1 rule uses, so it does +//! not enforce those permissions. //! //! The plain register words sit in a sparse map keyed by offset, since the //! 0xc040-byte block holds only a few dozen of them. //! +//! Scratch 1 orders the PROM's reset barrier ([`Barrier`]): a processor's +//! first store after the block's reset that arrives once another +//! processor's later store has landed does not land. That is a model rule +//! with no hardware counterpart, standing in for processors that leave +//! reset on one clock edge (inferred: the processors share the node's +//! reset; no timing specification is retained). +//! //! Ported from hw/misc/sgi-bedrock-lb.c in jamesbraid/qemu-sgi-origami. use super::pi::Levels; @@ -22,7 +30,7 @@ use super::{Effect, NodeReset}; use crate::dev::w1; use parking_lot::Mutex; use std::collections::BTreeMap; -use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; /// Length of the block-relative register aperture. pub(super) const SIZE: u64 = 0xc040; @@ -60,6 +68,8 @@ pub mod reg { } const ID: u64 = (0x36 << 1) | (0x3012 << 12) | (2 << 28); +/// Scratch 1 in the plain scratch window. +const SCRATCH1: u64 = reg::PIO_WINDOW + 8; const VECTOR_MASK: u64 = 3 | (0x1fffff << 3) | (0xff << 32) | (0x7ff << 40); struct Regs { words: BTreeMap, @@ -68,13 +78,99 @@ struct Regs { region: u8, deadline: Option, sample: bool, + barrier: Barrier, } impl Default for Regs { fn default() -> Self { let mut words = BTreeMap::new(); words.insert(reg::PIO_PERMISSION, !0); words.insert(reg::INBOUND_PERMISSION, !0); - Self { words, scratch: [0; 5], node: 0, region: 0, deadline: None, sample: true } + Self { words, scratch: [0; 5], node: 0, region: 0, deadline: None, sample: true, barrier: Barrier::default() } + } +} + +/// The order of the processors' stores to scratch 1 since the block's last +/// reset, for the PROM's reset barrier. +/// +/// Every processor of a node clears scratch 1 on its reset path, then the +/// boot master stores its release there and the others poll for it. On +/// hardware every processor leaves reset on the same clock edge, so every +/// clear lands before the release (inferred: the processors share the +/// node's reset; no timing specification is retained). Here each processor is a host thread +/// that can start or stall late, and a late clear would erase the release: +/// the PROM then counts that processor as absent. So a processor's first +/// store of the epoch that arrives after another processor's second or +/// later store (the release) has landed does not land: it is ordered below +/// the release, as it would have been on hardware. Until that processor +/// stores again it reads the release, whatever has landed since: on +/// hardware it would have polled the release long before the others' +/// next stores (the four-processor PROM's come millions of instructions +/// later). +/// +/// Model rule, approximated: no hardware rule drops a store or answers one +/// processor differently from another. It is a function of the store +/// sequence alone (who stored, in what order), never of values or host +/// time. An access no processor makes (a monitor, a test, a peer node) +/// is plain storage and changes nothing here. +/// +/// Limitations: an L1 soft reset restarts the processors without an LB +/// reset, so a second PROM run in the same epoch gets no ordering. A +/// processor that cleared on time but stalls until after the others' joins +/// reads those, not the release. A peer node's store is plain storage, and +/// a late local clear after it is dropped, keeping the peer's value. The +/// requester is the calling thread's CPU number (`smp::on_cpu_thread`), +/// not a field of the access. +#[derive(Default)] +struct Barrier { + /// Processors, by CPU number, that have stored this epoch. + stored: u64, + /// The first processor's second or later store this epoch: the release. + release: Option, + /// Processors whose only store this epoch was ordered below the release. + behind: u64, +} + +/// What [`Barrier::store`] makes of one store. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Scratch1Store { + /// It lands. + Lands, + /// It lands, and is its processor's first after the release: the PROM's + /// presence join. Holds the processor's bit. + Joins(u64), + /// A first store after the release: it does not land, and its processor + /// reads the release until it stores again. + BelowRelease, +} + +fn cpu_bit(cpu: Option) -> Option { + cpu.and_then(|c| 1u64.checked_shl(c as u32)) +} + +impl Barrier { + /// Order a store of `value` from `cpu`, or from no processor. + fn store(&mut self, cpu: Option, value: u64) -> Scratch1Store { + let Some(bit) = cpu_bit(cpu) else { return Scratch1Store::Lands }; + if self.stored & bit == 0 { + self.stored |= bit; + if self.release.is_none() { + return Scratch1Store::Lands; + } + self.behind |= bit; + return Scratch1Store::BelowRelease; + } + self.behind &= !bit; + if self.release.is_some() { + return Scratch1Store::Joins(bit); + } + self.release = Some(value); + Scratch1Store::Lands + } + + /// What a read from `cpu` sees, if not the register: the release, for a + /// processor still behind it. + fn read(&self, cpu: Option) -> Option { + cpu_bit(cpu).filter(|bit| self.behind & bit != 0).and(self.release) } } impl Regs { @@ -85,11 +181,20 @@ impl Regs { pub(super) struct Lb { regs: Mutex, + /// Processors, by CPU number, whose presence join on scratch 1 has + /// landed since the block's last reset ([`Scratch1Store::Joins`]). + /// Written under the block's lock, read without it. + joined: AtomicU64, } impl Lb { pub(super) fn new() -> Self { - Self { regs: Mutex::new(Regs::default()) } + Self { regs: Mutex::new(Regs::default()), joined: AtomicU64::new(0) } + } + /// Whether processor `cpu`'s presence join on scratch 1 has landed since + /// the block's last reset. One atomic load. + pub(super) fn scratch1_joined(&self, cpu: usize) -> bool { + cpu_bit(Some(cpu)).is_some_and(|bit| self.joined.load(Ordering::Acquire) & bit != 0) } pub(super) fn locked(&self) -> bool { self.regs.is_locked() @@ -117,23 +222,27 @@ impl Lb { }) } /// The block's reset, under its lock: the scratch words 1 and 2 and the - /// error status survive it. The node number returns to 0. - fn reset(r: &mut Regs, node: &AtomicU8) { + /// error status survive it. The node number returns to 0, and the reset + /// barrier starts a new epoch. + fn reset(&self, r: &mut Regs, node: &AtomicU8) { let survivors = [r.scratch[1], r.scratch[2], r.words.get(®::ERROR_STATUS).copied().unwrap_or(0)]; *r = Regs::default(); + self.joined.store(0, Ordering::Release); r.scratch[1] = survivors[0]; r.scratch[2] = survivors[1]; r.words.insert(reg::ERROR_STATUS, survivors[2]); node.store(0, Ordering::Release); } pub(super) fn power_on(&self, node: &AtomicU8) { - Self::reset(&mut self.regs.lock(), node); + self.reset(&mut self.regs.lock(), node); } - pub(super) fn read(&self, a: u64, now: u64) -> u64 { + /// A doubleword read at `a` by processor `requester`, or by none. + pub(super) fn read(&self, a: u64, now: u64, requester: Option) -> u64 { let mut r = self.regs.lock(); Self::complete(&mut r, now); match a { reg::NODE_REGION => ID | ((r.node as u64) << 32) | ((r.region as u64) << 46), + SCRATCH1 => r.barrier.read(requester).unwrap_or(r.scratch[1]), reg::PIO_WINDOW..=reg::PIO_WINDOW_END => r.scratch[(a - reg::PIO_WINDOW) as usize / 8], reg::ATOMIC_READ0 | reg::ATOMIC_READ1 => { let n = 3 + (a - reg::ATOMIC_READ0) as usize / 8; @@ -150,11 +259,11 @@ impl Lb { } } } - /// A doubleword store at `a`. The error level reaches `levels` and the - /// node number `node` under the block's lock, so concurrent stores - /// publish in register order. A soft reset resets the block and asks - /// for the node's. - pub(super) fn write(&self, a: u64, v: u64, now: u64, levels: &Levels, node: &AtomicU8) -> Effect { + /// A doubleword store at `a` by processor `requester`, or by none. The + /// error level reaches `levels` and the node number `node` under the + /// block's lock, so concurrent stores publish in register order. A soft + /// reset resets the block and asks for the node's. + pub(super) fn write(&self, a: u64, v: u64, now: u64, requester: Option, levels: &Levels, node: &AtomicU8) -> Effect { let mut r = self.regs.lock(); Self::complete(&mut r, now); match a { @@ -168,6 +277,18 @@ impl Lb { node.store(r.node, Ordering::Release); } } + SCRATCH1 => { + let order = r.barrier.store(requester, v); + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB scratch 1 store {v:#x} from CPU {requester:?}: {order:?}"); + match order { + Scratch1Store::BelowRelease => {} + Scratch1Store::Lands => r.scratch[1] = v, + Scratch1Store::Joins(bit) => { + r.scratch[1] = v; + self.joined.fetch_or(bit, Ordering::Release); + } + } + } reg::PIO_WINDOW..=reg::PIO_WINDOW_END => { let n = (a - reg::PIO_WINDOW) as usize / 8; r.scratch[n] = if n < 3 { v } else { v & 1 }; @@ -185,7 +306,7 @@ impl Lb { } reg::SOFT_RESET => { if v & 1 != 0 { - Self::reset(&mut r, node); + self.reset(&mut r, node); return Effect::Reset(NodeReset::LocalBlock); } } @@ -307,6 +428,177 @@ mod tests { rig.set_time(3000); assert_eq!(rig.r(LB_BASE + reg::MICRO_LAN), 0); } + const RELEASE: u64 = 0x07f8_0000; + /// The PROM's flags word that follows the joins. + const FLAGS: u64 = 0x6000_0000_0000_0000; + const SCRATCH1_AT: u64 = LB_BASE + SCRATCH1; + + /// Run `f` on a thread that is processor `cpu`. + fn as_cpu(cpu: u8, f: impl FnOnce() -> T + Send + 'static) -> T { + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(cpu); + f() + }) + .join() + .unwrap() + } + fn store_as(rig: &Rig, cpu: u8, value: u64) { + let regs = rig.regs.clone(); + assert_eq!(as_cpu(cpu, move || regs.write64(SCRATCH1_AT, value)), crate::traits::BUS_OK); + } + fn read_as(rig: &Rig, cpu: u8) -> u64 { + let regs = rig.regs.clone(); + as_cpu(cpu, move || regs.read64(SCRATCH1_AT).data) + } + /// Scratch 1 as a reader that is no processor sees it. + fn scratch1(rig: &Rig) -> u64 { + rig.r(SCRATCH1_AT) + } + fn joined(rig: &Rig, cpu: usize) -> bool { + rig.chip.scratch1_joined(cpu) + } + + /// A processor's reset-path clear that arrives after the master's + /// release does not erase it, and that processor's next store joins. + #[test] + fn a_late_clear_lands_below_the_release() { + let rig = Rig::new(); + store_as(&rig, 0, 0); + store_as(&rig, 0, RELEASE); + store_as(&rig, 1, 0); + assert_eq!(scratch1(&rig), RELEASE, "the late clear did not land"); + assert_eq!(read_as(&rig, 1), RELEASE); + assert!(!joined(&rig, 1)); + store_as(&rig, 1, 0x101); + assert_eq!(scratch1(&rig), 0x101); + assert!(joined(&rig, 1) && !joined(&rig, 0)); + } + + /// A processor whose clear came after the release reads the release + /// until it stores again, even once another processor's next store has + /// replaced it; everyone else reads the register. + #[test] + fn a_late_processor_reads_the_release_until_it_stores_again() { + let rig = Rig::new(); + store_as(&rig, 0, 0); + store_as(&rig, 1, 0); + store_as(&rig, 0, RELEASE); + store_as(&rig, 2, 0); + store_as(&rig, 1, 0); + assert_eq!(read_as(&rig, 2), RELEASE, "CPU 2 is still behind the release"); + assert_eq!((read_as(&rig, 1), read_as(&rig, 0), scratch1(&rig)), (0, 0, 0)); + store_as(&rig, 2, 0x102); + assert_eq!(read_as(&rig, 2), 0x102); + assert!(joined(&rig, 2)); + } + + /// Clears that come before the release land, the release lands over + /// them, and the release itself is no join. + #[test] + fn a_release_after_every_clear_lands() { + let rig = Rig::new(); + for cpu in 0..4 { + store_as(&rig, cpu, 0x55); + assert_eq!(scratch1(&rig), 0x55); + } + store_as(&rig, 0, RELEASE); + assert_eq!(scratch1(&rig), RELEASE); + assert!((0..4).all(|c| !joined(&rig, c))); + for cpu in 0..4u8 { + store_as(&rig, cpu, 0x100 + u64::from(cpu)); + assert_eq!(scratch1(&rig), 0x100 + u64::from(cpu)); + assert!(joined(&rig, usize::from(cpu))); + } + } + + /// The block's reset, at power-on or from its soft-reset bit, starts a + /// new epoch: every processor's next store is a first store again, and + /// the joins are forgotten. + #[test] + fn each_block_reset_starts_a_new_barrier() { + let rig = Rig::new(); + let epoch = |late: u8| { + store_as(&rig, 1 - late, 0); + assert_eq!(scratch1(&rig), 0, "a first store"); + store_as(&rig, 1 - late, RELEASE); + store_as(&rig, late, 0); + assert_eq!(scratch1(&rig), RELEASE); + store_as(&rig, late, 0x100); + assert!(joined(&rig, usize::from(late))); + }; + epoch(1); + rig.chip.lb.power_on(&rig.chip.node); + assert!(!joined(&rig, 1)); + assert_eq!(scratch1(&rig), 0x100, "scratch 1 survives the reset"); + epoch(0); + rig.w(LB_BASE + reg::SOFT_RESET, 1); + assert!(!joined(&rig, 0)); + epoch(1); + } + + /// Stores no processor makes (a monitor, a test, a peer node's vector + /// write) always land and move nothing. + #[test] + fn a_store_from_no_processor_always_lands() { + let rig = Rig::new(); + for value in [RELEASE, 0, RELEASE, 0] { + rig.w(SCRATCH1_AT, value); + assert_eq!(scratch1(&rig), value); + } + store_as(&rig, 1, 0); + assert_eq!(scratch1(&rig), 0, "no release has landed from a processor"); + } + + /// (CPU, value stored, scratch 1 after) for the four-processor PROM's + /// stores to scratch 1 up to its flags word, in the order a Tezro boot + /// with every processor on time logged them reaching the LB: four + /// clears, CPU 0's release, each processor's join (a store of 0), then + /// each processor's flags word. + const ON_TIME_PROM_SEQUENCE: &[(u8, u64, u64)] = &[ + (0, 0, 0), (1, 0, 0), (3, 0, 0), (2, 0, 0), + (0, RELEASE, RELEASE), + (1, 0, 0), (0, 0, 0), (2, 0, 0), (3, 0, 0), + (3, FLAGS, FLAGS), (2, FLAGS, FLAGS), (1, FLAGS, FLAGS), (0, FLAGS, FLAGS), + ]; + + /// With every processor on time the rule changes nothing. + #[test] + fn the_on_time_prom_sequence_is_plain_storage() { + let rig = Rig::new(); + for &(cpu, value, after) in ON_TIME_PROM_SEQUENCE { + store_as(&rig, cpu, value); + assert_eq!(scratch1(&rig), after, "CPU {cpu} storing {value:#x}"); + } + assert!((0..4).all(|c| joined(&rig, c))); + } + + /// The same, from a four-processor Tezro boot in which CPUs 2 and 3 + /// cleared after CPU 0's release (and CPU 1 joined before either had + /// stored again). + const LATE_PROM_SEQUENCE: &[(u8, u64, u64)] = &[ + (1, 0, 0), (0, 0, 0), + (0, RELEASE, RELEASE), + (2, 0, RELEASE), (3, 0, RELEASE), + (1, 0, 0), (2, 0, 0), (3, 0, 0), (0, 0, 0), + (2, FLAGS, FLAGS), (0, FLAGS, FLAGS), (3, FLAGS, FLAGS), (1, FLAGS, FLAGS), + ]; + + /// Replayed, the late clears do not erase the release, and the late + /// processors read the release until their joins even after CPU 1's + /// join has replaced it. + #[test] + fn the_logged_late_prom_sequence_keeps_the_release() { + let rig = Rig::new(); + for (i, &(cpu, value, after)) in LATE_PROM_SEQUENCE.iter().enumerate() { + store_as(&rig, cpu, value); + assert_eq!(scratch1(&rig), after, "store {i}: CPU {cpu} storing {value:#x}"); + if i == 5 { + assert_eq!((read_as(&rig, 2), read_as(&rig, 3)), (RELEASE, RELEASE)); + } + } + assert!((0..4).all(|c| joined(&rig, c))); + } + /// The soft-reset bit resets the block, as a power-on does, and asks the /// board for a node reset once; the other bits do neither. #[test] diff --git a/src/dev/bedrock/mod.rs b/src/dev/bedrock/mod.rs index fa1bcc0c2..472b78e03 100644 --- a/src/dev/bedrock/mod.rs +++ b/src/dev/bedrock/mod.rs @@ -264,6 +264,12 @@ impl Bedrock { pub fn rebuild_memory(&self) { self.md.rebuild_memory(); } + /// Whether processor `cpu`'s join after the PROM's reset barrier has + /// landed on LB scratch 1 since the LB's last reset: the first store it + /// made there after the boot master's release. One atomic load. + pub fn scratch1_joined(&self, cpu: usize) -> bool { + self.lb.scratch1_joined(cpu) + } #[cfg(feature = "jitv2")] pub(crate) fn generation_base(&self) -> *mut AtomicU64 { self.md.generation_base() @@ -454,8 +460,8 @@ impl Bedrock { let (data, effect) = match (base, value) { (reg::IO_BASE, None) => (self.io.read(a), Effect::None), (reg::IO_BASE, Some(v)) => (0, self.io.write(a, v)), - (reg::LB_BASE, None) => (self.lb.read(a, now), Effect::None), - (reg::LB_BASE, Some(v)) => (0, self.lb.write(a, v, now, &self.levels, &self.node)), + (reg::LB_BASE, None) => (self.lb.read(a, now, crate::cpu::smp::on_cpu_thread()), Effect::None), + (reg::LB_BASE, Some(v)) => (0, self.lb.write(a, v, now, crate::cpu::smp::on_cpu_thread(), &self.levels, &self.node)), (reg::NI_BASE, None) => self.ni.read(a, &self.levels), (reg::NI_BASE, Some(v)) => (0, self.ni.write(a, v, &self.levels)), (_, None) => (self.xb.read(a), Effect::None), From 0b8ca448212bab58edfcad7fddfeb585e0c385e0 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 20:53:38 -0700 Subject: [PATCH 15/33] ioc_serial_dma, deadline_timers: add the IOC serial ring engine and its timers The IOC4 puts in front of its four 16550s the ring-DMA engine the IOC3 has, and the two chips are meant to track the QEMU models, which share one engine (sgi-ioc-serial-dma). Add it as its own module under QEMU's names (SerialDma and friends). It reaches the chip around it through a host trait: the 16550s, the interrupt bits, the bus master and the error report. Two parameters cover what the IOC3 and IOC4 do differently, and a chip can halt a port on a failed transfer until a channel reset and receive the failed address. The host-time timer slots (one pending one-shot per deadline kind, replaced when its deadline moves, removed under the lock that keeps a late arm from outliving a stop) are DeadlineTimers. The engine follows QEMU's timing: it writes a character only when the 16550 reports transmit-ready, a looped-back character takes its character time on the line, and the engine wakes from a timer if nothing touches the chip meanwhile. Engine tests drive a stand-in host. The IOC3 keeps its own engine and its timing unchanged, and the 16550 is unchanged: it gains read-only accessors for its character time and transmit-ready state, and the IOC3's span helpers become visible to the crate. Moving the IOC3 onto this engine belongs with the IOC3's own split into functions, which is landing separately; with the IOC3 untouched here, that change rebases without conflicts and can then switch the IOC3 over in one commit. --- HACKING.md | 3 + src/dev/deadline_timers.rs | 200 +++++++ src/dev/ioc3.rs | 10 +- src/dev/ioc_serial_dma.rs | 1124 ++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 2 + src/dev/ns16550.rs | 15 + 6 files changed, 1349 insertions(+), 5 deletions(-) create mode 100644 src/dev/deadline_timers.rs create mode 100644 src/dev/ioc_serial_dma.rs diff --git a/HACKING.md b/HACKING.md index bc8640087..f09ef9687 100644 --- a/HACKING.md +++ b/HACKING.md @@ -177,6 +177,9 @@ screenshots), `CpuDevice` (what `Machine` holds for the CPU). Serial lines have two halves. A UART model owns the registers: the Indy's `Z85c30`, and `Ns16550` (`src/dev/ns16550.rs`), the 16550A core that the O2's `MaceUart` (lane decode, interrupt line, monitor) and the Octane's IOC3 wrap. +The IOC4's serial ring-DMA engine is its own module, `ioc_serial_dma.rs`, which +the chip drives through its `SerialDmaHost` trait (its 16550s, interrupt bits and +bus master); the IOC3 keeps its own copy for now. The host end is a `SerialBackend` from `src/dev/serial_host.rs`, which the machine constructors pick with `console_backends`: the CI backend, a TCP console, or a log tee. A UART's transmit is instant and its receive side diff --git a/src/dev/deadline_timers.rs b/src/dev/deadline_timers.rs new file mode 100644 index 000000000..6331fd08f --- /dev/null +++ b/src/dev/deadline_timers.rs @@ -0,0 +1,200 @@ +//! One-shot timers for the host-time deadlines a device keeps. +//! +//! A device evaluates its deadlines lazily, on every access, and arms one +//! timer per deadline so the work still happens when the guest is idle. The +//! IOC3 and IOC4 do this for their serial ring timers and transmit delays, +//! and the IOC3 for its interrupt output. This holds the part they share: +//! which timer serves which deadline, replacing one whose deadline moved, +//! and taking them all away on stop. +//! +//! QEMU has no counterpart: its timers belong to the virtual clock and are +//! armed where they are set. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; +use std::time::{Duration, Instant}; + +use parking_lot::Mutex; + +use crate::hptimer::{TimerId, TimerManager, TimerReturn}; + +/// The timers of a device with `N` kinds of deadline. +pub struct DeadlineTimers { + manager: OnceLock>, + /// The timer pending for each kind, and the deadline it serves. + armed: Mutex<[Option<(TimerId, Instant)>; N]>, +} + +impl DeadlineTimers { + pub fn new() -> Self { + Self { manager: OnceLock::new(), armed: Mutex::new([None; N]) } + } + + /// The timer manager to arm on. Set it before the device starts. + pub fn set_manager(&self, tm: Arc) { + let _ = self.manager.set(tm); + } + + /// Make the pending timers match `want`: one one-shot per deadline. A + /// timer whose deadline moved is replaced. With no manager set, or the + /// device not `running`, deadlines are only evaluated lazily, by the next + /// access. `floor` is the least delay for a kind, for a deadline that can + /// recur faster than a timer should fire; `fire` runs on the timer thread + /// with the device and the kind and deadline it was armed for. + pub fn arm( + &self, + want: &[Option; N], + running: &AtomicBool, + device: &Weak, + floor: fn(usize) -> Duration, + fire: fn(&Weak, usize, Instant) -> TimerReturn, + ) { + let Some(tm) = self.manager.get() else { return }; + let mut armed = self.armed.lock(); + // Checked under the lock `cancel_all` clears the slots with: a pass + // that sees `running` set has its timers removed by that stop. + if !running.load(Ordering::SeqCst) { + return; + } + for (kind, w) in want.iter().enumerate() { + if armed[kind].map(|(_, d)| d) == *w { + continue; + } + if let Some((id, _)) = armed[kind].take() { + tm.remove(id); + } + let Some(due) = *w else { continue }; + let delay = due.saturating_duration_since(Instant::now()).max(floor(kind)); + let id = tm.add_one_shot(delay, device.clone(), move |d| fire(d, kind, due)); + armed[kind] = Some((id, due)); + } + } + + /// A timer for `kind` and `due` has fired: forget it, unless a newer + /// deadline has replaced it. + pub fn fired(&self, kind: usize, due: Instant) { + let mut armed = self.armed.lock(); + if armed[kind].map(|(_, d)| d) == Some(due) { + armed[kind] = None; + } + } + + /// Whether a timer is pending for `kind`. + #[cfg(test)] + pub fn is_armed(&self, kind: usize) -> bool { + self.armed.lock()[kind].is_some() + } + + /// Whether no timer is pending. + #[cfg(test)] + pub fn none_armed(&self) -> bool { + self.armed.lock().iter().all(|a| a.is_none()) + } + + /// Remove every pending timer. + pub fn cancel_all(&self) { + let mut armed = self.armed.lock(); + for slot in armed.iter_mut() { + if let (Some((id, _)), Some(tm)) = (slot.take(), self.manager.get()) { + tm.remove(id); + } + } + } +} + +impl Default for DeadlineTimers { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::AtomicUsize; + + struct Device { + timers: DeadlineTimers<2>, + running: AtomicBool, + fired: [AtomicUsize; 2], + } + + fn fire(me: &Weak, kind: usize, due: Instant) -> TimerReturn { + if let Some(d) = me.upgrade() { + d.timers.fired(kind, due); + d.fired[kind].fetch_add(1, Ordering::SeqCst); + } + TimerReturn::Delete + } + + fn device() -> Arc { + let d = Arc::new(Device { + timers: DeadlineTimers::new(), + running: AtomicBool::new(true), + fired: Default::default(), + }); + d.timers.set_manager(Arc::new(TimerManager::new())); + d + } + + fn wait_for(f: impl Fn() -> bool) -> bool { + for _ in 0..500 { + if f() { + return true; + } + std::thread::sleep(Duration::from_millis(2)); + } + false + } + + #[test] + fn a_deadline_fires_once_and_its_slot_empties() { + let d = device(); + let due = Instant::now() + Duration::from_millis(5); + d.timers.arm(&[Some(due), None], &d.running, &Arc::downgrade(&d), |_| Duration::ZERO, fire); + assert!(d.timers.is_armed(0)); + assert!(!d.timers.is_armed(1)); + assert!(wait_for(|| d.fired[0].load(Ordering::SeqCst) == 1)); + assert!(d.timers.none_armed()); + assert_eq!(d.fired[1].load(Ordering::SeqCst), 0); + } + + #[test] + fn a_moved_deadline_replaces_its_timer() { + let d = device(); + let me = Arc::downgrade(&d); + let far = Instant::now() + Duration::from_secs(3600); + d.timers.arm(&[Some(far), None], &d.running, &me, |_| Duration::ZERO, fire); + let near = Instant::now() + Duration::from_millis(5); + d.timers.arm(&[Some(near), None], &d.running, &me, |_| Duration::ZERO, fire); + assert!(wait_for(|| d.fired[0].load(Ordering::SeqCst) == 1)); + assert_eq!(d.fired[0].load(Ordering::SeqCst), 1, "the far timer was removed, not left to fire"); + } + + #[test] + fn a_stopped_device_arms_nothing_and_cancel_clears_what_was_armed() { + let d = device(); + let me = Arc::downgrade(&d); + let far = Some(Instant::now() + Duration::from_secs(3600)); + d.running.store(false, Ordering::SeqCst); + d.timers.arm(&[far, far], &d.running, &me, |_| Duration::ZERO, fire); + assert!(d.timers.none_armed()); + d.running.store(true, Ordering::SeqCst); + d.timers.arm(&[far, far], &d.running, &me, |_| Duration::ZERO, fire); + assert!(d.timers.is_armed(0) && d.timers.is_armed(1)); + d.timers.cancel_all(); + assert!(d.timers.none_armed()); + } + + #[test] + fn the_floor_delays_a_kind_that_recurs_quickly() { + let d = device(); + let now = Instant::now(); + d.timers.arm(&[Some(now), Some(now)], &d.running, &Arc::downgrade(&d), |k| { + if k == 0 { Duration::from_millis(40) } else { Duration::ZERO } + }, fire); + assert!(wait_for(|| d.fired[1].load(Ordering::SeqCst) == 1)); + assert_eq!(d.fired[0].load(Ordering::SeqCst), 0, "kind 0 is held back by its floor"); + assert!(wait_for(|| d.fired[0].load(Ordering::SeqCst) == 1)); + } +} diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index d8557c9bd..89422a064 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -2856,23 +2856,23 @@ impl PciFunction for Ioc3 { // was waiting out. Deadlines are encoded as nanoseconds left plus one, // zero for none. -fn until(due: Option, now: Instant) -> u64 { +pub(crate) fn until(due: Option, now: Instant) -> u64 { due.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1) } -fn due(left: u64, now: Instant) -> Option { +pub(crate) fn due(left: u64, now: Instant) -> Option { (left != 0).then(|| now + Duration::from_nanos(left - 1)) } -fn span(d: Option) -> u64 { +pub(crate) fn span(d: Option) -> u64 { d.map_or(0, |d| d.as_nanos() as u64 + 1) } -fn unspan(n: u64) -> Option { +pub(crate) fn unspan(n: u64) -> Option { (n != 0).then(|| Duration::from_nanos(n - 1)) } -fn usize_field(v: &toml::Value, key: &str, limit: usize) -> Result { +pub(crate) fn usize_field(v: &toml::Value, key: &str, limit: usize) -> Result { let n = field_u64(v, key)? as usize; if n > limit { return Err(format!("{key}: {n} is more than {limit}")); diff --git a/src/dev/ioc_serial_dma.rs b/src/dev/ioc_serial_dma.rs new file mode 100644 index 000000000..31c276a33 --- /dev/null +++ b/src/dev/ioc_serial_dma.rs @@ -0,0 +1,1124 @@ +//! The serial ring-DMA engine of the SGI IOC3 and IOC4. +//! +//! Both chips put the same engine in front of each of their 16550-compatible +//! ports: seven 32-bit registers per port, a ring of eight-byte entries in +//! host memory for each direction, and a 64-bit ring base shared by a pair of +//! ports. The transmit walk writes the port's 16550; the receive side drains +//! its FIFO into a packer that becomes one ring entry. A ring's address is +//! derived from its port and direction, and no register reads it back. +//! +//! The chip around the engine owns its register window, its interrupt status +//! words and the 16550s. The engine reaches them through [`SerialDmaHost`], so the same +//! walk serves the IOC3 (two ports, nine interrupt bits each) and the IOC4 +//! (four ports, eight bits each, DMA errors reported elsewhere). +//! +//! # What differs between the chips +//! +//! [`SerialDmaParams`] holds what the two do differently and nothing else: +//! +//! * IOC3 reports the packer's held-character count in the receive producer +//! index. IOC4's register map does not define the field, so whether the +//! chip has it is not known. +//! * IOC4 stops a port after a failed transfer until a channel reset (its +//! error sources are named "serial halted by mem err"). Whether an IOC3 +//! port halts is not known. +//! +//! # What is approximated here +//! +//! * The receive timer and a transmit delay run on host time. A pause freezes +//! both where they stand and a resume lets each finish the time it had +//! left. No source says what the part does. +//! * Receive-ring-full is a level that follows a held entry; what the part +//! does is not known. +//! +//! # Mapping to QEMU +//! +//! The split is QEMU's: this file is the shared engine, `ioc3.rs` and +//! `ioc4.rs` are the chips that use it. +//! +//! | QEMU (`hw/char/sgi-ioc-serial-dma.c`) | here | +//! |---|---| +//! | `SGIIOCSerialDMA`, `SGIIOCSerialDMAPort` | [`SerialDma`], [`SerialDmaPort`] | +//! | `SGIIOCSerialDMAParams` | [`SerialDmaParams`] | +//! | `SGIIOCSerialDMAOps` and its `opaque` | [`SerialDmaHost`], implemented by the chip | +//! | `SGIIOCSerialDMASource` | [`SerialDmaSource`] | +//! | the port's `SerialState *uart` | [`SerialDmaUart`] | +//! | `sgi_ioc_sdma_port_read`, `_port_write`, `_kick`, `_catch_up`, `_reset` | `port_read`, `port_write`, `run`, `catch_up`, `reset` | +//! | `sgi_ioc_sdma_base_read`, `_base_write` | `base_read`, `base_write` | +//! +//! Ported from hw/char/sgi-ioc-serial-dma.c in jamesbraid/qemu-sgi-origami, +//! in the form the IOC3 had before the engine was shared. + +use std::time::{Duration, Instant}; + +use crate::dev::ioc3::{due, span, unspan, until, usize_field}; +use crate::snapshot::{field, field_bool, field_u32, field_u32s, field_u64, field_u8s, StateTable}; + +use crate::dev::ns16550::Ns16550; + +use crate::dev::ns16550::com_reg::{ + LSR_BI, LSR_DR, LSR_FE, LSR_OE, LSR_PE, MCR_DTR, MCR_LOOP, MCR_OUT1, MCR_OUT2, MCR_RTS, MSR_CTS, MSR_DCD, + MSR_DCTS, MSR_DDCD, +}; +use crate::dev::ns16550::com_reg::{LSR as UART_LSR, MCR as UART_MCR, MSR as UART_MSR, THR as UART_THR}; + +/// Port DMA register offsets (bytes, from the port's first register). +pub mod off { + pub const CONTROL: u32 = 0x00; + pub const TX_PRODUCE: u32 = 0x04; + pub const TX_CONSUME: u32 = 0x08; + pub const RX_PRODUCE: u32 = 0x0c; + pub const RX_CONSUME: u32 = 0x10; + pub const RX_TIMER: u32 = 0x14; + pub const SHADOW: u32 = 0x18; + /// Registers per port. + pub const REGS: u32 = 7; +} + +/// Bits of the port's control register. +pub mod ctl { + pub const HIGH_WATER: u32 = 0x1ff; + pub const TX_TIMER_RUN: u32 = 1 << 16; + pub const FLOW_CONTROL: u32 = 1 << 17; + pub const RX_TO_MEMORY: u32 = 1 << 27; + pub const ENABLE: u32 = 1 << 28; + pub const PAUSE: u32 = 1 << 29; + pub const PAUSED: u32 = 1 << 30; + pub const RESET: u32 = 1 << 31; + /// Status and commands: these never read back as written. + pub const UNSTORED: u32 = RESET | RX_TO_MEMORY | PAUSED | TX_TIMER_RUN; +} + +/// The most ports one engine drives. +pub const MAX_PORTS: usize = 4; +/// Ports sharing one ring base. +const PAIR: usize = 2; + +pub const ENTRY: usize = 8; +pub const ENTRY_CHARS: usize = 4; +/// Bit 31 of the receive consumer index arms the receive timer. +pub const ARM_RX_TIMER: u32 = 1 << 31; +/// The packer's held count, in the receive producer index (IOC3 only). +pub const HELD_SHIFT: u32 = 24; +const TIMER_RELOAD: u32 = 0xfff; +const TIMER_COUNT_SHIFT: u32 = 16; +pub const TIMER_TICK_NS: u64 = 62_500; // 16 kHz +const RING_4K: u32 = 1; +const RING_ADDRESS: u32 = 0xffff_f000; +const RING_1K_SIZE: u32 = 1024; +const RING_4K_SIZE: u32 = 4096; +const TX_TYPE: u8 = 0xc0; +const TX_CHARACTER: u8 = 0x40; +const TX_MODEM: u8 = 0x80; +pub const TX_DELAY: u8 = 0xc0; +pub const TX_INTERRUPT: u8 = 0x20; +const TX_UNDEFINED: u8 = 0x1f; +const RX_OVERRUN: u8 = 0x01; +const RX_PARITY: u8 = 0x02; +const RX_FRAMING: u8 = 0x04; +const RX_BREAK: u8 = 0x08; +const RX_CTS: u8 = 0x10; +const RX_CD: u8 = 0x20; +const RX_MODEM_OK: u8 = 0x40; +const RX_DATA_OK: u8 = 0x80; + +/// A port's interrupt sources, numbered as the IOC3 lays them out in its +/// status word. The IOC4 places all but the DMA error the same way. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialDmaSource { + TxEmpty = 0, + RxFull = 1, + RxHighWater = 2, + RxTimer = 3, + TxExplicit = 7, + DmaError = 8, +} + +/// What differs between the chips, fixed per chip. +#[derive(Clone, Copy, Debug)] +pub struct SerialDmaParams { + /// The receive producer index reports the packer's held count. + pub held_counts: bool, + /// A failed transfer stops the port until a channel reset. + pub halt_on_dma_error: bool, +} + +/// A 16550 as the engine drives it. +pub trait SerialDmaUart { + fn loopback(&self) -> bool; + /// The port can take a character from the engine. The walk waits while + /// it cannot, and goes on when the chip next runs the engine. + fn tx_ready(&self) -> bool; + /// How long a character takes on the line at the programmed rate. + fn char_time(&self) -> Duration; + /// A read with the chip's side effects left out: the 16550 proper. + fn read_core(&mut self, reg: u32) -> u8; + fn write(&mut self, reg: u32, val: u8); + fn lsr(&self) -> u8; + fn mcr(&self) -> u8; + fn msr(&self) -> u8; +} + +/// The chip around the engine. +pub trait SerialDmaHost { + fn uart(&mut self, port: usize) -> &mut dyn SerialDmaUart; + /// Latch a source; the chip's write-one-to-clear acknowledges it. + fn raise(&mut self, port: usize, src: SerialDmaSource); + /// Drive a source that follows a level and ignores acknowledgement. + fn set_level(&mut self, port: usize, src: SerialDmaSource, level: bool); + /// The port's 16550 is fitted and the engine is aimed at it. + fn reachable(&self, port: usize) -> bool; + /// The 16550's interrupt line may have moved: reflect it in the chip's + /// pass-through source. + fn uart_line_changed(&mut self, port: usize); + fn dma_read(&self, addr: u64, buf: &mut [u8]) -> bool; + fn dma_write(&self, addr: u64, data: &[u8]) -> bool; + /// A transfer failed at `addr`: a receive post or a transmit fetch. + fn dma_error(&mut self, port: usize, _rx: bool, _addr: u64) { + self.raise(port, SerialDmaSource::DmaError); + } +} + +#[derive(Default)] +pub struct SerialDmaPort { + reg: [u32; off::REGS as usize], + packer: [u8; ENTRY], + packed: usize, + tx_entry: [u8; ENTRY], + tx_fetched: bool, + tx_slot: usize, + armed: bool, + drain_pending: bool, + busy: bool, + halted: bool, + /// The last post was turned away for lack of ring room. + post_refused: bool, + paused: bool, + pub rx_deadline: Option, + rx_idle_count: u32, + /// Time a frozen timer had left when a pause began. + rx_frozen: Option, + tx_delay_deadline: Option, + tx_delay_frozen: Option, + tx_delay_irq: bool, + /// A looped-back character still on the line: when it reaches the + /// receiver. The 16550 core loops it back the moment it is written, and + /// the engine does not look at the receiver until this time. + loop_due: Option, +} + +pub struct SerialDma { + params: SerialDmaParams, + base_hi: [u32; MAX_PORTS / PAIR], + base_lo: [u32; MAX_PORTS / PAIR], + /// Transfers under way, for the chip's idle status. Nonzero only inside + /// a call: a chip cannot read its own status mid-walk, so software sees + /// idle unless a bus master it reaches re-enters the chip. + in_flight: u32, + pub port: Vec, +} + +impl SerialDma { + pub fn new(ports: usize, params: SerialDmaParams) -> SerialDma { + assert!((1..=MAX_PORTS).contains(&ports)); + SerialDma { + params, + base_hi: [0; MAX_PORTS / PAIR], + base_lo: [0; MAX_PORTS / PAIR], + in_flight: 0, + port: (0..ports).map(|_| SerialDmaPort::default()).collect(), + } + } + + /// Back to the power-on state: the ring bases and every port. + pub fn reset(&mut self) { + *self = SerialDma::new(self.port.len(), self.params); + } + + pub fn base_read(&self, pair: usize, high: bool) -> u32 { + if high { self.base_hi[pair] } else { self.base_lo[pair] } + } + + pub fn base_write(&mut self, pair: usize, high: bool, val: u32) { + if high { + self.base_hi[pair] = val; + } else { + self.base_lo[pair] = val; + } + } + + /// A port's control register as stored. + pub fn control(&self, p: usize) -> u32 { + self.port[p].reg[0] + } + + /// A register as stored, without the status the read adds. + pub fn stored(&self, p: usize, offset: u32) -> u32 { + self.port[p].reg[(offset / 4) as usize] + } + + /// When the port's receive timer runs out, if it is running. + pub fn rx_deadline(&self, p: usize) -> Option { + self.port[p].rx_deadline + } + + /// When the port's transmit delay ends. A paused port keeps it frozen. + pub fn tx_delay_deadline(&self, p: usize) -> Option { + let port = &self.port[p]; + if port.paused { None } else { port.tx_delay_deadline } + } + + /// No transfer is under way: what a chip's DMA idle status reports. + pub fn idle(&self) -> bool { + self.in_flight == 0 + } + + /// A ring read, counted as in flight while the bus master has it. + fn dma_read(&mut self, host: &mut dyn SerialDmaHost, addr: u64, buf: &mut [u8]) -> bool { + self.in_flight += 1; + let ok = host.dma_read(addr, buf); + self.in_flight -= 1; + ok + } + + /// A ring write, counted as in flight while the bus master has it. + fn dma_write(&mut self, host: &mut dyn SerialDmaHost, addr: u64, data: &[u8]) -> bool { + self.in_flight += 1; + let ok = host.dma_write(addr, data); + self.in_flight -= 1; + ok + } + + /// When a looped-back character reaches the receiver, if one is on the + /// line. + pub fn loop_deadline(&self, p: usize) -> Option { + self.port[p].loop_due + } + + /// Nothing queued, held or pending on the port: what a chip that wants + /// the rings left alone while they move checks before it lets a guest + /// change a mode. + pub fn port_quiescent(&self, p: usize) -> bool { + let port = &self.port[p]; + port.reg[1] == port.reg[2] + && port.reg[3] == port.reg[4] + && !port.tx_fetched + && port.tx_delay_deadline.is_none() + && port.packed == 0 + && !port.armed + && !port.drain_pending + } + + /// Both ports of a pair are off, with their indices at zero and nothing + /// pending. + pub fn pair_quiescent(&self, pair: usize) -> bool { + (pair * PAIR..pair * PAIR + PAIR).all(|p| { + self.control(p) & (ctl::ENABLE | ctl::PAUSE) == 0 + && self.port[p].reg[1..=4].iter().all(|&r| r == 0) + && self.port_quiescent(p) + }) + } + + /// Whether the port is stopped by a failed transfer. + pub fn halted(&self, p: usize) -> bool { + self.port[p].halted + } + + /// Looped back with the engine's hardware flow control on, the port + /// reports clear-to-send asserted even with request-to-send clear. + pub fn flow_cts(&self, p: usize, loopback: bool) -> bool { + loopback && self.control(p) & ctl::FLOW_CONTROL != 0 + } + + fn ring_size(&self, p: usize) -> u32 { + if self.base_lo[p / PAIR] & RING_4K != 0 { RING_4K_SIZE } else { RING_1K_SIZE } + } + + /// The selected ring size is also every index register's pointer width. + fn mask(&self, p: usize) -> u32 { + (self.ring_size(p) - 1) & !(ENTRY as u32 - 1) + } + + /// The size-select bit is not address. + fn ring(&self, p: usize, dir: u32) -> u64 { + let pair = p / PAIR; + let base = (u64::from(self.base_hi[pair]) << 32) | u64::from(self.base_lo[pair] & RING_ADDRESS); + base.saturating_add(((p % PAIR) as u64 * 2 + u64::from(dir)) * u64::from(self.ring_size(p))) + } + + fn running(&self, p: usize) -> bool { + let c = self.control(p); + !self.port[p].halted && c & ctl::ENABLE != 0 && c & ctl::PAUSE == 0 + } + + /// A transfer the fabric refused: the chip reports it, and may halt. + fn dma_failed(&mut self, p: usize, rx: bool, addr: u64, host: &mut dyn SerialDmaHost) { + if self.params.halt_on_dma_error { + self.port[p].halted = true; + } + host.dma_error(p, rx, addr); + } + + /// Transmit-ring-empty is a level: ring DMA on, nothing left in the ring + /// or held by the engine. + fn update_tx_empty(&self, p: usize, host: &mut dyn SerialDmaHost) { + let mask = self.mask(p); + let port = &self.port[p]; + let empty = self.control(p) & ctl::ENABLE != 0 + && (port.reg[1] ^ port.reg[2]) & mask == 0 + && !port.tx_fetched + && port.tx_delay_deadline.is_none(); + host.set_level(p, SerialDmaSource::TxEmpty, empty); + } + + /// The receive high-water mark counts ring entries; a level. So is + /// receive-ring-full: set while ring DMA is on and an entry waits, turned + /// away for lack of ring room. It does not come back with every refused + /// retry after software has acknowledged it, and it falls once the + /// consumer makes room, whether or not the held entry has posted yet. A + /// ring filled exactly, with nothing waiting, does not raise it. A model + /// choice; what the part does is not known. + fn update_high_water(&mut self, p: usize, host: &mut dyn SerialDmaHost) { + let mask = self.mask(p); + let hw = self.control(p) & ctl::HIGH_WATER; + let enabled = self.control(p) & ctl::ENABLE != 0; + let port = &mut self.port[p]; + let waiting = port.reg[3].wrapping_sub(port.reg[4]) & mask; + host.set_level(p, SerialDmaSource::RxHighWater, hw != 0 && waiting / ENTRY as u32 >= hw); + if (port.reg[3] + ENTRY as u32) & mask != port.reg[4] & mask { + port.post_refused = false; + } + host.set_level(p, SerialDmaSource::RxFull, enabled && port.post_refused); + } + + /// Post the packer as one entry. Fails, keeping the characters, when the + /// ring is full or the write is refused. + fn post(&mut self, p: usize, host: &mut dyn SerialDmaHost) -> bool { + let mask = self.mask(p); + let ring = self.ring(p, 1); + let (produce, consume, packer) = { + let port = &self.port[p]; + (port.reg[3], port.reg[4], port.packer) + }; + let next = (produce + ENTRY as u32) & mask; + if next == consume & mask { + // Full: refuse. The characters keep in the packer and the FIFO. + self.port[p].post_refused = true; + return false; + } + let at = ring.saturating_add(u64::from(produce & mask)); + if !self.dma_write(host, at, &packer) { + self.dma_failed(p, true, at, host); + return false; + } + let port = &mut self.port[p]; + port.reg[3] = next; + port.post_refused = false; + port.packed = 0; + port.packer = [0; ENTRY]; + true + } + + fn rx_timer_start(&mut self, p: usize, since: Instant) { + let port = &mut self.port[p]; + let reload = port.reg[5] & TIMER_RELOAD; + port.rx_idle_count = 0; + let dur = Duration::from_nanos(u64::from(reload) * TIMER_TICK_NS); + if port.paused { + // Started while paused, it waits frozen at its full count. + port.rx_frozen = Some(dur); + } else { + port.rx_deadline = Some(since + dur); + } + } + + fn rx_timer_expired(&mut self, p: usize, host: &mut dyn SerialDmaHost) { + self.port[p].rx_idle_count = 0; + self.port[p].rx_deadline = None; + if self.port[p].packed != 0 { + self.post(p, host); + } + // Raised at every expiry, even over an empty ring; the arm is spent. + if self.port[p].armed { + host.raise(p, SerialDmaSource::RxTimer); + } + self.port[p].armed = false; + self.update_high_water(p, host); + } + + /// Expiries already due happen before an access. + pub fn catch_up(&mut self, now: Instant, host: &mut dyn SerialDmaHost) { + for p in 0..self.port.len() { + if let Some(d) = self.port[p].tx_delay_deadline { + if !self.port[p].paused && d <= now { + self.tx_delay_ended(p, d, host); + } + } + // Each arrival can send the next character; the ring bounds it. + while let Some(d) = self.port[p].loop_due.filter(|&d| d <= now) { + self.loop_arrived(p, d, host); + } + if let Some(d) = self.port[p].rx_deadline { + if d <= now { + self.rx_timer_expired(p, host); + } + } + } + } + + /// A looped-back character reaches the receiver; the walk goes on from + /// then. + fn loop_arrived(&mut self, p: usize, when: Instant, host: &mut dyn SerialDmaHost) { + self.port[p].loop_due = None; + self.run_from(p, when, host); + } + + fn tx_delay_ended(&mut self, p: usize, when: Instant, host: &mut dyn SerialDmaHost) { + let irq = { + let port = &mut self.port[p]; + port.tx_delay_deadline = None; + std::mem::take(&mut port.tx_delay_irq) + }; + if irq { + host.raise(p, SerialDmaSource::TxExplicit); + } + self.run_from(p, when, host); + } + + fn receive(&mut self, p: usize, since: Instant, host: &mut dyn SerialDmaHost) { + // The 16550 core loops a character back the moment it is written; on + // the part it takes a character time on the line to arrive. + if self.port[p].loop_due.is_some() { + return; + } + while self.running(p) { + if self.port[p].packed == ENTRY_CHARS && !self.post(p, host) { + break; + } + // Line status before the character: reading it clears the latches. + let uart = host.uart(p); + let lsr = uart.read_core(UART_LSR); + if lsr & LSR_DR == 0 { + break; + } + let data = uart.read_core(UART_THR); + let mut msr = uart.read_core(UART_MSR); + let looped = uart.loopback(); + if self.flow_cts(p, looped) { + msr |= MSR_CTS; + } + let mut status = RX_DATA_OK | RX_MODEM_OK; + for (bit, flag) in [(LSR_OE, RX_OVERRUN), (LSR_PE, RX_PARITY), (LSR_FE, RX_FRAMING), (LSR_BI, RX_BREAK)] { + if lsr & bit != 0 { + status |= flag; + } + } + if msr & MSR_CTS != 0 { + status |= RX_CTS; + } + if msr & MSR_DCD != 0 { + status |= RX_CD; + } + let start_timer = { + let port = &mut self.port[p]; + port.packer[port.packed] = data; + port.packer[ENTRY_CHARS + port.packed] = status; + port.packed += 1; + port.armed && port.rx_deadline.is_none() && port.rx_frozen.is_none() + }; + // Started by the first character after the arm; later ones leave + // a running timer alone. + if start_timer { + self.rx_timer_start(p, since); + } + // A fourth character completes the entry; fewer wait for the + // timer or a drain. + if self.port[p].packed == ENTRY_CHARS { + self.post(p, host); + } + } + host.uart_line_changed(p); + } + + /// The type field and bit 5 decide what a slot does; the low five bits + /// have no defined meaning and are reported, not obeyed. Report them once, + /// when the entry is fetched, for the slots up to the one that ends it. + fn check_entry(p: usize, entry: &[u8; ENTRY]) { + let mut undefined = 0; + for &control in &entry[ENTRY_CHARS..] { + undefined |= control & TX_UNDEFINED; + if control & TX_TYPE == 0 { + break; + } + } + if undefined != 0 { + crate::dlog!( + crate::devlog::LogModule::Ioc, + "serial DMA: undefined TX control bits {undefined:#04x} in an entry on port {p}" + ); + } + } + + /// Each control byte types its data byte; the empty type ends the entry, + /// wherever it sits. + fn transmit(&mut self, p: usize, since: Instant, host: &mut dyn SerialDmaHost) { + let mask = self.mask(p); + let ring = self.ring(p, 0); + if self.port[p].tx_delay_deadline.is_some() { + return; + } + loop { + let (produce, consume) = { + let port = &self.port[p]; + (port.reg[1], port.reg[2]) + }; + if !(self.running(p) && consume & mask != produce & mask) { + break; + } + let was_fetched = self.port[p].tx_fetched; + if !was_fetched { + let mut entry = [0u8; ENTRY]; + let at = ring.saturating_add(u64::from(consume & mask)); + if !self.dma_read(host, at, &mut entry) { + self.dma_failed(p, false, at, host); + break; + } + let port = &mut self.port[p]; + port.tx_entry = entry; + port.tx_fetched = true; + } + let entry = self.port[p].tx_entry; + if !was_fetched { + Self::check_entry(p, &entry); + } + let mut slot = self.port[p].tx_slot; + while slot < ENTRY_CHARS { + let control = entry[ENTRY_CHARS + slot]; + let ty = control & TX_TYPE; + if ty == 0 { + break; + } + match ty { + TX_CHARACTER => { + // In loopback the line carries one character at a time. + if !host.uart(p).tx_ready() || self.port[p].loop_due.is_some() { + return; + } + let looped = host.uart(p).loopback(); + // What has already arrived goes ahead of this one. + if looped { + self.receive(p, since, host); + } + host.uart(p).write(UART_THR, entry[slot]); + if looped { + self.port[p].loop_due = Some(since + host.uart(p).char_time()); + } + // Drain per character or loopback overruns the FIFO. + self.receive(p, since, host); + } + TX_MODEM => host.uart(p).write(UART_MCR, entry[slot]), + TX_DELAY => { + // The data byte is milliseconds of quiet. + if entry[slot] != 0 { + let port = &mut self.port[p]; + port.tx_slot = slot + 1; + port.tx_delay_deadline = Some(since + Duration::from_millis(u64::from(entry[slot]))); + port.tx_delay_irq = control & TX_INTERRUPT != 0; + return; + } + } + _ => unreachable!("the type field has two bits"), + } + slot += 1; + self.port[p].tx_slot = slot; + if control & TX_INTERRUPT != 0 { + host.raise(p, SerialDmaSource::TxExplicit); + } + } + let port = &mut self.port[p]; + port.tx_slot = 0; + port.tx_fetched = false; + port.reg[2] = (port.reg[2] + ENTRY as u32) & mask; + } + host.uart_line_changed(p); + } + + /// The drain posts what the packer holds and reads back set until it has; + /// it waits while ring DMA is off or paused and while the ring is full. + fn drain(&mut self, p: usize, host: &mut dyn SerialDmaHost) { + if !self.port[p].drain_pending || !self.running(p) { + return; + } + if self.port[p].packed != 0 && !self.post(p, host) { + return; + } + self.port[p].drain_pending = false; + } + + /// Walk the port's rings from `since`: where a transmit delay ended, or + /// now. + pub fn run_from(&mut self, p: usize, since: Instant, host: &mut dyn SerialDmaHost) { + if !host.reachable(p) || self.port[p].busy { + return; + } + self.port[p].busy = true; + self.transmit(p, since, host); + self.receive(p, since, host); + self.drain(p, host); + self.update_high_water(p, host); + self.update_tx_empty(p, host); + self.port[p].busy = false; + } + + pub fn run(&mut self, p: usize, host: &mut dyn SerialDmaHost) { + self.run_from(p, Instant::now(), host); + } + + /// A passive mirror of the 16550's lines; no read side effects. + fn shadow(&self, p: usize, host: &mut dyn SerialDmaHost) -> u32 { + let u = host.uart(p); + let (lsr, mcr, mut msr) = (u.lsr(), u.mcr(), u.msr()); + if self.flow_cts(p, u.loopback()) { + msr |= MSR_CTS; + } + let mut s = u32::from(lsr); + for (m, bit) in [(MSR_DCTS, 16), (MSR_DDCD, 19), (MSR_CTS, 20), (MSR_DCD, 23)] { + if msr & m != 0 { + s |= 1 << bit; + } + } + for (m, bit) in [(MCR_DTR, 24), (MCR_RTS, 25), (MCR_OUT1, 26), (MCR_OUT2, 27), (MCR_LOOP, 28)] { + if mcr & m != 0 { + s |= 1 << bit; + } + } + s + } + + pub fn port_read(&mut self, p: usize, offset: u32, host: &mut dyn SerialDmaHost) -> u32 { + let now = Instant::now(); + self.catch_up(now, host); + if offset == off::SHADOW { + return self.shadow(p, host); + } + let port = &self.port[p]; + let mut v = port.reg[(offset / 4) as usize]; + match offset { + off::CONTROL => { + if v & ctl::PAUSE != 0 { + v |= ctl::PAUSED; + } + if port.tx_delay_deadline.is_some() { + v |= ctl::TX_TIMER_RUN; + } + if port.drain_pending { + v |= ctl::RX_TO_MEMORY; + } + } + off::RX_CONSUME => { + if port.armed { + v |= ARM_RX_TIMER; + } + } + off::RX_PRODUCE if self.params.held_counts => v |= (port.packed as u32) << HELD_SHIFT, + off::RX_TIMER => { + // Ticks left while running or frozen; idle, whatever a reload + // write last loaded. + let ticks_in = |d: Duration| (d.as_nanos() as u64 / TIMER_TICK_NS).min(u64::from(TIMER_RELOAD)) as u32; + let mut ticks = port.rx_idle_count; + if let (true, Some(left)) = (port.paused, port.rx_frozen) { + ticks = ticks_in(left); + } else if let Some(d) = port.rx_deadline { + ticks = ticks_in(d.saturating_duration_since(now)); + } + v |= ticks << TIMER_COUNT_SHIFT; + } + _ => {} + } + v + } + + /// Resets the four indices and the work in progress; the settings + /// survive, and a port a failed transfer stopped runs again. + fn reset_port(&mut self, p: usize) { + let port = &mut self.port[p]; + port.halted = false; + port.reg[1..=4].fill(0); + port.armed = false; + port.drain_pending = false; + port.post_refused = false; + port.packed = 0; + port.packer = [0; ENTRY]; + port.tx_slot = 0; + port.tx_fetched = false; + port.tx_delay_deadline = None; + port.tx_delay_irq = false; + port.rx_deadline = None; + port.rx_frozen = None; + port.rx_idle_count = 0; + } + + pub fn port_write(&mut self, p: usize, offset: u32, val: u32, host: &mut dyn SerialDmaHost) { + let now = Instant::now(); + self.catch_up(now, host); + let mask = self.mask(p); + match offset { + off::CONTROL => { + self.port[p].reg[0] = val & !ctl::UNSTORED; + if val & ctl::RESET != 0 { + self.reset_port(p); + } + if val & ctl::RX_TO_MEMORY != 0 { + self.port[p].drain_pending = true; + } + let port = &mut self.port[p]; + // A pause freezes the receive timer and a transmit delay; + // resuming lets each finish the time it had left. A model + // choice; no source says what the part does. + if val & ctl::PAUSE != 0 && !port.paused { + port.paused = true; + port.rx_frozen = port.rx_deadline.take().map(|d| d.saturating_duration_since(now)); + port.tx_delay_frozen = port.tx_delay_deadline.take().map(|d| d.saturating_duration_since(now)); + } else if val & ctl::PAUSE == 0 && port.paused { + port.paused = false; + port.rx_deadline = port.rx_frozen.take().map(|l| now + l); + port.tx_delay_deadline = port.tx_delay_frozen.take().map(|l| now + l); + } + if val & ctl::ENABLE == 0 { + port.rx_deadline = None; + port.rx_frozen = None; + } + self.run(p, host); + } + off::TX_PRODUCE | off::TX_CONSUME | off::RX_PRODUCE => { + self.port[p].reg[(offset / 4) as usize] = val & mask; + self.run(p, host); + } + off::RX_CONSUME => { + // Bit 31 sets the arm; a write with it clear leaves it. + self.port[p].reg[4] = val & mask; + if val & ARM_RX_TIMER != 0 { + self.port[p].armed = true; + } + self.run(p, host); + } + off::RX_TIMER => { + let port = &mut self.port[p]; + port.reg[5] = val & TIMER_RELOAD; + port.rx_idle_count = val & TIMER_RELOAD; + } + // The shadow discards writes silently. + _ => {} + } + self.update_tx_empty(p, host); + self.update_high_water(p, host); + } +} + +impl SerialDmaUart for Ns16550 { + fn loopback(&self) -> bool { + Ns16550::loopback(self) + } + + fn tx_ready(&self) -> bool { + Ns16550::tx_ready(self) + } + + fn char_time(&self) -> Duration { + Ns16550::char_time(self) + } + + fn read_core(&mut self, r: u32) -> u8 { + self.read(r) + } + + fn write(&mut self, r: u32, v: u8) { + Ns16550::write(self, r, v); + } + + fn lsr(&self) -> u8 { + Ns16550::lsr(self) + } + + fn mcr(&self) -> u8 { + Ns16550::mcr(self) + } + + fn msr(&self) -> u8 { + Ns16550::msr(self) + } +} + +#[cfg(test)] +mod tests { + //! The engine against a stand-in chip. The IOC3 and IOC4 register tests + //! cover the engine through each chip; these cover what only the second + //! chip's parameters change. + + use super::*; + + #[derive(Default)] + struct FakeUart { + sent: Vec, + rx: std::collections::VecDeque, + } + + impl SerialDmaUart for FakeUart { + fn loopback(&self) -> bool { + false + } + fn tx_ready(&self) -> bool { + true + } + fn char_time(&self) -> Duration { + Duration::ZERO + } + fn read_core(&mut self, reg: u32) -> u8 { + match reg { + UART_LSR => u8::from(!self.rx.is_empty()), + UART_THR => self.rx.pop_front().unwrap_or(0), + _ => 0, + } + } + fn write(&mut self, reg: u32, val: u8) { + if reg == UART_THR { + self.sent.push(val); + } + } + fn lsr(&self) -> u8 { + 0 + } + fn mcr(&self) -> u8 { + 0 + } + fn msr(&self) -> u8 { + 0 + } + } + + struct Chip { + uart: [FakeUart; MAX_PORTS], + mem: Vec, + /// Transfers below this address are refused. + refuse_below: u64, + raised: Vec<(usize, SerialDmaSource)>, + errors: Vec<(usize, bool, u64)>, + /// Whether the chip reports errors itself, as IOC4 does. + reports_errors: bool, + } + + impl Chip { + fn new() -> Chip { + Chip { + uart: Default::default(), + mem: vec![0; 0x4000], + refuse_below: 0, + raised: Vec::new(), + errors: Vec::new(), + reports_errors: true, + } + } + } + + impl SerialDmaHost for Chip { + fn uart(&mut self, port: usize) -> &mut dyn SerialDmaUart { + &mut self.uart[port] + } + fn raise(&mut self, port: usize, src: SerialDmaSource) { + self.raised.push((port, src)); + } + fn set_level(&mut self, _port: usize, _src: SerialDmaSource, _level: bool) {} + fn reachable(&self, _port: usize) -> bool { + true + } + fn uart_line_changed(&mut self, _port: usize) {} + fn dma_read(&self, addr: u64, buf: &mut [u8]) -> bool { + if addr < self.refuse_below { + return false; + } + buf.copy_from_slice(&self.mem[addr as usize..addr as usize + buf.len()]); + true + } + fn dma_write(&self, _addr: u64, _data: &[u8]) -> bool { + true + } + fn dma_error(&mut self, port: usize, rx: bool, addr: u64) { + if self.reports_errors { + self.errors.push((port, rx, addr)); + } else { + self.raise(port, SerialDmaSource::DmaError); + } + } + } + + const FOUR: SerialDmaParams = SerialDmaParams { held_counts: false, halt_on_dma_error: true }; + const TWO: SerialDmaParams = SerialDmaParams { held_counts: true, halt_on_dma_error: false }; + + /// One transmit entry holding `byte`, in the port's transmit ring. + fn queue_char(sdma: &mut SerialDma, chip: &mut Chip, p: usize, byte: u8) { + let at = sdma.ring(p, 0) as usize; + chip.mem[at..at + ENTRY].copy_from_slice(&[byte, 0, 0, 0, TX_CHARACTER, 0, 0, 0]); + let produce = sdma.stored(p, off::TX_PRODUCE); + sdma.port_write(p, off::TX_PRODUCE, produce + ENTRY as u32, chip); + } + + fn enable(sdma: &mut SerialDma, chip: &mut Chip, p: usize) { + sdma.port_write(p, off::CONTROL, ctl::ENABLE, chip); + } + + #[test] + fn a_port_in_the_second_pair_uses_the_second_ring_base() { + let mut sdma = SerialDma::new(4, FOUR); + let mut chip = Chip::new(); + sdma.base_write(0, false, 0x1000); + sdma.base_write(1, false, 0x2000); + assert_eq!(sdma.ring(0, 0), 0x1000); + assert_eq!(sdma.ring(1, 0), 0x1000 + 2 * 1024); + assert_eq!(sdma.ring(2, 0), 0x2000); + assert_eq!(sdma.ring(3, 1), 0x2000 + 3 * 1024); + enable(&mut sdma, &mut chip, 3); + queue_char(&mut sdma, &mut chip, 3, b'x'); + assert_eq!(chip.uart[3].sent, b"x"); + assert!(chip.uart[0].sent.is_empty(), "the other ports saw nothing"); + } + + #[test] + fn a_failed_fetch_halts_the_port_until_a_channel_reset() { + let mut sdma = SerialDma::new(4, FOUR); + let mut chip = Chip::new(); + sdma.base_write(0, false, 0x1000); + enable(&mut sdma, &mut chip, 0); + chip.refuse_below = 0x2000; + queue_char(&mut sdma, &mut chip, 0, b'a'); + assert_eq!(chip.errors, [(0, false, 0x1000)], "the chip is told where the fetch failed"); + assert!(sdma.halted(0)); + chip.refuse_below = 0; + sdma.run(0, &mut chip); + assert!(chip.uart[0].sent.is_empty(), "a halted port does not resume by itself"); + sdma.port_write(0, off::CONTROL, ctl::ENABLE | ctl::RESET, &mut chip); + assert!(!sdma.halted(0)); + assert_eq!(sdma.stored(0, off::TX_PRODUCE), 0, "the reset cleared the indices"); + queue_char(&mut sdma, &mut chip, 0, b'b'); + assert_eq!(chip.uart[0].sent, b"b"); + } + + #[test] + fn a_failed_fetch_leaves_a_chip_that_does_not_halt_running() { + let mut sdma = SerialDma::new(2, TWO); + let mut chip = Chip::new(); + chip.reports_errors = false; + sdma.base_write(0, false, 0x1000); + enable(&mut sdma, &mut chip, 0); + chip.refuse_below = 0x2000; + queue_char(&mut sdma, &mut chip, 0, b'a'); + assert_eq!(chip.raised.last(), Some(&(0, SerialDmaSource::DmaError)), "the default report is the port's own source"); + assert!(!sdma.halted(0)); + chip.refuse_below = 0; + sdma.run(0, &mut chip); + assert_eq!(chip.uart[0].sent, b"a", "the walk retries once the transfer works"); + } + + #[test] + fn only_a_chip_that_reports_held_counts_shows_them() { + for (params, shown) in [(TWO, true), (FOUR, false)] { + let mut sdma = SerialDma::new(2, params); + let mut chip = Chip::new(); + sdma.base_write(0, false, 0x1000); + enable(&mut sdma, &mut chip, 0); + chip.uart[0].rx.extend([1, 2]); + sdma.run(0, &mut chip); + let produce = sdma.port_read(0, off::RX_PRODUCE, &mut chip); + assert_eq!(produce >> HELD_SHIFT & 7, if shown { 2 } else { 0 }); + } + } + + #[test] + fn the_engine_is_idle_between_calls_whether_a_transfer_worked_or_not() { + let mut sdma = SerialDma::new(4, FOUR); + let mut chip = Chip::new(); + assert!(sdma.idle()); + sdma.base_write(0, false, 0x1000); + enable(&mut sdma, &mut chip, 0); + queue_char(&mut sdma, &mut chip, 0, b'a'); + assert!(sdma.idle(), "after a good transfer"); + chip.refuse_below = 0x2000; + queue_char(&mut sdma, &mut chip, 0, b'b'); + assert!(sdma.halted(0)); + assert!(sdma.idle(), "after a failed one"); + } +} + +// The engine's state for a snapshot of the chip that carries it. Each timer +// is saved as the time it had left (see the IOC3's snapshot helpers). + +impl SerialDmaPort { + fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u32s("reg", &self.reg) + .u8s("packer", &self.packer) + .u64("packed", self.packed as u64) + .u8s("tx_entry", &self.tx_entry) + .bool("tx_fetched", self.tx_fetched) + .u64("tx_slot", self.tx_slot as u64) + .bool("armed", self.armed) + .bool("drain_pending", self.drain_pending) + .bool("busy", self.busy) + .bool("paused", self.paused) + .u64("rx_due", until(self.rx_deadline, now)) + .u32("rx_idle_count", self.rx_idle_count) + .u64("rx_frozen", span(self.rx_frozen)) + .u64("tx_delay_due", until(self.tx_delay_deadline, now)) + .u64("tx_delay_frozen", span(self.tx_delay_frozen)) + .bool("tx_delay_irq", self.tx_delay_irq) + .bool("post_refused", self.post_refused) + .bool("halted", self.halted) + .u64("loop_due", until(self.loop_due, now)) + .build() + } + fn load_state(v: &toml::Value, now: Instant) -> Result { + Ok(Self { + reg: field_u32s(v, "reg")?, + packer: field_u8s(v, "packer")?, + packed: usize_field(v, "packed", ENTRY)?, + tx_entry: field_u8s(v, "tx_entry")?, + tx_fetched: field_bool(v, "tx_fetched")?, + tx_slot: usize_field(v, "tx_slot", ENTRY)?, + armed: field_bool(v, "armed")?, + drain_pending: field_bool(v, "drain_pending")?, + busy: field_bool(v, "busy")?, + paused: field_bool(v, "paused")?, + rx_deadline: due(field_u64(v, "rx_due")?, now), + rx_idle_count: field_u32(v, "rx_idle_count")?, + rx_frozen: unspan(field_u64(v, "rx_frozen")?), + tx_delay_deadline: due(field_u64(v, "tx_delay_due")?, now), + tx_delay_frozen: unspan(field_u64(v, "tx_delay_frozen")?), + tx_delay_irq: field_bool(v, "tx_delay_irq")?, + post_refused: field_bool(v, "post_refused")?, + halted: field_bool(v, "halted")?, + loop_due: due(field_u64(v, "loop_due")?, now), + }) + } +} + +impl SerialDma { + /// The ring bases and each port's state, timers as the time left after + /// `now`. + pub fn save_state(&self, now: Instant) -> toml::Value { + let mut t = StateTable::new(); + t.u32s("base_hi", &self.base_hi).u32s("base_lo", &self.base_lo); + for (i, port) in self.port.iter().enumerate() { + t.value(&format!("port{i}"), port.save_state(now)); + } + t.build() + } + + /// The engine `save_state` described, with this engine's parameters + /// and port count, timers set that far after `now`. + pub fn loaded(&self, v: &toml::Value, now: Instant) -> Result { + let port = (0..self.port.len()) + .map(|i| SerialDmaPort::load_state(field(v, &format!("port{i}"))?, now)) + .collect::, String>>()?; + // Transfers are under way only inside a call, never at a save. + Ok(SerialDma { params: self.params, base_hi: field_u32s(v, "base_hi")?, base_lo: field_u32s(v, "base_lo")?, in_flight: 0, port }) + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 63e0fe2f3..9b1c8b9ab 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -13,10 +13,12 @@ pub mod mace_audio; pub mod mace_isa; pub mod crime; pub mod battery_clock; +pub mod deadline_timers; pub mod ds1687; pub mod heart; pub mod heart_mem; pub mod ioc3; +pub mod ioc_serial_dma; pub mod ioc3_eth; pub mod ioc3_port; pub mod isp1040; diff --git a/src/dev/ns16550.rs b/src/dev/ns16550.rs index 82a756d04..5be6990ed 100644 --- a/src/dev/ns16550.rs +++ b/src/dev/ns16550.rs @@ -117,6 +117,8 @@ pub mod com_reg { /// LSR: a received character is waiting. pub const LSR_DR: u8 = 0x01; pub const LSR_OE: u8 = 0x02; + pub const LSR_PE: u8 = 0x04; + pub const LSR_FE: u8 = 0x08; pub const LSR_BI: u8 = 0x10; /// LSR: transmit holding register empty. pub const LSR_THRE: u8 = 0x20; @@ -261,6 +263,19 @@ impl Ns16550 { self.lcr & LCR_DLAB != 0 } + /// How long a character takes on the line at the programmed rate and + /// frame. Nothing here waits for it; a DMA engine in front of the port + /// may. + pub fn char_time(&self) -> Duration { + self.char_transmit_time + } + + /// The holding register is empty, so a DMA engine may write a byte. + /// Transmit is instant, so it always is once the write returns. + pub fn tx_ready(&self) -> bool { + self.lsr & LSR_THRE != 0 + } + pub fn loopback(&self) -> bool { self.mcr & MCR_LOOP != 0 } From 589c617b26f4b429f77b94fe3f9e8920576a3fa2 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:53:36 -0700 Subject: [PATCH 16/33] ioc3_kbc: add the IOC PS/2 controller as its own model The IOC4 has the IOC3's keyboard and mouse controller at another place in its BAR, and QEMU shares one model between the two chips (sgi-ioc3-kbc). Add it as its own module: the register view, the per-port holdings, the clamp rules and the overflow flag. Each call that can bring in a character says so, and the chip latches its own source bit. The host-input line that pokes the chip is KbcOwner and KbcLine. It logs what QEMU logs. The IOC3 keeps its own copy, unchanged, until its split into functions moves it onto this one. --- src/dev/ioc3_kbc.rs | 340 ++++++++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 2 files changed, 341 insertions(+) create mode 100644 src/dev/ioc3_kbc.rs diff --git a/src/dev/ioc3_kbc.rs b/src/dev/ioc3_kbc.rs new file mode 100644 index 000000000..e8459ebc0 --- /dev/null +++ b/src/dev/ioc3_kbc.rs @@ -0,0 +1,340 @@ +//! SGI IOC3 PS/2 keyboard and mouse controller. +//! +//! A block of the IOC3 that the IOC4 carries too, at another place in its +//! BAR. It is a bit-level PS/2 line engine, not an 8042: five 32-bit +//! registers, a control and status register, a receive register per port +//! and a transmit register per port. Each port holds up to three received +//! characters; the clamp bits in the control register stop a port's device +//! once it has sent one or three, and with no clamp armed a fourth +//! character is lost and the receive register reports the overflow. +//! +//! The host's keyboard and mouse model is a [`Ps2Controller`], a byte queue +//! shared by both ports. This block drains it into the per-port holdings. +//! The chip around it owns the interrupt: every call that can move a +//! character in says so, and the chip latches its own source bit. +//! +//! # What is approximated here +//! +//! * The data-available line is asserted for every character that reaches a +//! holding, not once per drain. The silicon may pulse per character or hold +//! a level over the holding, and no public driver tells them apart. +//! * A device stopped by a clamp resumes inside the read that empties the +//! holding; the part takes a character time. A guest that reads and only +//! then acknowledges can clear the assertion those resumed characters +//! made. Linux acknowledges before it reads and OpenBSD reads until the +//! register comes back empty, so both avoid it. +//! * The control register's timeout bits (16, 17) and the hardware status +//! bits read clear: no stalled line is simulated, and a transmit completes +//! inside the write. +//! * Reset values and the register's endianness are the emulator's choice. +//! +//! Ported from hw/input/sgi-ioc3-kbc.c in jamesbraid/qemu-sgi-origami. QEMU's +//! two `ps2` devices are the shared [`Ps2Controller`] here, and its +//! re-entrancy guard around a fill is the owner's poke flag. + +use crate::dev::ioc3::usize_field; +use crate::snapshot::{field, field_bool, field_u32, field_u8s, StateTable}; +use std::sync::Weak; + +use crate::dev::ps2::Ps2Controller; +use crate::irq::InterruptController; + +/// Register offsets within the controller's window. +pub mod reg { + /// Control and status. + pub const CONTROL: u32 = 0x00; + /// Keyboard receive (read-only). + pub const KBD_RECEIVE: u32 = 0x04; + /// Mouse receive (read-only). + pub const AUX_RECEIVE: u32 = 0x08; + /// Keyboard transmit (write-only). + pub const KBD_TRANSMIT: u32 = 0x0c; + /// Mouse transmit (write-only). + pub const AUX_TRANSMIT: u32 = 0x10; + pub const MMIO_SIZE: u32 = AUX_TRANSMIT + 4; +} + +/// Characters a port holds. +pub const RX_SLOTS: usize = 3; +pub const PORT_KBD: usize = 0; +pub const PORT_AUX: usize = 1; + +/// Control bits, from the public Linux header. +pub mod ctrl { + pub const EMULATION: u32 = 1 << 12; + pub const FAST_CLOCK: u32 = 1 << 13; + pub const K_TO_EN: u32 = 1 << 18; + pub const M_TO_EN: u32 = 1 << 19; + pub const K_CLAMP_1: u32 = 1 << 20; + pub const M_CLAMP_1: u32 = 1 << 21; + pub const K_CLAMP_3: u32 = 1 << 22; + pub const M_CLAMP_3: u32 = 1 << 23; + pub const CLAMP_MASK: u32 = 0x00f0_0000; + /// Writable bits: the low half, the timeout enables and the clamps. + pub const MASK: u32 = 0x0000_ffff | K_TO_EN | M_TO_EN | CLAMP_MASK; +} + +/// Receive register: bit 27 says the characters came from the mouse, bit 28 +/// that one arrived with the slots full and was lost. +pub const RX_SOURCE_AUX: u32 = 1 << 27; +pub const RX_OVERFLOW: u32 = 1 << 28; + +/// The valid flag of receive slot `slot` (2 is the oldest character). +pub const fn rx_valid(slot: usize) -> u32 { + 1 << (29 + slot) +} + +/// What one port holds: the characters it has taken, oldest first, and +/// whether one was lost for want of room. +#[derive(Default, Clone, Copy)] +struct Holding { + data: [u8; RX_SLOTS], + count: usize, + overflow: bool, +} + +/// A chip that owns a [`Ioc3Kbc`] and wants to know when the host's PS/2 +/// model has a character. +pub trait KbcOwner: Send + Sync { + /// A device raised its line. If an access holds the chip (possibly this + /// very thread, from inside a transmit), that access picks the character + /// up when it finishes. + fn kbc_poke(&self); +} + +/// The line a PS/2 model drives when it has something to send. +pub struct KbcLine(pub Weak); + +impl InterruptController for KbcLine { + fn set_line(&self, _line: u32, _level: bool) { + if let Some(owner) = self.0.upgrade() { + owner.kbc_poke(); + } + } +} + +pub struct Ioc3Kbc { + /// Board wiring: whether the connectors are fitted. + ports_present: bool, + control: u32, + port: [Holding; 2], +} + +impl Ioc3Kbc { + pub fn new(ports_present: bool) -> Ioc3Kbc { + Ioc3Kbc { ports_present, control: 0, port: [Holding::default(); 2] } + } + + /// Reset: the control register and both holdings clear. + pub fn reset(&mut self) { + self.control = 0; + self.port = [Holding::default(); 2]; + } + + /// How many characters this port may take before its clock line is held + /// low. Zero means no clamp is armed, and then the controller has no way + /// to stop the device. Arming both clamps is not a combination any driver + /// writes, so the tighter one is taken. + fn clamp(&self, port: usize) -> usize { + let (one, three) = if port == PORT_AUX { + (ctrl::M_CLAMP_1, ctrl::M_CLAMP_3) + } else { + (ctrl::K_CLAMP_1, ctrl::K_CLAMP_3) + }; + if self.control & one != 0 { + 1 + } else if self.control & three != 0 { + RX_SLOTS + } else { + 0 + } + } + + /// Move what the device has sent into this port's holding, up to what + /// the clamp allows. A clamped port stops with its clock held low and the + /// rest still in the device; an unclamped one loses the surplus and says + /// so through the overflow flag. Returns whether a character arrived, + /// which is when the chip latches its source. + pub fn fill(&mut self, port: usize, ps2: &Ps2Controller) -> bool { + if !self.ports_present { + return false; + } + let clamp = self.clamp(port); + let mut arrived = false; + loop { + if clamp != 0 && self.port[port].count >= clamp { + break; + } + let Some(byte) = ps2.pop_from(port == PORT_AUX) else { break }; + let p = &mut self.port[port]; + if p.count >= RX_SLOTS { + p.overflow = true; + continue; + } + p.data[p.count] = byte; + p.count += 1; + arrived = true; + } + arrived + } + + /// Fill both ports. + pub fn fill_all(&mut self, ps2: &Ps2Controller) -> bool { + let kbd = self.fill(PORT_KBD, ps2); + let aux = self.fill(PORT_AUX, ps2); + kbd || aux + } + + /// Report what the port holds, oldest character in the highest slot, and + /// empty the holding: the register packs the characters since the last + /// read, so the read clears them, their flags and the overflow flag + /// together. The clamp releases with it. Returns the value and whether + /// the device's resumed characters arrived. + fn receive(&mut self, port: usize, ps2: &Ps2Controller) -> (u32, bool) { + let p = self.port[port]; + let mut val = 0u32; + for (i, &b) in p.data.iter().enumerate().take(p.count) { + let slot = (RX_SLOTS - 1) - i; + val |= u32::from(b) << (8 * slot); + val |= rx_valid(slot); + // The per-character error flag (bit 24 + slot) stays clear: the + // PS/2 layer delivers clean bytes. + } + if p.count != 0 && port == PORT_AUX { + val |= RX_SOURCE_AUX; + } + if p.overflow { + val |= RX_OVERFLOW; + } + self.port[port] = Holding::default(); + let arrived = self.fill(port, ps2); + (val, arrived) + } + + /// A 32-bit read at `off`. Returns the value, and whether a character + /// arrived as a result. + pub fn read(&mut self, off: u32, ps2: &Ps2Controller) -> (u32, bool) { + match off { + // Write-pending and the other status bits read clear: a + // synchronous transmit completes within the register write. + reg::CONTROL => (self.control, false), + reg::KBD_RECEIVE => self.receive(PORT_KBD, ps2), + reg::AUX_RECEIVE => self.receive(PORT_AUX, ps2), + _ => { + crate::dlog!(crate::devlog::LogModule::Ioc, "ioc3-kbc: read of write-only register {off:#x}"); + (0, false) + } + } + } + + /// A 32-bit write at `off`. Returns whether a character arrived. + pub fn write(&mut self, off: u32, val: u32, ps2: &Ps2Controller) -> bool { + match off { + reg::CONTROL => { + self.control = val & ctrl::MASK; + if val & (ctrl::EMULATION | ctrl::FAST_CLOCK) != 0 { + crate::dlog!(crate::devlog::LogModule::Ioc, "ioc3-kbc: emulation/fast-clock effects are not modeled"); + } + // Releasing a clamp lets a stopped device send again. + self.fill_all(ps2) + } + reg::KBD_TRANSMIT | reg::AUX_TRANSMIT => { + if !self.ports_present { + crate::dlog!(crate::devlog::LogModule::Ioc, "ioc3-kbc: transmit on an unwired port"); + return false; + } + ps2.write_to(val as u8, off == reg::AUX_TRANSMIT); + self.fill_all(ps2) + } + _ => { + crate::dlog!(crate::devlog::LogModule::Ioc, "ioc3-kbc: write of {val:#x} to read-only register {off:#x}"); + false + } + } + } +} + +// The controller's state for a snapshot of the IOC3 that carries it: the +// control register and each port's held characters. Whether the connectors +// are fitted is the board's. + +impl Holding { + fn save_state(&self) -> toml::Value { + StateTable::new() + .u8s("data", &self.data) + .u64("count", self.count as u64) + .bool("overflow", self.overflow) + .build() + } + fn load_state(v: &toml::Value) -> Result { + Ok(Self { + data: field_u8s(v, "data")?, + count: usize_field(v, "count", RX_SLOTS)?, + overflow: field_bool(v, "overflow")?, + }) + } +} + +impl Ioc3Kbc { + pub fn save_state(&self) -> toml::Value { + StateTable::new() + .u32("control", self.control) + .value("port0", self.port[0].save_state()) + .value("port1", self.port[1].save_state()) + .build() + } + + /// The controller `save_state` described, on this board's connectors. + pub fn loaded(&self, v: &toml::Value) -> Result { + Ok(Ioc3Kbc { + ports_present: self.ports_present, + control: field_u32(v, "control")?, + port: [Holding::load_state(field(v, "port0")?)?, Holding::load_state(field(v, "port1")?)?], + }) + } +} + +#[cfg(test)] +mod tests { + // Cases follow the sgi-ioc3-kbc qtests in jamesbraid/qemu-sgi-origami: + // clamp-after-one-splits-a-reply and the unclamped overflow. + use super::*; + + fn kbc() -> (Ioc3Kbc, Ps2Controller) { + (Ioc3Kbc::new(true), Ps2Controller::new(None)) + } + + /// Clamping after one character splits a reset reply: the acknowledge + /// first and the self-test byte next, each alone in the oldest slot, and + /// each read lets the stopped keyboard send the next. + #[test] + fn clamp_after_one_splits_a_reset_reply() { + let (mut k, ps2) = kbc(); + assert!(!k.write(reg::CONTROL, ctrl::K_CLAMP_1, &ps2)); + assert!(k.write(reg::KBD_TRANSMIT, 0xff, &ps2), "the acknowledge arrives"); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2), (0x80fa_0000, true), "and the self-test byte follows the read"); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2), (0x80aa_0000, false)); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2), (0, false)); + } + + /// With no clamp a port holds three characters, oldest in the highest + /// slot; a fourth is lost and the read reports the overflow, which the + /// read clears. + #[test] + fn an_unclamped_port_holds_three_and_reports_a_lost_fourth() { + let (mut k, ps2) = kbc(); + k.write(reg::KBD_TRANSMIT, 0xff, &ps2); + k.write(reg::KBD_TRANSMIT, 0xff, &ps2); + let all = rx_valid(0) | rx_valid(1) | rx_valid(2); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2).0, all | RX_OVERFLOW | 0x00fa_aafa); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2).0, 0, "the read cleared the holding and the flag"); + } + + /// A board without the connectors takes no transmit and holds nothing. + #[test] + fn unwired_ports_hold_nothing() { + let (mut k, ps2) = (Ioc3Kbc::new(false), Ps2Controller::new(None)); + assert!(!k.write(reg::KBD_TRANSMIT, 0xff, &ps2)); + assert_eq!(k.read(reg::KBD_RECEIVE, &ps2), (0, false)); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 9b1c8b9ab..f27a3b96f 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -20,6 +20,7 @@ pub mod heart_mem; pub mod ioc3; pub mod ioc_serial_dma; pub mod ioc3_eth; +pub mod ioc3_kbc; pub mod ioc3_port; pub mod isp1040; pub mod mace_id; From fa758b0330999ec8e271fc2373d8e6128b9d11e8 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:11:18 -0700 Subject: [PATCH 17/33] ds1744: add the DS1744-compatible timekeeper The Tezro's IO9 carries a 32 KiB timekeeping SRAM with a BCD calendar in its last eight bytes. The Fuel's M48T35 model has the same geometry and clock offsets but not the same registers: run against the DS1744 contract it reads zero from the control register where the part shows the century, 0x40 where the part shows 0x60 after a read request, has no weekday, and takes a month of 0x13 as a wrapped date where the part refuses the commit. That is a different calendar, so the DS1744 is its own model rather than a flag on the Timekeeper. The control register carries the century beside the write and read bits, the weekday advances with the date, the oscillator-stop bit rides in the seconds, a commit that is not a date is refused, and test mode is modelled. The clock is ported from QEMU's ds1742 and the two have to keep in step, so the tests replay the QEMU model's register sequences as one table. --- src/dev/ds1744.rs | 450 ++++++++++++++++++++++++++++++++++++++++ src/dev/ds1744_tests.rs | 305 +++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 756 insertions(+) create mode 100644 src/dev/ds1744.rs create mode 100644 src/dev/ds1744_tests.rs diff --git a/src/dev/ds1744.rs b/src/dev/ds1744.rs new file mode 100644 index 000000000..e5e1c30bd --- /dev/null +++ b/src/dev/ds1744.rs @@ -0,0 +1,450 @@ +//! DS1744-compatible timekeeping SRAM: 32 KiB of battery-backed bytes whose +//! last eight are a BCD calendar. +//! +//! The Tezro's IO9 maps one in the IOC4's BAR (byte-bus +//! select 0 of the IOC4). The part reads like the Fuel's M48T35 [`Timekeeper`] at a +//! glance, and has the same geometry and clock offsets, but differs where +//! software looks: the control register carries the century beside the +//! write and read bits, the day register holds a weekday that advances +//! with the calendar, an oscillator-stop bit rides in the seconds, a commit +//! with an impossible date is refused, and a test-mode bit makes the +//! seconds' low bit run at 1024 Hz. Those are a different calendar rather +//! than a flag on the M48T35's, so this is its own model. +//! +//! The guest's write bit stages a calendar image and the clearing of it +//! commits it; the read bit freezes the visible calendar while the real one +//! runs on. +//! +//! # What is approximated here +//! +//! * The fitted IO9 part and its physical address wiring are unknown. The +//! 32 KiB geometry is the profile's choice. +//! * With a fresh-year base of 1968 the guest's year digits count from that +//! year, as its driver expects: the host date is shifted into 2000 to +//! 2099 and the weekday comes from the unshifted date, so the weekday +//! follows the host's real calendar while the digits follow the shifted +//! one. +//! * Host time and a backing file stand in for the oscillator and battery. +//! A restart restores a completed calendar and lets host time pass; an +//! unfinished write or read transaction is not restored. +//! +//! Ported from hw/rtc/ds1742.c in jamesbraid/qemu-sgi-origami. + +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use parking_lot::Mutex; + +use crate::traits::{BusDevice, BusRead8, BUS_ERR, BUS_OK}; + +pub const SIZE: usize = 0x8000; +/// The control register, first of the eight calendar bytes. +pub const CONTROL: usize = 0x7ff8; +const WRITE: u8 = 0x80; +const READ: u8 = 0x40; +/// Seconds bit 7: the oscillator is stopped. +const OSCILLATOR_STOP: u8 = 0x80; +/// Day bit 6: test mode; bit 7: the battery flag, read-only and always set. +const TEST_MODE: u8 = 0x40; +const BATTERY_FLAG: u8 = 0x80; +const CLOCK_REF_BYTES: usize = 16; +const CLOCK_REF_MAGIC: &[u8; 8] = b"DS1742R1"; +/// Test mode flips the seconds' low bit every 976562 ns. +const TEST_BIT_NS: i64 = 976_562; + +/// How the guest's year digits relate to the host's calendar. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum YearBase { + /// The host's own year. + Gregorian, + /// The year digits count from this year: the host date is shifted into + /// 2000 to 2099. Only 1968 is accepted. + From(u32), +} + +fn bcd(v: i64) -> u8 { + ((v / 10) * 16 + v % 10) as u8 +} + +fn from_bcd(v: u8) -> i64 { + i64::from((v >> 4) * 10 + (v & 15)) +} + +fn bcd_in(v: u8, lo: i64, hi: i64) -> bool { + v & 15 < 10 && v >> 4 < 10 && (lo..=hi).contains(&from_bcd(v)) +} + +fn is_leap(year: i64) -> bool { + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) +} + +fn month_days(year: i64, month: i64) -> i64 { + match month { + 2 => 28 + i64::from(is_leap(year)), + 4 | 6 | 9 | 11 => 30, + _ => 31, + } +} + +/// Days since 1970-01-01 of a Gregorian date. +fn days_from_civil(year: i64, month: i64, day: i64) -> i64 { + let y = if month <= 2 { year - 1 } else { year }; + let era = y.div_euclid(400); + let yoe = y - era * 400; + let doy = (153 * ((month + 9) % 12) + 2) / 5 + day - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + era * 146_097 + doe - 719_468 +} + +/// The Gregorian date of a day count since 1970-01-01. +fn civil_from_days(days: i64) -> (i64, i64, i64) { + let z = days + 719_468; + let era = z.div_euclid(146_097); + let doe = z - era * 146_097; + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = doy - (153 * mp + 2) / 5 + 1; + let month = if mp < 10 { mp + 3 } else { mp - 9 }; + let year = yoe + era * 400 + i64::from(month <= 2); + (year, month, day) +} + +/// Day number of `seconds`, rounding down. +fn day_index(seconds: i64) -> i64 { + seconds.div_euclid(86_400) +} + +/// The 1-based weekday (Sunday is 1) of a count of days since 1970-01-01, +/// a Thursday. +fn weekday_of_day(days: i64) -> u8 { + (days + 4).rem_euclid(7) as u8 + 1 +} + +struct State { + ram: Vec, + control: u8, + /// Test-mode flag and weekday as last written (FT and bits 2:0). + weekday: u8, + staged: [u8; 8], + frozen: [u8; 8], + frozen_valid: bool, + /// Unix seconds of the calendar when it was last set. + epoch: i64, + /// Clock reading (ns) when it was last set. + reference_ns: i64, + stopped: bool, + file: Option, + /// A backing write failed; the next equal write retries it. + storage_failed: bool, +} + +/// The nanosecond clock the calendar runs on. +type Clock = Arc i64 + Send + Sync>; + +pub struct Ds1744 { + st: Mutex, + clock: Clock, +} + +impl Ds1744 { + /// A part whose calendar starts at the host's date and time. + pub fn new(base: YearBase) -> Result { + let host = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| e.to_string())?.as_secs() as i64; + let born = Instant::now(); + Ds1744::with_clock(host, base, Arc::new(move || born.elapsed().as_nanos() as i64)) + } + + /// A part seeded with `host_unix_secs` and running on `clock`, a count of + /// nanoseconds. Tests supply a clock they step. + pub fn with_clock(host_unix_secs: i64, base: YearBase, clock: Clock) -> Result { + let mut epoch = host_unix_secs; + let (year, month, day) = civil_from_days(day_index(host_unix_secs)); + let mut control = bcd(year / 100); + let weekday = weekday_of_day(day_index(host_unix_secs)); + match base { + YearBase::Gregorian => {} + YearBase::From(1968) => { + let base = 1968; + if !(base..=base + 99).contains(&year) { + return Err(format!("ds1744: fresh-year-base={base} cannot seed host year {year}")); + } + let shifted = 2000 + (year - base) % 100; + // A leap day in a shifted year without one is the next day. + epoch = days_from_civil(shifted, month, day) * 86_400 + host_unix_secs.rem_euclid(86_400); + control = bcd(shifted / 100); + } + YearBase::From(other) => return Err(format!("ds1744: fresh-year-base must be 0 or 1968, not {other}")), + } + let now = clock(); + Ok(Ds1744 { + st: Mutex::new(State { + ram: vec![0; CONTROL], + control, + weekday, + staged: [0; 8], + frozen: [0; 8], + frozen_valid: false, + epoch, + reference_ns: now, + stopped: false, + file: None, + storage_failed: false, + }), + clock, + }) + } + + fn now_ns(&self) -> i64 { + (self.clock)() + } + + fn seconds(&self, st: &State) -> i64 { + st.epoch + if st.stopped { 0 } else { (self.now_ns() - st.reference_ns) / 1_000_000_000 } + } + + /// The calendar bytes as the part would show them now. + fn image(&self, st: &State) -> [u8; 8] { + let now = self.seconds(st); + let (year, month, day) = civil_from_days(day_index(now)); + let sec = now.rem_euclid(86_400); + let weekday = ((i64::from(st.weekday & 7) - 1 + day_index(now) - day_index(st.epoch)).rem_euclid(7)) as u8; + [ + (st.control & 0xc0) | bcd(year / 100), + bcd(sec % 60) | if st.stopped { OSCILLATOR_STOP } else { 0 }, + bcd(sec / 60 % 60), + bcd(sec / 3600), + BATTERY_FLAG | (st.weekday & TEST_MODE) | (weekday + 1), + bcd(day), + bcd(month), + bcd(year % 100), + ] + } + + /// Make the staged image the calendar, if it is a date. A refusal leaves + /// the calendar as it was. + fn commit(&self, st: &mut State, century: u8) -> bool { + let s = st.staged; + let valid = bcd_in(century, 0, 39) + && bcd_in(s[1] & 0x7f, 0, 59) + && bcd_in(s[2], 0, 59) + && bcd_in(s[3], 0, 23) + && bcd_in(s[4] & 7, 1, 7) + && bcd_in(s[5], 1, 31) + && bcd_in(s[6], 1, 12) + && bcd_in(s[7], 0, 99); + if !valid { + crate::dlog!(crate::devlog::LogModule::Rtc, "DS1744: invalid calendar write"); + return false; + } + let year = from_bcd(century) * 100 + from_bcd(s[7]); + let (month, day) = (from_bcd(s[6]), from_bcd(s[5])); + if day > month_days(year, month) { + crate::dlog!(crate::devlog::LogModule::Rtc, "DS1744: unsupported calendar date {year:04}-{month:02}-{day:02}"); + return false; + } + st.epoch = days_from_civil(year, month, day) * 86_400 + + from_bcd(s[3]) * 3600 + + from_bcd(s[2]) * 60 + + from_bcd(s[1] & 0x7f); + st.reference_ns = self.now_ns(); + st.weekday = s[4] & (TEST_MODE | 7); + st.stopped = s[1] & OSCILLATOR_STOP != 0; + true + } + + // ----- backing file ----- + + /// Back the part with a 32 KiB file and a 16-byte clock reference beside + /// it (`.clock`). A missing file starts blank and is created; a file + /// of another size is refused. A saved calendar is restored and lets the + /// host time since it was saved pass; a blank one keeps the seed. + pub fn set_nvram_file(&self, path: &Path) -> Result<(), String> { + let clock_path = PathBuf::from(format!("{}.clock", path.display())); + let image = match std::fs::read(path) { + Ok(v) if v.len() == SIZE => Some(v), + Ok(_) => return Err("DS1744: backing image must contain exactly 32768 bytes".into()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + let reference = match std::fs::read(&clock_path) { + Ok(v) if v.len() >= CLOCK_REF_BYTES => Some(v), + Ok(_) => return Err("DS1744: clock reference is shorter than 16 bytes".into()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + let mut st = self.st.lock(); + if let Some(image) = image { + st.ram.copy_from_slice(&image[..CONTROL]); + let cal: [u8; 8] = image[CONTROL..].try_into().expect("eight calendar bytes"); + if [cal[1], cal[2], cal[3], cal[5], cal[6], cal[7]].iter().any(|&b| b != 0) { + self.restore_clock(&mut st, cal, reference.as_deref()); + } + } + st.file = Some(path.to_path_buf()); + self.save_clock(&mut st); + Ok(()) + } + + fn restore_clock(&self, st: &mut State, cal: [u8; 8], reference: Option<&[u8]>) { + st.staged = cal; + if !self.commit(st, cal[0] & 0x3f) { + crate::dlog!(crate::devlog::LogModule::Rtc, "DS1744: invalid saved calendar; using the seed"); + return; + } + st.control = cal[0] & 0x3f; + let mut elapsed = 0; + if !st.stopped { + if let Some(r) = reference.filter(|r| &r[..8] == CLOCK_REF_MAGIC) { + let saved = i64::from_le_bytes(r[8..16].try_into().expect("eight bytes")); + elapsed = (unix_now() - saved).max(0); + } + } + if elapsed != 0 { + let days = day_index(st.epoch + elapsed) - day_index(st.epoch); + st.weekday = (st.weekday & TEST_MODE) | (((i64::from(st.weekday & 7) - 1 + days % 7) % 7) as u8 + 1); + st.epoch += elapsed; + } + } + + /// Write the whole image and the clock reference. Errors are reported + /// and remembered so an equal write retries them. + fn save_clock(&self, st: &mut State) { + let Some(path) = st.file.clone() else { return }; + let mut image = st.ram.clone(); + image.extend_from_slice(&self.image(st)); + let mut reference = CLOCK_REF_MAGIC.to_vec(); + reference.extend_from_slice(&unix_now().to_le_bytes()); + let clock_path = format!("{}.clock", path.display()); + let result = std::fs::write(&path, &image).and_then(|()| std::fs::write(clock_path, &reference)); + st.storage_failed = result.is_err(); + if let Err(e) = result { + log::error!("DS1744: could not save {}: {e}", path.display()); + } + } + + /// Save the calendar now, as a stop or shutdown does. + pub fn flush(&self) { + let mut st = self.st.lock(); + self.save_clock(&mut st); + } + + fn read(&self, a: usize) -> u8 { + let st = self.st.lock(); + if a < CONTROL { + return st.ram[a]; + } + let image = self.image(&st); + if a == CONTROL { + return if st.control & WRITE != 0 { + st.control + } else if st.frozen_valid { + (st.control & 0xc0) | (st.frozen[0] & 0x3f) + } else { + image[0] + }; + } + let i = a - CONTROL; + let mut value = if st.control & WRITE != 0 { + st.staged[i] + } else if st.frozen_valid { + st.frozen[i] + } else { + image[i] + }; + if i == 4 { + value |= BATTERY_FLAG; + } + if i == 1 && st.weekday & TEST_MODE != 0 && !st.stopped { + value = (value & !1) | ((self.now_ns() / TEST_BIT_NS) & 1) as u8; + } + value + } + + fn write(&self, a: usize, value: u8) { + let mut st = self.st.lock(); + if a < CONTROL { + if st.ram[a] == value && !st.storage_failed { + return; + } + st.ram[a] = value; + if let Some(path) = &st.file { + use std::io::{Seek, SeekFrom, Write}; + let result = std::fs::OpenOptions::new().write(true).open(path).and_then(|mut f| { + f.seek(SeekFrom::Start(a as u64))?; + f.write_all(&[value]) + }); + st.storage_failed = result.is_err(); + if let Err(e) = result { + log::error!("DS1744: could not save SRAM byte at {a:#x}: {e}"); + } + } + return; + } + if a != CONTROL { + if st.control & WRITE != 0 { + st.staged[a - CONTROL] = value; + } else { + crate::dlog!(crate::devlog::LogModule::Rtc, "DS1744: calendar write with W clear at {a:#x}"); + } + return; + } + if st.control & (WRITE | READ) == 0 { + st.control = self.image(&st)[0]; + } + let old = st.control; + if value & READ != 0 && old & READ == 0 { + st.frozen = self.image(&st); + st.frozen_valid = true; + } else if value & READ == 0 { + st.frozen_valid = false; + } + if value & WRITE != 0 && old & WRITE == 0 { + st.staged = self.image(&st); + } + if value & WRITE == 0 && old & WRITE != 0 { + let valid = self.commit(&mut st, value & 0x3f); + st.control = (value & 0xc0) | if valid { value & 0x3f } else { old & 0x3f }; + if valid { + self.save_clock(&mut st); + } + return; + } + // The control rules preserve the century while W or R is set. + st.control = (value & 0xc0) | (old & 0x3f); + } +} + +fn unix_now() -> i64 { + SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64) +} + +impl BusDevice for Ds1744 { + fn read8(&self, addr: u64) -> BusRead8 { + match usize::try_from(addr) { + Ok(a) if a < SIZE => BusRead8 { status: BUS_OK, data: self.read(a) }, + _ => BusRead8::err(), + } + } + + fn write8(&self, addr: u64, val: u8) -> u32 { + match usize::try_from(addr) { + Ok(a) if a < SIZE => { + self.write(a, val); + BUS_OK + } + _ => BUS_ERR, + } + } +} + +impl Drop for Ds1744 { + fn drop(&mut self) { + self.flush(); + } +} + +#[cfg(test)] +#[path = "ds1744_tests.rs"] +mod tests; diff --git a/src/dev/ds1744_tests.rs b/src/dev/ds1744_tests.rs new file mode 100644 index 000000000..4ca13cca8 --- /dev/null +++ b/src/dev/ds1744_tests.rs @@ -0,0 +1,305 @@ +//! DS1744 register-contract tests, through the byte bus with a clock the +//! test steps. +//! +//! Ported from tests/qtest/ds1742-test.c in jamesbraid/qemu-sgi-origami. + +use super::*; +use std::sync::atomic::{AtomicI64, Ordering}; + +const SECOND: i64 = 1_000_000_000; + +fn unix(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> i64 { + days_from_civil(y, mo, d) * 86_400 + h * 3600 + mi * 60 + s +} + +struct Rig { + d: Ds1744, + ns: Arc, +} + +impl Rig { + fn at(host: i64, base: YearBase) -> Rig { + let ns = Arc::new(AtomicI64::new(0)); + let n = ns.clone(); + let d = Ds1744::with_clock(host, base, Arc::new(move || n.load(Ordering::SeqCst))).unwrap(); + Rig { d, ns } + } + + /// The qtest's default: January 1st 2024, on the host's calendar. + fn new() -> Rig { + Rig::at(unix(2024, 1, 1, 0, 0, 0), YearBase::Gregorian) + } + + fn step(&self, ns: i64) { + self.ns.fetch_add(ns, Ordering::SeqCst); + } + + fn rd(&self, a: usize) -> u8 { + let r = self.d.read8(a as u64); + assert_eq!(r.status, BUS_OK); + r.data + } + + fn wr(&self, a: usize, v: u8) { + assert_eq!(self.d.write8(a as u64, v), BUS_OK); + } + + /// Set the calendar through the write bit, with a weekday. + #[allow(clippy::too_many_arguments)] + fn set_time_weekday(&self, sec: u8, min: u8, hour: u8, date: u8, month: u8, year: u8, century: u8, weekday: u8) { + self.wr(CONTROL, 0x80); + for (i, v) in [sec, min, hour, weekday, date, month, year].into_iter().enumerate() { + self.wr(CONTROL + 1 + i, v); + } + self.wr(CONTROL, century); + } + + #[allow(clippy::too_many_arguments)] + fn set_time(&self, sec: u8, min: u8, hour: u8, date: u8, month: u8, year: u8, century: u8) { + self.set_time_weekday(sec, min, hour, date, month, year, century, 0x02); + } + + /// The guest-visible calendar: control, seconds, minutes, hours, weekday, + /// date, month, year. + fn calendar(&self) -> [u8; 8] { + std::array::from_fn(|i| self.rd(CONTROL + i)) + } +} + +/// Ports test_fresh_seed: with the 1968 base the guest's year digits count +/// from 1968 and the weekday follows the host's calendar, across a day. +#[test] +fn a_fresh_seed_counts_years_from_1968() { + let r = Rig::at(unix(2024, 2, 28, 23, 59, 59), YearBase::From(1968)); + assert_eq!(r.rd(CONTROL), 0x20); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x56, 0x02, 0x28)); + assert_eq!(r.rd(CONTROL + 4) & 7, 4); + r.step(2 * SECOND); + assert_eq!(r.rd(CONTROL) & 0x3f, 0x20); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x56, 0x02, 0x29)); + assert_eq!(r.rd(CONTROL + 1), 0x01); + assert_eq!(r.rd(CONTROL + 4) & 7, 5); + r.step(86_400 * SECOND); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x56, 0x03, 0x01)); + assert_eq!(r.rd(CONTROL + 4) & 7, 6); +} + +/// Ports test_fresh_seed_default: with no base the guest sees the host's year. +#[test] +fn without_a_base_the_guest_sees_the_host_year() { + let r = Rig::at(unix(2024, 2, 28, 23, 59, 59), YearBase::Gregorian); + assert_eq!(r.rd(CONTROL), 0x20); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x24, 0x02, 0x28)); + assert_eq!(r.rd(CONTROL + 4) & 7, 4); +} + +/// Ports test_fresh_seed_year_boundary: 2067 maps to 2099, the century +/// register steps to 21 and the shifted 2100 is not a leap year. +#[test] +fn the_shifted_century_rolls_and_2100_is_not_a_leap_year() { + let r = Rig::at(unix(2067, 12, 31, 23, 59, 59), YearBase::From(1968)); + assert_eq!(r.rd(CONTROL), 0x20); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x99, 0x12, 0x31)); + assert_eq!(r.rd(CONTROL + 4) & 7, 7); + r.step(SECOND); + assert_eq!(r.rd(CONTROL), 0x21); + assert_eq!((r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x00, 0x01, 0x01)); + assert_eq!(r.rd(CONTROL + 4) & 7, 1); + r.step((58 * 86_400 + 86_399) * SECOND); + assert_eq!((r.rd(CONTROL), r.rd(CONTROL + 7), r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x21, 0x00, 0x02, 0x28)); + assert_eq!(r.rd(CONTROL + 4) & 7, 3); + r.step(SECOND); + assert_eq!((r.rd(CONTROL + 6), r.rd(CONTROL + 5)), (0x03, 0x01)); + assert_eq!(r.rd(CONTROL + 4) & 7, 4); +} + +/// Ports test_ds1744_geometry: RAM below the calendar, including its last +/// byte, and an address past the part faults. +#[test] +fn ram_fills_the_part_below_the_calendar() { + let r = Rig::new(); + for (a, v) in [(0, 0x12), (0x800, 0x34), (0x7ff7, 0x56)] { + r.wr(a, v); + } + assert_eq!((r.rd(0), r.rd(0x800), r.rd(0x7ff7)), (0x12, 0x34, 0x56)); + r.set_time(0x12, 0x23, 0x10, 0x25, 0x09, 0x26, 0x20); + assert_eq!(r.rd(CONTROL), 0x20); + assert_eq!((r.rd(CONTROL + 1), r.rd(CONTROL + 2), r.rd(CONTROL + 3)), (0x12, 0x23, 0x10)); + assert_eq!((r.rd(CONTROL + 5), r.rd(CONTROL + 6), r.rd(CONTROL + 7)), (0x25, 0x09, 0x26)); + assert_eq!(r.rd(CONTROL + 4) & 7, 2); + assert_eq!(r.d.read8(SIZE as u64).status, BUS_ERR); + assert_eq!(r.d.write8(SIZE as u64, 1), BUS_ERR); +} + +/// Ports test_calendar: the control register shows the century beside the +/// write and read bits, the battery flag and weekday advance, and a +/// century carries. +#[test] +fn control_shows_the_century_and_the_calendar_advances() { + let r = Rig::new(); + assert_eq!(r.rd(CONTROL), 0x20); + r.wr(CONTROL, 0x40); + assert_eq!(r.rd(CONTROL), 0x60); + r.wr(CONTROL, 0x00); + assert_eq!(r.rd(CONTROL), 0x20); + r.wr(CONTROL, 0x80); + assert_eq!(r.rd(CONTROL), 0xa0); + r.wr(CONTROL, 0x20); + r.set_time(0x59, 0x59, 0x23, 0x31, 0x12, 0x24, 0x20); + assert_eq!(r.rd(CONTROL + 1), 0x59); + assert_eq!(r.rd(CONTROL + 4) & 0x80, 0x80, "battery flag"); + assert_eq!(r.rd(CONTROL + 4) & 7, 2); + r.step(SECOND); + assert_eq!(r.rd(CONTROL + 4) & 7, 3, "the weekday advances with the date"); + assert_eq!((r.rd(CONTROL + 1), r.rd(CONTROL + 5), r.rd(CONTROL + 6), r.rd(CONTROL + 7)), (0, 1, 1, 0x25)); + r.set_time(0x59, 0x59, 0x23, 0x31, 0x12, 0x99, 0x20); + r.step(SECOND); + assert_eq!(r.rd(CONTROL) & 0x3f, 0x21); + assert_eq!(r.rd(CONTROL + 7), 0); + // A commit that is a date is taken, with a different century. + r.wr(CONTROL, 0x80); + r.wr(CONTROL + 6, 0); + r.wr(CONTROL, 0x19); + assert_eq!(r.rd(CONTROL), 0x21, "month 0 is no date, so the century stays"); + assert_eq!(r.rd(CONTROL + 6), 1); + // The read bit freezes what the guest sees while the clock runs. + r.wr(CONTROL, 0x40); + r.step(2 * SECOND); + assert_eq!(r.rd(CONTROL + 1), 0); + r.wr(CONTROL, 0); + assert_eq!(r.rd(CONTROL + 1), 2); +} + +/// Ports test_invalid_dates: a commit that is not a date leaves the calendar +/// alone, a valid leap day is taken, and the stop bit rides in the seconds. +#[test] +fn a_commit_that_is_not_a_date_is_refused() { + let r = Rig::new(); + r.set_time(0x80 | 0x12, 0x34, 0x05, 0x15, 0x01, 0x24, 0x20); + for (date, month, year) in [(0x30, 0x02, 0x24), (0x29, 0x02, 0x23)] { + r.set_time(0x59, 0x59, 0x23, date, month, year, 0x20); + assert_eq!(r.rd(CONTROL + 1), 0x92, "stopped at the earlier time"); + assert_eq!((r.rd(CONTROL + 5), r.rd(CONTROL + 6), r.rd(CONTROL + 7)), (0x15, 0x01, 0x24)); + } + r.set_time(0x59, 0x59, 0x23, 0x29, 0x02, 0x24, 0x20); + r.step(SECOND); + assert_eq!((r.rd(CONTROL + 1), r.rd(CONTROL + 5), r.rd(CONTROL + 6), r.rd(CONTROL + 7)), (0, 1, 3, 0x24)); +} + +/// Ports test_stop_and_test_mode: a stopped oscillator holds the seconds, +/// the battery flag is read-only, and the test-mode bit flips the seconds' +/// low bit every 976562 ns unless the oscillator is stopped. +#[test] +fn test_mode_flips_the_low_second_bit() { + let r = Rig::new(); + r.set_time(0x80 | 0x12, 0x34, 0x05, 0x06, 0x07, 0x26, 0x20); + r.step(3 * SECOND); + assert_eq!(r.rd(CONTROL + 1), 0x92); + r.wr(CONTROL, 0x80); + r.wr(CONTROL + 1, 0x12); + r.wr(CONTROL + 4, 0xc2); + r.wr(CONTROL, 0x20); + assert_eq!(r.rd(CONTROL + 4), 0xc2); + assert_eq!(r.rd(CONTROL + 1) & 0xfe, 0x12); + let bit = r.rd(CONTROL + 1) & 1; + r.step(976_562); + assert_eq!(r.rd(CONTROL + 1) & 1, bit ^ 1); + r.wr(CONTROL, 0x80); + r.wr(CONTROL + 4, 0x02); + r.wr(CONTROL, 0x20); + let bit = r.rd(CONTROL + 1) & 1; + r.step(976_562); + assert_eq!(r.rd(CONTROL + 1) & 1, bit, "test mode off"); + r.wr(CONTROL, 0x80); + r.wr(CONTROL + 4, 0x42); + r.wr(CONTROL + 1, 0x92); + r.wr(CONTROL, 0x20); + let bit = r.rd(CONTROL + 1) & 1; + r.step(3 * 976_562); + assert_eq!(r.rd(CONTROL + 1) & 1, bit, "stopped, so it holds"); +} + +/// Ports test_restart_persistence: RAM and a completed calendar come back +/// from the file, the elapsed host time passes, a stopped clock does not +/// advance, and a file of the wrong size is refused. +#[test] +fn a_backing_file_restores_ram_and_the_calendar() { + let dir = std::env::temp_dir().join(format!("ds1744-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("clock"); + { + let r = Rig::new(); + r.d.set_nvram_file(&path).unwrap(); + r.wr(0x100, 0xa5); + r.set_time(0x30, 0x15, 0x08, 0x15, 0x06, 0x30, 0x20); + r.d.flush(); + } + let reopened = Rig::at(unix(2024, 1, 1, 0, 0, 0), YearBase::Gregorian); + reopened.d.set_nvram_file(&path).unwrap(); + assert_eq!(reopened.rd(0x100), 0xa5); + let c = reopened.calendar(); + assert_eq!((c[2], c[3], c[5], c[6], c[7]), (0x15, 0x08, 0x15, 0x06, 0x30)); + assert_eq!(c[0], 0x20); + + let short = dir.join("short"); + std::fs::write(&short, [0u8; 16]).unwrap(); + assert!(Rig::new().d.set_nvram_file(&short).is_err()); + std::fs::remove_dir_all(&dir).unwrap(); +} + +/// One step of a register sequence: write, read and expect, or let time pass. +enum Step { + W(usize, u8), + R(usize, u8), + Advance(i64), +} + +/// The register sequences QEMU's ds1742-test asserts, as one table so the +/// two models can be held to the same bytes. Offsets are from the 32 KiB +/// part's calendar (`CONTROL`); the seed is 2024-01-01T00:00:00. +/// +/// Ports test_calendar, test_invalid_dates and test_stop_and_test_mode +/// from tests/qtest/ds1742-test.c in jamesbraid/qemu-sgi-origami. The +/// model's answers come from the QEMU file's assertions, not from running +/// QEMU here; the end-to-end comparison with a live QEMU is not done. +#[test] +fn the_qemu_register_sequences_read_back_the_same() { + use Step::{Advance, R, W}; + const C: usize = CONTROL; + let set = |s, mi, h, wd, d, mo, y, cent| { + vec![W(C, 0x80), W(C + 1, s), W(C + 2, mi), W(C + 3, h), W(C + 4, wd), W(C + 5, d), W(C + 6, mo), W(C + 7, y), W(C, cent)] + }; + let mut steps = vec![R(C, 0x20), W(C, 0x40), R(C, 0x60), W(C, 0x00), R(C, 0x20), W(C, 0x80), R(C, 0xa0), W(C, 0x20)]; + // A rollover into 2025. + steps.extend(set(0x59, 0x59, 0x23, 0x02, 0x31, 0x12, 0x24, 0x20)); + steps.extend([R(C + 1, 0x59), R(C + 4, 0x82), Advance(1_000_000_000)]); + steps.extend([R(C + 4, 0x83), R(C + 1, 0x00), R(C + 5, 0x01), R(C + 6, 0x01), R(C + 7, 0x25)]); + // A century carry. + steps.extend(set(0x59, 0x59, 0x23, 0x02, 0x31, 0x12, 0x99, 0x20)); + steps.extend([Advance(1_000_000_000), R(C, 0x21), R(C + 7, 0x00)]); + // An impossible month leaves the calendar and its century alone. + steps.extend([W(C, 0x80), W(C + 6, 0x00), W(C, 0x19), R(C, 0x21), R(C + 6, 0x01)]); + // The read bit freezes the display; the clock runs on. + steps.extend([W(C, 0x40), Advance(2_000_000_000), R(C + 1, 0x00), W(C, 0x00), R(C + 1, 0x02)]); + // An impossible date is refused; a leap day is taken. + steps.extend(set(0x92, 0x34, 0x05, 0x02, 0x15, 0x01, 0x24, 0x20)); + steps.extend(set(0x59, 0x59, 0x23, 0x02, 0x30, 0x02, 0x24, 0x20)); + steps.extend([R(C + 1, 0x92), R(C + 5, 0x15), R(C + 6, 0x01), R(C + 7, 0x24)]); + steps.extend(set(0x59, 0x59, 0x23, 0x02, 0x29, 0x02, 0x23, 0x20)); + steps.extend([R(C + 1, 0x92), R(C + 5, 0x15)]); + steps.extend(set(0x59, 0x59, 0x23, 0x02, 0x29, 0x02, 0x24, 0x20)); + steps.extend([Advance(1_000_000_000), R(C + 1, 0x00), R(C + 5, 0x01), R(C + 6, 0x03), R(C + 7, 0x24)]); + // The stop bit holds the seconds; test mode makes the low bit run. + steps.extend(set(0x80 | 0x12, 0x34, 0x05, 0x02, 0x06, 0x07, 0x26, 0x20)); + steps.extend([Advance(3_000_000_000), R(C + 1, 0x92)]); + steps.extend([W(C, 0x80), W(C + 1, 0x12), W(C + 4, 0xc2), W(C, 0x20), R(C + 4, 0xc2)]); + + let r = Rig::new(); + for (i, step) in steps.iter().enumerate() { + match *step { + W(a, v) => r.wr(a, v), + R(a, want) => assert_eq!(r.rd(a), want, "step {i}: read of {a:#x}"), + Advance(ns) => r.step(ns), + } + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index f27a3b96f..e1f085889 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -15,6 +15,7 @@ pub mod crime; pub mod battery_clock; pub mod deadline_timers; pub mod ds1687; +pub mod ds1744; pub mod heart; pub mod heart_mem; pub mod ioc3; From 820d1a2c980436dc57a8d958fe4b67131aea5b1d Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:03:53 -0700 Subject: [PATCH 18/33] ioc4: add the IO9 peripheral controller The Tezro's IO9 board carries an IOC4 (PCI revision 0x53) where the Fuel has an IOC3. Its serial console is one of four byte-wide 16550 ports behind it, so the PROM and IRIX cannot reach a prompt without it. The model has the four ports on the shared 16550 core, each with the serial ring engine the IOC3 uses, a 66.67 or 33.33 MHz UART clock selected by the board, and the local interrupt words: eight serial bits per port, a second word for DMA errors, and one PCI pin for both. A failed ring transfer is captured with its address and requester and halts the port until a channel reset. The ATA DMA readback word the PROM diagnostic probes and the PCI status the IP53 policy asks for are latched only for a board that requests them. The DS1744 clock is mapped at 0x80000 to 0x87fff with the calendar at 0x87ff8: byte accesses only, wider ones refused and logged, a chip reset leaves it alone, and an IOC4 with no clock fitted reads zero there. The PS/2 controller sits at 0x200. The chip reads its 32-bit registers in the processor's byte order. PROM 6.210's serial DMA diagnostic timed out on UART A (diag_rc 75) because its writes arrived byte-reversed: port 0 DMA control as 0xff010210 and the TX producer as 0xf8030000 where QEMU's IOC4 on the same PROM records 0x100201ff and 0x3f8. The IO9 PIC carries the processor's bytes in address order and Iris hands a PCI function its BAR value as those bytes read little-endian, while the IOC4 reads them big-endian as the QEMU model's register region does. The UART and clock regions are byte-wide and unaffected. What the guest does that the model takes but cannot make sense of is logged as QEMU logs it: DMA enabled over a misaligned ring base or over PIO data in the FIFOs, DMA disabled with ring work pending, a pair base changed while busy, a PIO transmit while DMA is on, index writes outside the pointer field, receive-timer bits above the reload and control bits stored without effect. Behavior is unchanged by those. The tests port the QEMU model's qtests: the ring registers (4 KiB wrap, retained control bits, index pointer fields, empty-slot entries, timer arm, reload and expiry), receive-ring-full as a level held while ring DMA is on and an entry waits, loopback and overrun cases, the idle status sequence, and the absent clock, calendar window and wide-access refusal. The timer cases check the same transitions at host-time deadlines, since the QEMU file steps virtual time. --- src/dev/ioc4.rs | 1010 +++++++++++++++++++++++++ src/dev/ioc4_tests.rs | 1682 +++++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 2693 insertions(+) create mode 100644 src/dev/ioc4.rs create mode 100644 src/dev/ioc4_tests.rs diff --git a/src/dev/ioc4.rs b/src/dev/ioc4.rs new file mode 100644 index 000000000..6447b5667 --- /dev/null +++ b/src/dev/ioc4.rs @@ -0,0 +1,1010 @@ +//! SGI IOC4 base I/O controller, revision 0x53, as the IO9 board fits it. +//! +//! One PCI function with a 1 MiB memory BAR. Behind it: four 16550-compatible +//! serial ports with a ring-DMA engine each, a local interrupt controller for +//! them, the registers that capture a failed ring transfer, the UART clock +//! select, the ATA channel's DMA readback word the PROM's diagnostic probes, +//! and the timekeeping SRAM on byte-bus select 0 when the board fits one +//! ([`Ds1744`]), and the PS/2 keyboard and mouse controller the IO9 always +//! carries ([`Ioc3Kbc`], the block the IOC3 has too). The ATA channel, the +//! GPIO pins and the other byte-bus selects are not modelled; an access to +//! them reads zero or is dropped, and is logged. +//! +//! Behaviour follows the public Linux and OpenBSD drivers; where the model +//! approximates, the comment says so. +//! +//! # One interrupt output +//! +//! Everything the chip reports ORs into the PCI interrupt pin: the serial +//! status word (eight bits a port) and the other word (ATA, DMA errors, +//! keyboard), each behind its own enable. UART pass-through, transmit-ring- +//! empty, receive-ring-full and receive-high-water follow their source's level and ignore a +//! write-one-to-clear; the rest latch until acknowledged. +//! +//! # Byte order +//! +//! IOC4 orders its own bytes. Its slot is self-ordered: the bridge carries the +//! processor's bytes in address order and leaves the address alone, so a +//! byte access to UART register `r` of port `n` arrives at `0x380 + 8n + r`. +//! A PCI function's BAR value is its bytes read little-endian, and IOC4 reads +//! them big-endian, so the chip takes a 32-bit value byte-reversed: the +//! processor's value is the register's value, as QEMU's big-endian IOC4 +//! region reads it. Control registers and the +//! keyboard block take 32-bit accesses only; a UART window or the clock takes +//! bytes only. Anything else is refused (reads zero, writes dropped). Which widths the silicon accepts is not +//! known. +//! +//! # What is approximated here +//! +//! * The reset values, the BAR size and the PCI class are the emulator's +//! choices. +//! * The serial ring engine is the one the IOC3 uses (`ioc_serial_dma.rs`) and +//! carries its approximations: timers run on host time and receive-ring-full +//! is a level that follows a held entry. After a failed ring transfer the port +//! halts until a channel reset. +//! * Register 0x20 reads the DMA-idle bit unless a transfer is under way. +//! Transfers complete inside the access that starts them, so software +//! always finds the engine idle. +//! * The keyboard block's status bits (write-pending, line control, line +//! levels and idle flags, mask `0xc33f`) read clear and drop writes, as the +//! QEMU adapter does. Its port order, write-pending timing, and whether +//! the interrupt is a pulse per character or a level are not established, +//! and the model follows QEMU: a latch per arriving character, which an +//! acknowledge racing an arrival can lose. The IOC3-only receive flags are +//! masked out of the receive registers. The ports are always fitted, as the +//! IO9's are in QEMU. +//! * The ATA readback word at 0x148 is a plain 32-bit latch with no ATA +//! behind it. Only boards that ask for it have it. +//! * The PCI status register reads 0x0220 (66 MHz capable, medium DEVSEL) on +//! boards that ask for it: a value taken from another revision of the chip, +//! not measured on this one. +//! +//! Names follow QEMU's: `sgi-ioc4.c` has the same registers under the same +//! names, its `sgi_ioc4_sdma_*` callbacks are the chip's `SerialDmaHost`, and +//! the ring engine is `ioc_serial_dma.rs` (QEMU's `sgi-ioc-serial-dma.c`). +//! +//! Ported from hw/misc/sgi-ioc4.c in jamesbraid/qemu-sgi-origami. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; +use std::time::{Duration, Instant}; + +use parking_lot::Mutex; + +use crate::dev::ds1744::Ds1744; +use crate::dev::ioc3_kbc::{self, Ioc3Kbc, KbcLine, KbcOwner}; +use crate::dev::ps2::Ps2Controller; +use crate::dev::ioc_serial_dma::{self, off as sdma_off, SerialDmaUart, SerialDmaHost, SerialDmaParams, SerialDma, SerialDmaSource}; +use crate::dev::ns16550::Ns16550; +use crate::dev::serial_host::SerialBackend; +use crate::dev::deadline_timers::DeadlineTimers; +use crate::hptimer::{TimerManager, TimerReturn}; +use crate::irq::IrqLine; +use crate::pci::{Bar, PciConfig, PciConfigPolicy, PciDma, PciFunction, PciFunctionState, PCI_INTA}; +use crate::traits::{BusDevice, Device, Resettable}; + +pub const VENDOR_ID: u16 = 0x10a9; +pub const DEVICE_ID: u16 = 0x100a; +pub const REVISION: u8 = 0x53; +/// PCI class, subclass and interface: "other", an emulator default. +const PCI_CLASS: u32 = 0x00_ff_00; +pub const BAR_SIZE: u32 = 1 << 20; +/// Status of a 66 MHz capable function with medium DEVSEL timing. +const PCI_STATUS_COMPAT: u16 = 0x0220; +pub const PORTS: usize = 4; + +/// Register offsets within the BAR. +pub mod reg { + /// Captured address of the first failed ring transfer, low and high. + pub const ERROR_LOW: u32 = 0x00; + pub const ERROR_HIGH: u32 = 0x04; + pub const SERIAL_PENDING: u32 = 0x08; + pub const OTHER_PENDING: u32 = 0x0c; + pub const SERIAL_ENABLE_SET: u32 = 0x10; + pub const OTHER_ENABLE_SET: u32 = 0x14; + pub const SERIAL_ENABLE_CLR: u32 = 0x18; + pub const OTHER_ENABLE_CLR: u32 = 0x1c; + pub const SERIAL_CONTROL: u32 = 0x20; + pub const MISC_CONTROL: u32 = 0x24; + /// A word of the ATA channel's DMA block the PROM writes and reads back. + pub const ATA_READBACK: u32 = 0x148; + /// Ring bases, a low and a high word for each pair of ports. + pub const SDMA_BASE: u32 = 0x300; + pub const SDMA_PORT: u32 = 0x310; + pub const SDMA_PORT_STRIDE: u32 = super::sdma_off::REGS * 4; + /// The first 16550 window; eight bytes a port. + pub const UART: u32 = 0x380; + pub const UART_STRIDE: u32 = 8; + /// Byte-bus select 0: the timekeeping SRAM, whose last eight bytes are + /// the clock (at 0x87ff8). + pub const CLOCK: u32 = 0x80000; + pub const CLOCK_SIZE: u32 = crate::dev::ds1744::SIZE as u32; + /// The PS/2 controller's five registers. + pub const KBC: u32 = 0x200; +} + +/// Bits of the serial status word, eight to a port. +pub mod int { + pub const PORT_STRIDE: u32 = 8; + pub const PASSTHROUGH: u32 = 6; + pub const fn serial(port: usize, src: u32) -> u32 { + 1 << (port as u32 * PORT_STRIDE + src) + } + pub const fn pass(port: usize) -> u32 { + serial(port, PASSTHROUGH) + } + /// The other word: a serial DMA error, one bit a port from bit 2. + pub const fn dma_error(port: usize) -> u32 { + 1 << (port as u32 + 2) + } +} + +/// Bits of the captured error address. +mod err { + pub const VALID: u32 = 1; + pub const MULTIPLE: u32 = 1 << 5; + pub const REQUESTER_SHIFT: u32 = 1; + /// The low six address bits are not captured. + pub const ADDRESS_MASK: u64 = 0xffff_ffc0; +} + +/// Other word bit 6: a keyboard or mouse character arrived. +const OTHER_KEYBOARD: u32 = 0x40; +/// Keyboard control bits that report state on the controller: write-pending +/// (0, 1), the last write's line-control bit (2, 3), the data and clock line +/// levels (4, 5, 8, 9) and the state machines' idle flags (14, 15). The model +/// does not simulate the lines, so they all read clear and a write to them is +/// dropped. +const KBC_STATUS: u32 = 0x0000_c33f; +/// The receive registers carry no IOC3-only flags: bits 24 to 28 (per-slot +/// errors, source and overflow) read clear. +const KBC_RX_MASK: u32 = 0xe0ff_ffff; + +/// Serial control bits the model acts on: the threshold, drain, enable, +/// pause and reset. The rest are stored and read back. +const SSCR_MODELLED: u32 = 0x1ff | (1 << 27) | (1 << 28) | (1 << 29) | (1 << 31); +/// The receive consume index also holds the timer arm in bit 31. +const SRCIR_TIMER_ARM: u32 = 1 << 31; +/// The ring size select in a pair base's low word. +const RING_4K: u32 = 1; + +/// Register 0x20: the DMA engines are idle. +const SERIAL_CONTROL_IDLE: u32 = 0x80; + +/// IOC4's engine halts a port on a failed transfer and does not define the +/// packer's held count. +const IOC4_SDMA_PARAMS: SerialDmaParams = SerialDmaParams { held_counts: false, halt_on_dma_error: true }; + +/// The UART input clock the board feeds the chip. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ioc4Clock { + Mhz33, + Mhz66, +} + +impl Ioc4Clock { + pub fn hz(self) -> u32 { + match self { + Ioc4Clock::Mhz33 => 33_333_333, + Ioc4Clock::Mhz66 => 66_666_667, + } + } +} + +/// What the board makes of the chip. +#[derive(Clone, Copy, Debug)] +pub struct Ioc4Config { + pub clock: Ioc4Clock, + /// Report PCI status 0x0220. + pub pci_status_compat: bool, + /// Latch the ATA DMA readback word. + pub ata_readback: bool, +} + +impl Ioc4Config { + /// The chip with nothing a board asks for beyond its clock. + pub fn bare(clock: Ioc4Clock) -> Self { + Self { clock, pci_status_compat: false, ata_readback: false } + } + + /// The Tezro's IO9: a 66.67 MHz clock, and the status and ATA readback + /// words its PROM expects. + pub fn tezro() -> Self { + Self { clock: Ioc4Clock::Mhz66, pci_status_compat: true, ata_readback: true } + } +} + +/// A 32-bit register in the ring-engine block. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SdmaReg { + Base { pair: usize, high: bool }, + Port { port: usize, off: u32 }, +} + +/// Ring bases sit at 0x300 (low then high word, a pair each), then seven +/// registers a port from 0x310. +fn sdma_decode(a: u32) -> Option { + if a & 3 != 0 { + return None; + } + if (reg::SDMA_BASE..reg::SDMA_PORT).contains(&a) { + let n = a - reg::SDMA_BASE; + return Some(SdmaReg::Base { pair: (n / 8) as usize, high: n & 4 != 0 }); + } + let end = reg::SDMA_PORT + PORTS as u32 * reg::SDMA_PORT_STRIDE; + if (reg::SDMA_PORT..end).contains(&a) { + let n = a - reg::SDMA_PORT; + return Some(SdmaReg::Port { port: (n / reg::SDMA_PORT_STRIDE) as usize, off: n % reg::SDMA_PORT_STRIDE }); + } + None +} + +/// Where a byte access to the UART windows lands. +fn uart_decode(a: u32) -> Option<(usize, u32)> { + let end = reg::UART + PORTS as u32 * reg::UART_STRIDE; + (reg::UART..end).contains(&a).then(|| (((a - reg::UART) / reg::UART_STRIDE) as usize, (a - reg::UART) % reg::UART_STRIDE)) +} + +fn in_kbc(a: u32) -> bool { + (reg::KBC..reg::KBC + ioc3_kbc::reg::MMIO_SIZE).contains(&a) +} + +fn in_clock(a: u32) -> bool { + (reg::CLOCK..reg::CLOCK + reg::CLOCK_SIZE).contains(&a) +} + +struct St { + cfg: Ioc4Config, + serial_pending: u32, + serial_enable: u32, + other_pending: u32, + other_enable: u32, + /// Sources that follow a level, in the serial word's layout. + level: u32, + error_low: u32, + error_high: u32, + ata_readback: u32, + uart: [Ns16550; PORTS], + sdma: SerialDma, + kbc: Ioc3Kbc, + ps2: Arc, + dma: Option>, +} + +impl St { + fn new(cfg: Ioc4Config, ps2: Arc) -> St { + St { + cfg, + serial_pending: 0, + serial_enable: 0, + other_pending: 0, + other_enable: 0, + level: 0, + error_low: 0, + error_high: 0, + ata_readback: 0, + uart: std::array::from_fn(|_| Ns16550::new(cfg.clock.hz() / 16)), + sdma: SerialDma::new(PORTS, IOC4_SDMA_PARAMS), + // Every IO9 brings the keyboard and mouse out. + kbc: Ioc3Kbc::new(true), + ps2, + dma: None, + } + } + + /// A PCI reset: the interrupt state, the ports and the engines. + fn reset(&mut self) { + self.serial_pending = 0; + self.serial_enable = 0; + self.other_pending = 0; + self.other_enable = 0; + self.level = 0; + self.error_low = 0; + self.error_high = 0; + self.ata_readback = 0; + for u in &mut self.uart { + u.reset(); + } + self.sdma.reset(); + self.kbc.reset(); + self.ps2.power_on(); + } + + /// Move what the host's PS/2 model has sent into the holdings, and latch + /// the keyboard source if a character arrived. + fn kbc_fill(&mut self) { + let ps2 = self.ps2.clone(); + if self.kbc.fill_all(&ps2) { + self.other_pending |= OTHER_KEYBOARD; + } + } + + /// A 32-bit read of the keyboard block at `off` in its window, as the + /// adapter presents it: no write-pending or other status, and none of + /// the IOC3-only receive flags. + fn kbc_read(&mut self, off: u32) -> u32 { + let ps2 = self.ps2.clone(); + let (raw, arrived) = self.kbc.read(off, &ps2); + if arrived { + self.other_pending |= OTHER_KEYBOARD; + } + if off == ioc3_kbc::reg::CONTROL { raw & !KBC_STATUS } else { raw & KBC_RX_MASK } + } + + fn kbc_write(&mut self, off: u32, val: u32) { + let mut val = val; + if off == ioc3_kbc::reg::CONTROL { + // The status bits read clear; a write that differs is a guest + // error and is dropped. + if val & KBC_STATUS != 0 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 write to read-only KBC status bits {:#x} ignored", val & KBC_STATUS); + } + val &= !KBC_STATUS; + } + let ps2 = self.ps2.clone(); + if self.kbc.write(off, val, &ps2) { + self.other_pending |= OTHER_KEYBOARD; + } + } + + /// The PCI interrupt pin: any enabled source. + fn line_level(&self) -> bool { + (self.serial_pending | self.level) & self.serial_enable != 0 || self.other_pending & self.other_enable != 0 + } + + /// The host-time deadlines the engines hold, for the timers. + fn deadlines(&self) -> Deadlines { + let mut d = [None; KINDS]; + for p in 0..PORTS { + d[K_RX + p] = self.sdma.rx_deadline(p); + d[K_TX_DELAY + p] = self.sdma.tx_delay_deadline(p); + d[K_LOOP + p] = self.sdma.loop_deadline(p); + } + d + } + + fn sdma_engine_split(&mut self) -> (&mut SerialDma, Ioc4SerialDmaHost<'_>) { + let host = Ioc4SerialDmaHost { + uart: &mut self.uart, + serial_pending: &mut self.serial_pending, + level: &mut self.level, + other_pending: &mut self.other_pending, + error_low: &mut self.error_low, + error_high: &mut self.error_high, + dma: self.dma.as_deref(), + }; + (&mut self.sdma, host) + } + + fn sdma_run(&mut self, p: usize) { + let (sdma, mut host) = self.sdma_engine_split(); + sdma.run(p, &mut host); + } + + fn sdma_catch_up(&mut self, now: Instant) { + let (sdma, mut host) = self.sdma_engine_split(); + sdma.catch_up(now, &mut host); + } + + // ----- the 16550s ----- + + /// Keep the pass-through source on the port's interrupt level, and let + /// the engine move: a ring driver receives with the 16550's own + /// interrupts disabled. + fn uart_changed(&mut self, p: usize) { + let level = self.uart[p].irq(); + set_level_bit(&mut self.level, int::pass(p), level); + self.sdma_run(p); + } + + /// Move host input into the port and let the receive timeout run out. + /// Returns whether anything changed. + fn uart_pump(&mut self, p: usize) -> bool { + let u = &mut self.uart[p]; + let moved = u.pump(); + u.check_timeout(Instant::now()) || moved + } + + fn uart_read(&mut self, p: usize, r: u32) -> u8 { + let v = self.uart[p].read(r); + self.uart_changed(p); + v + } + + fn uart_write(&mut self, p: usize, r: u32, v: u8) { + if r == 0 && !self.uart[p].dlab() && self.sdma.control(p) & ioc_serial_dma::ctl::ENABLE != 0 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 PIO transmit on port {p} while DMA is enabled"); + } + self.uart[p].write(r, v); + self.uart_changed(p); + } + + // ----- mode-switch diagnostics ----- + + /// Report the DMA mode switches and the writes the model takes but does + /// not act on. They are reported, not refused: the UART orders PIO and + /// ring DMA itself, and the write goes ahead. + fn check_port_write(&self, p: usize, off: u32, v: u32) { + let log = |what: std::fmt::Arguments| crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 {what}"); + match off { + sdma_off::CONTROL => { + if v & !SSCR_MODELLED != 0 { + log(format_args!("serial control bits {:#010x} on port {p} are stored but not modelled", v & !SSCR_MODELLED)); + } + self.control_check(p, v); + } + sdma_off::TX_PRODUCE | sdma_off::RX_CONSUME => { + let size = if self.sdma.base_read(p / 2, false) & RING_4K != 0 { 0x1000 } else { 0x400 }; + let keep = (size - 8) | if off == sdma_off::RX_CONSUME { SRCIR_TIMER_ARM } else { 0 }; + if v & !keep != 0 { + let what = if off == sdma_off::TX_PRODUCE { "TX producer" } else { "RX consumer" }; + log(format_args!("{what} {v:#010x} on port {p} has bits outside the pointer field")); + } + } + sdma_off::RX_TIMER if v & !0xfff != 0 => { + log(format_args!("unsupported RX timer bits {:#010x} on port {p}", v & !0xfff)); + } + _ => {} + } + } + + /// Enabling DMA over a base misaligned for its rings, or over PIO data in + /// the UART's FIFOs, and disabling it with ring work pending. + fn control_check(&self, p: usize, v: u32) { + use ioc_serial_dma::ctl; + let old_on = self.sdma.control(p) & ctl::ENABLE != 0; + let want_on = v & ctl::ENABLE != 0; + if v & ctl::RESET != 0 || old_on == want_on { + return; + } + let log = |what: std::fmt::Arguments| crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 {what}"); + let pair = p / 2; + if want_on { + let base = u64::from(self.sdma.base_read(pair, true)) << 32 | u64::from(self.sdma.base_read(pair, false) & !RING_4K); + let size: u64 = if self.sdma.base_read(pair, false) & RING_4K != 0 { 0x1000 } else { 0x400 }; + if base & (size * 4 - 1) != 0 { + log(format_args!("DMA enabled on port {p} with pair base {base:#x} misaligned for its rings")); + } + if self.uart[p].rx_pending() != 0 { + log(format_args!("DMA enabled on port {p} with PIO data in the UART's FIFOs")); + } + } else if !self.sdma.port_quiescent(p) { + log(format_args!("DMA disabled on port {p} with ring work pending")); + } + } + + // ----- control registers ----- + + fn read32(&mut self, a: u32) -> u32 { + self.sdma_catch_up(Instant::now()); + if let Some(r) = sdma_decode(a) { + let (sdma, mut host) = self.sdma_engine_split(); + return match r { + SdmaReg::Base { pair, high } => sdma.base_read(pair, high), + SdmaReg::Port { port, off } => sdma.port_read(port, off, &mut host), + }; + } + match a { + reg::ERROR_LOW => self.error_low, + reg::ERROR_HIGH => self.error_high, + reg::SERIAL_PENDING => self.serial_pending | self.level, + reg::OTHER_PENDING => self.other_pending, + reg::SERIAL_ENABLE_SET => self.serial_enable, + reg::OTHER_ENABLE_SET => self.other_enable, + reg::SERIAL_CONTROL => if self.sdma.idle() { SERIAL_CONTROL_IDLE } else { 0 }, + reg::MISC_CONTROL => u32::from(self.cfg.clock == Ioc4Clock::Mhz66), + reg::ATA_READBACK if self.cfg.ata_readback => self.ata_readback, + _ => { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 read of unsupported register {a:#x}"); + 0 + } + } + } + + fn write32(&mut self, a: u32, v: u32) { + self.sdma_catch_up(Instant::now()); + if let Some(r) = sdma_decode(a) { + match r { + SdmaReg::Base { pair, high } => { + if !self.sdma.pair_quiescent(pair) { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 DMA base of pair {pair} changed while it is active"); + } + self.sdma.base_write(pair, high, v); + } + SdmaReg::Port { port, off } => { + self.check_port_write(port, off, v); + let (sdma, mut host) = self.sdma_engine_split(); + sdma.port_write(port, off, v, &mut host); + } + } + return; + } + match a { + // Acknowledge: write one to clear. Level sources ignore it. + reg::SERIAL_PENDING => self.serial_pending &= !v, + reg::OTHER_PENDING => self.other_pending &= !v, + reg::SERIAL_ENABLE_SET => self.serial_enable |= v, + reg::OTHER_ENABLE_SET => self.other_enable |= v, + reg::SERIAL_ENABLE_CLR => self.serial_enable &= !v, + reg::OTHER_ENABLE_CLR => self.other_enable &= !v, + reg::ATA_READBACK if self.cfg.ata_readback => self.ata_readback = v, + reg::ERROR_LOW | reg::ERROR_HIGH | reg::SERIAL_CONTROL | reg::MISC_CONTROL => { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 write {v:#x} to read-only register {a:#x}"); + } + _ => crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 write {v:#x} to unsupported register {a:#x}"), + } + } +} + +fn set_level_bit(word: &mut u32, mask: u32, level: bool) { + if level { + *word |= mask; + } else { + *word &= !mask; + } +} + +/// IOC4 as the ring engine sees it: four 16550s, the serial status word, +/// the error capture registers and the bus master. +struct Ioc4SerialDmaHost<'a> { + uart: &'a mut [Ns16550; PORTS], + serial_pending: &'a mut u32, + level: &'a mut u32, + other_pending: &'a mut u32, + error_low: &'a mut u32, + error_high: &'a mut u32, + dma: Option<&'a dyn PciDma>, +} + +impl SerialDmaHost for Ioc4SerialDmaHost<'_> { + fn uart(&mut self, port: usize) -> &mut dyn SerialDmaUart { + &mut self.uart[port] + } + + fn raise(&mut self, port: usize, src: SerialDmaSource) { + *self.serial_pending |= int::serial(port, src as u32); + } + + fn set_level(&mut self, port: usize, src: SerialDmaSource, level: bool) { + set_level_bit(self.level, int::serial(port, src as u32), level); + } + + /// The engines always reach their ports. + fn reachable(&self, _port: usize) -> bool { + true + } + + fn uart_line_changed(&mut self, port: usize) { + let level = self.uart[port].irq(); + set_level_bit(self.level, int::pass(port), level); + } + + fn dma_read(&self, addr: u64, buf: &mut [u8]) -> bool { + self.dma.is_some_and(|d| d.dma_read(addr, buf).is_ok()) + } + + fn dma_write(&self, addr: u64, data: &[u8]) -> bool { + self.dma.is_some_and(|d| d.dma_write(addr, data).is_ok()) + } + + /// The first failure captures its address and its requester: twice the + /// port for a transmit fetch, one more for a receive post. Later ones + /// only set the multiple-error bit. The port's source is in the other + /// word. + fn dma_error(&mut self, port: usize, rx: bool, addr: u64) { + if *self.error_low & err::VALID == 0 { + let requester = (2 * port + usize::from(rx)) as u32; + *self.error_low = (addr & err::ADDRESS_MASK) as u32 | requester << err::REQUESTER_SHIFT | err::VALID; + *self.error_high = (addr >> 32) as u32; + } else { + *self.error_low |= err::MULTIPLE; + } + *self.other_pending |= int::dma_error(port); + crate::dlog!( + crate::devlog::LogModule::Ioc, + "IOC4 serial DMA {} failed for port {port} at {addr:#x}", + if rx { "write" } else { "read" } + ); + } +} + +// --------------------------------------------------------------------------- +// The device +// --------------------------------------------------------------------------- + +/// How long a serial receive thread sleeps when its backend has nothing: +/// `SerialBackend` has no blocking read to wait on. +const RX_IDLE: Duration = Duration::from_millis(2); + +/// The deadlines the chip keeps on host time, one timer slot each. +const K_RX: usize = 0; +const K_TX_DELAY: usize = PORTS; +const K_LOOP: usize = 2 * PORTS; +const KINDS: usize = 3 * PORTS; +type Deadlines = [Option; KINDS]; + +/// Lock order: `gate`, then `line_order`, then `st`; `armed` and `line` are +/// leaves. The engines move their rings with `st` held: the DMA goes down +/// through the bridge, which never calls back into the chip. +struct Shared { + pci: PciFunctionState, + st: Mutex, + /// PCI interrupt pin A. + line: Mutex>, + /// What the line was last driven to. + line_level: AtomicBool, + /// Serialises driving the line; see [`Shared::sync_line`]. + line_order: Mutex<()>, + me: Weak, + timers: DeadlineTimers, + /// A PS/2 character arrived while an access held the chip. + kbc_dirty: AtomicBool, + /// Between `start` and `stop`. + running: AtomicBool, + /// Held by a timer callback while it runs, so `stop` can wait one out. + gate: Mutex<()>, +} + +impl KbcOwner for Shared { + /// A device raised its line. If an access holds the chip (possibly this + /// very thread, from inside a transmit), that access picks the character + /// up when it finishes: the flag is set before the lock is tried, so + /// either this thread gets the lock or the holder sees the flag. + fn kbc_poke(&self) { + self.kbc_dirty.store(true, Ordering::SeqCst); + if let Some(mut st) = self.st.try_lock() { + if self.kbc_dirty.swap(false, Ordering::SeqCst) { + st.kbc_fill(); + } + drop(st); + self.sync_line(); + } + } +} + +impl Shared { + /// Put the interrupt line at the level the chip's state gives, then pick + /// up any PS/2 character that arrived while the chip was held (a poke + /// sets the flag before it tries the lock, so whoever holds the lock sees + /// it here, after releasing). + fn sync_line(&self) { + loop { + self.drive_line(); + if !self.kbc_dirty.swap(false, Ordering::SeqCst) { + break; + } + self.st.lock().kbc_fill(); + } + } + + /// The level is read after taking `line_order`, so whichever thread + /// drives last has seen the latest state. `st` is never held when this + /// is called. + fn drive_line(&self) { + let _order = self.line_order.lock(); + let level = self.st.lock().line_level(); + if self.line_level.swap(level, Ordering::SeqCst) != level { + if let Some(l) = self.line.lock().as_ref() { + l.set(level); + } + } + } + + /// Run `f` with the chip locked, then arm the timers for the deadlines it + /// left and drive the line from its state. + fn with_st(&self, f: impl FnOnce(&mut St) -> R) -> R { + let (r, want) = { + let mut st = self.st.lock(); + let r = f(&mut st); + (r, st.deadlines()) + }; + self.arm(want); + self.sync_line(); + r + } + + /// Arm a timer for each deadline the chip holds. + fn arm(&self, want: Deadlines) { + self.timers.arm(&want, &self.running, &self.me, |_| Duration::ZERO, Shared::fire); + } + + /// A deadline passed with no guest access to notice it. Runs on the timer + /// thread: short, no waiting beyond the chip's own locks. + fn fire(me: &Weak, kind: usize, due: Instant) -> TimerReturn { + let Some(sh) = me.upgrade() else { return TimerReturn::Delete }; + let _gate = sh.gate.lock(); + if !sh.running.load(Ordering::SeqCst) { + return TimerReturn::Delete; + } + sh.timers.fired(kind, due); + sh.with_st(|st| st.sdma_catch_up(Instant::now())); + TimerReturn::Delete + } + + /// Feed host input from serial backend `port` into its FIFO until the + /// chip stops. + fn rx_loop(me: Weak, port: usize) { + loop { + { + let Some(sh) = me.upgrade() else { return }; + if !sh.running.load(Ordering::SeqCst) { + return; + } + sh.with_st(|st| { + if st.uart_pump(port) { + st.uart_changed(port); + } + }); + } + std::thread::park_timeout(RX_IDLE); + } + } + + /// One synchronous pass of what the receive threads and timers do. + #[cfg(test)] + fn service(&self) { + self.with_st(|st| { + for p in 0..PORTS { + st.uart_pump(p); + st.uart_changed(p); + } + st.sdma_catch_up(Instant::now()); + for p in 0..PORTS { + st.sdma_run(p); + } + }); + } +} + +/// The IOC4 PCI function. +pub struct Ioc4 { + sh: Arc, + /// The DS1744 on byte-bus select 0, when the board fits one. + clock: OnceLock>, + /// The serial receive threads, joined in `stop`. + threads: Mutex>>, +} + +impl Ioc4 { + pub fn new(cfg: Ioc4Config) -> Ioc4 { + let sh = Arc::new_cyclic(|weak: &Weak| { + let line = IrqLine::new(Arc::new(KbcLine(weak.clone())), 0); + let ps2 = Arc::new(Ps2Controller::new(Some(line))); + // Host key and mouse events are accepted from the start (this + // only sets a flag); `stop` and `start` follow the chip's. + Device::start(&*ps2); + let bars = [Bar::Mem(BAR_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused]; + let policy = PciConfigPolicy { + status: if cfg.pci_status_compat { PCI_STATUS_COMPAT } else { 0 }, + status_w1c_mask: 0, + command_mask: 0xffff, + ..Default::default() + }; + Shared { + pci: PciFunctionState::new(PciConfig::new_with_policy(VENDOR_ID, DEVICE_ID, PCI_CLASS, REVISION, bars, PCI_INTA, policy)), + st: Mutex::new(St::new(cfg, ps2)), + line: Mutex::new(None), + line_level: AtomicBool::new(false), + line_order: Mutex::new(()), + kbc_dirty: AtomicBool::new(false), + me: weak.clone(), + timers: DeadlineTimers::new(), + running: AtomicBool::new(false), + gate: Mutex::new(()), + } + }); + Ioc4 { sh, clock: OnceLock::new(), threads: Mutex::new(Vec::new()) } + } + + /// Fit the timekeeping SRAM on byte-bus select 0. Its 32 KiB fill the + /// BAR from 0x80000 and its calendar sits in the last eight bytes. The + /// chip's reset does not touch it: it is memory with a battery. + pub fn attach_clock(&self, clock: Arc) { + assert!(self.clock.set(clock).is_ok(), "IOC4 clock attached more than once"); + } + + /// The timer manager for device deadlines. Set it before start. + pub fn set_timer_manager(&self, tm: Arc) { + self.sh.timers.set_manager(tm); + } + + /// Wire the host end of serial port `port` (0 is the console): a TCP + /// listener, a log tee, or nothing. + pub fn set_serial_backend(&self, port: usize, backend: Arc) { + self.sh.st.lock().uart[port].set_backend(backend); + } + + /// The PS/2 keyboard and mouse model, for the host side to push keys and + /// pointer motion into. + pub fn ps2(&self) -> Arc { + self.sh.st.lock().ps2.clone() + } + + /// A PCI reset of the chip. + pub fn reset(&self) { + self.sh.pci.config().reset(); + self.sh.with_st(St::reset); + } + + /// One synchronous pass of the work the receive threads and the timers + /// do between guest accesses. Tests use it to settle the chip without + /// waiting on a thread. + #[cfg(test)] + pub fn service(&self) { + self.sh.service(); + } + + /// Name the chip's long-lived locks in the lock registry, which holds + /// only a weak reference to them. + pub fn register_locks(&self) { + for (what, probe) in [ + ("st", (|s: &Shared| s.st.is_locked()) as fn(&Shared) -> bool), + ("line_order", |s| s.line_order.is_locked()), + ] { + let sh = Arc::downgrade(&self.sh); + crate::locks::register_lock(format!("ioc4::{what}"), move || sh.upgrade().is_some_and(|s| probe(&s))); + } + } + + /// Say that an access the chip does not take was refused. + fn refuse(what: &str, width: usize, off: u32) { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC4 {what} of width {width} at {off:#x} refused"); + } +} + +impl Device for Ioc4 { + fn step(&self, _cycles: u64) {} + + /// Start the serial receive threads and arm the timers for any deadline + /// the chip already holds. + fn start(&self) { + if self.sh.running.swap(true, Ordering::SeqCst) { + return; + } + Device::start(&*self.sh.st.lock().ps2); + let mut threads = self.threads.lock(); + for p in 0..PORTS { + let me = self.sh.me.clone(); + threads.push( + std::thread::Builder::new() + .name(format!("IOC4-RX-{p}")) + .spawn(move || Shared::rx_loop(me, p)) + .expect("spawn ioc4 receive thread"), + ); + } + drop(threads); + let want = self.sh.st.lock().deadlines(); + self.sh.arm(want); + } + + /// Remove the timers, wait out a callback in flight and join the threads. + /// Nothing touches memory or the line afterwards, until `start`. + fn stop(&self) { + if !self.sh.running.swap(false, Ordering::SeqCst) { + return; + } + self.sh.timers.cancel_all(); + drop(self.sh.gate.lock()); + let mut threads = self.threads.lock(); + for t in threads.iter() { + t.thread().unpark(); + } + for t in threads.drain(..) { + let _ = t.join(); + } + drop(threads); + Device::stop(&*self.sh.st.lock().ps2); + } + + fn is_running(&self) -> bool { + self.sh.running.load(Ordering::SeqCst) + } + + fn get_clock(&self) -> u64 { + 0 + } +} + +impl Resettable for Ioc4 { + fn power_on(&self) { + self.reset(); + } +} + +impl Drop for Ioc4 { + fn drop(&mut self) { + Device::stop(&*self); + } +} + +impl PciFunction for Ioc4 { + fn state(&self) -> &PciFunctionState { + &self.sh.pci + } + + fn bar_read(&self, bar: usize, off: u32, width: usize) -> u32 { + if bar != 0 || off >= BAR_SIZE { + return 0; + } + if let Some((p, r)) = uart_decode(off) { + if width != 1 { + Ioc4::refuse("UART read", width, off); + return 0; + } + return u32::from(self.sh.with_st(|st| st.uart_read(p, r))); + } + if let Some(clock) = self.clock.get().filter(|_| in_clock(off)) { + if width != 1 { + Ioc4::refuse("clock read", width, off); + return 0; + } + return u32::from(clock.read8(u64::from(off - reg::CLOCK)).data); + } + if in_kbc(off) { + if width != 4 || off & 3 != 0 { + Ioc4::refuse("KBC read", width, off); + return 0; + } + return self.sh.with_st(|st| st.kbc_read(off - reg::KBC)).swap_bytes(); + } + if width != 4 || off & 3 != 0 { + Ioc4::refuse("read", width, off); + return 0; + } + self.sh.with_st(|st| st.read32(off)).swap_bytes() + } + + fn bar_write(&self, bar: usize, off: u32, val: u32, width: usize) { + if bar != 0 || off >= BAR_SIZE { + return; + } + if let Some((p, r)) = uart_decode(off) { + if width != 1 { + Ioc4::refuse("UART write", width, off); + return; + } + self.sh.with_st(|st| st.uart_write(p, r, val as u8)); + return; + } + if let Some(clock) = self.clock.get().filter(|_| in_clock(off)) { + if width != 1 { + Ioc4::refuse("clock write", width, off); + return; + } + clock.write8(u64::from(off - reg::CLOCK), val as u8); + return; + } + if in_kbc(off) { + if width != 4 || off & 3 != 0 { + Ioc4::refuse("KBC write", width, off); + return; + } + self.sh.with_st(|st| st.kbc_write(off - reg::KBC, val.swap_bytes())); + return; + } + if width != 4 || off & 3 != 0 { + Ioc4::refuse("write", width, off); + return; + } + self.sh.with_st(|st| st.write32(off, val.swap_bytes())); + } + + /// The chip's DMA, and the PCI interrupt pin: any enabled source. + fn attached(&self) { + if let Some(dma) = self.sh.pci.dma() { + self.sh.st.lock().dma = Some(dma.clone()); + } + if let Some(line) = self.sh.pci.irq() { + { + let _order = self.sh.line_order.lock(); + line.set(self.sh.line_level.load(Ordering::SeqCst)); + *self.sh.line.lock() = Some(line.clone()); + } + self.sh.sync_line(); + } + } +} + +#[cfg(test)] +#[path = "ioc4_tests.rs"] +mod tests; diff --git a/src/dev/ioc4_tests.rs b/src/dev/ioc4_tests.rs new file mode 100644 index 000000000..c2530d0dd --- /dev/null +++ b/src/dev/ioc4_tests.rs @@ -0,0 +1,1682 @@ +//! IOC4 register-contract tests. +//! +//! Accesses go through `PciFunction` the way the bridge makes them: IOC4 +//! orders its own bytes, so a register is at its address and a UART register +//! is a byte access at its window. Each test builds only the chip and a +//! block of bus memory for the rings. +//! +//! Ported from tests/qtest/sgi-ioc4-test.c in jamesbraid/qemu-sgi-origami. +//! That file drives the chip through an XBridge with chardev sockets and the +//! virtual clock; here the sockets are a `Wire` per port and time is the host +//! clock, so timers are given a few milliseconds and `service` plays the part +//! of the main loop. + +use super::*; +use crate::dev::ds1744::{self, Ds1744, YearBase}; +use crate::irq::LevelProbe; +use std::sync::atomic::{AtomicI64, Ordering as AtomicOrdering}; +use crate::pci::PciDmaStatus; +use std::collections::VecDeque; + +const MEM_SIZE: usize = 0x10000; +/// Where the rings sit in bus memory. +const RINGS: u64 = 0x4000; +/// An address no memory answers. +const BAD_BASE: u64 = 0x20_0000; +const TX_VALID: u8 = 0x40; +const TX_EXPLICIT: u8 = 0x20; +const TX_MCR: u8 = 0x80; +const TX_DELAY: u8 = 0xc0; +/// Received status from the line: data and modem valid, CTS and DCD up. +const RX_FROM_LINE: u8 = 0xf0; +/// Received status looped back: RTS returns as CTS, and OUT2 clear is no DCD. +const RX_LOOPBACK: u8 = 0xd0; +const ENABLE: u32 = 1 << 28; +const RESET: u32 = 1 << 31; +const THRESHOLD: u32 = 1; +const ARM: u32 = 1 << 31; +const DRAIN: u32 = 1 << 27; +const SHADOW_OUTPUTS: u32 = 0x1f00_0000; +const ERROR_VALID: u32 = 1; +const ERROR_MULTIPLE: u32 = 1 << 5; + +/// A sink for a serial line: what the guest sent, and what the host types. +#[derive(Default)] +struct Wire { + out: std::sync::Mutex>, + inp: std::sync::Mutex>, +} + +impl SerialBackend for Wire { + fn send_byte(&self, b: u8) { + self.out.lock().unwrap().push(b); + } + fn recv_byte(&self) -> std::io::Result { + self.inp + .lock() + .unwrap() + .pop_front() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::WouldBlock, "empty")) + } +} + +/// Bus memory, as bytes at bus addresses. Addresses past its end abort. +struct TestMem(std::sync::Mutex>); + +impl PciDma for TestMem { + fn dma_read(&self, addr: u64, buf: &mut [u8]) -> PciDmaStatus { + let m = self.0.lock().unwrap(); + let a = addr as usize; + if addr > u64::from(u32::MAX) || a + buf.len() > m.len() { + buf.fill(0); + return PciDmaStatus::MasterAbort; + } + buf.copy_from_slice(&m[a..a + buf.len()]); + PciDmaStatus::Ok + } + fn dma_write(&self, addr: u64, data: &[u8]) -> PciDmaStatus { + let mut m = self.0.lock().unwrap(); + let a = addr as usize; + if addr > u64::from(u32::MAX) || a + data.len() > m.len() { + return PciDmaStatus::MasterAbort; + } + m[a..a + data.len()].copy_from_slice(data); + PciDmaStatus::Ok + } +} + +struct Rig { + c: Ioc4, + mem: Arc, + wire: [Arc; PORTS], + pin: Arc, +} + +impl Rig { + fn new() -> Rig { + Rig::with(Ioc4Config::bare(Ioc4Clock::Mhz33)) + } + + fn with(cfg: Ioc4Config) -> Rig { + let c = Ioc4::new(cfg); + let mem = Arc::new(TestMem(std::sync::Mutex::new(vec![0; MEM_SIZE]))); + c.state().connect_dma(mem.clone()); + let wire: [Arc; PORTS] = std::array::from_fn(|_| Arc::new(Wire::default())); + for (p, w) in wire.iter().enumerate() { + c.set_serial_backend(p, w.clone()); + } + let (pin, line) = LevelProbe::line(); + c.state().connect_irq(line); + c.attached(); + Rig { c, mem, wire, pin } + } + + /// A 32-bit register as the processor sees it through the self-ordered + /// slot: the BAR value is the address-order bytes read little-endian. + fn r32(&self, a: u32) -> u32 { + self.c.bar_read(0, a, 4).swap_bytes() + } + fn w32(&self, a: u32, v: u32) { + self.c.bar_write(0, a, v.swap_bytes(), 4) + } + fn r8(&self, a: u32) -> u8 { + self.c.bar_read(0, a, 1) as u8 + } + fn w8(&self, a: u32, v: u8) { + self.c.bar_write(0, a, u32::from(v), 1) + } + + /// UART register `r` of port `p`. + fn ur(&self, p: usize, r: u32) -> u8 { + self.r8(reg::UART + reg::UART_STRIDE * p as u32 + r) + } + fn uw(&self, p: usize, r: u32, v: u8) { + self.w8(reg::UART + reg::UART_STRIDE * p as u32 + r, v) + } + + /// A port's DMA register. + fn sr(&self, p: usize, off: u32) -> u32 { + self.r32(reg::SDMA_PORT + reg::SDMA_PORT_STRIDE * p as u32 + off) + } + fn sw(&self, p: usize, off: u32, v: u32) { + self.w32(reg::SDMA_PORT + reg::SDMA_PORT_STRIDE * p as u32 + off, v) + } + + /// Point a pair's rings at `base` with `size` byte rings. + fn pair_base(&self, pair: u32, base: u64, size: u32) { + let lo = reg::SDMA_BASE + 8 * pair; + self.w32(lo + 4, (base >> 32) as u32); + self.w32(lo, base as u32 | u32::from(size == 0x1000)); + } + + /// Where port `p`'s ring for `dir` (0 transmit, 1 receive) is, with its + /// pair based at `RINGS`. + fn ring(&self, p: usize, dir: u64, size: u64) -> usize { + (RINGS + ((p as u64 & 1) * 2 + dir) * size) as usize + } + + fn put(&self, at: usize, bytes: &[u8]) { + self.mem.0.lock().unwrap()[at..at + bytes.len()].copy_from_slice(bytes); + } + + fn get(&self, at: usize, n: usize) -> Vec { + self.mem.0.lock().unwrap()[at..at + n].to_vec() + } + + /// One transmit entry in port `p`'s ring, `at` bytes in. + fn tx_entry(&self, p: usize, size: u64, at: usize, data: [u8; 4], control: [u8; 4]) { + let ring = self.ring(p, 0, size) + at; + self.put(ring, &data); + self.put(ring + 4, &control); + } + + /// Wait out `ms` of host time, then play the timers. + fn settle(&self, ms: u64) { + std::thread::sleep(Duration::from_millis(ms)); + self.c.service(); + } + + /// Play the timers until `done`, a millisecond of host time apart. + fn until(&self, what: &str, done: impl Fn(&Rig) -> bool) { + for _ in 0..3000 { + self.c.service(); + if done(self) { + return; + } + std::thread::sleep(Duration::from_millis(1)); + } + panic!("timed out waiting for {what}"); + } + + /// Divisor one: a character crosses the line in about five microseconds + /// instead of a millisecond, so a long loopback run stays short. + fn fast_line(&self, p: usize) { + self.uw(p, 3, 0x80); + self.uw(p, 0, 1); + self.uw(p, 1, 0); + self.uw(p, 3, 0x03); + } + + fn sent(&self, p: usize) -> Vec { + self.wire[p].out.lock().unwrap().clone() + } + + fn type_in(&self, p: usize, bytes: &[u8]) { + self.wire[p].inp.lock().unwrap().extend(bytes.iter().copied()); + self.c.service(); + } +} + +// --------------------------------------------------------------------------- +// PCI function +// --------------------------------------------------------------------------- + +/// Ports test_identity_and_reset: vendor, device, revision 0x53, the +/// emulator's class and interrupt pin, and a 1 MiB BAR. +#[test] +fn identity_and_bar() { + let r = Rig::new(); + let cfg = r.c.state().config(); + assert_eq!(cfg.read(0), 0x100a_10a9); + assert_eq!(cfg.read(2), 0x00ff_0053); + assert_eq!(cfg.interrupt_pin(), PCI_INTA); + cfg.write(4, 0xffff_ffff); + assert_eq!(cfg.read(4), 0xfff0_0000, "one megabyte of memory space"); +} + +/// The IP53 status policy: 0x0220 survives every write and a reset, and an +/// instance without the policy keeps its own value. +#[test] +fn status_follows_the_board() { + let r = Rig::with(Ioc4Config::tezro()); + let cfg = r.c.state().config(); + assert_eq!(cfg.read(1) >> 16, 0x0220); + cfg.write(1, 0x0000_0006); + assert_eq!(cfg.read(1), 0x0220_0006); + cfg.write(1, 0xffff_0000); + assert_eq!(cfg.read(1) >> 16, 0x0220, "a status write of ones leaves the read-only fields"); + r.c.reset(); + assert_eq!(cfg.read(1), 0x0220_0000); + + let bare = Rig::new(); + assert_eq!(bare.c.state().config().read(1) >> 16, 0); +} + +/// Ports the reset half of test_identity_and_reset: the driver's +/// disable-and-acknowledge is not a reset, but a reset clears what it left. +#[test] +fn reset_clears_the_interrupt_state_and_the_ports() { + let r = Rig::new(); + r.w32(reg::SERIAL_ENABLE_SET, int::pass(0)); + r.w32(reg::OTHER_ENABLE_SET, 0x40); + r.uw(0, 3, 0x80); + r.uw(0, 0, 0x34); + r.uw(0, 3, 0x03); + r.uw(0, 1, 0x02); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0)); + assert!(r.pin.level()); + + r.c.reset(); + assert_eq!(r.r32(reg::SERIAL_ENABLE_SET), 0); + assert_eq!(r.r32(reg::OTHER_ENABLE_SET), 0); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), 0); + assert!(!r.pin.level()); + assert_eq!(r.ur(0, 3), 0, "line control"); + assert_eq!(r.ur(0, 1), 0, "interrupt enable"); + assert_eq!(r.ur(0, 2) & 0xc0, 0, "FIFOs off"); + // The driver's own initialisation: raw pending, separate from enabled. + r.uw(0, 1, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0)); + assert!(!r.pin.level(), "pending but not enabled"); +} + +/// The pin follows the enabled sources of either word. Ports the IRQ half +/// of test_identity_and_reset. +#[test] +fn the_pin_follows_enabled_sources() { + let r = Rig::new(); + r.uw(0, 1, 0x02); + assert!(!r.pin.level()); + r.w32(reg::SERIAL_ENABLE_SET, int::pass(0)); + assert!(r.pin.level()); + r.w32(reg::SERIAL_ENABLE_CLR, int::pass(0)); + assert!(!r.pin.level()); + r.w32(reg::OTHER_ENABLE_SET, int::dma_error(1)); + assert!(!r.pin.level(), "nothing pending in the other word"); +} + +// --------------------------------------------------------------------------- +// The four 16550s and the local interrupt controller +// --------------------------------------------------------------------------- + +/// Ports test_uart_and_clock: the misc-control bit names the board's clock, +/// writes to it are ignored, and each port's input is the clock over 16. +#[test] +fn clock_select_reads_back_and_sets_the_baud_base() { + for (clock, bit, hz) in [(Ioc4Clock::Mhz33, 0, 33_333_333u32), (Ioc4Clock::Mhz66, 1, 66_666_667)] { + let r = Rig::with(Ioc4Config::bare(clock)); + assert_eq!(r.r32(reg::MISC_CONTROL) & 1, bit); + r.w32(reg::MISC_CONTROL, u32::from(bit == 0)); + assert_eq!(r.r32(reg::MISC_CONTROL) & 1, bit, "the clock is the board's"); + for p in 0..PORTS { + assert_eq!(r.c.sh.st.lock().uart[p].baudbase(), hz / 16); + } + } +} + +/// Ports test_uart_and_clock's per-port register loop. +#[test] +fn every_port_has_its_own_registers() { + let r = Rig::new(); + for n in 0..PORTS { + let n8 = n as u8; + assert_eq!(r.ur(n, 5) & 0x60, 0x60, "transmitter empty after reset"); + r.uw(n, 3, 0x80); + r.uw(n, 0, 0x31 + n8); + r.uw(n, 1, 0x12 + n8); + assert_eq!(r.ur(n, 0), 0x31 + n8, "divisor low"); + assert_eq!(r.ur(n, 1), 0x12 + n8, "divisor high"); + r.uw(n, 3, 3); + r.uw(n, 2, 1); + assert_eq!(r.ur(n, 2) & 0xc0, 0xc0, "FIFOs enabled"); + r.uw(n, 2, 7); + } + for n in 0..PORTS { + assert_eq!(r.ur(n, 3), 3, "no port inherited another's line control"); + } +} + +/// Ports test_interrupts: the enable-set and enable-clear aliases, raw +/// pending against enabled, and one port's source isolated from the others'. +#[test] +fn serial_interrupts_mask_pending_and_acknowledge() { + let r = Rig::new(); + r.w32(reg::SERIAL_ENABLE_CLR, u32::MAX); + assert_eq!(r.r32(reg::SERIAL_ENABLE_SET), 0); + r.w32(reg::OTHER_ENABLE_CLR, u32::MAX); + assert_eq!(r.r32(reg::OTHER_ENABLE_SET), 0); + r.w32(reg::SERIAL_PENDING, u32::MAX); + r.w32(reg::OTHER_PENDING, u32::MAX); + for n in 0..PORTS { + r.uw(n, 1, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(n), int::pass(n)); + assert!(!r.pin.level(), "pending but masked"); + r.w32(reg::SERIAL_ENABLE_SET, int::pass(n)); + assert_eq!(r.r32(reg::SERIAL_ENABLE_SET), int::pass(n)); + assert!(r.pin.level()); + r.w32(reg::SERIAL_PENDING, int::pass(n)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(n), int::pass(n), "a level ignores the acknowledge"); + assert!(r.pin.level()); + assert_eq!(r.ur(n, 2) & 0xf, 2, "transmit-empty cause"); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(n), 0, "the identification read quieted it"); + assert!(!r.pin.level()); + r.w32(reg::SERIAL_ENABLE_CLR, int::pass(n)); + assert_eq!(r.r32(reg::SERIAL_ENABLE_SET), 0); + r.uw(n, 1, 0); + } + for n in 0..PORTS { + r.uw(n, 1, 2); + r.w32(reg::SERIAL_ENABLE_SET, int::pass(n)); + } + let all = (0..PORTS).fold(0, |m, n| m | int::pass(n)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & all, all); + r.ur(0, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & all, all & !int::pass(0), "only port 0 quieted"); + assert!(r.pin.level()); + assert_eq!(r.r32(reg::OTHER_PENDING), 0, "no ATA or error source"); + r.w32(reg::OTHER_ENABLE_SET, 0x40); + assert_eq!(r.r32(reg::OTHER_ENABLE_SET), 0x40); + r.w32(reg::OTHER_ENABLE_CLR, 0x40); + assert_eq!(r.r32(reg::OTHER_ENABLE_SET), 0); +} + +/// Ports test_uart_pass_through_is_a_level: a write cannot clear a UART's +/// source, only the UART can. +#[test] +fn uart_pass_through_follows_the_uart() { + let r = Rig::new(); + r.w32(reg::SERIAL_PENDING, u32::MAX); + r.uw(0, 1, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0)); + r.w32(reg::SERIAL_PENDING, int::pass(0)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0)); + r.uw(1, 1, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(1), int::pass(1)); + assert_eq!(r.ur(1, 2) & 0xf, 2); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(1), 0, "quieted by the UART alone, it falls"); +} + +/// Ports test_pio_fifo: a loopback burst fills the sixteen-byte FIFO and +/// comes back in order, and a receive FIFO reset drops what is held. +#[test] +fn programmed_io_goes_through_the_fifo() { + let r = Rig::new(); + for n in 0..PORTS { + r.uw(n, 3, 3); + r.uw(n, 2, 1); + r.uw(n, 4, 0x10); + for i in 0..16 { + r.uw(n, 0, 0x40 + i); + } + assert_eq!(r.ur(n, 5) & 1, 1); + for i in 0..16 { + assert_eq!(r.ur(n, 0), 0x40 + i); + } + assert_eq!(r.ur(n, 5) & 1, 0); + r.uw(n, 0, 0xaa); + assert_eq!(r.ur(n, 5) & 1, 1); + r.uw(n, 2, 3); + assert_eq!(r.ur(n, 5) & 1, 0, "a receive FIFO reset drops it"); + } +} + +/// Ports test_uart_rejects_aligned_wide_write: a wider access to a UART +/// window is refused whole and reaches no register. +#[test] +fn uart_windows_take_bytes_only() { + let r = Rig::new(); + r.uw(0, 4, 0x10); + assert_eq!(r.ur(0, 4), 0x10); + r.c.bar_write(0, reg::UART, 0x5a5a_5a5a, 4); + assert_eq!(r.ur(0, 5) & 1, 0, "no character arrived"); + r.c.bar_write(0, reg::UART, 0x5a5a, 2); + assert_eq!(r.ur(0, 5) & 1, 0); + assert_eq!(r.c.bar_read(0, reg::UART, 4), 0); + r.uw(0, 0, 0x5a); + assert_eq!(r.ur(0, 5) & 1, 1); + assert_eq!(r.ur(0, 0), 0x5a); +} + +/// Ports test_ata_readback_rejects_width: the control registers take 32-bit +/// accesses and nothing narrower, and the words around the latch are not it. +#[test] +fn control_registers_take_words_only() { + let r = Rig::with(Ioc4Config::tezro()); + r.w32(reg::ATA_READBACK, 0x1234_5678); + assert_eq!(r.c.bar_read(0, reg::ATA_READBACK, 1), 0); + assert_eq!(r.c.bar_read(0, reg::ATA_READBACK, 2), 0); + r.c.bar_write(0, reg::ATA_READBACK, 0xaa, 1); + r.c.bar_write(0, reg::ATA_READBACK, 0xbbcc, 2); + assert_eq!(r.r32(reg::ATA_READBACK), 0x1234_5678); + assert_eq!(r.r32(reg::ATA_READBACK - 4), 0); + assert_eq!(r.r32(reg::ATA_READBACK + 4), 0); + r.w32(reg::ATA_READBACK - 4, 0xaabb_ccdd); + r.w32(reg::ATA_READBACK + 4, 0x5566_7788); + assert_eq!(r.r32(reg::ATA_READBACK), 0x1234_5678); +} + +/// Ports test_ata_readback_latch and test_ata_readback_default_unsupported: +/// every one-hot value and all ones read back, a reset clears the word, and +/// a chip whose board did not ask for it reads zero. +#[test] +fn ata_readback_is_a_latch_where_the_board_asks_for_it() { + let r = Rig::with(Ioc4Config::tezro()); + let before = r.get(RINGS as usize, 8); + for bit in 0..32 { + r.w32(reg::ATA_READBACK, 1 << bit); + assert_eq!(r.r32(reg::ATA_READBACK), 1 << bit); + } + r.w32(reg::ATA_READBACK, u32::MAX); + assert_eq!(r.r32(reg::ATA_READBACK), u32::MAX); + assert_eq!(r.get(RINGS as usize, 8), before, "no DMA behind it"); + assert!(!r.pin.level()); + r.c.reset(); + assert_eq!(r.r32(reg::ATA_READBACK), 0); + + let bare = Rig::new(); + bare.w32(reg::ATA_READBACK, u32::MAX); + assert_eq!(bare.r32(reg::ATA_READBACK), 0); +} + +/// Ports test_serial_idle_status_default_pio: with every transfer finishing +/// inside the access that starts it, the DMA status reads idle, read-only. +#[test] +fn serial_control_reads_idle() { + let r = Rig::new(); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + r.w32(reg::SERIAL_CONTROL, 0); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); +} + +// --------------------------------------------------------------------------- +// Serial ring DMA +// --------------------------------------------------------------------------- + +/// Ports test_dma_tx_control_entries: a modem-control slot writes the 16550's +/// register, a delay slot holds the walk for its milliseconds, and an +/// explicit-interrupt flag raises the port's completion bit. +#[test] +fn tx_control_entries_drive_the_uart() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, [0x1f, 3, 0, 0], [TX_MCR | TX_EXPLICIT, TX_DELAY | TX_EXPLICIT, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, SHADOW_OUTPUTS, "the modem control reached the UART"); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 7), int::serial(0, 7), "the first slot's completion"); + r.w32(reg::SERIAL_PENDING, int::serial(0, 7)); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0, "the delay holds the entry"); + assert_eq!(r.sr(0, sdma_off::CONTROL) & (1 << 16), 1 << 16, "and reads busy"); + r.settle(10); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 7), int::serial(0, 7), "the delay's completion"); +} + +/// Ports test_dma_tx_wrap_1k: the consumer wraps at the 1 KiB ring's last +/// entry, and the empty source comes back with it. +#[test] +fn tx_ring_wraps_at_one_kilobyte() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::TX_PRODUCE, 0x3f8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0x3f8, "empty entries are skipped"); + r.tx_entry(0, 0x400, 0x3f8, [0x5e, 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 0); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0, "wrapped"); + assert_eq!(r.sent(0), [0x5e]); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 0), int::serial(0, 0), "transmit ring empty"); +} + +/// Ports test_dma_tx_empty_follows_the_ring: transmit-ring-empty is a level +/// that is clear while an entry is in the engine and deaf to a write. +#[test] +fn tx_empty_follows_the_ring() { + let r = Rig::new(); + let empty = int::serial(0, 0); + r.pair_base(0, RINGS, 0x1000); + assert_eq!(r.r32(reg::SERIAL_PENDING) & empty, 0, "off with ring DMA off"); + r.sw(0, sdma_off::CONTROL, ENABLE); + assert_eq!(r.r32(reg::SERIAL_PENDING) & empty, empty); + r.w32(reg::SERIAL_PENDING, empty); + assert_eq!(r.r32(reg::SERIAL_PENDING) & empty, empty); + r.tx_entry(0, 0x1000, 0, [3, 0, 0, 0], [TX_DELAY, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & empty, 0); + r.settle(10); + assert_eq!(r.r32(reg::SERIAL_PENDING) & empty, empty); +} + +/// Ports test_dma_rx_drain_and_threshold: characters wait in the packer +/// until a drain posts them, with the line's modem state, and the high-water +/// source follows the entries waiting. +#[test] +fn rx_drain_posts_the_packer_and_raises_the_threshold() { + let r = Rig::new(); + let high = int::serial(0, 2); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + // A long reload, so the drain and not the timer posts the entry. + r.sw(0, sdma_off::RX_TIMER, 0xfff); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, b"abc"); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0xff8, 0); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD | DRAIN); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0xff8, 8); + let entry = r.get(r.ring(0, 1, 0x1000), 8); + assert_eq!(entry, [b'a', b'b', b'c', 0, RX_FROM_LINE, RX_FROM_LINE, RX_FROM_LINE, 0]); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, high); + r.w32(reg::SERIAL_PENDING, high); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, high, "a level"); + r.sw(0, sdma_off::RX_CONSUME, 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, 0, "the consumer made room"); +} + +/// Ports test_dma_rx_timer_expiry: the timer posts a lone character when +/// its reload runs out and raises the expiry and threshold sources. +#[test] +fn rx_timer_posts_a_lone_character() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + r.sw(0, sdma_off::RX_TIMER, 2); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x6d]); + r.settle(10); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0xff8, 8); + let entry = r.get(r.ring(0, 1, 0x1000), 8); + assert_eq!((entry[0], entry[4]), (0x6d, RX_FROM_LINE)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 3), int::serial(0, 3), "timer expired"); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 2), int::serial(0, 2), "threshold"); +} + +/// Ports test_dma_threshold_is_a_level. +#[test] +fn threshold_is_a_level() { + let r = Rig::new(); + let high = int::serial(0, 2); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, THRESHOLD); + r.sw(0, sdma_off::RX_PRODUCE, 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, high); + r.w32(reg::SERIAL_PENDING, high); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, high); + r.sw(0, sdma_off::RX_CONSUME, 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & high, 0); +} + +/// Ports test_dma_channel_reset_keeps_settings: a channel reset zeroes the +/// four indices and keeps the timer reload, the UART's registers and what +/// the write that reset it stored. +#[test] +fn channel_reset_keeps_settings() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::RX_TIMER, 0x140); + r.uw(0, 4, 0x03); + r.sw(0, sdma_off::TX_PRODUCE, 0x40); + r.sw(0, sdma_off::CONTROL, RESET | 5); + assert_eq!(r.sr(0, sdma_off::TX_PRODUCE), 0); + assert_eq!(r.sr(0, sdma_off::RX_TIMER) & 0xfff, 0x140); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x0300_0000); + assert_eq!(r.sr(0, sdma_off::CONTROL), 5); +} + +/// Ports test_dma_full_reset_clears_shadow: the shadow mirrors the UART and +/// ignores a write, and a chip reset puts it back to the UART's reset +/// modem control. +#[test] +fn shadow_mirrors_the_uart() { + let r = Rig::new(); + r.uw(0, 4, 0x13); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1300_0000); + r.sw(0, sdma_off::SHADOW, 0x1f00_0000); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1300_0000); + r.c.reset(); + let mcr = u32::from(r.ur(0, 4)); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, mcr << 24); +} + +/// Ports test_dma_loopback_tx_mcr in miniature: with the UART looped back +/// the engine's transmitted characters return through the receive ring with +/// the looped modem state (DTR and RTS set, so RTS comes back as CTS), and +/// none reach the line. +#[test] +fn loopback_returns_through_the_rings() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.fast_line(0); + r.uw(0, 4, 0x13); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + r.tx_entry(0, 0x400, 0, *b"wxyz", [TX_VALID; 4]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + r.until("the receive entry", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 8); + assert_eq!(r.get(r.ring(0, 1, 0x400), 8), [b'w', b'x', b'y', b'z', RX_LOOPBACK, RX_LOOPBACK, RX_LOOPBACK, RX_LOOPBACK]); + assert!(r.sent(0).is_empty(), "nothing reached the line"); +} + +/// A looped-back character takes a character time to reach the receiver, and +/// the walk waits for it: at 9600 baud the second character of an entry is +/// not sent for a millisecond. Ports the "still on the line" case of +/// test_dma_loopback_tx_mcr. +#[test] +fn a_looped_back_character_takes_a_character_time() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.uw(0, 4, 0x13); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + r.tx_entry(0, 0x400, 0, *b"abcd", [TX_VALID; 4]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert!(r.c.sh.st.lock().sdma.loop_deadline(0).is_some(), "the first character is on the line"); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0, "the entry is not done"); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8, 0, "and nothing has been posted"); + r.until("the entry", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 8); +} + +/// Ports test_dma_loopback_pio_mcr's loopback half: a full 127-entry transmit +/// ring crosses the line and fills the receive ring, entry for entry. +#[test] +fn loopback_fills_the_receive_ring() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.fast_line(0); + r.uw(0, 4, 0x13); + assert_eq!(r.ur(0, 4), 0x13); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1300_0000); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + let expected = fill_loopback_ring(&r); + r.until("the transmit ring", |r| r.sr(0, sdma_off::TX_CONSUME) & 0x3f8 == 0x3f8); + r.until("the receive ring", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0x3f8); + assert_eq!((r.sr(0, sdma_off::TX_PRODUCE), r.sr(0, sdma_off::TX_CONSUME)), (0x3f8, 0x3f8)); + assert_eq!(r.sr(0, sdma_off::RX_CONSUME), 0); + assert_eq!(r.get(r.ring(0, 1, 0x400), 0x3f8), expected); + assert!(r.sent(0).is_empty()); +} + +/// Ports test_dma_loopback_tx_mcr: a modem-control slot ahead of the +/// characters writes the UART's own register, and its characters then loop +/// back, entry by entry, the first holding three. +#[test] +fn a_modem_control_slot_leads_a_looped_back_run() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.fast_line(0); + r.uw(0, 4, 0); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + let mut expected = vec![0u8; 0x3f8]; + let mut count = 0usize; + for i in 0..127usize { + let (data, control): ([u8; 4], [u8; 4]) = if i == 0 { + ([0x13, 0, 1, 2], [TX_MCR, TX_VALID, TX_VALID, TX_VALID]) + } else { + (std::array::from_fn(|j| (i * 4 + j - 1) as u8), [TX_VALID; 4]) + }; + for &byte in &data[usize::from(i == 0)..] { + expected[count / 4 * 8 + count % 4] = byte; + expected[count / 4 * 8 + 4 + count % 4] = RX_LOOPBACK; + count += 1; + } + r.tx_entry(0, 0x400, i * 8, data, control); + } + assert_eq!(count, 507); + r.sw(0, sdma_off::TX_PRODUCE, 0x3f8); + r.until("the transmit ring", |r| r.sr(0, sdma_off::TX_CONSUME) & 0x3f8 == 0x3f8); + r.until("the last full entry", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0x3f0); + assert_eq!(r.ur(0, 4), 0x13, "the modem-control slot wrote the UART's register"); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1300_0000); + // Three characters wait in the packer for a drain. + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD | DRAIN); + r.until("the drain", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0x3f8); + assert_eq!(r.get(r.ring(0, 1, 0x400), 0x3f8), expected); + assert!(r.sent(0).is_empty()); +} + +/// Ports test_dma_loopback_shadow_reset: a write to the shadow changes +/// nothing, and a channel reset leaves the outputs as they were. +#[test] +fn the_shadow_ignores_writes_and_a_channel_reset() { + let r = Rig::new(); + r.uw(0, 4, 0); + r.sw(0, sdma_off::SHADOW, SHADOW_OUTPUTS); + assert_eq!(r.ur(0, 4), 0); + r.uw(0, 4, 0x13); + r.sw(0, sdma_off::SHADOW, 0x1300_0000); + assert_eq!(r.ur(0, 4), 0x13); + r.sw(0, sdma_off::CONTROL, RESET); + assert_eq!(r.sr(0, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1300_0000); + assert_eq!(r.ur(0, 4), 0x13); +} + +/// A full 127-entry transmit ring of valid characters; the receive ring it +/// should fill. +fn fill_loopback_ring(r: &Rig) -> Vec { + let mut expected = vec![0u8; 0x3f8]; + for i in 0..127usize { + let data: [u8; 4] = std::array::from_fn(|j| (i * 4 + j) as u8); + expected[i * 8..i * 8 + 4].copy_from_slice(&data); + expected[i * 8 + 4..i * 8 + 8].fill(RX_LOOPBACK); + r.tx_entry(0, 0x400, i * 8, data, [TX_VALID; 4]); + } + r.sw(0, sdma_off::TX_PRODUCE, 0x3f8); + expected +} + +/// Ports test_dma_requester_ids' use of both pairs: ports 2 and 3 take their +/// rings from the second pair's base, apart from the first pair's. +#[test] +fn second_pair_has_its_own_base() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.pair_base(1, RINGS + 0x2000, 0x400); + assert_eq!(r.r32(reg::SDMA_BASE), RINGS as u32); + assert_eq!(r.r32(reg::SDMA_BASE + 8), (RINGS + 0x2000) as u32); + for p in [0usize, 2, 3] { + r.sw(p, sdma_off::CONTROL, ENABLE); + } + let at = |pair: u64, p: u64| (RINGS + pair * 0x2000 + (p & 1) * 2 * 0x400) as usize; + r.put(at(1, 2), &[b'c', 0, 0, 0, TX_VALID, 0, 0, 0]); + r.put(at(1, 3), &[b'd', 0, 0, 0, TX_VALID, 0, 0, 0]); + r.put(at(0, 0), &[b'a', 0, 0, 0, TX_VALID, 0, 0, 0]); + r.sw(3, sdma_off::TX_PRODUCE, 8); + r.sw(2, sdma_off::TX_PRODUCE, 8); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!((r.sent(0), r.sent(1), r.sent(2), r.sent(3)), (b"a".to_vec(), vec![], b"c".to_vec(), b"d".to_vec())); +} + +/// Ports test_dma_error_capture_and_reset: the first failed transfer +/// captures its address, a second sets the multiple bit, the port's source +/// is in the other word, and a channel reset recovers the port but leaves +/// the capture and the source for software to acknowledge. +#[test] +fn a_failed_ring_transfer_is_captured_and_halts_the_port() { + let r = Rig::new(); + let bad = BAD_BASE + (1 << 32); + let low = BAD_BASE as u32 | ERROR_VALID; + r.pair_base(0, bad, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0, "the fetch failed"); + assert_eq!(r.r32(reg::ERROR_LOW), low); + assert_eq!(r.r32(reg::ERROR_HIGH), 1); + assert_eq!(r.r32(reg::OTHER_PENDING) & int::dma_error(0), int::dma_error(0)); + + r.sw(0, sdma_off::CONTROL, RESET); + assert_eq!(r.r32(reg::ERROR_LOW), low, "a channel reset leaves the capture"); + assert_eq!(r.r32(reg::OTHER_PENDING) & int::dma_error(0), int::dma_error(0)); + r.w32(reg::OTHER_PENDING, int::dma_error(0)); + assert_eq!(r.r32(reg::OTHER_PENDING) & int::dma_error(0), 0); + r.w32(reg::ERROR_LOW, 0); + assert_eq!(r.r32(reg::ERROR_LOW), low, "the capture is read-only"); + + r.sw(0, sdma_off::CONTROL, ENABLE); + r.type_in(0, &[0x73]); + r.sw(0, sdma_off::CONTROL, ENABLE | DRAIN); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0xff8, 0, "the post failed"); + assert_eq!(r.r32(reg::ERROR_LOW), low | ERROR_MULTIPLE); + assert_eq!(r.r32(reg::OTHER_PENDING) & int::dma_error(0), int::dma_error(0)); + r.c.reset(); + assert_eq!((r.r32(reg::ERROR_LOW), r.r32(reg::ERROR_HIGH), r.r32(reg::OTHER_PENDING)), (0, 0, 0)); +} + +/// Ports test_dma_requester_ids: a transmit fetch names twice the port and a +/// receive post one more, on every port. +#[test] +fn the_error_capture_names_the_requester() { + for p in 0..PORTS { + let r = Rig::new(); + r.pair_base(p as u32 / 2, BAD_BASE, 0x400); + r.sw(p, sdma_off::CONTROL, ENABLE); + r.sw(p, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.r32(reg::OTHER_PENDING), int::dma_error(p), "only this port's bit"); + assert_eq!(r.r32(reg::ERROR_LOW) >> 1 & 0xf, 2 * p as u32); + assert_eq!(r.r32(reg::ERROR_LOW) & ERROR_VALID, ERROR_VALID); + + r.c.reset(); + r.pair_base(p as u32 / 2, BAD_BASE, 0x400); + r.sw(p, sdma_off::CONTROL, ENABLE); + r.type_in(p, &[1, 2, 3, 4]); + assert_eq!(r.r32(reg::ERROR_LOW) >> 1 & 0xf, 2 * p as u32 + 1); + assert_eq!(r.sr(p, sdma_off::RX_PRODUCE), 0); + } +} + +/// Ports test_dma_port_fault_isolation_and_recovery: a fault on one port +/// leaves its pair's other port running, and the faulted port runs again +/// once reset. +#[test] +fn a_fault_halts_only_its_own_port() { + let r = Rig::new(); + r.pair_base(0, BAD_BASE, 0x400); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(1, sdma_off::CONTROL, ENABLE); + r.sw(1, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.r32(reg::ERROR_LOW) >> 1 & 0xf, 2); + assert_eq!(r.r32(reg::OTHER_PENDING), int::dma_error(1)); + + // The base is shared, so quiet both ports before changing it. + r.sw(0, sdma_off::CONTROL, 0); + r.sw(1, sdma_off::CONTROL, RESET); + assert_eq!((r.sr(1, sdma_off::TX_PRODUCE), r.sr(1, sdma_off::TX_CONSUME)), (0, 0)); + r.pair_base(0, RINGS, 0x400); + assert_eq!(r.r32(reg::SDMA_BASE), RINGS as u32); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x400, 0, [b'p', 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sent(0), b"p"); + assert_eq!(r.sr(1, sdma_off::TX_CONSUME), 0); + + r.sw(1, sdma_off::CONTROL, ENABLE); + r.tx_entry(1, 0x400, 0, [0x15, 0, 0, 0], [TX_MCR, 0, 0, 0]); + r.sw(1, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(1, sdma_off::TX_CONSUME), 8, "the reset port runs"); + assert_eq!(r.sr(1, sdma_off::SHADOW) & SHADOW_OUTPUTS, 0x1500_0000); +} + +// --------------------------------------------------------------------------- +// The clock on byte-bus select 0 +// --------------------------------------------------------------------------- + +const CLOCK_CONTROL: u32 = reg::CLOCK + ds1744::CONTROL as u32; + +/// A chip with a clock on a stepped nanosecond counter, seeded on +/// 2024-01-01. +fn with_clock() -> (Rig, Arc) { + let r = Rig::new(); + let ns = Arc::new(AtomicI64::new(0)); + let n = ns.clone(); + let seed = 19_723 * 86_400; // 2024-01-01 + let clock = Ds1744::with_clock(seed, YearBase::Gregorian, Arc::new(move || n.load(AtomicOrdering::SeqCst))); + r.c.attach_clock(Arc::new(clock.unwrap())); + (r, ns) +} + +/// Set the calendar through the write bit: seconds, minutes, hours, date, +/// month and year, then the century that commits it. +fn set_time(r: &Rig, [sec, min, hour, date, month, year]: [u8; 6], century: u8) { + r.w8(CLOCK_CONTROL, 0x80); + for (i, v) in [sec, min, hour, 0x02, date, month, year].into_iter().enumerate() { + r.w8(CLOCK_CONTROL + 1 + i as u32, v); + } + r.w8(CLOCK_CONTROL, century); +} + +/// Ports test_rtc_absent: with no clock fitted the window reads zero and +/// drops a write. +#[test] +fn an_unfitted_clock_reads_zero() { + let r = Rig::new(); + assert_eq!(r.r8(CLOCK_CONTROL), 0); + r.w8(CLOCK_CONTROL, 0x55); + assert_eq!(r.r8(CLOCK_CONTROL), 0); + for a in [reg::CLOCK + 0x7ff7, reg::CLOCK + 0x8000, reg::CLOCK] { + assert_eq!(r.r8(a), 0); + } +} + +/// Ports test_rtc_alias and test_nvram_window: the SRAM fills the BAR from +/// 0x80000 to the calendar at 0x87ff8, the frozen calendar reads through it, +/// and neither side of the window answers. +#[test] +fn the_clock_window_holds_ram_and_the_calendar() { + let (r, ns) = with_clock(); + let frozen = [0x60u8, 0x59, 0x59, 0x23, 0x82, 0x31, 0x12, 0x24]; + r.w8(reg::CLOCK, 0xa5); + r.w8(reg::CLOCK + 0x7ff7, 0x5a); + for (i, a) in [0x0000e, 0x004bc, 0x07fcb, 0x07fd0].into_iter().enumerate() { + r.w8(reg::CLOCK + a, 0x31 + i as u8); + } + assert_eq!((r.r8(reg::CLOCK), r.r8(reg::CLOCK + 0x7ff7)), (0xa5, 0x5a)); + for (i, a) in [0x0000e, 0x004bc, 0x07fcb, 0x07fd0].into_iter().enumerate() { + assert_eq!(r.r8(reg::CLOCK + a), 0x31 + i as u8); + } + + set_time(&r, [0x59, 0x59, 0x23, 0x31, 0x12, 0x24], 0x20); + r.w8(CLOCK_CONTROL, 0x40); + ns.fetch_add(2_000_000_000, AtomicOrdering::SeqCst); + for (i, want) in frozen.into_iter().enumerate() { + assert_eq!(r.r8(CLOCK_CONTROL + i as u32), want, "calendar byte {i}"); + } + assert!(!r.pin.level()); + r.w8(CLOCK_CONTROL, 0); + assert_eq!((r.r8(CLOCK_CONTROL + 1), r.r8(CLOCK_CONTROL + 5), r.r8(CLOCK_CONTROL + 6), r.r8(CLOCK_CONTROL + 7)), (1, 1, 1, 0x25)); + + // Past the part on either side nothing answers. + assert_eq!(r.r8(reg::CLOCK + 0x8000), 0); + assert_eq!(r.r8(reg::CLOCK + 0x8008), 0); + assert_eq!(r.r8(reg::CLOCK - 1), 0); + + // A chip reset leaves the SRAM and the calendar alone. + r.w8(CLOCK_CONTROL, 0x40); + r.c.reset(); + assert_eq!((r.r8(reg::CLOCK), r.r8(reg::CLOCK + 0x7ff7)), (0xa5, 0x5a)); + assert_eq!(r.r8(CLOCK_CONTROL), 0x60, "even a read in progress"); +} + +/// Ports test_rtc_rejects_wide_access: a halfword or word to the clock is +/// refused whole and reaches no register. +#[test] +fn the_clock_takes_bytes_only() { + let (r, _) = with_clock(); + set_time(&r, [0x12, 0x34, 0x05, 0x15, 0x01, 0x24], 0x20); + r.w8(CLOCK_CONTROL, 0x80); + r.w8(CLOCK_CONTROL + 1, 0x11); + r.w8(CLOCK_CONTROL + 2, 0x22); + r.c.bar_write(0, CLOCK_CONTROL + 2, 0x4433, 2); + r.c.bar_write(0, CLOCK_CONTROL, 0x2000_0000, 4); + assert_eq!(r.c.bar_read(0, CLOCK_CONTROL + 2, 2), 0); + assert_eq!(r.c.bar_read(0, CLOCK_CONTROL, 4), 0); + assert_eq!((r.r8(CLOCK_CONTROL + 2), r.r8(CLOCK_CONTROL + 3)), (0x22, 0x05)); + r.w8(CLOCK_CONTROL, 0x20); + assert_eq!((r.r8(CLOCK_CONTROL + 2), r.r8(CLOCK_CONTROL + 3)), (0x22, 0x05)); +} + +// --------------------------------------------------------------------------- +// A receive ring with no room +// --------------------------------------------------------------------------- + +const RX_FULL: u32 = int::serial(0, 1); + +/// Ports test_dma_rx_full_and_staged_recovery: a character the full ring +/// cannot take waits, receive-ring-full is a level that stays while an +/// entry waits for room and ignores an acknowledge, and the entries post as +/// the consumer makes room. Filled exactly with nothing waiting, it stays +/// down. +#[test] +fn receive_ring_full_is_a_level_while_an_entry_waits() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.sw(0, sdma_off::CONTROL, ENABLE); + for i in 0..127u32 { + r.type_in(0, &[0x31, 0x32, 0x33, 0x34]); + r.until("the entry", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == ((i + 1) * 8) & 0x3f8); + } + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0x3f8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & RX_FULL, 0, "full, but nothing waits for room"); + + r.sw(0, sdma_off::RX_TIMER, 1); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0xa1, 0xa2, 0xa3, 0xa4, 0xb5]); + r.until("the source", |r| r.r32(reg::SERIAL_PENDING) & RX_FULL != 0); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0x3f8); + r.w32(reg::SERIAL_PENDING, RX_FULL); + assert_eq!(r.r32(reg::SERIAL_PENDING) & RX_FULL, RX_FULL, "a level ignores the acknowledge"); + + r.sw(0, sdma_off::RX_CONSUME, 16 | ARM); + r.until("room", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0); + let entry = r.get(r.ring(0, 1, 0x400) + 0x3f8, 8); + assert_eq!((&entry[..4], entry[4], entry[7]), (&[0xa1, 0xa2, 0xa3, 0xa4][..], RX_FROM_LINE, RX_FROM_LINE)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & RX_FULL, 0, "it fell once the consumer made room"); + + r.until("the last character", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 8); + let entry = r.get(r.ring(0, 1, 0x400), 8); + assert_eq!((entry[0], entry[1], entry[4], entry[5]), (0xb5, 0, RX_FROM_LINE, 0)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & RX_FULL, 0, "full again, but nothing waits for room"); +} + +/// Ports test_dma_loopback_full_ring_recovery: a full receive ring does not +/// stall the transmit walk in loopback. Both entries go out and the eight +/// characters wait in the packer and the 16550's FIFO, then land as the +/// consumer frees the ring. +#[test] +fn a_full_ring_does_not_stall_a_loopback_walk() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.fast_line(0); + r.uw(0, 2, 0x01); + r.uw(0, 4, 0x13); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + let expected = fill_loopback_ring(&r); + r.until("the transmit ring", |r| r.sr(0, sdma_off::TX_CONSUME) & 0x3f8 == 0x3f8); + r.until("the receive ring", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0x3f8); + assert_eq!(r.get(r.ring(0, 1, 0x400), 0x3f8), expected); + + let looped = |c: [u8; 4]| [c[0], c[1], c[2], c[3], RX_LOOPBACK, RX_LOOPBACK, RX_LOOPBACK, RX_LOOPBACK]; + r.tx_entry(0, 0x400, 0x3f8, [0xa1, 0xa2, 0xa3, 0xa4], [TX_VALID; 4]); + r.tx_entry(0, 0x400, 0, [0xb1, 0xb2, 0xb3, 0xb4], [TX_VALID; 4]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + r.until("both entries sent", |r| r.sr(0, sdma_off::TX_CONSUME) & 0x3f8 == 8); + r.until("the source", |r| r.r32(reg::SERIAL_PENDING) & RX_FULL != 0); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0x3f8); + + r.sw(0, sdma_off::RX_CONSUME, 8); + r.until("the first", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 0); + assert_eq!(r.get(r.ring(0, 1, 0x400) + 0x3f8, 8), looped([0xa1, 0xa2, 0xa3, 0xa4])); + r.sw(0, sdma_off::RX_CONSUME, 16); + r.until("the second", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8 == 8); + assert_eq!(r.r32(reg::SERIAL_PENDING) & RX_FULL, 0, "filled exactly with nothing waiting"); + assert_eq!(r.get(r.ring(0, 1, 0x400), 8), looped([0xb1, 0xb2, 0xb3, 0xb4])); + assert!(r.sent(0).is_empty()); +} + +/// Ports test_dma_loopback_overrun_reaches_status: with the receive ring +/// out of room the characters that do not fit are lost to the 16550's +/// overrun, which shows in the status byte of the next character read. +#[test] +fn a_loopback_overrun_reaches_the_status_byte() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x400); + r.fast_line(0); + r.uw(0, 2, 0x01); + r.uw(0, 4, 0x13); + r.sw(0, sdma_off::CONTROL, ENABLE); + // Room for one entry before the ring is full. + r.sw(0, sdma_off::RX_CONSUME, 16); + for i in 0..8usize { + r.tx_entry(0, 0x400, 8 * i, std::array::from_fn(|j| (4 * i + j) as u8), [TX_VALID; 4]); + } + r.sw(0, sdma_off::TX_PRODUCE, 64); + r.until("the walk", |r| r.sr(0, sdma_off::TX_CONSUME) & 0x3f8 == 64); + r.settle(5); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8, 8); + + r.sw(0, sdma_off::RX_CONSUME, 8); + r.sw(0, sdma_off::CONTROL, ENABLE | DRAIN); + r.until("the drain", |r| r.sr(0, sdma_off::CONTROL) & DRAIN == 0); + let posted = (r.sr(0, sdma_off::RX_PRODUCE) & 0x3f8) / 8; + assert!(posted > 1); + let overrun = (1..posted as usize).any(|i| r.get(r.ring(0, 1, 0x400) + 8 * i + 4, 4).iter().any(|s| s & 1 != 0)); + assert!(overrun, "the loss is marked on a later character"); + assert!(posted * 4 < 32, "and not everything came back"); +} + +/// Ports test_serial_idle_status: the serial control word reads the idle bit +/// set after a write, a pause, a reset, PIO and a ring run, and the writes to +/// it and to the unsupported ATA window disturb no interrupt word. +#[test] +fn serial_idle_status_survives_writes_and_ring_runs() { + let r = Rig::new(); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + r.w32(reg::SERIAL_CONTROL, 0x80); + r.c.bar_read(0, reg::SERIAL_CONTROL, 1); + r.c.bar_write(0, reg::SERIAL_CONTROL, 0x80, 1); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + assert_eq!(r.r32(0x100), 0); + r.w32(0x100, u32::MAX); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE | (1 << 29)); + r.tx_entry(0, 0x1000, 0, [0x5a, 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_ne!(r.sr(0, sdma_off::CONTROL) & (1 << 30), 0, "paused"); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + + r.c.reset(); + r.uw(0, 0, 0x5a); + assert_eq!(r.sent(0), [0x5a]); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); + + r.pair_base(0, RINGS, 0x1000); + r.tx_entry(1, 0x1000, 0, [0x15, 0, 0, 0], [TX_MCR | TX_EXPLICIT, 0, 0, 0]); + r.sw(1, sdma_off::CONTROL, ENABLE); + r.sw(1, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(1, sdma_off::TX_CONSUME), 8); + let (serial, other) = (r.r32(reg::SERIAL_PENDING), r.r32(reg::OTHER_PENDING)); + assert_ne!(serial, 0); + r.w32(reg::SERIAL_CONTROL, 0x80); + r.w32(0x100, u32::MAX); + assert_eq!((r.r32(reg::SERIAL_PENDING), r.r32(reg::OTHER_PENDING)), (serial, other)); + assert_eq!(r.r32(reg::SERIAL_CONTROL), 0x80); +} + +// --------------------------------------------------------------------------- +// The host end of a port +// --------------------------------------------------------------------------- + +/// Ports test_dma_loopback_discards_external_input: in loopback the UART's +/// serial input is disconnected, as on a PC16550D. Input from outside is +/// discarded on arrival, not kept for later, and new input arrives as usual +/// once the loopback is off. +#[test] +fn loopback_discards_input_from_outside() { + let r = Rig::new(); + let input = [0xa1, 0xa2, 0xa3, 0xa4]; + r.pair_base(0, RINGS, 0x400); + r.uw(0, 4, 0x10); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD); + r.type_in(0, &input); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD | DRAIN); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0); + + // Out of loopback nothing comes back: it was discarded, not held. + r.uw(0, 4, 0); + r.c.service(); + r.sw(0, sdma_off::CONTROL, ENABLE | THRESHOLD | DRAIN); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0); + + r.type_in(0, &input); + r.until("the entry", |r| r.sr(0, sdma_off::RX_PRODUCE) == 8); + assert_eq!(&r.get(r.ring(0, 1, 0x400), 4), &input); +} + +/// Ports test_dma_chardev_reconnect: a port's backend can be replaced while +/// the machine runs, and both PIO and the ring engine then use the new one. +#[test] +fn replacing_the_backend_keeps_the_engine_working() { + let r = Rig::new(); + let new = Arc::new(Wire::default()); + r.c.set_serial_backend(0, new.clone()); + r.c.set_serial_backend(0, new.clone()); + r.uw(0, 0, b'p'); + assert_eq!(*new.out.lock().unwrap(), b"p"); + assert!(r.sent(0).is_empty(), "the old backend saw nothing"); + + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, [0x61, 0x62, 0, 0], [TX_VALID, TX_VALID, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 8); + assert_eq!(*new.out.lock().unwrap(), b"pab"); + new.inp.lock().unwrap().push_back(0x7a); + r.c.service(); + r.sw(0, sdma_off::CONTROL, ENABLE | DRAIN); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE) & 0xff8, 8, "and its input arrives"); + assert_eq!(r.get(r.ring(0, 1, 0x1000), 1), [0x7a]); +} + +// --------------------------------------------------------------------------- +// The rest of the ring engine's register cases +// --------------------------------------------------------------------------- + +const TIMER_EXPIRED: u32 = int::serial(0, 3); +const TX_COMPLETION: u32 = int::serial(0, 7); + +/// Ports test_dma_tx_wrap_4k. +#[test] +fn tx_ring_wraps_at_four_kilobytes() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::TX_PRODUCE, 0xff8); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0xff8); + r.tx_entry(0, 0x1000, 0xff8, [0x6e, 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 0); + assert_eq!(r.sent(0), [0x6e]); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::serial(0, 0), int::serial(0, 0)); +} + +/// Ports test_dma_control_stores_its_bits: the control register keeps every +/// writable bit as written, including ones the model does not act on. +#[test] +fn control_stores_the_bits_it_does_not_act_on() { + let r = Rig::new(); + let written = (1 << 17) | (1 << 20) | 0x1ff; + r.sw(0, sdma_off::CONTROL, written); + assert_eq!(r.sr(0, sdma_off::CONTROL), written); +} + +/// Ports test_dma_engine_indices_take_writes and +/// test_dma_host_index_masks_its_pointer: the two indices the engine moves +/// still take a write, and every index keeps only its pointer field. +#[test] +fn indices_keep_their_pointer_field() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::TX_CONSUME, u32::MAX); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 0xff8); + r.sw(0, sdma_off::RX_PRODUCE, 0x18); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 0x18); + r.sw(0, sdma_off::TX_PRODUCE, 0x10014); + assert_eq!(r.sr(0, sdma_off::TX_PRODUCE), 0x10); + r.sw(0, sdma_off::RX_CONSUME, 0x2c); + assert_eq!(r.sr(0, sdma_off::RX_CONSUME) & 0xff8, 0x28); +} + +/// Ports test_dma_empty_slot_ends_the_entry: an empty slot ends its entry +/// wherever it sits, so the slots behind it are not sent. +#[test] +fn an_empty_slot_ends_the_entry() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, *b"abcd", [TX_VALID, 0, TX_VALID, TX_VALID]); + r.tx_entry(0, 0x1000, 8, [b'e', 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 16); + assert_eq!(r.sent(0), b"ae"); +} + +/// Ports test_dma_tx_control_decodes_type_and_bit5: a control byte is +/// decoded by its type field and bit 5, undefined low bits do not stop the +/// port, and a slot of type none raises nothing even with bit 5 set. +#[test] +fn a_control_byte_is_decoded_by_type_and_bit_five() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, [b'a', b'b', b'c', 0], [TX_VALID | 0x10, TX_EXPLICIT, TX_VALID, 0]); + r.tx_entry(0, 0x1000, 8, [b'd', 0, 0, 0], [TX_VALID, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 16); + assert_eq!(r.sent(0), b"ad"); + assert_eq!(r.sr(0, sdma_off::TX_CONSUME), 16); + assert_eq!(r.r32(reg::SERIAL_PENDING) & TX_COMPLETION, 0); +} + +/// Ports test_dma_tx_delay_sets_the_busy_bit: control bit 16 reads set while +/// a delay entry runs and clear once it has ended. +#[test] +fn a_delay_entry_sets_the_busy_bit() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, [3, 0, 0, 0], [TX_DELAY, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sr(0, sdma_off::CONTROL) & (1 << 16), 1 << 16); + r.settle(10); + assert_eq!(r.sr(0, sdma_off::CONTROL) & (1 << 16), 0); +} + +/// Ports test_dma_rx_timer_due_before_access: an expiry already due +/// happens before a register access, with no timer thread to run it. +#[test] +fn a_due_receive_timer_expires_before_an_access() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_TIMER, 2); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x64]); + std::thread::sleep(Duration::from_millis(5)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED, TIMER_EXPIRED); +} + +/// Ports test_dma_rx_timer_count_reads_back: a reload write loads the live +/// count too and an idle timer holds it; a running one counts down. +#[test] +fn the_receive_timer_count_reads_back() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::RX_TIMER, 0xfff); + assert_eq!(r.sr(0, sdma_off::RX_TIMER) & 0x0fff_0fff, 0x0fff_0fff); + std::thread::sleep(Duration::from_millis(2)); + assert_eq!(r.sr(0, sdma_off::RX_TIMER) & 0x0fff_0fff, 0x0fff_0fff, "idle, it holds"); + r.sw(0, sdma_off::RX_TIMER, 0x140); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x63]); + let count = r.sr(0, sdma_off::RX_TIMER) >> 16 & 0xfff; + assert!((1..=0x140).contains(&count), "running: {count:#x} of 0x140 left"); +} + +/// Ports test_dma_consume_without_arm_keeps_it: a consume write with bit 31 +/// clear leaves the arm, which reads back and still starts the timer. +#[test] +fn a_consume_write_without_the_arm_keeps_it() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_TIMER, 2); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.sw(0, sdma_off::RX_CONSUME, 0); + assert_eq!(r.sr(0, sdma_off::RX_CONSUME), ARM); + r.type_in(0, &[0x6b]); + r.until("the expiry", |r| r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED != 0); + assert_eq!(r.sr(0, sdma_off::RX_CONSUME), 0, "and the expiry spent it"); +} + +/// Ports test_dma_zero_reload_expires_at_once. +#[test] +fn a_zero_reload_expires_at_once() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_TIMER, 0); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x7a]); + r.until("the expiry", |r| r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED != 0); + assert_eq!(r.sr(0, sdma_off::RX_PRODUCE), 8); +} + +/// Ports test_dma_rx_status_carries_modem_state: a character from the line +/// carries the live CTS and DCD levels, as the UART's modem status shows. +#[test] +fn a_received_character_carries_the_modem_state() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.type_in(0, &[0x6e]); + r.sw(0, sdma_off::CONTROL, ENABLE | DRAIN); + let entry = r.get(r.ring(0, 1, 0x1000), 8); + assert_eq!((entry[0], entry[4]), (0x6e, RX_FROM_LINE)); + assert_eq!(r.ur(0, 6) & 0xb0, 0xb0); +} + +/// Ports test_dma_rx_timer_expiry_after_full_entry: a full entry posts at +/// once, the timer still raises its source when it runs out, and once +/// acknowledged it does not come back without another arm. +#[test] +fn the_timer_still_expires_after_a_full_entry() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_TIMER, 2); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x31, 0x32, 0x33, 0x34]); + r.until("the entry", |r| r.sr(0, sdma_off::RX_PRODUCE) & 0xff8 == 8); + let entry = r.get(r.ring(0, 1, 0x1000), 8); + assert_eq!((&entry[..4], entry[4], entry[7]), (&[0x31, 0x32, 0x33, 0x34][..], RX_FROM_LINE, RX_FROM_LINE)); + r.until("the expiry", |r| r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED != 0); + r.w32(reg::SERIAL_PENDING, TIMER_EXPIRED); + r.settle(5); + assert_eq!(r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED, 0); +} + +/// Ports test_dma_rx_disable_stops_timer_keeps_arm: clearing the enable stops +/// a running receive timer and discards its time, and the arm stays so the +/// first character after re-enabling starts it afresh. +#[test] +fn disabling_the_ring_stops_the_timer_and_keeps_the_arm() { + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.sw(0, sdma_off::RX_TIMER, 0x100); + r.sw(0, sdma_off::RX_CONSUME, ARM); + r.type_in(0, &[0x31]); + assert!(r.c.sh.st.lock().sdma.rx_deadline(0).is_some(), "running"); + r.sw(0, sdma_off::CONTROL, 0); + assert!(r.c.sh.st.lock().sdma.rx_deadline(0).is_none(), "stopped"); + r.settle(20); + assert_eq!(r.r32(reg::SERIAL_PENDING) & TIMER_EXPIRED, 0); + r.sw(0, sdma_off::CONTROL, ENABLE); + assert_eq!(r.sr(0, sdma_off::RX_CONSUME) & ARM, ARM, "the arm stays"); + r.type_in(0, &[0x32]); + assert!(r.c.sh.st.lock().sdma.rx_deadline(0).is_some(), "the next character starts it afresh"); +} + +// --------------------------------------------------------------------------- +// The PS/2 keyboard and mouse controller +// --------------------------------------------------------------------------- + +const KBC_CONTROL: u32 = reg::KBC; +const KBC_KBD_RECEIVE: u32 = reg::KBC + 0x04; +const KBC_AUX_RECEIVE: u32 = reg::KBC + 0x08; +const KBC_KBD_TRANSMIT: u32 = reg::KBC + 0x0c; +const KBC_AUX_TRANSMIT: u32 = reg::KBC + 0x10; +const KBC_IRQ: u32 = 0x40; +const KBC_WRITE_PENDING: u32 = 0x03; + +fn key(r: &Rig, code: winit::keyboard::KeyCode, down: bool) { + r.c.ps2().push_kb(code, down); +} + +/// Ports test_ps2_adapter (QEMU with the read-only status +/// bits): the five registers through the adapter, reply bytes in BE32 +/// presentation, write-pending never set, the keyboard source in the other +/// word with its own enable and acknowledge, isolation from the serial +/// words, a mouse report, and reset. +#[test] +fn the_ps2_adapter_presents_the_controller() { + use winit::keyboard::KeyCode::KeyA; + let r = Rig::new(); + assert_eq!((r.r32(KBC_CONTROL), r.r32(KBC_KBD_RECEIVE), r.r32(KBC_AUX_RECEIVE)), (0, 0, 0)); + r.w32(KBC_CONTROL, u32::MAX); + assert_eq!(r.r32(KBC_CONTROL), 0x00fc_3cc0, "the status bits read clear and drop the write"); + r.w32(KBC_CONTROL, 0); + r.c.bar_read(0, KBC_CONTROL, 1); + r.c.bar_write(0, KBC_KBD_TRANSMIT, 0xff, 2); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0, "a narrow write reached nothing"); + + // 0xff000000 is a zero byte to the keyboard: it only acknowledges. + r.w32(KBC_KBD_TRANSMIT, 0xff00_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80fa_0000); + r.w32(KBC_KBD_TRANSMIT, 0xff); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0xc0fa_aa00); + assert_eq!(r.r32(KBC_CONTROL) & KBC_WRITE_PENDING, 0); + // QEMU's keyboard scans again after the reset command; this core waits + // for the driver's enable, as the O2's and Octane's PROMs give it. + r.w32(KBC_KBD_TRANSMIT, 0xf4); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80fa_0000); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + + key(&r, KeyA, true); + assert_eq!(r.r32(reg::OTHER_PENDING) & KBC_IRQ, KBC_IRQ); + assert!(!r.pin.level(), "pending but not enabled"); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + assert!(r.pin.level()); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x801c_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE) & 0x1f00_0000, 0); + assert_eq!(r.r32(reg::OTHER_PENDING) & KBC_IRQ, KBC_IRQ, "a drain does not acknowledge"); + + // A serial source holds the pin up when the keyboard's goes. + r.uw(0, 3, 0x80); + r.uw(0, 0, 0x34); + r.uw(0, 1, 0x12); + r.uw(0, 3, 0x03); + r.uw(0, 2, 0x01); + r.uw(0, 1, 0x02); + r.w32(reg::SERIAL_ENABLE_SET, int::pass(0)); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0)); + assert!(r.pin.level()); + r.w32(reg::OTHER_ENABLE_CLR, KBC_IRQ); + assert!(r.pin.level()); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + assert_eq!(r.r32(reg::OTHER_PENDING) & KBC_IRQ, 0); + assert_eq!(r.r32(reg::SERIAL_PENDING) & int::pass(0), int::pass(0), "the serial source is another word"); + assert!(r.pin.level()); + r.uw(0, 1, 0); + assert!(!r.pin.level()); + + r.w32(KBC_AUX_TRANSMIT, 0xff); + assert_eq!(r.r32(KBC_AUX_RECEIVE), 0xe0fa_aa00); + assert_eq!(r.r32(KBC_CONTROL) & KBC_WRITE_PENDING, 0); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + r.w32(KBC_AUX_TRANSMIT, 0xf4); + assert_eq!(r.r32(KBC_AUX_RECEIVE), 0x80fa_0000); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + r.c.ps2().push_mouse_input(1, 4, 0, 0); + assert_eq!(r.r32(KBC_AUX_RECEIVE), 0xe009_0400); + + r.c.reset(); + assert_eq!((r.r32(KBC_CONTROL), r.r32(KBC_KBD_RECEIVE), r.r32(KBC_AUX_RECEIVE)), (0, 0, 0)); + assert_eq!((r.r32(reg::OTHER_PENDING) & KBC_IRQ, r.r32(reg::OTHER_ENABLE_SET) & KBC_IRQ), (0, 0)); + r.w32(KBC_KBD_TRANSMIT, 0xff); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0xc0fa_aa00); +} + +/// Ports test_ps2_ports_present: every IOC4 has both ports; there is no +/// switch to leave them unplugged. +#[test] +fn both_ps2_ports_are_always_fitted() { + let r = Rig::with(Ioc4Config::bare(Ioc4Clock::Mhz33)); + r.w32(KBC_KBD_TRANSMIT, 0xff); + r.w32(KBC_AUX_TRANSMIT, 0xff); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0xc0fa_aa00); + assert_eq!(r.r32(KBC_AUX_RECEIVE), 0xe0fa_aa00); +} + +/// Ports test_ps2_driver_sequence, the attach and interrupt sequence of +/// OpenBSD's public IOC4 keyboard driver: read-modify-write the control +/// register to clamp after one character, transmit after the write-pending +/// poll, then on each interrupt mask the source, read each receive register +/// until no valid flag is left, acknowledge and unmask. Also the +/// clamp-after-three delivery, and an unclamped port losing a fourth +/// character without showing the IOC3-only overflow flag. +#[test] +fn the_ps2_driver_sequence() { + use winit::keyboard::KeyCode::{KeyA, KeyB, KeyC, KeyD}; + let r = Rig::new(); + let mut csr = r.r32(KBC_CONTROL); + csr &= !0x00c0_0ccc; + csr |= 0x0030_0000; + r.w32(KBC_CONTROL, csr); + assert_eq!(r.r32(KBC_CONTROL), 0x0030_0000); + // Status bits are not storage: a write to them is dropped and logged. + r.w32(KBC_CONTROL, 0x0030_0000 | 0xc33f); + assert_eq!(r.r32(KBC_CONTROL), 0x0030_0000); + r.w32(KBC_CONTROL, 0x0030_0000 | 0x3cc0); + assert_eq!(r.r32(KBC_CONTROL), 0x0030_3cc0, "the writable bits among them stick"); + r.w32(KBC_CONTROL, 0x0030_0000); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + + // A reset reply is two characters, so the clamp holds the second back. + assert_eq!(r.r32(KBC_CONTROL) & KBC_WRITE_PENDING, 0); + r.w32(KBC_KBD_TRANSMIT, 0xff); + assert!(r.pin.level()); + assert_eq!(r.r32(reg::OTHER_PENDING) & KBC_IRQ, KBC_IRQ); + r.w32(reg::OTHER_ENABLE_CLR, KBC_IRQ); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80fa_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80aa_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0); + assert_eq!(r.r32(KBC_AUX_RECEIVE), 0); + // As in the adapter test: this keyboard model needs the driver's enable. + r.w32(KBC_KBD_TRANSMIT, 0xf4); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80fa_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + assert!(!r.pin.level()); + + // Press and release: three characters, one per read under the clamp. + key(&r, KeyA, true); + key(&r, KeyA, false); + assert!(r.pin.level()); + r.w32(reg::OTHER_ENABLE_CLR, KBC_IRQ); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x801c_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x80f0_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0x801c_0000); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + assert!(!r.pin.level()); + + // Clamp after three: the same press and release arrive in one read. + r.w32(KBC_CONTROL, 0x0040_0000); + key(&r, KeyA, true); + key(&r, KeyA, false); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0xe01c_f01c); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0); + + // No clamp: a fourth character is lost and the flag is not reported. + r.w32(KBC_CONTROL, 0); + for k in [KeyA, KeyB, KeyC, KeyD] { + key(&r, k, true); + } + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0xe01c_3221); + assert_eq!(r.r32(KBC_KBD_RECEIVE), 0); +} + +/// Ports the width rule of test_ps2_adapter: the keyboard block takes +/// aligned 32-bit accesses only, and a refused one reaches no register. +#[test] +fn the_ps2_block_takes_aligned_words_only() { + let r = Rig::new(); + r.w32(KBC_CONTROL, 0x0030_0000); + assert_eq!(r.c.bar_read(0, KBC_CONTROL, 1), 0); + assert_eq!(r.c.bar_read(0, KBC_CONTROL, 2), 0); + r.c.bar_write(0, KBC_CONTROL, 0, 2); + r.c.bar_write(0, KBC_CONTROL + 1, 0, 1); + assert_eq!(r.r32(KBC_CONTROL), 0x0030_0000); + assert_eq!(r.c.bar_read(0, KBC_CONTROL + 2, 4), 0, "unaligned"); + // Beyond the five registers is not the controller. + assert_eq!(r.r32(reg::KBC + 0x14), 0); +} + +/// A key pressed on the host reaches the pin with no guest access, from the +/// host's thread, as the IOC3's does. +#[test] +fn a_host_key_raises_the_pin_without_a_guest_access() { + let r = Rig::new(); + r.w32(reg::OTHER_ENABLE_SET, KBC_IRQ); + r.w32(KBC_KBD_TRANSMIT, 0xf4); + let _ = r.r32(KBC_KBD_RECEIVE); + r.w32(reg::OTHER_PENDING, KBC_IRQ); + assert!(!r.pin.level()); + key(&r, winit::keyboard::KeyCode::KeyA, true); + assert!(r.pin.level()); +} + +// --------------------------------------------------------------------------- +// Reported, not refused +// --------------------------------------------------------------------------- + +/// Everything the `ioc` log module has said in this process. Tests run side +/// by side and share it, so a check looks for its own message text. +fn device_log() -> String { + use std::io::Write; + struct Sink(Arc>>); + impl Write for Sink { + fn write(&mut self, b: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(b); + Ok(b.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + static LOG: std::sync::OnceLock>>> = std::sync::OnceLock::new(); + let log = LOG.get_or_init(|| { + let buf = Arc::new(std::sync::Mutex::new(Vec::new())); + let dl = crate::devlog::DEVLOG.get().cloned().unwrap_or_else(crate::devlog::init_devlog); + dl.add_sink(Arc::new(parking_lot::Mutex::new(Sink(buf.clone())))); + dl.enable(crate::devlog::LogModule::Ioc); + buf + }); + String::from_utf8_lossy(&log.lock().unwrap()).into_owned() +} + +/// Ports test_dma_reports_bad_base_and_pointer: a misaligned pair base and a +/// base change on a busy pair are reported, not refused; the enable and the +/// new base both take effect, and an index keeps its pointer field while the +/// stray bits are reported. +#[test] +fn a_bad_base_and_a_stray_pointer_bit_are_reported_not_refused() { + let _ = device_log(); + let r = Rig::new(); + r.pair_base(0, RINGS + 8, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + assert_eq!(r.sr(0, sdma_off::CONTROL) & ENABLE, ENABLE); + r.sw(0, sdma_off::CONTROL, 0); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.pair_base(0, RINGS + 0x4000, 0x1000); + assert_eq!(r.r32(reg::SDMA_BASE), (RINGS + 0x4000) as u32 | 1); + r.sw(0, sdma_off::CONTROL, RESET); + r.sw(0, sdma_off::TX_PRODUCE, 4); + assert_eq!(r.sr(0, sdma_off::TX_PRODUCE), 0); + r.sw(0, sdma_off::RX_TIMER, 0x1_0002); + r.sw(0, sdma_off::CONTROL, 1 << 20); + let log = device_log(); + for what in [ + "misaligned for its rings", + "changed while it is active", + "TX producer 0x00000004 on port 0 has bits outside the pointer field", + "unsupported RX timer bits 0x00010000 on port 0", + "serial control bits 0x00100000 on port 0 are stored but not modelled", + ] { + assert!(log.contains(what), "no report of: {what}"); + } +} + +/// Ports test_dma_mode_switch_over_pio_is_reported and +/// test_dma_tx_disable_reports_pending_delay: the UART orders PIO and ring +/// DMA itself, so enabling DMA over queued PIO data, a PIO transmit while DMA +/// is on, and clearing the enable over a running delay are reported and go +/// ahead. +#[test] +fn a_mode_switch_over_pio_is_reported_and_goes_ahead() { + let _ = device_log(); + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.uw(0, 2, 0x07); + r.type_in(0, &[0x5a]); + assert_eq!(r.ur(0, 5) & 1, 1); + r.sw(0, sdma_off::CONTROL, ENABLE); + assert_eq!(r.sr(0, sdma_off::CONTROL) & ENABLE, ENABLE, "the enable took effect"); + r.uw(0, 0, 0xa5); + assert_eq!(r.sent(0), [0xa5], "and so did the PIO byte"); + + r.tx_entry(0, 0x1000, 0, [30, 0, 0, 0], [TX_DELAY, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + r.sw(0, sdma_off::CONTROL, 0); + assert_eq!(r.sr(0, sdma_off::CONTROL) & ENABLE, 0); + let log = device_log(); + assert!(log.contains("with PIO data in the UART's FIFOs")); + assert!(log.contains("PIO transmit on port 0 while DMA is enabled")); + assert!(log.contains("DMA disabled on port 0 with ring work pending")); +} + +/// Ports the undefined-bits check of test_dma_tx_control_decodes_type_and_bit5. +#[test] +fn undefined_transmit_control_bits_are_reported() { + let _ = device_log(); + let r = Rig::new(); + r.pair_base(0, RINGS, 0x1000); + r.sw(0, sdma_off::CONTROL, ENABLE); + r.tx_entry(0, 0x1000, 0, [b'q', 0, 0, 0], [TX_VALID | 0x10, 0, 0, 0]); + r.sw(0, sdma_off::TX_PRODUCE, 8); + assert_eq!(r.sent(0), b"q", "the slot is obeyed by its type"); + assert!(device_log().contains("undefined TX control bits 0x10 in an entry on port 0")); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index e1f085889..9dc1603f0 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -23,6 +23,7 @@ pub mod ioc_serial_dma; pub mod ioc3_eth; pub mod ioc3_kbc; pub mod ioc3_port; +pub mod ioc4; pub mod isp1040; pub mod mace_id; pub mod mace_pci; From b108e8bcb1006479a2773f627143552f5a2bc7ce Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:16:36 -0700 Subject: [PATCH 19/33] l1: add the router brick reply and a configurable port speed The Tezro tower's PROM asks the L1 for its two port-speed integers and prints a complaint about the link running at 400 MHz instead of 600 when the first reads zero. The L1 answered [0, 0] unconditionally. The first integer selects a PROM display branch rather than naming a speed, so make it a setting (zero or one, as the model it is ported from allows) with the default at zero, which keeps the Fuel's reply unchanged. Add the router command interpreter's brick request for the Chimera brick types, off by default. It arrives as 0x3001, 0x3002 and 0x3003 chunks on one of 32 subchannels; each subchannel reassembles its own message and a new start replaces a pending one. A single-fragment request whose descriptor fields match the capture is answered with the rack, slot, partition, brick type name and serial, in 113-byte chunks; the partition reads "none", since this L1 is never partitioned. A malformed request, any other interpreter command, a brick type with no name and a message of more than one fragment are logged and get no reply, because the descriptor layout for the longer messages is not known. The model's second UART transport is not present here, so only transport 0 exists. The tests port the port-speed case and the single-transport router cases from sn1-l1-test.c, including the Chimera WS identity the Tezro uses, less the cases of a partitioned L1. Ported from hw/misc/sgi-l1.c in jamesbraid/qemu-sgi-origami. --- src/dev/l1.rs | 7 +- src/dev/l1/model.rs | 214 ++++++++++++++++++++++++++++++++++++++++++- src/dev/l1/tests.rs | 219 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 437 insertions(+), 3 deletions(-) diff --git a/src/dev/l1.rs b/src/dev/l1.rs index 9436599ff..a93e005ef 100644 --- a/src/dev/l1.rs +++ b/src/dev/l1.rs @@ -7,7 +7,9 @@ //! as unknown, with a log line. Power requests take effect at once: power //! down (0x2005) turns the node off, and a power cycle (0x2006) turns it off //! and on again, which restarts it. Remote routing and controller -//! diagnostics are absent. +//! diagnostics are absent. A configured router interpreter answers +//! the single-fragment `brick` request for the Chimera brick types; other +//! interpreter commands and multi-fragment messages are refused and logged. //! //! Ported from hw/misc/sgi-l1.c in jamesbraid/qemu-sgi-origami. @@ -133,6 +135,9 @@ impl L1 { if config.rack > 0x3ff || config.bay > 0x3f || config.node_position > 3 { return Err("L1 rack, bay or node position out of range".into()); } + if config.port_speed_first > 1 { + return Err("L1 first port-speed value must be zero or one".into()); + } if config.mac == [0; 6] || config.mac[0] & 1 != 0 { return Err("L1 MAC must be nonzero unicast".into()); } diff --git a/src/dev/l1/model.rs b/src/dev/l1/model.rs index 70e2fd519..97d204877 100644 --- a/src/dev/l1/model.rs +++ b/src/dev/l1/model.rs @@ -50,6 +50,12 @@ pub struct L1Config { pub mac: [u8; 6], pub brick_serial: Option, pub auto_power: bool, + /// First integer of the port-speed reply (request 0xa). The PROM uses it + /// to select a display branch; it is not a link speed. Zero or one. + pub port_speed_first: u32, + /// Answer the router command-interpreter `brick` request. Only the brick + /// types in `router_type_name` have a reply. + pub router: bool, } impl Default for L1Config { fn default() -> Self { @@ -62,6 +68,8 @@ impl Default for L1Config { mac: [8, 0, 0x69, 0x12, 0x34, 0x56], brick_serial: Some(DEFAULT_BRICK_SERIAL.into()), auto_power: true, + port_speed_first: 0, + router: false, } } } @@ -90,6 +98,30 @@ pub(super) struct State { port: u8, events: Option, power_down_pending: bool, + /// One reassembly per subchannel. + router: [Option; 32], +} + +/// A router message being reassembled from 0x3001, 0x3002 and 0x3003 chunks. +struct RouterAssembly { + fragment_count: u8, + fragments_left: u8, + first_length: usize, + expected: usize, + received: usize, + data: Vec, +} +const ROUTER_MAX_FRAGMENT: usize = 0x400; +const ROUTER_CHUNK_DATA: usize = 113; + +/// Brick types for which the router interpreter reply exists. +fn router_type_name(type_code: u32) -> Option<&'static str> { + match type_code { + 0x37 => Some("Chimera Server"), + 0x39 => Some("Chimera WS"), + 0x51 => Some("Rackmount WS"), + _ => None, + } } fn text_field(out: &mut Vec, text: &[u8]) { out.push(1); @@ -189,6 +221,7 @@ impl State { port, events: None, power_down_pending: false, + router: std::array::from_fn(|_| None), } } pub fn reset(&mut self) { @@ -322,6 +355,16 @@ impl State { } fn request(&mut self, p: &[u8]) { let channel = p[0] & 31; + if self.config.router + && p.len() >= 7 + && p[0] & 0xe0 == 0 + && word(&p[1..]) == 0 + && p[5] == 0x30 + && matches!(p[6], 1..=3) + { + self.router_chunk(p); + return; + } if p[0] & 0xe0 == 0x40 { self.port = (self.port & 0x60) | channel; if self.console && self.powered { @@ -427,7 +470,7 @@ impl State { } // Not partitioned. 9 => values.push(0), - 0xa => values.extend([0, 0]), + 0xa => values.extend([self.config.port_speed_first, 0]), // Zero: the PROM's partition check goes no further. 0xb => values.push(0), 0xc => values.extend([0; 8]), @@ -480,6 +523,175 @@ impl State { } self.reply(channel, error, field_count, &fields); } + /// The router replies are chunked and reassembled with the same three + /// commands; a malformed chunk drops the pending message. + fn router_chunk(&mut self, p: &[u8]) { + let channel = p[0] & 0x1f; + let slot = usize::from(channel); + if p.len() < 10 { + crate::dlog!(LogModule::L1, "L1 malformed router chunk"); + return; + } + let command = u16::from_be_bytes([p[5], p[6]]); + let payload = &p[10..]; + let fragment_length = u16::from_be_bytes([p[8], p[9]]) as usize; + match command { + 0x3001 => { + let fragments = p[7]; + if self.router[slot].take().is_some() { + crate::dlog!(LogModule::L1, "L1 replacing pending router request"); + } + if fragments == 0 + || fragments >= 128 + || fragment_length == 0 + || fragment_length > ROUTER_MAX_FRAGMENT + || payload.len() > fragment_length + { + crate::dlog!(LogModule::L1, "L1 unsupported router message start"); + return; + } + self.router[slot] = Some(RouterAssembly { + fragment_count: fragments, + fragments_left: fragments, + first_length: 0, + expected: fragment_length, + received: payload.len(), + data: payload.to_vec(), + }); + } + 0x3002 => { + let ok = self.router[slot].as_ref().is_some_and(|a| { + a.fragments_left != 0 + && a.received == a.expected + && fragment_length != 0 + && fragment_length <= ROUTER_MAX_FRAGMENT - a.data.len() + && payload.len() <= fragment_length + }); + let Some(a) = self.router[slot].as_mut().filter(|_| ok) else { + self.router[slot] = None; + crate::dlog!(LogModule::L1, "L1 invalid router next fragment"); + return; + }; + a.expected = fragment_length; + a.received = payload.len(); + a.data.extend_from_slice(payload); + } + _ => { + let ok = self.router[slot].as_ref().is_some_and(|a| { + a.received != a.expected + && payload.len() <= a.expected - a.received + && payload.len() <= ROUTER_MAX_FRAGMENT - a.data.len() + }); + let Some(a) = self.router[slot].as_mut().filter(|_| ok) else { + self.router[slot] = None; + crate::dlog!(LogModule::L1, "L1 router continuation without valid message"); + return; + }; + a.received += payload.len(); + a.data.extend_from_slice(payload); + } + } + self.router_complete(channel); + } + fn router_complete(&mut self, channel: u8) { + let slot = usize::from(channel); + let Some(a) = self.router[slot].as_mut() else { return }; + if a.received != a.expected { + return; + } + if a.first_length == 0 { + a.first_length = a.data.len(); + } + a.fragments_left -= 1; + if a.fragments_left != 0 { + return; + } + let a = self.router[slot].take().unwrap(); + // Only the single-fragment layout has a known descriptor mapping. + if a.fragment_count == 1 { + self.router_reply(channel, &a.data[..a.first_length], a.fragment_count); + } else { + crate::dlog!(LogModule::L1, "L1 multi-fragment router dispatch is unsupported"); + } + } + fn router_reply(&mut self, channel: u8, request: &[u8], fragment_count: u8) { + let Some(type_name) = router_type_name(self.config.type_code) else { + crate::dlog!(LogModule::L1, "L1 unsupported or malformed router request"); + return; + }; + let n = request.len(); + // Fixed header, an all-zero scalar block, then the string descriptor + // (kind 0x0a, 1) carrying the interpreter command. + let well_formed = n >= 37 + && usize::from(u16::from_be_bytes([request[0], request[1]])) == n + && request[2] == 2 + && request[3] == 0 + && request[5] == 0x80 | fragment_count + && request[6] == 0 + && request[7] == 0 + && word(&request[8..]) == 0x0fff_f003 + && request[16] == 2 + && request[20..28] == [0; 8] + && request[28] == 0x0a + && request[29] == 1 + && word(&request[32..]) == 52 + && usize::from(u16::from_be_bytes([request[30], request[31]])) == n - 36 + && request[n - 1] == 0; + if !well_formed { + crate::dlog!(LogModule::L1, "L1 unsupported or malformed router request"); + return; + } + // The captured form has one trailing space; the plain spelling is + // accepted too. Anything else is refused. + if !matches!(&request[36..], b"brick\0" | b"brick \0") { + crate::dlog!(LogModule::L1, "L1 router command interpreter operation refused"); + return; + } + // The node is not partitioned (see request 9). + let partition = "none"; + let serial = self.config.brick_serial.as_deref().unwrap_or("(null)"); + let mut text = format!( + "rack: {:03}, slot: {:02}, partition: {partition}, type: {type_name} \ + [?MB flash], serial:{serial}, source: OTHER\n\n", + self.config.rack, self.config.bay + ) + .into_bytes(); + text.push(0); + let total = 36 + text.len(); + let node_id = (self.config.rack << 6) | self.config.bay; + let mut response = vec![0u8; 36]; + response[..2].copy_from_slice(&(total as u16).to_be_bytes()); + response[2] = 2; + response[4] = request[4]; + response[5] = 0x81; + response[6] = 1; + // The reply goes back to the requesting channel on this node. + response[8..12].copy_from_slice(&((node_id << 12) | (u32::from(channel) << 5) | 0x15).to_be_bytes()); + response[12..16].copy_from_slice(&((node_id << 12) | 3).to_be_bytes()); + response[16] = 2; + response[17] = request[17]; + response[18..20].copy_from_slice(&request[18..20]); + response[28] = 0x0a; + response[29] = 1; + response[30..32].copy_from_slice(&(text.len() as u16).to_be_bytes()); + response[32..36].copy_from_slice(&0x34u32.to_be_bytes()); + response.extend_from_slice(&text); + if total > ROUTER_MAX_FRAGMENT { + crate::dlog!(LogModule::L1, "L1 router brick response exceeds fragment limit"); + return; + } + for (i, part) in response.chunks(ROUTER_CHUNK_DATA).enumerate() { + let mut chunk = vec![0x20 | channel, 0, 0, 0, 0, 0x30, if i == 0 { 1 } else { 3 }]; + if i == 0 { + chunk.push(1); + chunk.extend_from_slice(&(total as u16).to_be_bytes()); + } else { + chunk.extend_from_slice(&[0, 0, 0]); + } + chunk.extend_from_slice(part); + self.emit(&chunk); + } + } fn reset_node(&mut self) { if self.powered { self.actions.push(L1Event::Reset); diff --git a/src/dev/l1/tests.rs b/src/dev/l1/tests.rs index 3f7d0d19d..5da541263 100644 --- a/src/dev/l1/tests.rs +++ b/src/dev/l1/tests.rs @@ -1,5 +1,6 @@ // Cases follow tests/qtest/sn1-l1-test.c in jamesbraid/qemu-sgi-origami: -// uart, frames, identity, eeprom, errors and reset. +// uart, frames, identity, eeprom, errors and reset, plus the port-speed and +// router brick-reply cases (test_port_speed, test_irouter_*). use super::*; @@ -374,3 +375,219 @@ fn concurrent_l1_outputs_and_actions_are_delivered_in_order() { assert_eq!(*backend.output.lock(), b"ABCD"); assert_eq!(*actions.lock(), [L1Event::Reset, L1Event::Shutdown]); } + +/// Request 0xa returns two integers; the first is a PROM display selector. +/// Ported from test_port_speed in tests/qtest/sn1-l1-test.c. +#[test] +fn the_port_speed_reply_carries_the_configured_first_integer() { + let l1 = L1::new(Default::default()).unwrap(); + assert_eq!(request(&l1, 0xa, &[], 0), integer_reply(&[0, 0])); + let l1 = L1::new(L1Config { port_speed_first: 1, ..Default::default() }).unwrap(); + assert_eq!(request(&l1, 0xa, &[], 0), integer_reply(&[1, 0])); + l1.power_on(); + drain(&l1); + assert_eq!(request(&l1, 0xa, &[], 0), integer_reply(&[1, 0])); + assert!(L1::new(L1Config { port_speed_first: 2, ..Default::default() }).is_err()); +} + +const BRICK: [u8; 53] = [ + 0x10, 0, 0, 0, 0, 0x30, 1, 1, 0, 0x2b, 0, 0x2b, 2, 0, 0, 0x81, 0, 0, 0x0f, 0xff, 0xf0, 3, 0x2f, + 0xff, 0xf0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0a, 1, 0, 7, 0, 0, 0, 0x34, b'b', b'r', + b'i', b'c', b'k', b' ', 0, +]; + +fn router_l1(type_code: u32, rack: u32, bay: u32, serial: Option<&str>) -> L1 { + L1::new(L1Config { + router: true, + type_code, + rack, + bay, + brick_serial: serial.map(str::to_string), + ..Default::default() + }) + .unwrap() +} + +fn chunk(channel: u8, command: u16, count: u8, length: u16, data: &[u8]) -> Vec { + let mut p = vec![channel, 0, 0, 0, 0, (command >> 8) as u8, command as u8, count]; + p.extend_from_slice(&length.to_be_bytes()); + p.extend_from_slice(data); + p +} + +fn send_packet(l1: &L1, packet: &[u8]) { + send(l1, &encode(packet)); +} + +/// Collect one reply message from its chunks: the channel, and the inner bytes. +fn router_receive(l1: &L1) -> (u8, Vec) { + let wire = drain(l1); + let mut frames = wire.split(|b| *b == 0x7e).filter(|f| !f.is_empty()); + let mut decode = || -> Vec { + let raw = frames.next().expect("a reply frame"); + let mut out = Vec::new(); + let mut it = raw.iter(); + while let Some(&b) = it.next() { + out.push(if b == 0x7d { it.next().unwrap() ^ 0x20 } else { b }); + } + out.truncate(out.len() - 2); + out + }; + let first = decode(); + assert_eq!(first[1..5], [0; 4]); + assert_eq!(first[5..8], [0x30, 1, 1]); + let channel = first[0]; + let mut inner = first[10..].to_vec(); + while inner.len() < usize::from(u16::from_be_bytes([inner[0], inner[1]])) { + let next = decode(); + assert_eq!(next[0], channel); + assert_eq!(next[5..10], [0x30, 3, 0, 0, 0]); + inner.extend_from_slice(&next[10..]); + } + assert!(frames.next().is_none()); + (channel, inner) +} + +fn expect_brick(l1: &L1, channel: u8, destination: u32, source: u32, text: &str) { + let (got, inner) = router_receive(l1); + assert_eq!(got, channel); + assert_eq!(usize::from(u16::from_be_bytes([inner[0], inner[1]])), inner.len()); + assert_eq!(u32::from_be_bytes(inner[8..12].try_into().unwrap()), destination); + assert_eq!(u32::from_be_bytes(inner[12..16].try_into().unwrap()), source); + assert_eq!(&inner[36..], [text.as_bytes(), &[0]].concat()); +} + +const DEFAULT_TEXT: &str = "rack: 001, slot: 04, partition: none, type: Chimera Server \ + [?MB flash], serial:(null), source: OTHER\n\n"; + +/// Ported from test_irouter_brick_exact in tests/qtest/sn1-l1-test.c. +#[test] +fn the_router_brick_reply_matches_the_captured_layout() { + let l1 = router_l1(0x37, 1, 4, None); + send_packet(&l1, &BRICK); + let mut inner = vec![ + 0, 0x8b, 2, 0, 0, 0x81, 1, 0, 0, 0x04, 0x42, 0x15, 0, 0x04, 0x40, 3, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0x0a, 1, 0, 0x67, 0, 0, 0, 0x34, + ]; + inner.extend_from_slice(DEFAULT_TEXT.as_bytes()); + inner.push(0); + assert_eq!(inner.len(), 139); + let mut first = vec![0x30, 0, 0, 0, 0, 0x30, 1, 1, 0, 139]; + first.extend_from_slice(&inner[..113]); + let mut rest = vec![0x30, 0, 0, 0, 0, 0x30, 3, 0, 0, 0]; + rest.extend_from_slice(&inner[113..]); + let mut expected = encode(&first); + expected.extend(encode(&rest)); + assert_eq!(drain(&l1), expected); +} + +/// Subchannels are answered in place, and a request split across chunks +/// completes on the last continuation. Ported from +/// test_irouter_fragment_channel_transport (the single-transport part). +#[test] +fn the_router_reassembles_chunks_and_answers_on_the_request_subchannel() { + let l1 = router_l1(0x37, 1, 4, None); + let rest = &BRICK[10..]; + send_packet(&l1, &chunk(0x10, 0x3001, 1, 43, &rest[..12])); + assert!(drain(&l1).is_empty()); + let mut other = BRICK; + other[0] = 0x11; + send_packet(&l1, &other); + expect_brick(&l1, 0x31, 0x0004_4235, 0x0004_4003, DEFAULT_TEXT); + send_packet(&l1, &chunk(0x10, 0x3003, 0, 0, &rest[12..])); + expect_brick(&l1, 0x30, 0x0004_4215, 0x0004_4003, DEFAULT_TEXT); +} + +/// Ported from test_irouter_reset_and_negative. +#[test] +fn a_reset_drops_a_pending_router_message_and_unsupported_types_stay_silent() { + let l1 = router_l1(0x37, 1, 4, None); + let rest = &BRICK[10..]; + send_packet(&l1, &chunk(0x10, 0x3001, 1, 43, &rest[..12])); + l1.power_on(); + drain(&l1); + send_packet(&l1, &chunk(0x10, 0x3003, 0, 0, &rest[12..])); + assert!(drain(&l1).is_empty()); + send_packet(&l1, &BRICK); + expect_brick(&l1, 0x30, 0x0004_4215, 0x0004_4003, DEFAULT_TEXT); + + let off = L1::new(Default::default()).unwrap(); + send_packet(&off, &BRICK); + assert_eq!(drain(&off), encode(&[0x30, 255, 255, 255, 255, 0, 0, 0, 8])); + + let l1 = router_l1(0x37, 1, 4, None); + send_packet(&l1, &BRICK[..7]); + assert!(drain(&l1).is_empty()); + send_packet(&l1, &chunk(0x10, 0x3001, 1, 43, &rest[..12])); + send_packet(&l1, &BRICK); + expect_brick(&l1, 0x30, 0x0004_4215, 0x0004_4003, DEFAULT_TEXT); + + let l1 = router_l1(0x34, 1, 4, None); + send_packet(&l1, &BRICK); + send_packet(&l1, &chunk(0x10, 0x3003, 0, 0, rest)); + assert!(drain(&l1).is_empty()); + + let l1 = router_l1(0x37, 1, 4, None); + let mut reset = BRICK; + reset[46..52].copy_from_slice(b"reset\0"); + send_packet(&l1, &reset); + assert!(drain(&l1).is_empty()); +} + +/// A new start replaces a pending message. Ported from +/// test_irouter_replace_pending. +#[test] +fn a_new_router_start_replaces_the_pending_message() { + let l1 = router_l1(0x37, 1, 4, None); + let mut old = BRICK; + old[14] = 0x12; + old[27] = 0x34; + send_packet(&l1, &chunk(0x10, 0x3001, 1, 43, &old[10..22])); + send_packet(&l1, &BRICK); + expect_brick(&l1, 0x30, 0x0004_4215, 0x0004_4003, DEFAULT_TEXT); + send_packet(&l1, &chunk(0x10, 0x3003, 0, 0, &old[22..])); + assert!(drain(&l1).is_empty()); +} + +/// Messages of more than one fragment have no known descriptor layout. +/// Ported from test_irouter_multifragment_refusal. +#[test] +fn a_multi_fragment_router_message_is_refused_and_later_requests_work() { + let l1 = router_l1(0x37, 1, 4, None); + let mut request = BRICK; + request[15] = 0x82; + send_packet(&l1, &chunk(0x10, 0x3001, 2, 43, &request[10..])); + assert!(drain(&l1).is_empty()); + send_packet(&l1, &chunk(0x10, 0x3002, 0, 4, &[0xde, 0xad, 0xbe, 0xef])); + assert!(drain(&l1).is_empty()); + request[0] = 0x11; + request[15] = 0x81; + send_packet(&l1, &request); + expect_brick(&l1, 0x31, 0x0004_4235, 0x0004_4003, DEFAULT_TEXT); +} + +/// Ported from test_irouter_identity_overrides, less its partition cases: +/// this L1 is never partitioned. The type 0x39 case is the Tezro identity. +#[test] +fn the_router_reply_reports_rack_slot_type_and_serial() { + let l1 = router_l1(0x39, 2, 5, Some("SERIAL-X")); + send_packet(&l1, &BRICK); + expect_brick( + &l1, + 0x30, + 0x0008_5215, + 0x0008_5003, + "rack: 002, slot: 05, partition: none, type: Chimera WS [?MB flash], \ + serial:SERIAL-X, source: OTHER\n\n", + ); + let l1 = router_l1(0x51, 3, 2, Some("")); + send_packet(&l1, &BRICK); + expect_brick( + &l1, + 0x30, + 0x000c_2215, + 0x000c_2003, + "rack: 003, slot: 02, partition: none, type: Rackmount WS [?MB flash], \ + serial:, source: OTHER\n\n", + ); +} From 3f243ff22906bd275250139ad4925ec16ec126eb Mon Sep 17 00:00:00 2001 From: James Braid Date: Mon, 9 Nov 2026 20:54:48 -0700 Subject: [PATCH 20/33] l1: carry a node board's CPU configuration in its board record At every start the IP35 PROM checks the configuration block in its boot flash against the node board's CPU configuration. It reads that from the board area of the L1's component 0 record. When the area carries none, it checks against a row of its own table chosen by the Bedrock board word. A block that matches neither draws "CPU configuration values mismatch ... reflash the PROM image" before the maintenance menu. The Tezro's board word 0x2001 picks a 400 MHz row, so its 700 MHz flash block failed the check on every boot. Let a board give the L1 a mode word and CPU and Hub clocks. The board area then takes format 2 and the CPU fields after its identity, in the layout QEMU's L1 gives the Origin 350 and Tezro boards and the stock PROM 6.210 accepts. Only Config0 and the two clocks are known, so the other fields stay empty rather than zero. No factory record has been read: the bytes are a software input, not a capture. A board can also name its node board in that area: product name, part number and revision. Unnamed, the area carries the Fuel's placeholder system board, which answers as both the node board (component 0) and the I/O board (component 1, with the station address). Named, component 0 carries the name given and no I/O board record is given, as QEMU's L1 does for an IP53 node. A board that sets neither keeps its records byte for byte, so the Fuel is unchanged. --- src/dev/l1.rs | 2 +- src/dev/l1/model.rs | 70 +++++++++++++++++++++++++++++++++++++++++---- src/dev/l1/tests.rs | 52 +++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/dev/l1.rs b/src/dev/l1.rs index a93e005ef..d370f96e8 100644 --- a/src/dev/l1.rs +++ b/src/dev/l1.rs @@ -28,7 +28,7 @@ use std::sync::{ mod model; use model::State; -pub use model::{parse_revision, L1Config, L1Event, DEFAULT_BRICK_SERIAL, DEFAULT_REVISION}; +pub use model::{parse_revision, BoardIdentity, CpuContext, L1Config, L1Event, DEFAULT_BRICK_SERIAL, DEFAULT_REVISION}; fn fcs(mut crc: u16, byte: u8) -> u16 { crc ^= byte as u16; diff --git a/src/dev/l1/model.rs b/src/dev/l1/model.rs index 97d204877..2c24fe175 100644 --- a/src/dev/l1/model.rs +++ b/src/dev/l1/model.rs @@ -56,6 +56,45 @@ pub struct L1Config { /// Answer the router command-interpreter `brick` request. Only the brick /// types in `router_type_name` have a reply. pub router: bool, + /// The CPU configuration the node board's record carries, if any. + pub cpu_context: Option, + /// The node board's identity in component 0's board area. Unset, the + /// board is the Fuel's system board, which answers as both the node + /// board and the I/O board, the I/O area carrying the station address. + /// Set, component 0 names this board and no I/O board record is given, + /// as QEMU's L1 does for an IP53 node. + pub node_board: Option, +} + +/// A board's identity as its board area names it: product name, part number +/// and revision. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BoardIdentity { + pub name: &'static str, + pub part: &'static str, + pub revision: &'static str, +} +impl BoardIdentity { + /// The Fuel's system board as the model names it: a placeholder, not a + /// read record. + pub const FUEL: Self = Self { name: "IP34", part: "030-1707-003", revision: "-G" }; +} + +/// The CPU configuration an IP35 node board's EEPROM record carries beside +/// its identity: the processor mode word and the processor and Hub clocks. +/// At start-up the PROM checks the configuration block in its boot flash +/// against these, and when a board record carries none it falls back to a +/// table row chosen by the board word; a block that matches neither draws +/// "CPU configuration values mismatch". The encoding is the one QEMU's L1 +/// gives the Tezro and Origin 350 boards, which the stock PROM 6.210 +/// accepts; no factory record has been read, so the bytes are a software +/// input, not a capture. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CpuContext { + /// The full 32-bit mode word, as the configuration block holds it. + pub mode_word: u32, + pub cpu_mhz: u32, + pub hub_mhz: u32, } impl Default for L1Config { fn default() -> Self { @@ -70,6 +109,8 @@ impl Default for L1Config { auto_power: true, port_speed_first: 0, router: false, + cpu_context: None, + node_board: None, } } } @@ -170,15 +211,31 @@ fn ia_text(area: &mut Vec, text: &str) { area.push(0xc0 | text.len() as u8); area.extend_from_slice(text.as_bytes()); } -fn board(mac: Option<[u8; 6]>) -> Vec { +fn board(identity: BoardIdentity, mac: Option<[u8; 6]>, cpu: Option) -> Vec { let mut b = vec![0; 6]; - for s in ["SYNTHETIC", "IP34", "EMUN00", "030-1707-003"] { + for s in ["SYNTHETIC", identity.name, "EMUN00", identity.part] { ia_text(&mut b, s); } b.push(0); - ia_text(&mut b, "-G"); + ia_text(&mut b, identity.revision); b.extend_from_slice(&[1, 0]); ia_text(&mut b, "00"); + if let Some(cpu) = cpu { + // Format 2 and a one-byte binary 3 select the CPU fields: the Hub + // mode bits, Config0 to Config3, the Hub and CPU clocks in MHz and + // three minimum versions. Only Config0 and the clocks are known; + // the rest are empty fields, not zeros. + b[0] = 2; + b.extend_from_slice(&[1, 3, 0]); + b.push(4); + b.extend_from_slice(&cpu.mode_word.to_be_bytes()); + b.extend_from_slice(&[0, 0, 0]); + for mhz in [cpu.hub_mhz, cpu.cpu_mhz] { + b.push(4); + b.extend_from_slice(&mhz.to_be_bytes()); + } + b.extend_from_slice(&[0, 0, 0]); + } for _ in 0..3 { b.extend_from_slice(&[4, 0, 0, 0, 0]); } @@ -198,9 +255,10 @@ impl State { let mut records: [[Vec; 3]; 16] = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); let chassis = area_finish(vec![0, 2, 0, 0xc2, b'N', b'A', 0xc2, b'N', b'A', 0xc1]); - for c in 0..2 { - records[c][0] = chassis.clone(); - records[c][1] = board(if c == 1 { Some(config.mac) } else { None }); + let identity = config.node_board.unwrap_or(BoardIdentity::FUEL); + records[0] = [chassis.clone(), board(identity, None, config.cpu_context), Vec::new()]; + if config.node_board.is_none() { + records[1] = [chassis, board(identity, Some(config.mac), None), Vec::new()]; } let powered = config.auto_power; let port = config.node_position << 5; diff --git a/src/dev/l1/tests.rs b/src/dev/l1/tests.rs index 5da541263..d224b0063 100644 --- a/src/dev/l1/tests.rs +++ b/src/dev/l1/tests.rs @@ -88,6 +88,58 @@ fn the_l1_returns_identity_and_validated_eeprom_records() { assert_eq!(request(&l1, 5, &[], 0), integer_reply(&[1, 48, 1])); } +/// A configured CPU configuration goes into component 0's board area, after +/// the identity fields, as format 2 with the one-byte selector 3, the Hub mode +/// bits, Config0 to Config3, the Hub and CPU MHz and three versions, the +/// unknown ones as empty fields; the I/O board's area and the default record +/// carry none. The layout is QEMU's (`l1_build_board_area`). +#[test] +fn the_node_board_record_carries_a_configured_cpu_configuration() { + let plain = L1::new(Default::default()).unwrap().record(0, 2).unwrap(); + let cpu = CpuContext { mode_word: 0x589b_ad85, cpu_mhz: 700, hub_mhz: 200 }; + let l1 = L1::new(L1Config { cpu_context: Some(cpu), ..Default::default() }).unwrap(); + let area = l1.record(0, 2).unwrap(); + assert_eq!(area.iter().fold(0u8, |s, x| s.wrapping_add(*x)), 0); + assert_eq!((area.len() % 8, usize::from(area[1]) * 8), (0, area.len())); + assert_eq!((plain[0], area[0]), (0, 2)); + // The identity fields end with the two-character text field "00". + let identity = plain.windows(3).position(|w| w == [0xc2, b'0', b'0']).unwrap() + 3; + assert_eq!(&area[2..identity], &plain[2..identity], "the identity fields are unchanged"); + let mut fields = vec![1, 3, 0, 4, 0x58, 0x9b, 0xad, 0x85, 0, 0, 0, 4, 0, 0, 0, 200, 4, 0, 0, 0x02, 0xbc, 0, 0, 0]; + for _ in 0..3 { + fields.extend_from_slice(&[4, 0, 0, 0, 0]); + } + fields.push(0xc1); + assert_eq!(&area[identity..identity + fields.len()], fields); + assert_eq!(l1.record(1, 2), L1::new(Default::default()).unwrap().record(1, 2), "the I/O board's area"); +} + +/// A board that names its node board gets that identity in component 0's +/// board area, empty fields included, and no I/O board record; with none +/// named the Fuel's placeholder answers in both, as before. +#[test] +fn a_named_node_board_answers_in_component_0_alone() { + let ip53 = BoardIdentity { name: "IP53_4CPU", part: "030-1868-001", revision: "-C" }; + let l1 = L1::new(L1Config { node_board: Some(ip53), ..Default::default() }).unwrap(); + let area = l1.record(0, 2).unwrap(); + assert_eq!(area.iter().fold(0u8, |s, x| s.wrapping_add(*x)), 0); + for text in [b"IP53_4CPU".as_slice(), b"030-1868-001", b"-C"] { + assert!(area.windows(text.len()).any(|x| x == text), "{}", String::from_utf8_lossy(text)); + } + assert!(!area.windows(4).any(|x| x == b"IP34")); + assert_eq!(l1.record(1, 2), None, "no I/O board record"); + assert_eq!(l1.record(1, 1), None); + let empty = BoardIdentity { name: "", part: "", revision: "" }; + let area = L1::new(L1Config { node_board: Some(empty), ..Default::default() }).unwrap().record(0, 2).unwrap(); + // SYNTHETIC, then an empty name, the serial, an empty part, the FRU file + // byte and an empty revision. + let at = area.windows(10).position(|x| x == b"\xc9SYNTHETIC").unwrap() + 10; + assert_eq!(&area[at..at + 11], b"\xc0\xc6EMUN00\xc0\x00\xc0"); + let fuel = L1::new(Default::default()).unwrap(); + assert_eq!(fuel.record(0, 2), L1::new(L1Config { node_board: None, ..Default::default() }).unwrap().record(0, 2)); + assert!(fuel.record(1, 2).is_some()); +} + /// A successful reply carrying one text field. fn text_reply(text: &str) -> Vec { let mut p = vec![0x30, 0, 0, 0, 0, 1]; From cb09db6f847c74463c86e8d3ff18b8ad966e3081 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sun, 8 Nov 2026 20:19:24 -0700 Subject: [PATCH 21/33] pic: add the PCI host bridge Tezro's interface board puts its PCI buses behind the PIC, not the XBridge Fuel uses. One chip fills one XIO widget with two heads, at offset 0 and 0x800000, and the two- and four-processor board fits a second PIC at widget 11 beside the one at widget 15, so a Pic is a self-contained device the machine builds once per widget. Each head owns a shared PciBus and is its PciHost, as the Bridge heads are: it supplies a slot's DMA and takes the level of its eight interrupt inputs, device d's INTA reaching input d - 1, and it publishes its CPU windows (the four short device-I/O windows and the 1 GiB PCI memory window) so a whole BAR page is served from the head's own table. The model is the revision 3 profile the QEMU model selects for Tezro: identity with bit 0 set, the control, Device and DIR_MAP masks, the CPU word view of the eight-byte registers, direct-map and direct64 DMA, the force registers and the invalid address interrupt. Translated DMA and PCI-X errors stay unsupported and observable. PCI slots are hooks only; the machine attaches its functions. Device d's INTA on input d - 1 is what IRIX 6.5.30's pin rule assigns (INTA and INTC to input d - 1, INTB and INTD to (d - 1) ^ 4), and an Origin 350's IO9 reports its IOC4, ISP12160 and BCM5701 on inputs 0, 2 and 3 under OpenBSD: established as what software expects. The board's wiring is inferred from that; INTB to INTD are unknown and left unwired, since no IO9 function uses them. A conventional adapter sees a numeric value at the processor's address with the low two bits corrected, as on MACE; a slot marked self-ordered (IOC4) gets the processor's byte sequence. Which order the silicon gives a 64-bit access to a conventional adapter is not known; it takes word values. A configuration read of an unclaimed slot fails as a master timeout instead of returning all ones: PROM 6.210 reads interrupt status bit 11 after each probe, and an all-ones header recorded two phantom components per empty device, enough with a second PIC to exceed the PROM's 66-component limit and lose its CPU records. The error view at 0x170 records the timeout, the status only while enable bit 11 is set, and the error interrupt follows. That fault and its enable gating are inferred. An input's status bit is set while it is active, and a head sends a set packet only when an enabled input rises, or when an enable or a reset write finds it active. Software asks for another through the write-only force registers, which ignore the data: force-always at 0x180 + 8n sends input n's packet whatever its level, force-with-pin at 0x1c0 + 8n only while the input is active. IRIX writes force-with-pin at the end of each interrupt dispatch (established from the shipped drivers): in a four-processor Tezro's NFS and TCP transfers it wrote the BCM5701's register 437,568 times against 440,795 set packets, and 3,171 of those writes found the input still active and sent it again. The chip has no timer that resends a packet, and the model has none: an interrupt the processor clears while the input stays high comes back through that write. A packet the peer refuses is dropped and logged and the status bit stays set; whether the chip retries one is unknown, and the next force write recovers it. Whether the enable gates a forced packet is unknown; the model does not gate it. Direct64 address bit 55 is the swap attribute, and bit 19 of a Device register the 32-bit direct window's; bits 56 to 59 (barrier, virtual channel, precise, prefetch) change nothing here. A swap-clear direct64 transfer, and a swapping slot's direct-map one, put each byte at its address with the low bits flipped, an exchange of byte positions within each bus data phase: each 32-bit word, or each doubleword for a slot that drives REQ64# (inferred from the sibling XBridge). Each whole aligned unit reaches memory as one write. A run of complete units leaves as one packet, which the peer applies one naturally aligned piece at a time, so a word the device updates is never seen half written, while the burst as a whole is not atomic, as on the bus. A ragged end writes only its own bytes of the unit, as one packet, and byte-stream packets end on doubleword boundaries, so no aligned halfword, word or doubleword is split between packets: 11 stream bytes from 0x..23 were cut at 0x..2b, through the halfword at 0x..2a. How the chip applies a three-byte end, and its byte enables under swap, are inferred. Carried a byte at a time, the BCM5701's status block reached memory in single bytes, and a processor could read its return-ring producer half written as it rolled over from 0x6ff to 0x700. IRIX's tg driver then took several hundred stale return descriptors as new, emptied its receive buffer table and faulted on the first empty slot, a TLBMISS kernel fault in its receive path during NFS reads. The shared bus decides which of two functions that decode the same range answers, as a probing PROM leaves them: the lower device. Real silicon with two matching comparators is undefined, and the QEMU model's newest-wins rule is not carried over. The register offsets the PIC shares with the Bridge family and the whole-page BAR table are the PIC's own: the XBridge is now built from the Bridge model, whose register map the PIC's heads do not share whole. The PIC reaches the Bedrock PIs only through the peer it sends its packets to. --- src/dev/mod.rs | 1 + src/dev/pic.rs | 1403 ++++++++++++++++++++++++++++++++++++++++++ src/dev/pic/tests.rs | 1189 +++++++++++++++++++++++++++++++++++ 3 files changed, 2593 insertions(+) create mode 100644 src/dev/pic.rs create mode 100644 src/dev/pic/tests.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 9dc1603f0..ed2ce8d46 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -87,6 +87,7 @@ pub mod ics1890; pub mod isp12160; pub mod l1; pub mod ohci; +pub mod pic; pub mod prom_image; pub mod timekeeper; pub mod xbridge; diff --git a/src/dev/pic.rs b/src/dev/pic.rs new file mode 100644 index 000000000..28f78b86f --- /dev/null +++ b/src/dev/pic.rs @@ -0,0 +1,1403 @@ +//! The PIC, the PCI host bridge on the interface board of the Origin 350 and +//! Tezro (IP53 family). One chip sits in one XIO widget and carries two +//! independent bridge heads, at widget offset 0 and `0x800000`, each with its +//! own register block and PCI bus. Widget 15 holds the first PIC; the +//! two- and four-processor Tezro board adds a second at widget 11. +//! +//! A [`Pic`] is self-contained: the machine builds one per widget, hands the +//! processor's view of it to Bedrock IO ([`Pic::widget`]), gives it a +//! [`PicPeer`] for the packets it originates (interrupts and DMA), and +//! attaches PCI functions to the bus slots. Each head owns a shared [`PciBus`] (`src/pci/`) for its +//! functions, BAR decode and interrupt pins, and is that bus's [`PciHost`]: +//! it supplies the DMA a slot gets and takes the level of each of its eight +//! interrupt inputs. The bus slots are hooks only: IOC4 at device 1 (PIC +//! input 0), the ISP12160 at device 3 (input 2) and the BCM5701 at device 4 +//! (input 3) are the board's choice, made by the machine; device `d`'s INTA +//! reaches input `d - 1`. +//! +//! The register offsets are the ones the Bridge family shares ([`reg`]); +//! the PIC leaves out the ATE, the alias windows and the second and third +//! write-request banks' meaning. +//! +//! # What is approximated here +//! +//! The model is the compatibility profile QEMU selects for Tezro: revision 3 +//! with identity bit 0 set, `DIR_MAP` bits 0 to 16 and 20 to 23, the control +//! and Device masks, and the reset values below are emulator choices that let +//! the PROM and IRIX run, not measured reset state. The CPU word view (a +//! 32-bit access to the low half of an eight-byte slot) and the processor's +//! byte-lane placement for PCI cycles are inferred from the public drivers. +//! Translated (ATE) DMA, PCI-X errors and the bridge's own PCI function are +//! unsupported and observable ([`Pic::unsupported`]). The packet face a peer +//! could read the heads through has no user and is not modeled. All +//! transfers are synchronous. +//! +//! An input's status bit is set while it is active, enabled or not, and a +//! head sends one set packet when an enabled input rises. Software asks for +//! another through the write-only force registers, which ignore the data: +//! force-always (0x180 + 8n) sends input n's packet whatever its level, and +//! force-with-pin (0x1c0 + 8n) only while it is active. The shipped drivers +//! write force-with-pin at the end of each interrupt dispatch, so an input a +//! handler leaves active is delivered again (established from the drivers); +//! the chip has no timer that sends a packet again, and neither has the +//! model. A packet the peer refuses is dropped and logged and the status +//! bit stays set; whether the chip retries one is unknown, and the model +//! leaves recovery to the next force write. Whether the interrupt enable +//! gates a forced packet is unknown; the model does not gate it. +//! +//! Direct64 DMA address bit 55 is the swap attribute and bit 19 of a Device +//! register the 32-bit direct window's (established bit names). A swap is +//! taken to exchange byte positions within each bus data phase, a 32-bit +//! word or, for a slot that drives REQ64#, a 64-bit doubleword (inferred +//! from the sibling XBridge). Each whole aligned unit reaches memory as one +//! write, so a word a device updates is never seen torn; a burst as a whole +//! is not atomic. A ragged end writes only its own bytes of the unit, as +//! one packet; the peer then applies them as naturally aligned pieces, so +//! an odd-sized end of three bytes is two writes (inferred; the chip's own +//! byte-enable handling is unknown). +//! +//! A configuration read that no function claims is a PCI master timeout: +//! the processor gets a bus error back through Bedrock, the error view +//! records bit 11 whatever the enable, and interrupt status records it only +//! while enable bit 11 is set (the gating is inferred). Status clears one +//! bit at a time; any reset-register write clears the view. Device 0, which +//! the PIC keeps for itself, reads as unclaimed: an approximation, since +//! what the chip answers there is unknown. Which bit a bus in PCI-X mode +//! sets (11 or 34) is unknown; the model sets 11. An unclaimed +//! configuration write goes nowhere and records nothing. +//! +//! Ported from hw/pci-host/sgi-pic.c in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use crate::irq::IrqLine; +use crate::pci::{ + BarDevice, BarMapping, BoardIrq, ByteOrder, CpuWindow, DevFn, IntxRouting, PciBus, PciDma, PciDmaStatus, PciFunction, + PciHost, Space, +}; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::{Mutex, RwLock}; +use std::sync::{Arc, OnceLock, Weak}; + +/// Where the second head sits in the widget. +pub const HEAD_OFFSET: u64 = 0x80_0000; +/// The widget's address space. +pub const WIDGET_SIZE: u64 = 1 << 32; +/// Interrupt inputs per head. +pub const PINS: usize = 8; +/// PCI devices behind a head: configuration devices 1 to 4. Device 0 is the +/// bridge itself, and no PCI identity for it is established. +pub const DEVICES: u8 = 4; + +const REVISION: u32 = 3; +const MANUFACTURER: u32 = 0x24; +const PARTS: [u32; 2] = [0xd102, 0xd112]; + +const REGISTER_BYTES: usize = 0x640; +const REQUEST_BANKS: usize = 3; +const REQUEST_ENTRIES: usize = 256; +const REQUEST_BASE: usize = 0x18000; +const REQUEST_END: usize = 0x19800; +const REQUEST_BANK_SIZE: usize = 0x800; +const CONFIG_BASE: u64 = 0x20000; +const CONFIG_TYPE1: u64 = 0x28000; +const CONFIG_END: u64 = 0x29000; +const PCI_WINDOW: [u64; 2] = [0x4000_0000, 0xc000_0000]; +const PCI_WINDOW_SIZE: u64 = 1 << 30; +const SHORT_BASE: u64 = 0x20_0000; +/// The packet window of the 48-bit XIO address: bits 63:60 of a direct64 DMA +/// address name the destination widget, and a payload keeps 47 bits. +const PAYLOAD_BITS: u32 = 48; +const UNSUPPORTED_LIMIT: usize = 32; + +/// The widget flush register (read to order earlier writes). +const FLUSH: usize = 0x50; +/// The link-side error address slot, whose low-word view the PROM writes. +const ERROR_ADDRESS_WORD: u64 = 0x7c; + +const CONTROL_MASK: u64 = 0x0f00_f20f; +const CONTROL_RESET: u64 = 0x0f00_320f; +const REQUEST_TIMEOUT_RESET: u64 = 0xfffff; +const DIRECT_MAP_SUPPORTED: u64 = 0x1ffff | (0xf << 20); +const DEVICE_MASK: u64 = 0x1858_3fff; +const DEVICE_RESET: [u64; DEVICES as usize] = [0x1800_1000, 0x1800_1002, 0x1800_1004, 0x1800_1005]; +const DEVICE_SWAP: u64 = 1 << 19; +const DEVICE_STREAM: u64 = 1 << 13; +const DEVICE_WIDE: u64 = 1 << 22; +const DEVICE_MEMORY: u64 = 1 << 12; +const INVALID_ADDRESS: u64 = 1 << 24; +/// PCI master timeout: a cycle no device claimed. The same bit in the +/// interrupt status, enable and error view registers. +const MASTER_TIMEOUT: u64 = 1 << 11; +/// The error view: the error bits, whether or not they are enabled. +/// Read-only; any write to the reset register clears it. +const ERROR_VIEW: usize = reg::INTERRUPT_MULTIPLE; +const SET: u64 = 0x100; +/// The force registers, one write-only slot per input: a write to +/// force-always sends the input's set packet whatever its level, and to +/// force-with-pin only while the input is active. The data is ignored. +const FORCE_ALWAYS: usize = 0x180; +const FORCE_PIN: usize = 0x1c0; +const FORCE_END: usize = 0x200; +const LEVELS: std::ops::RangeInclusive = 7..=127; + +/// The head registers the PIC shares with the Bridge family, as byte offsets +/// into a head's register block. +pub mod reg { + pub const IDENTIFICATION: usize = 0x0; + pub const CONTROL: usize = 0x20; + pub const REQUEST_TIMEOUT: usize = 0x28; + pub const INT_DEST_UPPER: usize = 0x30; + pub const INT_DEST_LOWER: usize = 0x38; + pub const DIRECT_MAP: usize = 0x80; + pub const PCI_TYPE1_CONFIG: usize = 0xc8; + pub const INTERRUPT_STATUS: usize = 0x100; + pub const INTERRUPT_ENABLE: usize = 0x108; + pub const INTERRUPT_RESET: usize = 0x110; + pub const INTERRUPT_MODE: usize = 0x118; + pub const INTERRUPT_DEVICE: usize = 0x120; + pub const INTERRUPT_ERROR_HOST: usize = 0x128; + pub const INTERRUPT_HOST_BASE: usize = 0x130; + pub const INTERRUPT_HOST_LAST: usize = 0x168; + pub const INTERRUPT_MULTIPLE: usize = 0x170; + pub const DEVICE_BASE: usize = 0x200; +} + +/// A run of a head's address space that one BAR covers whole, and the bus +/// device that answers it. +struct PioRun { + start: u64, + end: u64, + dev: Arc, +} + +/// Whole BAR-page runs are few and carry no RAM or native fault-handler state. +/// Readers clone one endpoint and release the table lock before calling a device. +#[derive(Default)] +struct PioDecode { + runs: RwLock>, +} +impl PioDecode { + fn publish(&self, mut runs: Vec) -> Vec { + runs.sort_unstable_by_key(|run| run.start); + debug_assert!(runs.windows(2).all(|pair| pair[0].end <= pair[1].start)); + std::mem::replace(&mut *self.runs.write(), runs) + } + fn target(&self, address: u64, size: usize) -> Option> { + let runs = self.runs.read(); + let i = runs.partition_point(|run| run.start <= address).checked_sub(1)?; + let run = &runs[i]; + (address.checked_add(size as u64)? <= run.end).then(|| run.dev.clone()) + } +} + +/// The widget identity word of a head: revision, part and manufacturer +/// packed as the common widget header lays them out, with bit 0 set. +pub const fn identity(head: usize) -> u32 { + (REVISION << 28) | (PARTS[head] << 12) | (MANUFACTURER << 1) | 1 +} + +/// A configuration cycle no device claimed: a PCI master timeout. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Unclaimed; + +/// What the machine's side of the XIO link refused: no target, a permission +/// check, or an address with nothing behind it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refused; + +/// The far end of the link: where the packets this chip originates go. On +/// Tezro that is Bedrock IO. Bytes are in address order, as the XIO packet +/// carries them. +pub trait PicPeer: Send + Sync { + /// An interrupt packet: `payload` is the vector, with bit 8 for assert, + /// written to the 48-bit destination `address`. + fn interrupt(&self, source_widget: u8, address: u64, payload: u64) -> Result<(), Refused>; + + /// A DMA read of `data.len()` bytes at the 48-bit `address` of `widget`. + fn dma_read(&self, source_widget: u8, widget: u8, address: u64, data: &mut [u8]) -> Result<(), Refused>; + + fn dma_write(&self, source_widget: u8, widget: u8, address: u64, data: &[u8]) -> Result<(), Refused>; +} + +/// How the board uses a PCI function's slot. +#[derive(Clone, Copy, Debug, Default)] +pub struct Function { + /// The function's registers keep the processor's byte sequence (IOC4), + /// so the bridge leaves the address and value of a processor access + /// alone instead of placing the bytes for a little-endian adapter. + pub self_ordered_pio: bool, + /// The slot drives REQ64# and the function takes 64-bit data phases: + /// its swap-clear direct64 DMA reverses each doubleword, not each word. + pub req64: bool, +} + +/// The peer was already connected. +#[derive(Debug, PartialEq, Eq)] +pub struct AlreadyConnected; + +/// Wiring set once before the machine runs. +struct Wiring { + peer: OnceLock>, +} + +struct State { + reg: [u64; REGISTER_BYTES / 8], + request: [[u64; REQUEST_ENTRIES]; REQUEST_BANKS], + pins: u8, + set_requests: [u32; PINS], + clear_requests: [u32; PINS], +} + +impl State { + fn new() -> Self { + let mut s = Self { + reg: [0; REGISTER_BYTES / 8], + request: [[0; REQUEST_ENTRIES]; REQUEST_BANKS], + pins: 0, + set_requests: [0; PINS], + clear_requests: [0; PINS], + }; + s.reset(); + s + } + + fn r(&self, off: usize) -> u64 { + self.reg[off / 8] + } + + fn set(&mut self, off: usize, value: u64) { + self.reg[off / 8] = value; + } + + fn reset(&mut self) { + *self = Self { + reg: [0; REGISTER_BYTES / 8], + request: [[0; REQUEST_ENTRIES]; REQUEST_BANKS], + pins: 0, + set_requests: [0; PINS], + clear_requests: [0; PINS], + }; + self.set(reg::REQUEST_TIMEOUT, REQUEST_TIMEOUT_RESET); + self.set(reg::CONTROL, CONTROL_RESET); + for (i, v) in DEVICE_RESET.iter().enumerate() { + self.set(reg::DEVICE_BASE + i * 8, *v); + } + } +} + +/// The head's PCI interrupt routing. Device `d`'s INTA reaches input `d - 1`; +/// INTB to INTD are not wired. The eight inputs are shared with the board's +/// own interrupts, which reach a head through [`Pic::pin_line`]. Device 0 is +/// the bridge itself and holds no function. +/// +/// INTA of device `d` on input `d - 1` is what IRIX 6.5.30's pin rule +/// assigns (INTA and INTC to input `d - 1`, INTB and INTD to input +/// `(d - 1) ^ 4`), and an Origin 350's IO9 reports its IOC4, ISP12160 and +/// BCM5701 on inputs 0, 2 and 3 under OpenBSD: established as what software +/// expects. The board's own wiring is inferred from that, and INTB to INTD +/// are unknown; no function on the IO9 uses them. +static PIC_ROUTING: IntxRouting = IntxRouting { + devices: DEVICES + 1, + irqs: PINS as u8, + intx: &[ + (1, [Some(0), None, None, None]), + (2, [Some(1), None, None, None]), + (3, [Some(2), None, None, None]), + (4, [Some(3), None, None, None]), + ], + board_irqs: &[0, 1, 2, 3, 4, 5, 6, 7], +}; + +/// The head's CPU windows, by [`CpuWindow`] index: the four short device-I/O +/// windows, then the large PCI memory window. Addresses are the widget's. +const SHORT_WINDOWS: usize = 4; +const BIG_WINDOW: usize = 4; + +/// The short windows' extents in the head's own space. +const SHORT: [(u64, u64); SHORT_WINDOWS] = [(0x20_0000, 0x20_0000), (0x40_0000, 0x20_0000), (0x60_0000, 0x10_0000), (0x70_0000, 0x10_0000)]; + +fn short_window(addr: u64) -> Option<(usize, u64, u64)> { + match addr { + 0x20_0000..=0x3f_ffff => Some((0, addr - 0x20_0000, 0x20_0000)), + 0x40_0000..=0x5f_ffff => Some((1, addr - 0x40_0000, 0x20_0000)), + 0x60_0000..=0x6f_ffff => Some((2, addr - 0x60_0000, 0x10_0000)), + 0x70_0000..=0x7f_ffff => Some((3, addr - 0x70_0000, 0x10_0000)), + _ => None, + } +} + +fn defined(off: usize) -> bool { + matches!( + off, + reg::IDENTIFICATION + | reg::CONTROL + | reg::REQUEST_TIMEOUT + | reg::INT_DEST_UPPER + | reg::INT_DEST_LOWER + | FLUSH + | reg::DIRECT_MAP + | reg::PCI_TYPE1_CONFIG + | reg::INTERRUPT_STATUS + | reg::INTERRUPT_ENABLE + | reg::INTERRUPT_RESET + | reg::INTERRUPT_MODE + | reg::INTERRUPT_DEVICE + | reg::INTERRUPT_ERROR_HOST + | reg::INTERRUPT_HOST_BASE..=reg::INTERRUPT_HOST_LAST + | ERROR_VIEW + | reg::DEVICE_BASE..=0x218 + ) +} + +fn request_slot(off: usize) -> (usize, usize) { + let rel = off - REQUEST_BASE; + (rel / REQUEST_BANK_SIZE, (rel % REQUEST_BANK_SIZE) / 8) +} + +/// The largest naturally aligned access of at most eight bytes that starts +/// at `address` and fits in `remaining`. +fn natural(address: u64, remaining: usize) -> usize { + let mut n = 8; + while n > remaining || !address.is_multiple_of(n as u64) { + n /= 2; + } + n +} + +fn le_bytes(size: usize, value: u64) -> [u8; 8] { + let mut b = [0; 8]; + b[..size].copy_from_slice(&value.to_le_bytes()[..size]); + b +} + +fn be_bytes(size: usize, value: u64) -> [u8; 8] { + let mut b = [0; 8]; + b[..size].copy_from_slice(&value.to_be_bytes()[8 - size..]); + b +} + +fn le_value(bytes: &[u8]) -> u64 { + bytes.iter().rev().fold(0, |v, &b| (v << 8) | b as u64) +} + +fn be_value(bytes: &[u8]) -> u64 { + bytes.iter().fold(0, |v, &b| (v << 8) | b as u64) +} + +/// One bridge head: registers, one PCI bus, and its interrupt inputs. +// Lock order: `order`, then `st`. Packets go to the peer and the PIs with +// `st` dropped; `order` keeps two threads' packets in the order decided. +struct Head { + index: usize, + wiring: Arc, + st: Mutex, + order: Mutex<()>, + /// The head's PCI bus: its functions, BAR windows and interrupt pins. + bus: PciBus, + /// How the board uses each device's slot, by device number. + slots: RwLock<[Option; DEVICES as usize + 1]>, + /// Whole-page BAR mappings of the windows, which the processor reaches + /// without a search of the bus. + pio: PioDecode, + unsupported: Mutex>, + /// This head, for the DMA a function is given: it must not keep the head alive. + me: Weak, +} + +struct SlotDma { + head: Weak, + device: u8, +} + +impl PciDma for SlotDma { + fn dma_read(&self, address: u64, buf: &mut [u8]) -> PciDmaStatus { + match self.head.upgrade() { + Some(head) => head.dma(self.device, address, buf, false), + None => { + buf.fill(0); + PciDmaStatus::MasterAbort + } + } + } + + fn dma_write(&self, address: u64, data: &[u8]) -> PciDmaStatus { + match self.head.upgrade() { + Some(head) => head.dma(self.device, address, &mut data.to_vec(), true), + None => PciDmaStatus::MasterAbort, + } + } +} + +/// What the head's bus asks of it: the DMA of a requester, and an input's +/// level, as the OR of every device line and board interrupt routed to it. +impl PciHost for Head { + fn dma_for(&self, requester: u8) -> Arc { + Arc::new(SlotDma { head: self.me.clone(), device: requester }) + } + + fn set_irq(&self, pin: u8, level: bool) { + self.pin(pin as usize, level); + } +} + +impl Head { + fn new(index: usize, wiring: Arc) -> Arc { + let this = Arc::new_cyclic(|me: &Weak| Self { + index, + wiring, + st: Mutex::new(State::new()), + order: Mutex::new(()), + // iris's bus numbers count per bus tree; the heads are separate trees. + bus: PciBus::new(index as u8 * 16, &PIC_ROUTING, me.clone()), + slots: RwLock::new([None; DEVICES as usize + 1]), + pio: PioDecode::default(), + unsupported: Mutex::new(Vec::new()), + me: me.clone(), + }); + this.bus.set_host_windows(&|| this.windows()); + let head = Arc::downgrade(&this); + this.bus.on_bar_mappings(Box::new(move |runs| { + if let Some(head) = head.upgrade() { + head.publish_mappings(runs); + } + })); + this + } + + fn note_unsupported(&self, addr: u64, size: usize, write: bool, value: u64) { + let mut seen = self.unsupported.lock(); + if seen.len() >= UNSUPPORTED_LIMIT { + return; + } + seen.push((self.index, write, addr, size)); + crate::dlog!( + LogModule::Bedrock, + "pic: head {} {} offset {addr:#x} size {size} value {value:#x} unsupported{}", + self.index, + if write { "write" } else { "read" }, + if seen.len() == UNSUPPORTED_LIMIT { "; further accesses suppressed" } else { "" } + ); + } + + fn source_widget(&self) -> u8 { + (self.st.lock().r(reg::CONTROL) & 0xf) as u8 + } + + // ---- interrupts ---- + + /// Send pin `pin`'s set or clear packet. True when the peer took it. + fn packet(&self, pin: usize, set: bool) -> bool { + let (destination, source) = { + let mut st = self.st.lock(); + if set { + st.set_requests[pin] += 1; + } else { + st.clear_requests[pin] += 1; + } + (st.r(reg::INTERRUPT_HOST_BASE + pin * 8), (st.r(reg::CONTROL) & 0xf) as u8) + }; + let level = destination >> 48; + if !LEVELS.contains(&level) { + crate::dlog!(LogModule::Bedrock, "pic: head {} pin {pin} rejected interrupt level {level:#x}", self.index); + return false; + } + let address = destination & ((1 << PAYLOAD_BITS) - 1); + let payload = level | if set { SET } else { 0 }; + let sent = self.send_interrupt(source, address, payload); + if !sent { + crate::dlog!(LogModule::Bedrock, "pic: head {} interrupt destination {destination:#x} refused", self.index); + } + sent + } + + fn send_interrupt(&self, source: u8, address: u64, payload: u64) -> bool { + match self.wiring.peer.get() { + Some(peer) => peer.interrupt(source, address, payload).is_ok(), + None => { + crate::dlog!(LogModule::Bedrock, "pic: head {} interrupt has no peer", self.index); + false + } + } + } + + fn pin(&self, pin: usize, level: bool) { + if pin >= PINS { + return; + } + let _order = self.order.lock(); + let bit = 1u8 << pin; + let (was_high, enabled, mode) = { + let mut st = self.st.lock(); + let was_high = st.pins & bit != 0; + st.pins = (st.pins & !bit) | if level { bit } else { 0 }; + if level && !was_high { + let status = st.r(reg::INTERRUPT_STATUS) | bit as u64; + st.set(reg::INTERRUPT_STATUS, status); + } + (was_high, st.r(reg::INTERRUPT_ENABLE) & bit as u64 != 0, st.r(reg::INTERRUPT_MODE) & bit as u64 != 0) + }; + if level && !was_high && enabled { + self.packet(pin, true); + } + if was_high && !level && mode { + self.packet(pin, false); + } + } + + /// The PROM's write to the link-side error address slot latches the + /// invalid-address condition and, when enabled, sends one packet. + fn invalid_address(&self) { + let _order = self.order.lock(); + let enabled = { + let mut st = self.st.lock(); + let status = st.r(reg::INTERRUPT_STATUS) | INVALID_ADDRESS; + st.set(reg::INTERRUPT_STATUS, status); + st.r(reg::INTERRUPT_ENABLE) & INVALID_ADDRESS != 0 + }; + if enabled { + self.error_interrupt(); + } + } + + /// An unclaimed configuration read: the error view always records the + /// master timeout, and interrupt status only while it is enabled, when + /// it also sends the error interrupt. + fn master_timeout(&self) { + let _order = self.order.lock(); + let enabled = { + let mut st = self.st.lock(); + let view = st.r(ERROR_VIEW) | MASTER_TIMEOUT; + st.set(ERROR_VIEW, view); + let enabled = st.r(reg::INTERRUPT_ENABLE) & MASTER_TIMEOUT != 0; + if enabled { + let status = st.r(reg::INTERRUPT_STATUS) | MASTER_TIMEOUT; + st.set(reg::INTERRUPT_STATUS, status); + } + enabled + }; + if enabled { + self.error_interrupt(); + } + } + + /// One set packet for the error interrupt, to the widget destination. + /// With `order` held. + fn error_interrupt(&self) { + let number = self.st.lock().r(reg::INTERRUPT_ERROR_HOST); + if number > 0x7f { + crate::dlog!(LogModule::Bedrock, "pic: head {} refused error interrupt number {number:#x}", self.index); + return; + } + let Some(address) = self.error_destination() else { return }; + let source = self.source_widget(); + if !self.send_interrupt(source, address, number | SET) { + crate::dlog!(LogModule::Bedrock, "pic: head {} error destination {address:#x} refused", self.index); + } + } + + /// The error interrupt's address from the widget destination pair: a + /// lower register that uses bits 47:32 is the address (the upper half + /// may repeat them), otherwise the pair joins into 48 bits. + fn error_destination(&self) -> Option { + let (upper, lower) = { + let st = self.st.lock(); + (st.r(reg::INT_DEST_UPPER), st.r(reg::INT_DEST_LOWER)) + }; + let upper_address = upper & 0xffff; + let lower_high = lower >> 32; + let refuse = |why: &str| { + crate::dlog!(LogModule::Bedrock, "pic: head {} error destination {why} (upper {upper:#x}, lower {lower:#x})", self.index); + None + }; + if upper >> 32 != 0 { + return refuse("has unsupported high bits"); + } + if lower >> 48 != 0 { + return refuse("exceeds 48 bits"); + } + if upper_address != 0 && lower_high != 0 && upper_address != lower_high { + return refuse("has conflicting address fields"); + } + let address = if lower_high != 0 { lower } else { (upper_address << 32) | (lower & 0xffff_ffff) }; + if address == 0 { + return refuse("is zero"); + } + Some(address) + } + + // ---- registers ---- + + fn identity(&self) -> u64 { + identity(self.index) as u64 + } + + /// The widget address of head address `addr`. + fn widget_address(&self, addr: u64) -> u64 { + self.index as u64 * HEAD_OFFSET + addr + } + + /// `None` is a read the processor gets back as a bus error: only an + /// unclaimed configuration read fails that way. + fn read(&self, addr: u64, size: usize) -> Option { + if short_window(addr).is_some() { + return Some(self.short(self.widget_address(addr), size, None).unwrap_or(0)); + } + if (CONFIG_BASE..CONFIG_END).contains(&addr) && size <= 4 { + return self.config(addr, size, None).ok(); + } + let off = addr as usize; + if size == 4 && off % 8 == 4 { + return Some(self.word_read(off)); + } + if size == 8 && off == 0 { + return Some(self.identity()); + } + if size == 8 && off % 8 == 0 && defined(off) { + return Some(self.st.lock().r(off)); + } + if size == 8 && off % 8 == 0 && (REQUEST_BASE..REQUEST_END).contains(&off) { + let (bank, index) = request_slot(off); + return Some(self.st.lock().request[bank][index]); + } + self.note_unsupported(addr, size, false, 0); + Some(0) + } + + fn write(&self, addr: u64, size: usize, value: u64) { + if short_window(addr).is_some() { + self.short(self.widget_address(addr), size, Some(value)); + return; + } + if addr == ERROR_ADDRESS_WORD && size == 4 { + self.invalid_address(); + return; + } + if (CONFIG_BASE..CONFIG_END).contains(&addr) && size <= 4 { + let _ = self.config(addr, size, Some(value)); + return; + } + let off = addr as usize; + if (FORCE_ALWAYS..FORCE_END).contains(&off) { + self.force_write(off, size, value); + return; + } + if size == 4 && off % 8 == 4 { + self.word_write(off, value as u32); + return; + } + self.register_write(off, size, value); + } + + /// A write to a force register: a doubleword to its slot or a word to + /// the slot's low half, as for the other registers. Other widths and + /// every read are unsupported and observable. + fn force_write(&self, off: usize, size: usize, value: u64) { + let slot = off & !7; + if !matches!((size, off - slot), (8, 0) | (4, 4)) { + self.note_unsupported(off as u64, size, true, value); + return; + } + let pin = (slot - FORCE_ALWAYS) / 8 % PINS; + self.force(pin, slot < FORCE_PIN); + } + + /// Send input `pin`'s set packet again on software's request: always, + /// or only while the input is active. Software writes force-with-pin + /// at the end of each interrupt dispatch, so an input that is still + /// active after its handler ran is delivered again; the chip itself + /// sends a packet only when an input rises. The status bit follows the + /// input and is not changed. Whether the interrupt enable gates a + /// forced packet is unknown; the model does not gate it. + fn force(&self, pin: usize, always: bool) { + let _order = self.order.lock(); + let active = self.st.lock().pins & (1 << pin) != 0; + if always || active { + self.packet(pin, true); + } + } + + /// A 32-bit access to the low half of an eight-byte slot. + fn word_slot(off: usize) -> Option { + let slot = off.checked_sub(4).filter(|slot| slot % 8 == 0)?; + ((REQUEST_BASE..REQUEST_END).contains(&slot) || (defined(slot) && slot < REGISTER_BYTES)).then_some(slot) + } + + fn word_read(&self, off: usize) -> u64 { + match Self::word_slot(off) { + Some(slot) if slot != FLUSH => { + if slot == reg::IDENTIFICATION { + return self.identity(); + } + let st = self.st.lock(); + let full = if slot >= REQUEST_BASE { + let (bank, index) = request_slot(slot); + st.request[bank][index] + } else { + st.r(slot) + }; + full as u32 as u64 + } + _ => { + self.note_unsupported(off as u64, 4, false, 0); + 0 + } + } + } + + fn word_write(&self, off: usize, value: u32) { + let slot = match Self::word_slot(off) { + Some(slot) if !matches!(slot, reg::IDENTIFICATION | FLUSH | reg::INTERRUPT_STATUS | reg::INTERRUPT_DEVICE | ERROR_VIEW) => slot, + _ => { + self.note_unsupported(off as u64, 4, true, value as u64); + return; + } + }; + if slot >= REQUEST_BASE { + let (bank, index) = request_slot(slot); + let entry = &mut self.st.lock().request[bank][index]; + *entry = (*entry & 0xffff_ffff_0000_0000) | value as u64; + } else if slot == reg::INTERRUPT_RESET { + self.register_write(slot, 8, value as u64); + } else { + let merged = (self.st.lock().r(slot) & 0xffff_ffff_0000_0000) | value as u64; + self.register_write(slot, 8, merged); + } + } + + fn register_write(&self, off: usize, size: usize, value: u64) { + if size != 8 || off % 8 != 0 { + self.note_unsupported(off as u64, size, true, value); + return; + } + if (REQUEST_BASE..REQUEST_END).contains(&off) { + let (bank, index) = request_slot(off); + self.st.lock().request[bank][index] = value; + return; + } + if !defined(off) || off >= REGISTER_BYTES { + self.note_unsupported(off as u64, size, true, value); + return; + } + match off { + reg::IDENTIFICATION | FLUSH | reg::INTERRUPT_STATUS | ERROR_VIEW => { + self.note_unsupported(off as u64, size, true, value) + } + reg::CONTROL => { + if value & !CONTROL_MASK != 0 { + self.note_unsupported(off as u64, size, true, value); + } + self.st.lock().set(off, value & CONTROL_MASK); + } + reg::DIRECT_MAP => { + if value & !DIRECT_MAP_SUPPORTED != 0 { + self.note_unsupported(off as u64, size, true, value); + } + self.st.lock().set(off, value & DIRECT_MAP_SUPPORTED); + } + reg::INTERRUPT_ENABLE => { + let _order = self.order.lock(); + let (newly, pins, status) = { + let mut st = self.st.lock(); + let newly = value & !st.r(off); + st.set(off, value); + (newly, st.pins as u64, st.r(reg::INTERRUPT_STATUS)) + }; + for pin in 0..PINS { + if newly & status & pins & (1 << pin) != 0 { + self.packet(pin, true); + } + } + } + reg::INTERRUPT_RESET => { + let _order = self.order.lock(); + let (pins, enable) = { + let mut st = self.st.lock(); + // Each error bit clears on its own one; any write + // clears the view. + let status = (st.r(reg::INTERRUPT_STATUS) & !value) | st.pins as u64; + st.set(reg::INTERRUPT_STATUS, status); + st.set(ERROR_VIEW, 0); + (st.pins as u64, st.r(reg::INTERRUPT_ENABLE)) + }; + for pin in 0..PINS { + if value & pins & enable & (1 << pin) != 0 { + self.packet(pin, true); + } + } + } + reg::INTERRUPT_DEVICE => { + // The device-to-pin map has no effect here; the board wires + // each device to its pin. + self.note_unsupported(off as u64, size, true, value); + self.st.lock().set(off, value); + } + _ => { + let device = (reg::DEVICE_BASE..=0x218).contains(&off); + let stored = if device { + if value & !DEVICE_MASK != 0 { + self.note_unsupported(off as u64, size, true, value); + } + value & DEVICE_MASK + } else { + value + }; + self.st.lock().set(off, stored); + if device { + self.bus.set_host_windows(&|| self.windows()); + } + } + } + } + + // ---- PCI configuration ---- + + /// A processor configuration cycle. Bus 0 devices sit at `0x1000` + /// strides from `0x20000`; the type-1 window addresses the bus and + /// device `PCI_TYPE1_CONFIG` names. A read no function claims, device 0 + /// (the bridge's own) and every function but 0 included, is a master + /// timeout and fails; a write nobody claims goes nowhere. + fn config(&self, addr: u64, size: usize, value: Option) -> Result { + let mask = u32::MAX >> (32 - size * 8); + let function = ((addr >> 8) & 7) as u8; + let (bus, device) = if addr < CONFIG_TYPE1 { + (Some(self.bus.clone()), ((addr - CONFIG_BASE) >> 12) as u8) + } else { + let t1 = self.st.lock().r(reg::PCI_TYPE1_CONFIG); + (self.bus.find_bus(((t1 >> 16) & 0xff) as u8), ((t1 >> 11) & 0x1f) as u8) + }; + let register = ((addr & 0xff) as usize) ^ (4 - size); + let at = DevFn { device, function }; + let answered = match value { + Some(value) => bus.is_some_and(|bus| bus.config_write(at, register, size, value as u32 & mask)), + None => { + let read = bus.and_then(|bus| bus.config_read(at, register, size)); + if let Some(read) = read { + return Ok(read as u64); + } + false + } + }; + if !answered { + crate::dlog!( + LogModule::Bedrock, + "pic: head {} configuration {} miss at {addr:#x}", + self.index, + if value.is_some() { "write" } else { "read" } + ); + } + if value.is_some() { + return Ok(0); + } + self.master_timeout(); + Err(Unclaimed) + } + + // ---- PCI memory and I/O cycles ---- + + /// The CPU windows, from the Device registers; see [`SHORT_WINDOWS`]. A + /// short window whose Device register asks for a byte stream or a wide + /// device is one the model refuses, and has no run. + fn windows(&self) -> Vec { + let st = self.st.lock(); + let base = self.index as u64 * HEAD_OFFSET; + (0..=SHORT_WINDOWS) + .map(|kind| { + if kind < SHORT_WINDOWS { + let device = st.r(reg::DEVICE_BASE + kind * 8); + let (space, pci) = Self::device(device, kind); + let (start, len) = SHORT[kind]; + let len = if device & (DEVICE_STREAM | DEVICE_WIDE) != 0 { 0 } else { len }; + CpuWindow { space, cpu: base + start, pci, len, mask: u64::MAX } + } else { + CpuWindow { space: Space::Mem, cpu: PCI_WINDOW[self.index], pci: 0, len: PCI_WINDOW_SIZE, mask: u64::MAX } + } + }) + .collect() + } + + /// Device register `device` of short window `kind`: the space it opens + /// onto and the PCI address of the window's first byte. + fn device(device: u64, kind: usize) -> (Space, u32) { + let memory = device & DEVICE_MEMORY != 0; + let mut base = (device & if memory { 0x3ff } else { 0xfff }) as u32; + if kind < 2 { + base &= !1; + } + (if memory { Space::Mem } else { Space::Io }, base << 20) + } + + /// The byte order of a processor access reaching `f`. A conventional + /// adapter sees a numeric value at the address with its low two bits + /// corrected (MACE's rule); a self-ordered function (IOC4) keeps the + /// processor's byte sequence at the address as it is. The order of a + /// 64-bit access to a conventional adapter is unknown. + fn order(&self, f: &dyn PciFunction) -> ByteOrder { + let same = |g: &Arc| std::ptr::addr_eq(Arc::as_ptr(g), f as *const dyn PciFunction); + let device = (1..=DEVICES).find(|&device| self.bus.function(DevFn { device, function: 0 }).is_some_and(same)); + let native = device.is_some_and(|device| self.slots.read()[device as usize].is_some_and(|slot| slot.self_ordered_pio)); + if native { ByteOrder::Stream } else { ByteOrder::values(4) } + } + + /// The bus reports the BAR mappings the head's windows hold: serve them + /// from the head's own table. + fn publish_mappings(&self, runs: Vec) { + let runs = runs + .iter() + .map(|r| { + let base = r.cpu.wrapping_sub(r.off as u64); + PioRun { + start: r.cpu, + end: r.cpu + r.len, + dev: Arc::new(BarDevice::new(r.function.clone(), r.bar, base, u64::MAX, self.order(&*r.function))), + } + }) + .collect(); + // Replaced runs may run shutdown callbacks from their destructor. + drop(self.pio.publish(runs)); + } + + /// A processor access to the widget address `addr` of a short window or + /// the PCI memory window: a whole BAR page answers it from the head's + /// table, anything else through the bus's decode. Nothing claiming it + /// reads zero and drops a write; the caller says where that is a + /// refusal worth logging. + fn pci_access(&self, addr: u64, size: usize, value: Option) -> Option { + let crossing = addr & 0xfff > 0x1000 - size as u64; + if !crossing { + if let Some(endpoint) = self.pio.target(addr, size) { + return Some(match value { + Some(v) => { + match size { + 1 => endpoint.write8(addr, v as u8), + 2 => endpoint.write16(addr, v as u16), + 4 => endpoint.write32(addr, v as u32), + _ => endpoint.write64(addr, v), + }; + 0 + } + None => match size { + 1 => endpoint.read8(addr).data as u64, + 2 => endpoint.read16(addr).data as u64, + 4 => endpoint.read32(addr).data as u64, + _ => endpoint.read64(addr).data, + }, + }); + } + } + let (space, pci) = self.pci_address(addr)?; + let (function, bar, offset) = self.bus.decode(space, pci)?; + let order = self.order(function); + Some(match value { + None => order.read(function, bar, offset, size), + Some(v) => { + order.write(function, bar, offset, size, v); + 0 + } + }) + } + + /// The space and PCI address the widget address `addr` reaches. + fn pci_address(&self, addr: u64) -> Option<(Space, u32)> { + let rel = addr.checked_sub(self.index as u64 * HEAD_OFFSET); + if let Some((window, offset, _)) = rel.and_then(short_window) { + let (space, base) = Self::device(self.st.lock().r(reg::DEVICE_BASE + window * 8), window); + return Some((space, base.wrapping_add(offset as u32))); + } + let offset = addr.checked_sub(PCI_WINDOW[self.index])?; + (offset < PCI_WINDOW_SIZE).then_some((Space::Mem, offset as u32)) + } + + /// The short device-I/O windows: the Device register of the window + /// picks PCI memory or I/O space and the megabyte of it. + fn short(&self, addr: u64, size: usize, value: Option) -> Option { + let write = value.is_some(); + let (window, offset, window_size) = short_window(addr % HEAD_OFFSET)?; + if size as u64 > window_size - offset { + crate::dlog!(LogModule::Bedrock, "pic: head {} short window {addr:#x} rejects boundary-crossing {size}-byte access", self.index); + return None; + } + let device = self.st.lock().r(reg::DEVICE_BASE + window * 8); + let refused = device & (DEVICE_STREAM | DEVICE_WIDE); + if refused != 0 { + crate::dlog!( + LogModule::Bedrock, + "pic: head {} short window {addr:#x} refuses unsupported Device attributes {refused:#x}", + self.index + ); + return None; + } + let result = self.pci_access(addr, size, value); + if result.is_none() { + crate::dlog!( + LogModule::Bedrock, + "pic: head {} short window {addr:#x} PCI {} failed for {size} bytes", + self.index, + if write { "write" } else { "read" } + ); + } + result + } + + /// The large PCI memory window of this head. + fn pci_window(&self, addr: u64, size: usize, value: Option) -> u64 { + let result = self.pci_access(addr, size, value); + if result.is_none() { + self.note_unsupported(addr, size, value.is_some(), value.unwrap_or(0)); + } + result.unwrap_or(0) + } + + // ---- DMA ---- + + fn dma(&self, device: u8, address: u64, bytes: &mut [u8], write: bool) -> PciDmaStatus { + if !(1..=DEVICES).contains(&device) { + crate::dlog!(LogModule::Bedrock, "pic: head {} unsupported requester device {device}", self.index); + if !write { + bytes.fill(0); + } + return PciDmaStatus::MasterAbort; + } + let mut done = 0; + while done < bytes.len() { + let at = address.wrapping_add(done as u64); + let n = (0x1000 - (at as usize & 0xfff)).min(bytes.len() - done); + let ok = address.checked_add(done as u64).is_some() && self.dma_page(device, at, &mut bytes[done..done + n], write); + if !ok { + if !write { + bytes[done..].fill(0); + } + return PciDmaStatus::MasterAbort; + } + done += n; + } + PciDmaStatus::Ok + } + + fn dma_page(&self, device: u8, at: u64, chunk: &mut [u8], write: bool) -> bool { + if at > u32::MAX as u64 { + return self.direct64(device, at, chunk, write); + } + if at < 0x8000_0000 { + crate::dlog!(LogModule::Bedrock, "pic: head {} unsupported DMA {at:#x} from device {device}", self.index); + return false; + } + self.direct32(device, at, chunk, write) + } + + fn send_dma(&self, source: u8, widget: u8, address: u64, data: &mut [u8], write: bool) -> bool { + let Some(peer) = self.wiring.peer.get() else { + crate::dlog!(LogModule::Bedrock, "pic: head {} DMA has no peer", self.index); + return false; + }; + if write { + peer.dma_write(source, widget, address, data).is_ok() + } else { + peer.dma_read(source, widget, address, data).is_ok() + } + } + + /// The direct map: PCI `0x80000000` and up reach the widget `DIR_MAP` + /// names, at the address its high bits and the PCI offset make. + fn direct32(&self, device: u8, at: u64, chunk: &mut [u8], write: bool) -> bool { + let (direct, attributes, source) = { + let st = self.st.lock(); + (st.r(reg::DIRECT_MAP), st.r(reg::DEVICE_BASE + (device as usize - 1) * 8), (st.r(reg::CONTROL) & 0xf) as u8) + }; + let widget = ((direct >> 20) & 0xf) as u8; + let target = ((direct & 0x1ffff) << 31) | (at & 0x7fff_ffff); + if attributes & DEVICE_SWAP != 0 { + // A swapped slot puts each byte at its address with the low two + // bits flipped. + return self.send_flipped(source, widget, target, chunk, write, 3, |_, _| true); + } + let mut done = 0; + while done < chunk.len() { + let n = natural(target + done as u64, chunk.len() - done); + if !self.send_dma(source, widget, target + done as u64, &mut chunk[done..done + n], write) { + return false; + } + done += n; + } + true + } + + /// Why a direct64 request cannot be carried, if it cannot. It is + /// checked in full before any packet leaves. + fn direct64_refusal(address: u64, size: usize) -> Option<&'static str> { + let payload = address & ((1 << PAYLOAD_BITS) - 1); + if address >> 60 == 0 { + Some("target-zero") + } else if address & ((1 << 54) | (0x3f << 48)) != 0 { + Some("reserved-bits") + } else if payload & (1 << 47) != 0 { + Some("register-space") + } else if size as u64 > (1 << 47) - payload { + Some("range") + } else { + None + } + } + + /// A PCI address above 32 bits carries the target widget in bits 63:60 + /// and the XIO payload below. Bit 55 is the swap attribute: set, the + /// bytes go as an ordered byte stream; clear, they keep 32-bit numeric + /// values (the byte address low bits flip by 3, or by 7 for a REQ64 + /// slot's 64-bit requester). Bits 56 to 59 (barrier, virtual channel, + /// precise, prefetch) change nothing here. + fn direct64(&self, device: u8, at: u64, chunk: &mut [u8], write: bool) -> bool { + let wide = self.slots.read()[device as usize].is_some_and(|slot| slot.req64); + let source = self.source_widget(); + let check = |address: u64, size: usize| match Self::direct64_refusal(address, size) { + Some(why) => { + crate::dlog!(LogModule::Bedrock, "pic: head {} direct64 {why} address {address:#018x} from device {device}", self.index); + false + } + None => true, + }; + if !check(at, 1) { + return false; + } + let widget = (at >> 60) as u8; + let payload = at & ((1 << PAYLOAD_BITS) - 1); + if at & (1 << 55) == 0 { + let mask = if wide { 7 } else { 3 }; + return self.send_flipped(source, widget, payload, chunk, write, mask, |offset, n| check(at + offset, n)); + } + let mut done = 0; + while done < chunk.len() { + // Packets end on doubleword boundaries, so an aligned field + // never straddles two of them. + let n = (8 - ((payload + done as u64) & 7) as usize).min(chunk.len() - done); + if !check(at + done as u64, n) || !self.send_dma(source, widget, payload + done as u64, &mut chunk[done..done + n], write) { + return false; + } + done += n; + } + true + } + + /// Carry a transfer whose bytes land at their addresses with the low + /// bits in `mask` flipped, which reverses the bytes of each aligned + /// group of `mask + 1`, the data phase. The flip moves bytes within a + /// group, not the transfer apart: a run of whole groups goes as one + /// packet, which the peer applies one aligned group at a time, so a + /// processor reading a word the device is updating sees all of it old + /// or all of it new; the run as a whole is not atomic, as a burst on + /// the bus is not. A group the transfer covers in part writes only + /// those bytes, still contiguous once flipped, as one packet. + /// `fits` vets each packet by its offset in `chunk` and its length + /// before it leaves. + #[allow(clippy::too_many_arguments)] + fn send_flipped( + &self, + source: u8, + widget: u8, + address: u64, + chunk: &mut [u8], + write: bool, + mask: u64, + fits: impl Fn(u64, usize) -> bool, + ) -> bool { + let group = mask as usize + 1; + let mut done = 0; + while done < chunk.len() { + let here = address + done as u64; + let whole = if here & mask == 0 { (chunk.len() - done) / group * group } else { 0 }; + // A partial group's bytes reverse into the bytes below its + // flipped last address. + let (n, at) = if whole == 0 { + let n = (group - (here & mask) as usize).min(chunk.len() - done); + (n, (here + n as u64 - 1) ^ mask) + } else { + (whole, here) + }; + let part = &mut chunk[done..done + n]; + let mut flipped = part.to_vec(); + let n_group = if whole == 0 { n } else { group }; + if write { + flipped.chunks_mut(n_group).for_each(<[u8]>::reverse); + } + if !fits(done as u64, n) || !self.send_dma(source, widget, at, &mut flipped, write) { + return false; + } + if !write { + flipped.chunks_mut(n_group).for_each(<[u8]>::reverse); + part.copy_from_slice(&flipped); + } + done += n; + } + true + } + + fn attach(&self, device: u8, function: Arc, options: Function) { + assert!((1..=DEVICES).contains(&device), "PIC devices are 1 to {DEVICES}"); + self.slots.write()[device as usize] = Some(options); + self.bus.attach(DevFn { device, function: 0 }, function); + } + + fn reset(&self) { + { + let _order = self.order.lock(); + self.st.lock().reset(); + } + // RST#: headers, then functions, which drop their own lines. The + // Device registers are back at reset, so the windows move too. + self.bus.reset(); + self.bus.set_host_windows(&|| self.windows()); + self.unsupported.lock().clear(); + } +} + +/// The PCI host bridge in one XIO widget. +pub struct Pic { + widget: u8, + wiring: Arc, + heads: [Arc; 2], +} + +impl Pic { + /// A PIC that answers as XIO widget `widget`. + pub fn new(widget: u8) -> Arc { + let wiring = Arc::new(Wiring { + peer: OnceLock::new(), + }); + Arc::new(Self { + widget, + heads: [Head::new(0, wiring.clone()), Head::new(1, wiring.clone())], + wiring, + }) + } + + pub fn widget_number(&self) -> u8 { + self.widget + } + + /// The widget as the processor reaches it: both heads, the short + /// device-I/O windows and the PCI memory windows. Bedrock IO takes this. + pub fn widget(self: &Arc) -> Arc { + Arc::new(CpuWidget { heads: self.heads.clone() }) + } + + /// Where interrupts and DMA this chip originates go. Once, before the + /// machine runs. + pub fn connect_peer(&self, peer: Arc) -> Result<(), AlreadyConnected> { + self.wiring.peer.set(peer).map_err(|_| AlreadyConnected) + } + + /// Put a PCI function at configuration `device` (1 to 4) of `head`'s + /// bus. It gets its DMA path and, if its header names an interrupt pin, + /// the line of input `device - 1`. + pub fn attach(&self, head: usize, device: u8, function: Arc, options: Function) { + self.heads[head].attach(device, function, options); + } + + /// The PCI bus of `head`. + pub fn bus(&self, head: usize) -> &PciBus { + &self.heads[head].bus + } + + /// Interrupt input `pin` (0 to 7) of `head`, for a device whose + /// interrupt the board wires to it directly. + pub fn pin_line(&self, head: usize, pin: usize) -> IrqLine { + assert!(pin < PINS); + self.heads[head].bus.board_irq(BoardIrq(pin as u8)) + } + + /// Set interrupt input `pin` of `head`. + pub fn set_pin(&self, head: usize, pin: usize, level: bool) { + self.pin_line(head, pin).set(level); + } + + /// The set and clear packets `head` has requested for `pin`, delivered + /// or not. + pub fn request_counts(&self, head: usize, pin: usize) -> (u32, u32) { + let st = self.heads[head].st.lock(); + (st.set_requests[pin], st.clear_requests[pin]) + } + + /// Accesses the model refused or could not carry out, as (head, write, + /// offset, size); the first 32 per head. + pub fn unsupported(&self) -> Vec<(usize, bool, u64, usize)> { + self.heads.iter().flat_map(|head| head.unsupported.lock().clone()).collect() + } + + pub fn register_locks(self: &Arc) { + use crate::locks::register_lock_fn; + for head in &self.heads { + for (what, probe) in [ + ("state", (|h: &Head| h.st.is_locked()) as fn(&Head) -> bool), + ("irq_order", |h| h.order.is_locked()), + ] { + let h = Arc::downgrade(head); + register_lock_fn(format!("pic{:x}::head{}::{what}", self.widget, head.index), move || { + h.upgrade().is_some_and(|h| probe(&h)) + }); + } + } + } +} + +impl Resettable for Pic { + fn power_on(&self) { + for head in &self.heads { + head.reset(); + } + } +} + +/// The processor's view of the widget. +struct CpuWidget { + heads: [Arc; 2], +} + +impl CpuWidget { + /// `None` is a bus error: a malformed access or an unclaimed + /// configuration read. Accesses the PIC cannot make sense of that are + /// not malformed (unsupported registers, refused windows) read zero and + /// write nowhere without one. + fn access(&self, addr: u64, size: usize, value: Option) -> Option { + let write = value.is_some(); + let short = addr < 2 * HEAD_OFFSET && short_window(addr % HEAD_OFFSET).is_some(); + if !short && !addr.is_multiple_of(size as u64) { + return None; + } + if addr >= 2 * HEAD_OFFSET { + if addr < 2 * HEAD_OFFSET + 8 { + crate::dlog!(LogModule::Bedrock, "pic: short-window guard {} at {addr:#x} refused for {size} bytes", if write { "write" } else { "read" }); + return Some(0); + } + let window = PCI_WINDOW.iter().position(|&base| (base..base + PCI_WINDOW_SIZE).contains(&addr))?; + return Some(self.heads[window].pci_window(addr, size, value)); + } + let head = &self.heads[(addr / HEAD_OFFSET) as usize]; + let rel = addr % HEAD_OFFSET; + match value { + Some(v) => { + head.write(rel, size, v); + Some(0) + } + None => head.read(rel, size), + } + } +} + +macro_rules! cpu_access { + ($r:ident, $w:ident, $t:ty, $out:ident, $n:expr) => { + fn $r(&self, addr: u64) -> $out { + self.access(addr, $n, None).map_or_else($out::err, |v| $out::ok(v as $t)) + } + fn $w(&self, addr: u64, value: $t) -> u32 { + if self.access(addr, $n, Some(value as u64)).is_some() { BUS_OK } else { BUS_ERR } + } + }; +} + +impl BusDevice for CpuWidget { + cpu_access!(read8, write8, u8, BusRead8, 1); + cpu_access!(read16, write16, u16, BusRead16, 2); + cpu_access!(read32, write32, u32, BusRead32, 4); + cpu_access!(read64, write64, u64, BusRead64, 8); +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/pic/tests.rs b/src/dev/pic/tests.rs new file mode 100644 index 000000000..a6edbf938 --- /dev/null +++ b/src/dev/pic/tests.rs @@ -0,0 +1,1189 @@ +// Cases follow tests/qtest/sgi-pic-test.c in jamesbraid/qemu-sgi-origami, for +// the Tezro runtime profile only: identity-fields, runtime-identity-bit, +// heads-and-widths, runtime-reset-compatibility-trial, widget-registers, +// cpu-word-view-registers, runtime-host-error-field, +// cpu-pio-overlapping-bars-use-active-function, +// runtime-dir-map-unsupported-fields, configuration, slot-bounds, +// runtime-profile-reserved-device-and-dma, type1-configuration, +// cpu-pio-conventional-lanes, cpu-pio-actual-ioc4, +// short-device-io-windows, short-device-io-attributes-boundaries, +// direct-dma, direct-dma-head-targets, direct64-card-write, +// direct64-requester-width, direct64-rejects-unsupported-addresses, +// direct64-range-boundary-is-split, direct64-target-error-preserves-data, +// pin-and-reset, remaining-pin-edges, bedrock-interrupt-level-bounds, +// runtime-invalid-address-error-trial and unwired-pci-interrupt. The qtest +// fixtures' packet face, translated DMA, migration streams and profile +// selection have no counterpart here, nor do its Bedrock reassertion cases: +// this model sends a packet only on a rising input, an enable, a reset +// write or a force write, and has no timer that sends one again. +use super::*; +use crate::dev::bedrock::{reg::{pi as pi_reg, PI1_BASE}, Bedrock, BedrockConfig}; +use crate::irq::LevelProbe; +use crate::traits::Device; +use crate::pci::{Bar, PciConfig, PciFunctionState, PCI_INTA}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +const HEAD1: u64 = HEAD_OFFSET; +const BAR_BASE: u32 = 0x0100_0000; +const MEMORY_AND_MASTER: u32 = 6; + +/// A PCI function with one 4 KiB memory BAR that stores what it is given. +struct Card { + state: PciFunctionState, + bar: Mutex<[u8; 0x1000]>, +} + +impl Card { + fn new(device: u16) -> Arc { + let bars = [Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused]; + Arc::new(Self { + state: PciFunctionState::new(PciConfig::new(0x1234, device, 0x028000, 1, bars, PCI_INTA)), + bar: Mutex::new([0; 0x1000]), + }) + } + + fn dma(&self) -> &Arc { + self.state.dma().expect("attached to a bridge") + } + + fn raise(&self, level: bool) { + self.state.irq().expect("attached to a bridge").set(level); + } +} + +impl PciFunction for Card { + fn state(&self) -> &PciFunctionState { + &self.state + } + + fn bar_read(&self, _bar: usize, off: u32, width: usize) -> u32 { + le_value(&self.bar.lock()[off as usize..off as usize + width]) as u32 + } + + fn bar_write(&self, _bar: usize, off: u32, val: u32, width: usize) { + self.bar.lock()[off as usize..off as usize + width].copy_from_slice(&val.to_le_bytes()[..width]); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Dma { + source: u8, + widget: u8, + address: u64, + len: usize, + write: bool, +} + +/// The link's far end: sparse memory per widget, and a log of what arrived. +#[derive(Default)] +struct Peer { + interrupts: Mutex>, + dma: Mutex>, + memory: Mutex>, + refuse: AtomicBool, +} + +impl Peer { + fn byte(&self, widget: u8, address: u64) -> u8 { + self.memory.lock().get(&(widget, address)).copied().unwrap_or(0) + } + + fn bytes(&self, widget: u8, address: u64, len: usize) -> Vec { + (0..len as u64).map(|i| self.byte(widget, address + i)).collect() + } + + fn put(&self, widget: u8, address: u64, data: &[u8]) { + for (i, b) in data.iter().enumerate() { + self.memory.lock().insert((widget, address + i as u64), *b); + } + } +} + +impl PicPeer for Peer { + fn interrupt(&self, source_widget: u8, address: u64, payload: u64) -> Result<(), Refused> { + if self.refuse.load(Ordering::SeqCst) { + return Err(Refused); + } + self.interrupts.lock().push((source_widget, address, payload)); + Ok(()) + } + + fn dma_read(&self, source: u8, widget: u8, address: u64, data: &mut [u8]) -> Result<(), Refused> { + self.dma.lock().push(Dma { source, widget, address, len: data.len(), write: false }); + if self.refuse.load(Ordering::SeqCst) { + return Err(Refused); + } + for (i, b) in data.iter_mut().enumerate() { + *b = self.byte(widget, address + i as u64); + } + Ok(()) + } + + fn dma_write(&self, source: u8, widget: u8, address: u64, data: &[u8]) -> Result<(), Refused> { + self.dma.lock().push(Dma { source, widget, address, len: data.len(), write: true }); + if self.refuse.load(Ordering::SeqCst) { + return Err(Refused); + } + self.put(widget, address, data); + Ok(()) + } +} + +struct Rig { + pic: Arc, + bus: Arc, + peer: Arc, +} + +fn rig() -> Rig { + let pic = Pic::new(15); + let peer = Arc::new(Peer::default()); + pic.connect_peer(peer.clone()).unwrap(); + Rig { bus: pic.widget(), pic, peer } +} + +impl Rig { + fn r64(&self, addr: u64) -> u64 { + let r = self.bus.read64(addr); + assert!(r.is_ok(), "read64 {addr:#x}"); + r.data + } + + fn r32(&self, addr: u64) -> u32 { + let r = self.bus.read32(addr); + assert!(r.is_ok(), "read32 {addr:#x}"); + r.data + } + + fn w64(&self, addr: u64, v: u64) { + assert_eq!(self.bus.write64(addr, v), BUS_OK, "write64 {addr:#x}"); + } + + fn w32(&self, addr: u64, v: u32) { + assert_eq!(self.bus.write32(addr, v), BUS_OK, "write32 {addr:#x}"); + } + + /// Fit `card` at `device` of `head` and map its BAR at `BAR_BASE`. + fn fit(&self, head: usize, device: u8, card: &Arc, options: Function) { + self.pic.attach(head, device, card.clone(), options); + let cfg = config(head, device as u64); + self.w32(cfg + 0x10, BAR_BASE); + self.w32(cfg + 4, MEMORY_AND_MASTER); + } +} + +fn head_base(head: usize) -> u64 { + head as u64 * HEAD_OFFSET +} + +fn config(head: usize, device: u64) -> u64 { + head_base(head) + CONFIG_BASE + device * 0x1000 +} + +/// The processor address of byte `off` of a card's BAR through the large +/// PCI memory window. +fn window(head: usize, off: u64) -> u64 { + PCI_WINDOW[head] + BAR_BASE as u64 + off +} + +fn decode(word: u64, head: usize) { + assert_eq!((word >> 28) & 0xf, 3, "revision"); + assert_eq!((word >> 12) & 0xffff, [0xd102, 0xd112][head], "part"); + assert_eq!((word & 0xffe) >> 1, 0x24, "manufacturer, packed one bit up"); + assert_eq!(word & 1, 1, "bit 0, which the PROM's identity check needs"); +} + +/// One of a Bedrock's PIs, through the chip's register window. +#[derive(Clone)] +struct Pi { + chip: Arc, + regs: Arc, + base: u64, +} +impl Pi { + fn read64(&self, at: u64) -> BusRead64 { + self.regs.read64(self.base + at) + } + fn write64(&self, at: u64, value: u64) -> u32 { + self.regs.write64(self.base + at, value) + } + /// The chip's other PI. + fn other(&self) -> Self { + Self { base: PI1_BASE - self.base, ..self.clone() } + } +} + +/// A Bedrock's PI0, which the PIC raises its levels on, and the chip's clock. +fn pi_with_clock() -> (Pi, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let clock = now.clone(); + let chip = Bedrock::new(BedrockConfig { + name: "test", + clock: Arc::new(move || clock.load(Ordering::SeqCst)), + processors: Default::default(), + banks_mib: &[64], + events: None, + }) + .unwrap(); + chip.start(); + let regs = chip.registers(); + (Pi { chip, regs, base: 0 }, now) +} + +/// The destination a pin's packet goes to: `level` in bits 63:48 and an +/// address in the PI's window. +fn destination(level: u64) -> u64 { + (level << 48) | 0x1800090 +} + +/// The PIC's identity is the one thing the PROM checks before it trusts the +/// bridge, and each head has its own. +#[test] +fn each_head_has_its_own_identity_at_both_read_widths() { + let r = rig(); + for head in 0..2 { + decode(r.r64(head_base(head)), head); + assert_eq!(r.r32(head_base(head) + 4) as u64, r.r64(head_base(head))); + assert_eq!(identity(head) as u64, r.r64(head_base(head))); + } + assert_ne!(r.r64(0), r.r64(HEAD1)); + r.w64(0x20, 5); + r.pic.power_on(); + decode(r.r64(0), 0); + assert_eq!(r.r64(0x20), 0x0f00_320f, "reset restores control but not identity edits"); +} + +#[test] +fn reset_restores_the_selected_control_and_device_values() { + let r = rig(); + for head in 0..2 { + assert_eq!(r.r64(head_base(head) + 0x20), 0x0f00_320f); + for (i, v) in [0x18001000, 0x18001002, 0x18001004, 0x18001005].into_iter().enumerate() { + assert_eq!(r.r64(head_base(head) + 0x200 + i as u64 * 8), v); + } + assert_eq!(r.r64(head_base(head) + 0x28), 0xfffff); + } + // The low word of control merges into the register, per head. + r.w32(0x24, 0x0f00_420f); + assert_eq!(r.r32(0x24), 0x0f00_420f); + assert_eq!(r.r32(HEAD1 + 0x24), 0x0f00_320f); + r.w64(HEAD1 + 0x210, 0x1858_1007); + assert_eq!(r.r64(HEAD1 + 0x210), 0x1858_1007); + for head in 0..2 { + for i in 0..4 { + r.w64(head_base(head) + 0x200 + i * 8, 0xdead_0000 + head as u64 * 0x100 + i); + } + } + r.pic.power_on(); + for head in 0..2 { + assert_eq!(r.r64(head_base(head) + 0x20), 0x0f00_320f); + assert_eq!(r.r64(head_base(head) + 0x200), 0x18001000); + assert_eq!(r.r64(head_base(head) + 0x218), 0x18001005); + } +} + +/// The PROM writes the widget registers a word at a time through the low +/// half of each slot; a full doubleword write is the other form. +#[test] +fn widget_registers_take_full_and_low_word_writes_on_each_head() { + let r = rig(); + for head in 0..2 { + let base = head_base(head); + for (n, seed) in [0xfffff_u64, 0, 0].into_iter().enumerate() { + let slot = base + 0x28 + n as u64 * 8; + let initial = (0x1234_5678_0000_0000 | seed) ^ ((head as u64) << 40); + let low = if n == 1 && head == 1 { 0x5a5a_5a5a } else { 0xa5a5_a5a5 }; + assert_eq!(r.r64(slot), seed); + r.w64(slot, initial); + assert_eq!(r.r64(slot), initial); + r.w32(slot + 4, low); + assert_eq!(r.r32(slot + 4), low); + assert_eq!(r.r64(slot), (initial & 0xffff_ffff_0000_0000) | low as u64); + r.w64(slot, initial ^ 0x55aa_55aa); + // Other widths are refused and change nothing. + assert_eq!(r.bus.write8(slot, 0x7e), BUS_OK); + assert_eq!(r.bus.write16(slot, 0x7e5a), BUS_OK); + r.w32(slot, 0x7e5a_3c1d); + assert_eq!(r.bus.read8(slot).data, 0); + assert_eq!(r.bus.read16(slot).data, 0); + assert_eq!(r.r32(slot), 0); + assert_eq!(r.r64(slot), initial ^ 0x55aa_55aa); + } + } + assert!(r.pic.unsupported().iter().any(|&(_, write, off, size)| write && off == 0x28 && size == 1)); + r.pic.power_on(); + for head in 0..2 { + assert_eq!(r.r64(head_base(head) + 0x28), 0xfffff); + assert_eq!(r.r64(head_base(head) + 0x30), 0); + assert_eq!(r.r32(head_base(head) + 0x2c), 0xfffff); + } +} + +#[test] +fn the_host_error_field_has_a_low_word_view() { + let r = rig(); + for head in 0..2u64 { + let base = head * HEAD_OFFSET; + assert_eq!(r.r64(base + 0x128), 0); + r.w64(base + 0x128, 0x1234_5678_0000_0000 | (head + 1)); + assert_eq!(r.r32(base + 0x12c) as u64, head + 1); + r.w32(base + 0x12c, 0x55 + head as u32); + assert_eq!(r.r64(base + 0x128), 0x1234_5678_0000_0000 | (0x55 + head)); + assert_eq!(r.r64(base + 0x100), 0, "the write raised no status"); + } + r.pic.power_on(); + assert_eq!(r.r64(0x128), 0); +} + +#[test] +fn dir_map_keeps_only_the_implemented_bits() { + let r = rig(); + for head in 0..2 { + let at = head_base(head) + 0x80; + r.w64(at, 0x1ffff | (0xf << 20)); + assert_eq!(r.r64(at), 0x1ffff | (0xf << 20)); + let before = r.pic.unsupported().len(); + r.w64(at, (1 << 17) | (1 << 40) | 0x1234); + assert_eq!(r.r64(at), 0x1234, "unsupported bits clear and the rest stay"); + assert!(r.pic.unsupported().len() > before, "the refusal is observable"); + } +} + +/// Device 0 is the bridge and answers nothing; the function at device `n` +/// is `n` windows up, and every miss reads all ones. +#[test] +fn configuration_cycles_reach_the_function_one_window_above_its_device() { + let r = rig(); + let a = Card::new(0x5001); + let b = Card::new(0x5002); + r.pic.attach(0, 1, a.clone(), Function::default()); + r.pic.attach(0, 3, b.clone(), Function::default()); + assert_eq!(r.r32(config(0, 1)), 0x5001_1234); + assert_eq!(r.r32(config(0, 3)), 0x5002_1234); + assert!(!r.bus.read32(config(1, 1)).is_ok(), "head 1 has its own bus"); + for device in [0, 2, 4, 5, 7] { + assert!(!r.bus.read32(config(0, device)).is_ok(), "device {device}"); + r.w32(config(0, device) + 4, 7); + } + // Bytes and halfwords of the big-endian processor land on the right + // little-endian bytes of the register. + assert_eq!(r.bus.read16(config(0, 1)).data, 0x5001, "offset 0 is the register's upper half"); + assert_eq!(r.bus.read16(config(0, 1) + 2).data, 0x1234); + assert_eq!(r.bus.read8(config(0, 1)).data, 0x50); + assert_eq!(r.bus.read8(config(0, 1) + 3).data, 0x34); + // Only function 0 answers. + assert!(!r.bus.read32(config(0, 1) + 0x100).is_ok()); + r.w32(config(0, 1) + 4, MEMORY_AND_MASTER); + assert_eq!(r.r32(config(0, 1) + 4) & 0xffff, MEMORY_AND_MASTER); + r.w32(config(0, 1) + 0x10, BAR_BASE); + assert_eq!(r.r32(config(0, 1) + 0x10), BAR_BASE); + assert_eq!(a.state.config().bar_base(0), BAR_BASE); +} + +#[test] +fn type_one_configuration_selects_the_bus_and_device_through_the_register() { + let r = rig(); + let a = Card::new(0x5001); + r.pic.attach(0, 2, a, Function::default()); + r.w64(0xc8, 2 << 11); + assert_eq!(r.r32(CONFIG_TYPE1), 0x5001_1234); + r.w64(0xc8, (1 << 16) | (2 << 11)); + assert!(!r.bus.read32(CONFIG_TYPE1).is_ok(), "a subordinate bus has nothing"); + r.w64(0xc8, 3 << 11); + assert!(!r.bus.read32(CONFIG_TYPE1).is_ok()); + r.w64(0xc8, 2 << 11); + assert_eq!(r.r32(CONFIG_TYPE1 + 8), 0x0280_0001, "class and revision"); +} + +/// The processor's byte lanes: an adapter wired little-endian sees the +/// numeric value of a word, and a halfword at offset 2 reaches offset 0. +#[test] +fn a_conventional_function_sees_cpu_pio_with_its_address_corrected() { + let r = rig(); + let card = Card::new(0x6001); + r.fit(0, 1, &card, Function::default()); + r.w32(window(0, 0x10), 0x1234_5678); + assert_eq!(&card.bar.lock()[0x10..0x14], [0x78, 0x56, 0x34, 0x12]); + assert_eq!(r.r32(window(0, 0x10)), 0x1234_5678); + assert_eq!(r.bus.write16(window(0, 0x22), 0x9abc), BUS_OK); + assert_eq!(&card.bar.lock()[0x20..0x22], [0xbc, 0x9a], "offset 2 reaches offset 0"); + assert_eq!(r.bus.read16(window(0, 0x22)).data, 0x9abc); + assert_eq!(r.bus.write8(window(0, 0x33), 0x5a), BUS_OK); + assert_eq!(card.bar.lock()[0x30], 0x5a, "byte offset 3 reaches offset 0"); + assert_eq!(r.bus.read8(window(0, 0x33)).data, 0x5a); + assert_eq!(r.bus.read8(window(0, 0x30)).data, 0, "offset 0 is a different byte"); + // The same card through the short window of head 0, memory space. + r.w64(0x200, (1 << 12) | (BAR_BASE >> 20) as u64); + r.w32(SHORT_BASE + 0x40, 0xcafe_f00d); + assert_eq!(&card.bar.lock()[0x40..0x44], [0x0d, 0xf0, 0xfe, 0xca]); + assert_eq!(r.r32(SHORT_BASE + 0x40), 0xcafe_f00d); +} + +/// IOC4 keeps the processor's byte sequence and the address as written. +#[test] +fn a_self_ordered_function_keeps_the_processors_byte_sequence() { + let r = rig(); + let card = Card::new(0x6002); + r.fit(1, 1, &card, Function { self_ordered_pio: true, req64: false }); + r.w32(window(1, 0x10), 0x1234_5678); + assert_eq!(&card.bar.lock()[0x10..0x14], [0x12, 0x34, 0x56, 0x78]); + assert_eq!(r.r32(window(1, 0x10)), 0x1234_5678); + assert_eq!(r.bus.write8(window(1, 0x21), 0xa5), BUS_OK); + assert_eq!(card.bar.lock()[0x21], 0xa5, "no address correction"); + assert_eq!(r.bus.read8(window(1, 0x21)).data, 0xa5); + assert_eq!(r.bus.write16(window(1, 0x22), 0x9abc), BUS_OK); + assert_eq!(&card.bar.lock()[0x22..0x24], [0x9a, 0xbc]); +} + +/// A short window follows its Device register: space, megabyte and size +/// limit, and it refuses the attributes it does not model. +#[test] +fn short_windows_follow_their_device_registers() { + let r = rig(); + let card = Card::new(0x6003); + r.fit(0, 1, &card, Function::default()); + // Window 0 and 1 are 2 MiB and ignore the low bit of the base; windows + // 2 and 3 are 1 MiB. + for (window_index, cpu) in [(0u64, 0x20_0000u64), (1, 0x40_0000), (2, 0x60_0000), (3, 0x70_0000)] { + // The two big windows ignore the base's low bit; the small ones keep it. + let base = (BAR_BASE >> 20) as u64 | if window_index < 2 { 1 } else { 0 }; + r.w64(0x200 + window_index * 8, (1 << 12) | base); + card.bar.lock().fill(0); + r.w32(cpu + 0x80, 0x1111_1111 * (window_index as u32 + 1)); + assert_eq!(le_value(&card.bar.lock()[0x80..0x84]), 0x1111_1111 * (window_index + 1), "window {window_index}"); + } + // The last word of a window fits; a doubleword across its end does not. + r.w32(0x70_0000 + 0xf_fffc, 7); + let before = r.pic.unsupported().len(); + assert_eq!(r.bus.write64(0x70_0000 + 0xf_fffc, 7), BUS_OK); + assert_eq!(r.r64(0x70_0000 + 0xf_fffc), 0, "a boundary-crossing access moves nothing"); + assert_eq!(r.pic.unsupported().len(), before, "it is logged, not listed as unsupported"); + // Swapped and wide programmed I/O are refused rather than guessed. + r.w64(0x200, (1 << 12) | (BAR_BASE >> 20) as u64 | (1 << 13)); + card.bar.lock().fill(0); + r.w32(SHORT_BASE + 0x40, 0xffff_ffff); + assert_eq!(card.bar.lock()[0x40], 0); + r.w64(0x200, (1 << 12) | (BAR_BASE >> 20) as u64 | (1 << 22)); + assert_eq!(r.r32(SHORT_BASE + 0x40), 0); + assert_eq!(card.bar.lock()[0x40], 0); +} + +#[test] +fn the_guard_after_the_second_head_reads_zero_and_the_gaps_fault() { + let r = rig(); + assert_eq!(r.r64(2 * HEAD_OFFSET), 0); + assert_eq!(r.bus.write64(2 * HEAD_OFFSET, 1), BUS_OK); + assert!(!r.bus.read64(2 * HEAD_OFFSET + 8).is_ok()); + assert!(!r.bus.read64(0x8000_0000).is_ok(), "the 64-bit alias window is not this chip's"); + assert!(!r.bus.read32(0x1_0000_0000).is_ok()); + assert!(r.bus.read32(0x20_0001).is_ok(), "short windows take unaligned accesses"); + assert!(!r.bus.read32(0x1002).is_ok(), "registers need natural alignment"); +} + +/// Both heads send direct-map DMA to the widget and address their own +/// `DIR_MAP` names. +#[test] +fn direct_map_dma_goes_where_each_heads_direct_map_says() { + let r = rig(); + let a = Card::new(0x7001); + let b = Card::new(0x7002); + r.fit(0, 1, &a, Function::default()); + r.fit(1, 1, &b, Function::default()); + r.w64(0x80, 10 << 20); + r.w64(HEAD1 + 0x80, (11 << 20) | 1); + let data: Vec = (0x11..0x19).collect(); + assert!(a.dma().dma_write(0x8000_1000, &data).is_ok()); + let other: Vec = (0x21..0x29).collect(); + assert!(b.dma().dma_write(0x8000_2000, &other).is_ok()); + assert_eq!(r.peer.bytes(10, 0x1000, 8), data); + assert_eq!(r.peer.bytes(11, (1 << 31) | 0x2000, 8), other); + assert_eq!(r.peer.bytes(10, 0x2000, 8), vec![0; 8], "the other head's target is untouched"); + let log = r.peer.dma.lock().clone(); + assert_eq!(log[0], Dma { source: 15, widget: 10, address: 0x1000, len: 8, write: true }); + // Reads come back through the same path. + let mut back = [0u8; 8]; + assert!(a.dma().dma_read(0x8000_1000, &mut back).is_ok()); + assert_eq!(back.to_vec(), data); + // A transfer at an odd address is cut into naturally aligned pieces, and + // one that crosses a page is translated again at the page. + r.peer.dma.lock().clear(); + assert!(a.dma().dma_write(0x8000_1ffd, &[1, 2, 3, 4, 5, 6]).is_ok()); + let sizes: Vec<_> = r.peer.dma.lock().iter().map(|p| (p.address, p.len)).collect(); + assert_eq!(sizes, [(0x1ffd, 1), (0x1ffe, 2), (0x2000, 2), (0x2002, 1)].map(|(a, l)| (a as u64, l))); +} + +/// A slot whose Device register sets the swap bit puts each byte at its +/// address with the low two bits flipped, and still moves whole words as +/// one packet. +#[test] +fn a_swapping_slot_flips_the_byte_address_within_each_word() { + let r = rig(); + let card = Card::new(0x7003); + r.fit(0, 1, &card, Function::default()); + r.w64(0x80, 10 << 20); + r.w64(0x200, 0x18001000 | (1 << 19)); + assert!(card.dma().dma_write(0x8000_0100, &[1, 2, 3, 4, 5, 6, 7, 8]).is_ok()); + assert_eq!(r.peer.bytes(10, 0x100, 8), [4, 3, 2, 1, 8, 7, 6, 5]); + let mut back = [0u8; 8]; + assert!(card.dma().dma_read(0x8000_0100, &mut back).is_ok()); + assert_eq!(back, [1, 2, 3, 4, 5, 6, 7, 8]); + let packets: Vec<_> = r.peer.dma.lock().iter().map(|p| (p.address, p.len)).collect(); + assert_eq!(packets, [(0x100, 8), (0x100, 8)]); +} + +#[test] +fn dma_outside_the_direct_window_is_refused_and_a_read_comes_back_zero() { + let r = rig(); + let card = Card::new(0x7004); + r.fit(0, 1, &card, Function::default()); + r.peer.put(0, 0, &[0xaa; 16]); + assert_eq!(card.dma().dma_write(0x7fff_f000, &[1; 8]), PciDmaStatus::MasterAbort, "below the window"); + let mut buf = [0xffu8; 8]; + assert_eq!(card.dma().dma_read(0x4000_0000, &mut buf), PciDmaStatus::MasterAbort, "translated DMA is unsupported"); + assert_eq!(buf, [0; 8]); + assert!(r.peer.dma.lock().is_empty(), "nothing reached the peer"); + // A peer that refuses ends the transfer and zeroes the unread rest. + r.peer.refuse.store(true, Ordering::SeqCst); + let mut buf = [0xffu8; 8]; + assert_eq!(card.dma().dma_read(0x8000_0000, &mut buf), PciDmaStatus::MasterAbort); + assert_eq!(buf, [0; 8]); + // Device 0 and devices past 4 have no DMA path. + r.peer.refuse.store(false, Ordering::SeqCst); + assert_eq!(r.pic.heads[0].dma(0, 0x8000_0000, &mut [0; 4], false), PciDmaStatus::MasterAbort); + assert_eq!(r.pic.heads[0].dma(5, 0x8000_0000, &mut [0; 4], false), PciDmaStatus::MasterAbort); +} + +fn direct64(widget: u64, flags: u64, payload: u64) -> u64 { + (widget << 60) | flags | payload +} + +/// Bit 55 set is an ordered byte stream; clear, each 32-bit word's bytes +/// swap, or each doubleword's for a 64-bit requester. +#[test] +fn direct64_dma_keeps_a_stream_and_swaps_the_words_of_a_swap_clear_request() { + let r = rig(); + let narrow = Card::new(0x7101); + let wide = Card::new(0x7102); + r.fit(0, 1, &narrow, Function::default()); + r.fit(0, 4, &wide, Function { self_ordered_pio: false, req64: true }); + let data = [0x12u8, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]; + assert!(narrow.dma().dma_write(direct64(0xa, 1 << 55, 0x3000), &data).is_ok()); + assert_eq!(r.peer.bytes(0xa, 0x3000, 8), data, "ordered bytes unchanged"); + assert!(narrow.dma().dma_write(direct64(0xa, 0, 0x4000), &data).is_ok()); + assert_eq!(r.peer.bytes(0xa, 0x4000, 8), [0x78, 0x56, 0x34, 0x12, 0xf0, 0xde, 0xbc, 0x9a]); + assert!(wide.dma().dma_write(direct64(0xa, 0, 0x5000), &data).is_ok()); + assert_eq!(r.peer.bytes(0xa, 0x5000, 8), [0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12]); + assert!(wide.dma().dma_write(direct64(0xa, 1 << 55, 0x6000), &data).is_ok()); + assert_eq!(r.peer.bytes(0xa, 0x6000, 8), data, "a stream is a stream at either width"); + let mut back = [0u8; 8]; + assert!(narrow.dma().dma_read(direct64(0xa, 0, 0x4000), &mut back).is_ok()); + assert_eq!(back, data); + // Prefetch, precise, virtual channel 1 and barrier are recognized and + // ignored; DIR_MAP does not apply. + r.w64(0x80, 3 << 20); + for bits in [1 << 59, 1 << 58, 1 << 57, 1 << 56] { + assert!(narrow.dma().dma_write(direct64(0xa, bits | (1 << 55), 0x7000), &data).is_ok()); + } + assert!(r.peer.dma.lock().iter().all(|p| p.widget == 0xa)); +} + +/// The swap moves bytes within a word, not the transfer apart: whole words +/// leave as one packet, so a processor reading a word the card is updating +/// (a ring index in a status block) never sees half of it new. A ragged end +/// writes only its own bytes of the word, as one packet, so an aligned +/// halfword in it is not torn either. +#[test] +fn a_swap_clear_direct64_transfer_keeps_its_words_whole() { + let r = rig(); + let narrow = Card::new(0x7101); + let wide = Card::new(0x7102); + r.fit(0, 1, &narrow, Function::default()); + r.fit(0, 4, &wide, Function { self_ordered_pio: false, req64: true }); + let data: Vec = (0x10..0x30).collect(); + for (card, mask, at) in [(&narrow, 3u64, 0x4000u64), (&wide, 7, 0x5000)] { + r.peer.dma.lock().clear(); + assert!(card.dma().dma_write(direct64(0xa, 0, at), &data).is_ok()); + let packets: Vec<(u64, usize)> = r.peer.dma.lock().iter().map(|p| (p.address, p.len)).collect(); + assert_eq!(packets, [(at, data.len())], "mask {mask}: one packet"); + let want: Vec = (0..data.len() as u64).map(|i| data[(i ^ mask) as usize]).collect(); + assert_eq!(r.peer.bytes(0xa, at, data.len()), want, "mask {mask}: each word reversed"); + let mut back = vec![0u8; data.len()]; + assert!(card.dma().dma_read(direct64(0xa, 0, at), &mut back).is_ok()); + assert_eq!(back, data, "mask {mask}: read back"); + } + // Two bytes before a word and one after it: the word still goes whole. + r.peer.dma.lock().clear(); + assert!(narrow.dma().dma_write(direct64(0xa, 0, 0x6002), &[0xa1, 0xa2, 1, 2, 3, 4, 0xb1]).is_ok()); + let packets: Vec<(u64, usize)> = r.peer.dma.lock().iter().map(|p| (p.address, p.len)).collect(); + assert_eq!(packets, [(0x6000, 2), (0x6004, 4), (0x600b, 1)]); + assert_eq!(r.peer.bytes(0xa, 0x6000, 4), [0xa2, 0xa1, 0, 0], "the word's other bytes are left alone"); + assert_eq!(r.peer.bytes(0xa, 0x6004, 4), [4, 3, 2, 1]); + assert_eq!(r.peer.byte(0xa, 0x600b), 0xb1); + // Three bytes from a doubleword's second: one packet, read back too. + r.peer.dma.lock().clear(); + assert!(wide.dma().dma_write(direct64(0xa, 0, 0x7001), &[1, 2, 3]).is_ok()); + let packets: Vec<(u64, usize)> = r.peer.dma.lock().iter().map(|p| (p.address, p.len)).collect(); + assert_eq!(packets, [(0x7004, 3)]); + assert_eq!(r.peer.bytes(0xa, 0x7004, 3), [3, 2, 1]); + let mut back = [0u8; 3]; + assert!(wide.dma().dma_read(direct64(0xa, 0, 0x7001), &mut back).is_ok()); + assert_eq!(back, [1, 2, 3]); +} + +/// However a transfer starts and ends, no naturally aligned field of a data +/// phase that it writes whole is split between two packets: a ragged start +/// or end is a packet of its own bytes, never cut inside an aligned +/// halfword, word or (where the phase is 64 bits, or the bytes are a +/// stream) doubleword. 11 bytes from 0x..23 cut at 0x..2b would split the +/// halfword at 0x..2a. +#[test] +fn no_aligned_field_is_split_between_packets() { + let r = rig(); + let narrow = Card::new(0x7101); + let wide = Card::new(0x7102); + r.fit(0, 1, &narrow, Function::default()); + r.fit(0, 4, &wide, Function { self_ordered_pio: false, req64: true }); + for (card, flags, phase) in [(&narrow, 1u64 << 55, 8u64), (&narrow, 0, 4), (&wide, 0, 8), (&wide, 1 << 55, 8)] { + for start in 0x20u64..0x28 { + for len in 1..24usize { + r.peer.dma.lock().clear(); + let data: Vec = (0..len as u8).collect(); + assert!(card.dma().dma_write(direct64(0xa, flags, start), &data).is_ok()); + let packets: Vec<(u64, u64)> = + r.peer.dma.lock().iter().map(|p| (p.address, p.address + p.len as u64)).collect(); + let covered = |a: u64| packets.iter().any(|&(lo, hi)| (lo..hi).contains(&a)); + for size in [2u64, 4, 8].into_iter().filter(|&n| n <= phase) { + for field in (0x18u64..0x48).step_by(size as usize) { + if (field..field + size).all(covered) { + assert!( + packets.iter().any(|&(lo, hi)| lo <= field && field + size <= hi), + "flags {flags:#x} start {start:#x} len {len}: {size}-byte field at {field:#x} split in {packets:x?}" + ); + } + } + } + } + } + } +} + +#[test] +fn direct64_refuses_what_it_cannot_carry_before_any_packet_leaves() { + let r = rig(); + let card = Card::new(0x7103); + r.fit(1, 2, &card, Function::default()); + let sentinel = [0xa5u8; 8]; + r.peer.put(0xa, 0x1000, &sentinel); + for bit in 48..=54 { + assert_eq!(card.dma().dma_write(direct64(0xa, 1 << bit, 0x1000), &[1; 4]), PciDmaStatus::MasterAbort, "bit {bit}"); + } + assert_eq!(card.dma().dma_write(direct64(0xa, 0, (1 << 47) | 0x1000), &[1; 4]), PciDmaStatus::MasterAbort, "register space"); + assert_eq!(card.dma().dma_write(1 << 32, &[1; 4]), PciDmaStatus::MasterAbort, "target zero"); + assert!(r.peer.dma.lock().is_empty()); + assert_eq!(r.peer.bytes(0xa, 0x1000, 8), sentinel); +} + +/// A transfer that runs into the register-space boundary delivers the part +/// below it and fails at the page that crosses. +#[test] +fn direct64_splits_at_the_register_space_boundary() { + let r = rig(); + let card = Card::new(0x7104); + r.fit(0, 1, &card, Function::default()); + let boundary = 1u64 << 47; + let status = card.dma().dma_write(direct64(0xa, 1 << 55, boundary - 2), &[0x90, 0x91, 0x92, 0x93]); + assert_eq!(status, PciDmaStatus::MasterAbort); + assert_eq!(r.peer.bytes(0xa, boundary - 2, 2), [0x90, 0x91]); + assert_eq!(r.peer.dma.lock().len(), 1); +} + +#[test] +fn a_refused_direct64_write_leaves_the_target_as_it_was() { + let r = rig(); + let card = Card::new(0x7105); + r.fit(0, 1, &card, Function::default()); + r.peer.put(0xa, 0x3000, &[0xa5; 8]); + r.peer.refuse.store(true, Ordering::SeqCst); + for flags in [0, 1 << 55, 1 << 58, (1 << 56) | (1 << 55)] { + assert_eq!(card.dma().dma_write(direct64(0xa, flags, 0x3000), &[1; 8]), PciDmaStatus::MasterAbort); + } + assert_eq!(r.peer.bytes(0xa, 0x3000, 8), [0xa5; 8]); + assert!(r.peer.dma.lock().len() >= 4, "each attempt reached the peer"); +} + +/// An input that rises sets its status and, once enabled, sends the set +/// packet to its destination; a falling edge sends a clear if its mode bit +/// asks for one. +#[test] +fn a_rising_enabled_pin_sends_a_set_packet_and_a_falling_one_a_clear() { + let r = rig(); + for pin in 0..PINS as u64 { + r.w64(0x130 + pin * 8, destination(0x20 + pin)); + } + r.pic.set_pin(0, 3, true); + assert_eq!(r.r64(0x100), 1 << 3); + assert!(r.peer.interrupts.lock().is_empty(), "disabled"); + r.w64(0x108, 1 << 3); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, 0x1800090, 0x123)], "enabling a pending input sends it"); + assert_eq!(r.pic.request_counts(0, 3), (1, 0)); + r.pic.set_pin(0, 3, false); + assert_eq!(r.pic.request_counts(0, 3), (1, 0), "no clear without the mode bit"); + r.w64(0x118, 1 << 3); + r.pic.set_pin(0, 3, true); + r.pic.set_pin(0, 3, false); + assert_eq!(r.pic.request_counts(0, 3), (2, 1)); + assert_eq!(r.peer.interrupts.lock().last().copied(), Some((15, 0x1800090, 0x23))); + // The status bit stays until software clears it, and a pin that is + // still high comes back. + assert_eq!(r.r64(0x100), 1 << 3); + r.w64(0x110, 1 << 3); + assert_eq!(r.r64(0x100), 0); + r.pic.set_pin(1, 0, true); + assert_eq!(r.r64(HEAD1 + 0x100), 1, "the heads' pins are separate"); + assert_eq!(r.r64(0x100), 0); + // Reset drops pins and status. + r.pic.power_on(); + assert_eq!(r.r64(0x100), 0); + r.pic.set_pin(0, 3, true); + assert_eq!(r.pic.request_counts(0, 3), (0, 0), "counts restart and the enable is gone"); +} + +#[test] +fn a_clear_write_resends_a_pin_that_is_still_high_and_enabled() { + let r = rig(); + r.w64(0x130, destination(9)); + r.w64(0x108, 1); + r.pic.set_pin(0, 0, true); + assert_eq!(r.pic.request_counts(0, 0), (1, 0)); + r.w64(0x110, 1); + assert_eq!(r.pic.request_counts(0, 0), (2, 0), "the status bit comes back with the level, and so does the packet"); + assert_eq!(r.r64(0x100), 1); + // A masked pin stays quiet on a clear write. + r.w64(0x108, 0); + r.w64(0x110, 1); + assert_eq!(r.pic.request_counts(0, 0), (2, 0)); +} + +#[test] +fn only_levels_a_pi_can_take_leave_the_chip() { + let r = rig(); + r.pic.set_pin(0, 0, true); + for level in [0, 6, 128, 0x7fff] { + r.w64(0x130, destination(level)); + r.w64(0x108, 0); + r.w64(0x108, 1); + assert!(r.peer.interrupts.lock().is_empty(), "level {level}"); + } + assert_eq!(r.pic.request_counts(0, 0).0, 4, "each attempt was counted"); + for level in [7, 127] { + r.w64(0x130, destination(level)); + r.w64(0x108, 0); + r.w64(0x108, 1); + } + let sent: Vec<_> = r.peer.interrupts.lock().iter().map(|p| p.2).collect(); + assert_eq!(sent, [0x107, 0x17f]); + // A peer that refuses is not an error the guest sees, but it is counted. + r.peer.refuse.store(true, Ordering::SeqCst); + r.w64(0x108, 0); + r.w64(0x108, 1); + assert_eq!(r.peer.interrupts.lock().len(), 2); +} + +/// A PCI function's INTA reaches the input numbered one less than its +/// device, whichever functions are fitted, and nothing else raises it. +#[test] +fn pci_interrupts_reach_the_input_of_their_device() { + let r = rig(); + let card = Card::new(0x7201); + let other = Card::new(0x7202); + r.pic.attach(0, 3, card.clone(), Function::default()); + r.pic.attach(0, 4, other.clone(), Function::default()); + r.w64(0x130 + 2 * 8, destination(0x31)); + r.w64(0x108, 1 << 2); + card.raise(true); + assert_eq!(r.r64(0x100), 1 << 2, "device 3 is input 2"); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, 0x1800090, 0x131)]); + card.raise(false); + other.raise(true); + assert_eq!(r.r64(0x100), (1 << 2) | (1 << 3), "device 4 is input 3, which the enable leaves unsent"); + assert_eq!(r.peer.interrupts.lock().len(), 1); + // A board routes its own interrupt to an input through the bus. + r.pic.pin_line(0, 6).set(true); + assert_eq!(r.r64(0x100) & (1 << 6), 1 << 6); +} + +/// Force-always (0x180 + 8n) sends input n's set packet on any write, +/// whether the input is active or not and whether it was sent before. The +/// status bit follows the input, not the force. +#[test] +fn force_always_sends_the_set_packet_whatever_the_input() { + let r = rig(); + r.w64(0x130 + 3 * 8, destination(0x20)); + r.w64(0x180 + 3 * 8, 0); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, 0x1800090, 0x120)], "an inactive input"); + assert_eq!(r.r64(0x100), 0, "the force leaves status alone"); + r.w64(0x108, 1 << 3); + r.pic.set_pin(0, 3, true); + assert_eq!(r.peer.interrupts.lock().len(), 2, "the rising input"); + r.w64(0x180 + 3 * 8, 0); + assert_eq!(r.peer.interrupts.lock().len(), 3, "an input already active and sent"); + assert_eq!(r.pic.request_counts(0, 3), (3, 0)); + assert_eq!(r.pic.request_counts(1, 3), (0, 0), "the other head is separate"); +} + +/// Force-with-pin (0x1c0 + 8n) sends input n's set packet only while the +/// input is active: the write software makes at the end of each interrupt +/// dispatch, which brings back an input its handler left active. +#[test] +fn force_with_pin_sends_only_while_the_input_is_active() { + let r = rig(); + r.w64(0x130 + 3 * 8, destination(0x20)); + r.w64(0x108, 1 << 3); + r.w64(0x1c0 + 3 * 8, 0); + assert!(r.peer.interrupts.lock().is_empty(), "an inactive input sends nothing"); + r.pic.set_pin(0, 3, true); + r.w64(0x1c0 + 3 * 8, 0); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, 0x1800090, 0x120); 2], "rising, then forced"); + r.pic.set_pin(0, 3, false); + r.w64(0x1c0 + 3 * 8, 0); + assert_eq!(r.peer.interrupts.lock().len(), 2, "the input dropped"); + r.w64(0x1c0 + 2 * 8, 0); + assert_eq!(r.peer.interrupts.lock().len(), 2, "each input has its own register"); +} + +/// The force registers take a doubleword write or a word to the slot's +/// low half, as the other registers do, and ignore the data. Reads and +/// other widths are unsupported and recorded, and send nothing. +#[test] +fn the_force_registers_are_write_only_and_ignore_the_data() { + let r = rig(); + r.w64(0x130 + 3 * 8, destination(0x20)); + for value in [0, 1, u64::MAX] { + r.w64(0x180 + 3 * 8, value); + } + r.w32(0x180 + 3 * 8 + 4, 0); + assert_eq!(r.peer.interrupts.lock().len(), 4); + r.w32(0x180 + 3 * 8, 1); + assert_eq!(r.bus.write16(0x180 + 3 * 8, 1), BUS_OK); + assert_eq!(r.bus.write8(0x180 + 3 * 8, 1), BUS_OK); + assert_eq!(r.r64(0x180 + 3 * 8), 0); + assert_eq!(r.r32(0x1c0 + 3 * 8 + 4), 0); + assert_eq!(r.peer.interrupts.lock().len(), 4, "nothing else sent"); + let seen = r.pic.unsupported(); + for (write, offset, size) in [(true, 0x198, 4), (true, 0x198, 2), (true, 0x198, 1), (false, 0x198, 8), (false, 0x1dc, 4)] { + assert!(seen.contains(&(0, write, offset, size)), "{offset:#x} {size} recorded"); + } +} + +/// A refused set packet is dropped and logged and nothing sends it again +/// by itself: the status bit stays set, and software's force write +/// delivers it once the peer takes packets. +#[test] +fn a_refused_packet_is_dropped_and_a_force_write_sends_it_again() { + let r = rig(); + r.w64(0x130 + 3 * 8, destination(0x20)); + r.w64(0x108, 1 << 3); + r.peer.refuse.store(true, Ordering::SeqCst); + r.pic.set_pin(0, 3, true); + assert!(r.peer.interrupts.lock().is_empty()); + assert_eq!(r.pic.request_counts(0, 3), (1, 0), "one attempt"); + assert_eq!(r.r64(0x100), 1 << 3, "the status bit stays set"); + r.peer.refuse.store(false, Ordering::SeqCst); + assert!(r.peer.interrupts.lock().is_empty(), "no retry of its own"); + r.w64(0x1c0 + 3 * 8, 0); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, 0x1800090, 0x120)]); +} + +/// Both PIs, told apart the way the node does: by address bit 21. +struct ToPi([Pi; 2]); +impl PicPeer for ToPi { + fn interrupt(&self, _: u8, address: u64, payload: u64) -> Result<(), Refused> { + self.0[(address >> 21) as usize & 1].write64(pi_reg::PENDING_MODIFY, payload); + Ok(()) + } + fn dma_read(&self, _: u8, _: u8, _: u64, _: &mut [u8]) -> Result<(), Refused> { + Err(Refused) + } + fn dma_write(&self, _: u8, _: u8, _: u64, _: &[u8]) -> Result<(), Refused> { + Err(Refused) + } +} + +/// The end of an interrupt dispatch: the processor cleared the PI level, +/// its handler left the input active, and its force-with-pin write brings +/// the level back at once. +#[test] +fn a_force_write_after_dispatch_brings_back_an_active_input() { + let (pi, _now) = pi_with_clock(); + let pic = Pic::new(15); + pic.connect_peer(Arc::new(ToPi([pi.clone(), pi.other()]))).unwrap(); + let bus = pic.widget(); + bus.write64(0x130 + 3 * 8, destination(12)); + bus.write64(0x108, 1 << 3); + pic.set_pin(0, 3, true); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 1 << 12); + pi.write64(pi_reg::PENDING_MODIFY, 12); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 0); + bus.write64(0x1c0 + 3 * 8, 0); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 1 << 12, "the force write re-delivers"); + // With the input dropped, the same write leaves the level clear. + pi.write64(pi_reg::PENDING_MODIFY, 12); + pic.set_pin(0, 3, false); + bus.write64(0x1c0 + 3 * 8, 0); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 0); +} + +/// A processor on another thread that takes and clears the level the moment +/// the set packet lands, before the sending thread has gone on. +struct TakenAtOnce(Pi); +impl PicPeer for TakenAtOnce { + fn interrupt(&self, _: u8, _: u64, payload: u64) -> Result<(), Refused> { + self.0.write64(pi_reg::PENDING_MODIFY, payload); + self.0.write64(pi_reg::PENDING_MODIFY, payload & !SET); + Ok(()) + } + fn dma_read(&self, _: u8, _: u8, _: u64, _: &mut [u8]) -> Result<(), Refused> { + Err(Refused) + } + fn dma_write(&self, _: u8, _: u8, _: u64, _: &[u8]) -> Result<(), Refused> { + Err(Refused) + } +} + +/// The chip sends a set packet when an input rises and on a force write, +/// and at no other time: a level the processor clears while the input +/// stays high, however soon after the packet lands, stays clear until the +/// dispatch's force write brings it back. Both ways a packet leaves on its +/// own are covered: a rising input and an enable. +#[test] +fn a_level_cleared_while_its_input_stays_high_waits_for_the_force_write() { + for enable_last in [false, true] { + let what = if enable_last { "enable" } else { "rising input" }; + let (pi, now) = pi_with_clock(); + let pic = Pic::new(15); + pic.connect_peer(Arc::new(TakenAtOnce(pi.clone()))).unwrap(); + let bus = pic.widget(); + bus.write64(0x130, destination(12)); + if enable_last { + pic.set_pin(0, 0, true); + bus.write64(0x108, 1); + } else { + bus.write64(0x108, 1); + pic.set_pin(0, 0, true); + } + assert_eq!(pic.request_counts(0, 0), (1, 0), "{what}: one packet"); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 0, "{what}: the processor took it"); + now.store(1_000_000_000, Ordering::SeqCst); + assert_eq!(pi.read64(pi_reg::PENDING_STATUS_A).data, 0, "{what}: nothing comes back by itself"); + assert_eq!(pic.request_counts(0, 0), (1, 0), "{what}: no packet of the chip's own"); + bus.write64(0x1c0, 0); + assert_eq!(pic.request_counts(0, 0), (2, 0), "{what}: the force write sends it"); + } +} + +/// The PROM writes the link-side error address slot's low word; the chip +/// latches the invalid-address status and, if enabled, sends one packet to +/// the destination the widget registers name. +#[test] +fn an_invalid_address_write_latches_status_and_sends_one_packet() { + let r = rig(); + let invalid = 1u64 << 24; + // Other widths and the doubleword form of the slot do nothing. + r.bus.write8(0x7c, 0x11); + r.bus.write16(0x7c, 0x2233); + r.bus.write32(0x78, 0x4455_6677); + r.w64(0x78, 0x8899_aabb_ccdd_eeff); + assert_eq!(r.r64(0x100), 0); + r.w32(0x7c, 0xdead_beef); + assert_eq!(r.r32(0x104) as u64, invalid); + assert_eq!(r.r32(HEAD1 + 0x104), 0, "per head"); + assert!(r.peer.interrupts.lock().is_empty(), "not enabled"); + r.w32(0x114, invalid as u32); + assert_eq!(r.r32(0x104), 0, "written one to clear"); + let target = 0x1_0000_3000u64; + r.w64(0x30, 0x3400_0000); + r.w64(0x38, target); + r.w32(0x10c, invalid as u32); + for bad in [0x80u64, 0x100] { + r.w64(0x128, bad); + r.w32(0x7c, 0); + assert!(r.peer.interrupts.lock().is_empty(), "interrupt number {bad:#x}"); + } + r.w64(0x128, 0x65); + r.w32(0x7c, 0); + assert_eq!(r.peer.interrupts.lock().as_slice(), [(15, target, 0x165)]); + r.w32(0x7c, 0); + assert_eq!(r.peer.interrupts.lock().len(), 2, "every request sends while the status stays latched"); + // A repeat of the lower register's bits 47:32 in the upper register is + // accepted; a different one, or a lone upper word, is not. + r.w64(0x30, 1); + r.w32(0x7c, 0); + assert_eq!(r.peer.interrupts.lock().len(), 3); + r.w64(0x30, 2); + r.w32(0x7c, 0); + r.w64(0x30, 0x3400_0000); + r.w64(0x38, 0); + r.w32(0x7c, 0); + r.w64(0x30, 0x1_0000_0001); + r.w64(0x38, target); + r.w32(0x7c, 0); + r.w64(0x30, 0x3400_0000); + r.w64(0x38, 0x000f_0001_0000_3000); + r.w32(0x7c, 0); + assert_eq!(r.peer.interrupts.lock().len(), 3, "conflicts, zero and overflow are refused"); + // The PROM's two word writes join into the address. + r.w64(0x30, 0); + r.w64(0x38, 0); + r.w32(0x34, 1); + r.w32(0x3c, 0x3000); + r.w32(0x7c, 0); + assert_eq!(r.peer.interrupts.lock().last().map(|p| p.1), Some(target)); +} + +#[test] +fn two_pics_are_independent_devices() { + let a = Pic::new(11); + let b = Pic::new(15); + let peer_a = Arc::new(Peer::default()); + let peer_b = Arc::new(Peer::default()); + a.connect_peer(peer_a.clone()).unwrap(); + b.connect_peer(peer_b.clone()).unwrap(); + assert_eq!(a.connect_peer(peer_b.clone()), Err(AlreadyConnected)); + assert_eq!((a.widget_number(), b.widget_number()), (11, 15)); + let (bus_a, bus_b) = (a.widget(), b.widget()); + bus_a.write64(0x20, 0x0f00_3205); + assert_eq!(bus_b.read64(0x20).data, 0x0f00_320f, "state is per chip"); + bus_a.write64(0x130, destination(9)); + bus_a.write64(0x108, 1); + a.set_pin(0, 0, true); + assert_eq!(peer_a.interrupts.lock().as_slice(), [(5, 0x1800090, 0x109)], "the packet carries the control widget field"); + assert!(peer_b.interrupts.lock().is_empty()); + let card = Card::new(1); + a.attach(0, 1, card.clone(), Function::default()); + assert!(!bus_b.read32(config(0, 1)).is_ok(), "the other chip's slot stays empty"); +} + +// An empty slot's configuration read fails as a master timeout. QEMU's +// sgi-pic.c returns all ones with no fault and has no counterpart case. + +const MASTER_TIMEOUT_BIT: u64 = 1 << 11; +const STATUS: u64 = 0x100; +const ENABLE: u64 = 0x108; +const RESET: u64 = 0x110; +const VIEW: u64 = 0x170; + +/// With every enable clear, as the PROM probes: an empty device fails on +/// either head, function 0 or 1, for every width the processor uses. +/// Error view bit 11 records it and status bit 11 stays clear. +#[test] +fn an_empty_slot_read_faults_and_only_the_error_view_records_it() { + let r = rig(); + for head in [0, 1] { + let base = head_base(head); + for (device, function) in [(2u64, 0u64), (2, 1), (0, 0), (5, 1)] { + let at = config(head, device) + function * 0x100; + assert!(!r.bus.read32(at).is_ok(), "head {head} device {device}.{function}"); + assert!(!r.bus.read16(at + 2).is_ok()); + assert!(!r.bus.read8(at + 3).is_ok()); + assert_eq!(r.r64(base + VIEW) & MASTER_TIMEOUT_BIT, MASTER_TIMEOUT_BIT, "head {head}"); + assert_eq!(r.r32(base + VIEW + 4) as u64 & MASTER_TIMEOUT_BIT, MASTER_TIMEOUT_BIT, "low-word view"); + assert_eq!(r.r64(base + STATUS) & MASTER_TIMEOUT_BIT, 0, "status needs the enable"); + assert_eq!(r.r32(base + STATUS + 4) as u64 & MASTER_TIMEOUT_BIT, 0); + // The PROM's low-word write of 0x41 clears the view. + r.w32(base + RESET + 4, 0x41); + assert_eq!(r.r64(base + VIEW), 0); + } + // The other head's view is its own. + assert!(!r.bus.read32(config(head, 2)).is_ok()); + assert_eq!(r.r64(head_base(1 - head) + VIEW), 0); + r.w64(base + RESET, 0x41); + assert_eq!(r.r64(base + VIEW), 0, "an aligned write clears it too"); + } + // The view is read-only and an unclaimed write records nothing. + r.w64(VIEW, MASTER_TIMEOUT_BIT); + r.w32(config(0, 2) + 4, 7); + assert_eq!(r.r64(VIEW), 0); + assert!(r.pic.unsupported().iter().any(|&(_, write, off, _)| write && off == VIEW)); + assert!(r.peer.interrupts.lock().is_empty()); +} + +/// With enable bit 11 set, status bit 11 sets too and the error interrupt +/// goes out. Status clears one bit at a time: the PROM's 0x41 leaves it set, +/// a one at bit 11 clears it. +#[test] +fn an_enabled_master_timeout_sets_status_until_bit_11_is_written() { + let r = rig(); + r.w64(0x38, 0x1800090); + r.w64(0x128, 9); + r.w64(ENABLE, MASTER_TIMEOUT_BIT); + assert!(!r.bus.read32(config(0, 2)).is_ok()); + assert_eq!(r.r64(STATUS) & MASTER_TIMEOUT_BIT, MASTER_TIMEOUT_BIT); + assert_eq!(r.r64(VIEW) & MASTER_TIMEOUT_BIT, MASTER_TIMEOUT_BIT); + assert_eq!(r.peer.interrupts.lock().len(), 1, "one error interrupt"); + r.w32(RESET + 4, 0x41); + assert_eq!(r.r64(STATUS) & MASTER_TIMEOUT_BIT, MASTER_TIMEOUT_BIT, "0x41 leaves status bit 11"); + assert_eq!(r.r64(VIEW), 0, "but clears the view"); + r.w64(RESET, MASTER_TIMEOUT_BIT); + assert_eq!(r.r64(STATUS) & MASTER_TIMEOUT_BIT, 0); + assert_eq!(r.r64(HEAD1 + STATUS) & MASTER_TIMEOUT_BIT, 0, "head 1 untouched"); +} + +/// A populated device after an empty one still reads present once the +/// PROM's 0x41 has been written, as widget 15's second head lists device 3 +/// after an empty device 2 on a real Tezro. A present single-function card +/// has header-type bit 7 clear, so the PROM never asks for its function 1, +/// which would fault. +#[test] +fn a_device_after_an_empty_one_is_still_found() { + let r = rig(); + let card = Card::new(0x5003); + r.pic.attach(1, 3, card, Function::default()); + let probe = |device: u64| { + r.w32(HEAD1 + RESET + 4, 0x41); + let read = r.bus.read32(config(1, device)); + let absent = !read.is_ok() || r.r32(HEAD1 + STATUS + 4) as u64 & MASTER_TIMEOUT_BIT != 0; + (!absent).then_some(read.data) + }; + assert_eq!(probe(1), None); + assert_eq!(probe(2), None); + assert_eq!(probe(3), Some(0x5003_1234)); + assert_eq!(probe(4), None); + assert_eq!(r.r32(config(1, 3) + 0xc) >> 16 & 0x80, 0, "not multifunction"); + assert!(!r.bus.read32(config(1, 3) + 0x100).is_ok(), "its function 1 is unclaimed"); +} + +/// A reset clears the view with the rest of the head. +#[test] +fn reset_clears_the_error_view() { + let r = rig(); + assert!(!r.bus.read32(config(0, 2)).is_ok()); + assert_ne!(r.r64(VIEW), 0); + r.pic.power_on(); + assert_eq!(r.r64(VIEW), 0); +} + +/// Two functions that decode the same range, as a probing PROM leaves them: +/// the shared bus answers from the lower device, and the other answers when +/// that one stops decoding. What real silicon does with two comparators +/// matching is not defined. +/// (cpu-pio-overlapping-bars-use-active-function) +#[test] +fn overlapping_bars_go_to_the_lower_device() { + let r = rig(); + let first = Card::new(0x8001); + let second = Card::new(0x8002); + r.fit(0, 1, &first, Function::default()); + r.fit(0, 2, &second, Function::default()); + let at = window(0, 0x10); + r.w32(at, 0x40); + assert_eq!(le_value(&first.bar.lock()[0x10..0x14]), 0x40, "device 1 answers"); + assert_eq!(le_value(&second.bar.lock()[0x10..0x14]), 0); + // Switching the winner off leaves the other one. + r.w32(config(0, 1) + 4, 0); + r.w32(at, 0x20); + assert_eq!(le_value(&second.bar.lock()[0x10..0x14]), 0x20); + assert_eq!(r.r32(at), 0x20); + r.w32(config(0, 1) + 4, MEMORY_AND_MASTER); + assert_eq!(r.r32(at), 0x40, "device 1 answers again"); +} From 3def3f20f9f13d04176f66584447ff806f572ca8 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:29:07 -0700 Subject: [PATCH 22/33] pxbow, xbow: add the crossbow widget forwarding behind the PIC Behind the PIC that carries IO9, widget 0 is a PXBOW and the processor reaches the other widgets of the board through it. The Crossbow model already has the register map, so PXBOW is that model with part 0xd100 and manufacturer 0 instead of a second copy. It selects revision 5 behavior, which adds the reset fences and the resource lock the Octane's revision 4 leaves reserved, but reports revision 3 in its identification word (0x3d100001), as the Tezro, Origin 350 and Onyx 4 PROM inventories print it; part and manufacturer remain inferred, and a 2009 OpenBSD dmesg that names the part Bedrock is an open conflict. The Crossbow can also report a source widget in its status reads. PXBOW's own interrupt goes to the host widget as a packet whose address names the destination, through the Crossbow's packet host, and the PIC forwards it to its peer. Pxbow::route gives the machine a widget as the processor sees it through the switch. Link resets drop the link for the two core cycles QEMU models (20 ns on the machine clock, ended by the next register read or the PXBOW route's next link query, Xbow::carries_timed) and a warm reset takes the first reset in 1024 core cycles and ignores its echoes, each destination's fence deciding whether it admits the source. The Octane, with no clock, restores at once as before, and its link query, Xbow::carries, is the lock-free load it was. PROM 6.210's crossbar check writes 0xdeadbeef and 0 to the PXBOW's read-only error address registers. Iris turned the refusal into a Data Bus Error and the PROM reported an exception in its xbow_sanity diagnostic. The Crossbow specification makes such a write an error condition reported in widget 0's status bit 5 and its interrupt, and an uncached store cannot be answered with an error response (the R10000 accepts no completion response for it, NEC U10278EJ4V0UM p.104, table 6-1), so the PXBOW completes both writes, as QEMU's complete-posted-error-address-writes policy does. A standalone Crossbow, such as the Octane's, keeps refusing both; a test covers the upper register and both revisions. --- src/dev/mod.rs | 1 + src/dev/pic.rs | 47 +++++ src/dev/pxbow.rs | 183 ++++++++++++++++++ src/dev/pxbow/tests.rs | 239 +++++++++++++++++++++++ src/dev/xbow.rs | 420 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 875 insertions(+), 15 deletions(-) create mode 100644 src/dev/pxbow.rs create mode 100644 src/dev/pxbow/tests.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index ed2ce8d46..5111f9626 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -88,6 +88,7 @@ pub mod isp12160; pub mod l1; pub mod ohci; pub mod pic; +pub mod pxbow; pub mod prom_image; pub mod timekeeper; pub mod xbridge; diff --git a/src/dev/pic.rs b/src/dev/pic.rs index 28f78b86f..2f95d7d86 100644 --- a/src/dev/pic.rs +++ b/src/dev/pic.rs @@ -15,6 +15,9 @@ //! (input 3) are the board's choice, made by the machine; device `d`'s INTA //! reaches input `d - 1`. //! +//! Widget 0 of the board is the PXBOW of the PIC that carries IO9 +//! ([`Pic::enable_pxbow`]); the other PIC has none. +//! //! The register offsets are the ones the Bridge family shares ([`reg`]); //! the PIC leaves out the ATE, the alias windows and the second and third //! write-request banks' meaning. @@ -68,6 +71,8 @@ //! //! Ported from hw/pci-host/sgi-pic.c in jamesbraid/qemu-sgi-origami. +use super::heart::Clock; +use super::pxbow::{AttachError, Pxbow}; use crate::devlog::LogModule; use crate::irq::IrqLine; use crate::pci::{ @@ -190,6 +195,22 @@ impl PioDecode { } } +/// Widget 0's interrupt packets, which go to the peer as the PIC's do. +struct PxbowPackets(Arc); +impl crate::irq::InterruptController for PxbowPackets { + fn set_line(&self, line: u32, level: bool) { + self.packet(0, 0, line, level); + } + fn packet(&self, _source: u8, address: u64, line: u32, level: bool) -> bool { + let payload = u64::from(line) | if level { SET } else { 0 }; + let taken = self.0.peer.get().is_some_and(|peer| peer.interrupt(0, address, payload).is_ok()); + if !taken { + crate::dlog!(LogModule::Bedrock, "pic: PXBOW interrupt packet to {address:#x} refused"); + } + taken + } +} + /// The widget identity word of a head: revision, part and manufacturer /// packed as the common widget header lays them out, with bit 0 set. pub const fn identity(head: usize) -> u32 { @@ -1253,6 +1274,7 @@ pub struct Pic { widget: u8, wiring: Arc, heads: [Arc; 2], + pxbow: OnceLock>, } impl Pic { @@ -1265,6 +1287,7 @@ impl Pic { widget, heads: [Head::new(0, wiring.clone()), Head::new(1, wiring.clone())], wiring, + pxbow: OnceLock::new(), }) } @@ -1278,6 +1301,24 @@ impl Pic { Arc::new(CpuWidget { heads: self.heads.clone() }) } + /// Give the chip its PXBOW, the widget-0 face of the board that owns + /// it, reached from `host_widget`. On Tezro the PIC that carries IO9 + /// supplies widget 0; a second PIC gets none. The PIC's own widget is + /// attached behind it, and widget 0's interrupt goes to the peer when it + /// targets the host. Once, before the machine runs. + pub fn enable_pxbow(self: &Arc, host_widget: u8, clock: Clock) -> Result, AttachError> { + let pxbow = Pxbow::new(host_widget, self.widget, clock)?; + pxbow.attach_endpoint(self.widget, self.widget())?; + pxbow.connect_host(Arc::new(PxbowPackets(self.wiring.clone()))); + self.pxbow.set(pxbow.clone()).map_err(|_| AttachError::Occupied(self.widget))?; + Ok(pxbow) + } + + /// The chip's PXBOW, if it has one. + pub fn pxbow(&self) -> Option<&Arc> { + self.pxbow.get() + } + /// Where interrupts and DMA this chip originates go. Once, before the /// machine runs. pub fn connect_peer(&self, peer: Arc) -> Result<(), AlreadyConnected> { @@ -1334,11 +1375,17 @@ impl Pic { }); } } + if let Some(pxbow) = self.pxbow.get() { + pxbow.xbow().register_locks(); + } } } impl Resettable for Pic { fn power_on(&self) { + if let Some(pxbow) = self.pxbow.get() { + pxbow.xbow().power_on(); + } for head in &self.heads { head.reset(); } diff --git a/src/dev/pxbow.rs b/src/dev/pxbow.rs new file mode 100644 index 000000000..2476c1561 --- /dev/null +++ b/src/dev/pxbow.rs @@ -0,0 +1,183 @@ +//! PXBOW, the crossbar-like widget 0 that sits behind the PIC on Tezro and +//! the Origin 350, and the forwarding of processor accesses through it. +//! +//! The model reuses the Crossbow ([`Xbow`]) at revision 5 behaviour with +//! PXBOW's identity word `0x3d100001` (revision 3, part `0xd100`, +//! manufacturer 0), and a source field in its +//! status reads. That is a software choice, not a claim that the part is a +//! Crossbow 2.0 or that the processor's route to widget 0 passes a real +//! switch: its physical route, fitted revision and register set beyond the +//! reused ones are unknown and stay unsupported and observable +//! ([`Xbow::unsupported`]). +//! +//! The chip knows two links when it is built: the host's widget (Bedrock +//! IO, 10) and the PIC's own. A further widget, such as the optional V12 at +//! widget 12 on the two- and four-processor Tezro, becomes reachable when +//! the machine attaches it with [`Pxbow::attach_endpoint`]; until then its +//! link is absent and an access to it is a bus error. Each processor access +//! to a widget through [`Pxbow::route`] needs both the host's link and the +//! target's to be alive, so a link reset takes the PIC out of reach for the +//! 20 ns of its pulse. +//! +//! Ported from hw/pci-host/sgi-pic.c and hw/misc/sgi-xbow.c in +//! jamesbraid/qemu-sgi-origami. + +use super::heart::Clock; +use super::xbow::Xbow; +use crate::irq::InterruptController; +use crate::xtalk::XtalkHost; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_ERR}; +use parking_lot::RwLock; +use std::sync::Arc; + +/// The Crossbow revision the PIC's PXBOW selects for behaviour. +pub const REVISION: u32 = 5; +/// The revision its identification word reports. Established from real +/// Tezro, Origin 350 and Onyx 4 PROM inventories (revision 3). +pub const REPORTED_REVISION: u32 = 3; +// Part and manufacturer are INFERRED; a 2009 OpenBSD dmesg names the part +// Bedrock, a conflict that stays open. +pub const PART: u32 = 0xd100; +pub const MANUFACTURER: u32 = 0; + +/// Why a widget could not be attached. +#[derive(Debug, PartialEq, Eq)] +pub enum AttachError { + /// Links carry widgets 8 to 15 only. + NotALink(u8), + /// The host's own widget has no endpoint behind the switch. + Host(u8), + /// The widget already has an endpoint. + Occupied(u8), +} + +pub struct Pxbow { + xbow: Arc, + host: u8, + pic: u8, + endpoints: RwLock<[Option>; 16]>, +} + +impl Pxbow { + /// A PXBOW reached from `host` (the widget Bedrock IO answers as) with + /// its PIC at `pic`. Both links are present and alive. `clock` is the + /// machine's virtual time in nanoseconds, which times link resets. + pub fn new(host: u8, pic: u8, clock: Clock) -> Result, AttachError> { + for widget in [host, pic] { + if !(8..16).contains(&widget) { + return Err(AttachError::NotALink(widget)); + } + } + if host == pic { + return Err(AttachError::Occupied(pic)); + } + let xbow = Xbow::new(REVISION, (1 << host) | (1 << pic)) + .with_identity(PART, MANUFACTURER) + .with_reported_revision(REPORTED_REVISION) + .with_status_source(host) + // PROM 6.210's crossbar check writes the read-only lower error + // address on purpose. + .with_posted_error_address_writes() + .with_clock(clock); + Ok(Arc::new(Self { + xbow: Arc::new(xbow), + host, + pic, + endpoints: RwLock::new(std::array::from_fn(|_| None)), + })) + } + + pub fn host_widget(&self) -> u8 { + self.host + } + + pub fn pic_widget(&self) -> u8 { + self.pic + } + + /// The register block the processor reaches as widget 0. + pub fn xbow(&self) -> &Arc { + &self.xbow + } + + /// Widget 0 as a bus device, for Bedrock IO's widget 0. + pub fn widget_zero(&self) -> Arc { + self.xbow.clone() + } + + /// Where widget 0's own interrupt goes: packets to the host widget, + /// whose address names the destination. Once, before the machine runs. + pub fn connect_host(&self, host: Arc) { + let xtalk = XtalkHost { widget: self.host, int_packet: 0, int_packet_mask: 0, vectors: 256 }; + self.xbow.connect_host(xtalk, host); + } + + /// Put `device` behind the switch as `widget` and bring its link up. + /// The PIC's own endpoint is set this way too. + pub fn attach_endpoint(&self, widget: u8, device: Arc) -> Result<(), AttachError> { + if !(8..16).contains(&widget) { + return Err(AttachError::NotALink(widget)); + } + if widget == self.host { + return Err(AttachError::Host(widget)); + } + let mut endpoints = self.endpoints.write(); + if endpoints[widget as usize].is_some() { + return Err(AttachError::Occupied(widget)); + } + endpoints[widget as usize] = Some(device); + drop(endpoints); + self.xbow.attach(widget); + Ok(()) + } + + /// Widgets with an endpoint, as a mask with bit n for widget n. + pub fn endpoint_widgets(&self) -> u16 { + self.endpoints.read().iter().enumerate().fold(0, |m, (w, e)| m | (u16::from(e.is_some()) << w)) + } + + /// `widget` as the processor reaches it through the switch: forwarded + /// to the endpoint while the host's link and the widget's are alive, a + /// bus error otherwise. + pub fn route(self: &Arc, widget: u8) -> Arc { + assert!((8..16).contains(&widget)); + Arc::new(Routed { pxbow: self.clone(), widget }) + } +} + +struct Routed { + pxbow: Arc, + widget: u8, +} + +impl Routed { + /// The endpoint, if the route is open. + fn open(&self) -> Option> { + let p = &self.pxbow; + if !p.xbow.carries_timed(p.host) || !p.xbow.carries_timed(self.widget) { + return None; + } + p.endpoints.read()[self.widget as usize].clone() + } +} + +macro_rules! forward { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, addr: u64) -> $out { + self.open().map_or_else($out::err, |d| d.$r(addr)) + } + fn $w(&self, addr: u64, value: $t) -> u32 { + self.open().map_or(BUS_ERR, |d| d.$w(addr, value)) + } + }; +} + +impl BusDevice for Routed { + forward!(read8, write8, u8, BusRead8); + forward!(read16, write16, u16, BusRead16); + forward!(read32, write32, u32, BusRead32); + forward!(read64, write64, u64, BusRead64); +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/pxbow/tests.rs b/src/dev/pxbow/tests.rs new file mode 100644 index 000000000..d7e026387 --- /dev/null +++ b/src/dev/pxbow/tests.rs @@ -0,0 +1,239 @@ +// Cases follow tests/qtest/sn-builder-test.c in jamesbraid/qemu-sgi-origami: +// ip53-widget-zero-pxbow-identity, ip53-widget-zero-status-source and +// ip53-pxbow-route-fences-and-interrupt, and the widget-12 absence check of +// tests/qtest/sgi-graphics-pipes-test.c (ip53-v12). The qtest's machine +// properties, link-pulse timing and migration have no counterpart here. +use super::*; +use crate::dev::pic::{PicPeer, Refused, Pic}; +use crate::traits::{BusRead32, BUS_OK}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; + +const STATUS: u64 = 0x0c; +const STATUS_CLEAR: u64 = 0x54; +const LINK_STATUS_ALIVE: u32 = 1 << 31; +const AUX_PRESENT: u32 = 1 << 5; + +/// A clock the test advances, in nanoseconds. +fn clock() -> (Clock, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let c = now.clone(); + (Arc::new(move || c.load(Ordering::SeqCst)), now) +} + +fn link(widget: u64, register: u64) -> u64 { + 0x100 + (widget - 8) * 0x40 + register + 4 +} + +/// A widget that answers one word and remembers a write. +struct Stub(Mutex); +impl BusDevice for Stub { + fn read32(&self, addr: u64) -> BusRead32 { + if addr == 0x40 { BusRead32::ok(*self.0.lock()) } else { BusRead32::err() } + } + fn write32(&self, addr: u64, value: u32) -> u32 { + if addr != 0x40 { + return BUS_ERR; + } + *self.0.lock() = value; + BUS_OK + } +} + +#[derive(Default)] +struct Peer(Mutex>); +impl PicPeer for Peer { + fn interrupt(&self, source: u8, address: u64, payload: u64) -> Result<(), Refused> { + self.0.lock().push((source, address, payload)); + Ok(()) + } + fn dma_read(&self, _: u8, _: u8, _: u64, _: &mut [u8]) -> Result<(), Refused> { + Err(Refused) + } + fn dma_write(&self, _: u8, _: u8, _: u64, _: &[u8]) -> Result<(), Refused> { + Err(Refused) + } +} + +/// Widget zero names itself PXBOW, not Crossbow and not the PIC, and shows +/// the host's and the PIC's links up. +#[test] +fn widget_zero_identifies_as_pxbow_with_the_host_and_pic_links_up() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let w = p.widget_zero(); + let id = w.read32(4).data; + assert_eq!(id, 0x3d10_0001); + assert_eq!((id >> 28) & 0xf, 3); + assert_eq!((id >> 12) & 0xffff, 0xd100); + assert_eq!((id >> 1) & 0x7ff, 0); + assert_eq!(id & 1, 1); + for widget in [10, 15] { + assert_ne!(w.read32(link(widget, 0x10)).data & LINK_STATUS_ALIVE, 0, "widget {widget} alive"); + assert_ne!(w.read32(link(widget, 0x38)).data & AUX_PRESENT, 0); + } + for widget in [8, 9, 11, 12, 13, 14] { + assert_eq!(w.read32(link(widget, 0x10)).data & LINK_STATUS_ALIVE, 0, "widget {widget} absent"); + assert_eq!(w.read32(link(widget, 0x38)).data & AUX_PRESENT, 0); + } + assert_eq!(p.endpoint_widgets(), 0); +} + +#[test] +fn status_reads_carry_the_hosts_widget_as_their_source() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let w = p.widget_zero(); + // Seed the status with an access error first. + let _ = w.read32(0x1004); + let status = w.read32(STATUS).data; + assert_eq!((status >> 6) & 0xf, 10); + assert_ne!(status & (1 << 5), 0, "the seeded access error shows"); + assert_eq!((w.read32(STATUS_CLEAR).data >> 6) & 0xf, 10); + assert_eq!(w.read32(STATUS).data & (1 << 5), 0, "the alias acknowledged it"); + let other = Pxbow::new(11, 14, clock().0).unwrap(); + assert_eq!((other.widget_zero().read32(STATUS).data >> 6) & 0xf, 11); +} + +/// PROM 6.210's crossbar check on the Tezro writes 0xdeadbeef to the +/// read-only lower error address and 0 to the upper one. The store +/// completes, the registers keep their value, and widget 0's status reports +/// the access error (NEC U10278EJ4V0UM p.104). +#[test] +fn the_proms_error_address_writes_complete_and_are_reported() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let w = p.widget_zero(); + assert_eq!(w.write32(0x1c, 0xdead_beef), BUS_OK); + assert_eq!(w.write32(0x14, 0), BUS_OK); + assert_eq!(w.read32(0x1c).data, 0); + assert_ne!(w.read32(STATUS).data & (1 << 5), 0, "the access error shows"); + assert!(p.xbow().unsupported().contains(&(0x1c, 4, true)), "the write stays observable"); + assert_ne!(w.write32(0x04, 0), BUS_OK, "the identification register still refuses"); +} + +/// Revision 5 decodes the fences and the lock; the lock takes a write only +/// while it reads zero and any write to its clear alias releases it. +#[test] +fn fences_and_the_lock_are_stored_and_read_back() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let w = p.widget_zero(); + for (off, v) in [(0x78u64, 0xff_u32), (0x80, 0xff), (0xb8, 0xff), (0xc0, 0), (0xc8, 0)] { + assert_eq!(w.read32(off + 4).data, v, "reset value at {off:#x}"); + } + for (off, v) in [(0x78u64, 0x5a), (0x90, 0xa5), (0xb8, 0x3c)] { + assert_eq!(w.write32(off + 4, v), BUS_OK); + assert_eq!(w.read32(off + 4).data, v); + } + assert_eq!(w.read32(0x78 + 4).data, 0x5a); + assert_eq!(w.write32(0xc0 + 4, 0x11), BUS_OK); + assert_eq!(w.write32(0xc0 + 4, 0x22), BUS_OK); + assert_eq!(w.read32(0xc0 + 4).data, 0x11, "a held lock ignores a second write"); + assert_eq!(w.read32(0xc8 + 4).data, 0x11, "both offsets read the one value"); + assert_eq!(w.write32(0xc8 + 4, 0), BUS_OK); + assert_eq!(w.read32(0xc0 + 4).data, 0); + assert_eq!(w.write32(0xc0 + 4, 0x22), BUS_OK); + assert_eq!(w.read32(0xc0 + 4).data, 0x22); + crate::traits::Resettable::power_on(&*p.xbow().clone()); + assert_eq!(w.read32(0x78 + 4).data, 0xff, "reset restores the fences"); + assert_eq!(w.read32(0xc0 + 4).data, 0, "and releases the lock"); + // The upper word of a slot and an eight-byte access are still refused. + assert!(!w.read32(0x78).is_ok()); + assert!(!w.read64(0x78).is_ok()); +} + +/// Widget zero's own error interrupt is a packet to the host, with the +/// vector and address its destination registers hold. +#[test] +fn a_widget_zero_error_sends_an_interrupt_packet_to_the_host() { + let pic = Pic::new(15); + let peer = Arc::new(Peer::default()); + pic.connect_peer(peer.clone()).unwrap(); + let p = pic.enable_pxbow(10, clock().0).unwrap(); + let w = p.widget_zero(); + let address = 0x1_0180_0090u64; + assert_eq!(w.write32(0x34, (0x34 << 24) | (10 << 16) | ((address >> 32) as u32 & 0xffff)), BUS_OK); + assert_eq!(w.write32(0x3c, address as u32), BUS_OK); + assert_eq!(w.write32(0x24, 1 << 5), BUS_OK, "enable the access-error condition"); + assert!(peer.0.lock().is_empty()); + let _ = w.read32(0x1004); + assert_ne!(w.read32(STATUS).data & (1 << 23), 0, "the request bit is up"); + assert_eq!(peer.0.lock().as_slice(), [(0, address, 0x134)]); + let _ = w.read32(STATUS_CLEAR); + assert_eq!(peer.0.lock().last().copied(), Some((0, address, 0x34)), "acknowledging lowers it"); + // A target that is not the host goes nowhere. + assert_eq!(w.write32(0x34, (0x34 << 24) | (11 << 16)), BUS_OK); + let _ = w.read32(0x1004); + assert_eq!(peer.0.lock().len(), 2); +} + +/// The V12's widget is absent until the machine fits it, then forwards to +/// the endpoint with the widget-relative address unchanged. +#[test] +fn a_widget_is_unreachable_until_an_endpoint_is_attached() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let w12 = p.route(12); + assert!(!w12.read32(0x40).is_ok()); + assert_eq!(w12.write32(0x40, 1), BUS_ERR); + let stub = Arc::new(Stub(Mutex::new(0))); + p.attach_endpoint(12, stub.clone()).unwrap(); + assert_eq!(p.endpoint_widgets(), 1 << 12); + let wz = p.widget_zero(); + assert_ne!(wz.read32(link(12, 0x10)).data & LINK_STATUS_ALIVE, 0, "its link comes up"); + assert_ne!(wz.read32(link(12, 0x38)).data & AUX_PRESENT, 0); + assert_eq!(w12.write32(0x40, 0xcafe), BUS_OK); + assert_eq!(*stub.0.lock(), 0xcafe); + assert_eq!(w12.read32(0x40).data, 0xcafe); + assert!(!w12.read32(0x44).is_ok(), "the endpoint's own refusal passes through"); + assert!(!p.route(11).read32(0x40).is_ok(), "other widgets stay absent"); +} + +#[test] +fn attachment_refuses_the_host_a_taken_widget_and_a_non_link() { + let p = Pxbow::new(10, 15, clock().0).unwrap(); + let stub: Arc = Arc::new(Stub(Mutex::new(0))); + assert_eq!(p.attach_endpoint(10, stub.clone()), Err(AttachError::Host(10))); + assert_eq!(p.attach_endpoint(3, stub.clone()), Err(AttachError::NotALink(3))); + assert_eq!(p.attach_endpoint(12, stub.clone()), Ok(())); + assert_eq!(p.attach_endpoint(12, stub.clone()), Err(AttachError::Occupied(12))); + assert!(Pxbow::new(10, 10, clock().0).is_err()); + assert!(Pxbow::new(10, 2, clock().0).is_err()); +} + +/// The PIC that owns IO9 supplies widget 0; the other PIC does not, and +/// the PIC's own widget is reached through the switch. +#[test] +fn only_the_pic_that_enables_it_has_a_pxbow_and_it_forwards_to_the_pic() { + let with = Pic::new(15); + let without = Pic::new(11); + assert!(with.pxbow().is_none()); + let p = with.enable_pxbow(10, clock().0).unwrap(); + assert!(without.pxbow().is_none()); + assert_eq!(p.pic_widget(), 15); + assert_eq!(p.endpoint_widgets(), 1 << 15); + assert!(Arc::ptr_eq(with.pxbow().unwrap(), &p)); + assert!(with.enable_pxbow(10, clock().0).is_err(), "once"); + let via_switch = p.route(15); + let id = via_switch.read64(0).data; + assert_eq!((id >> 28) & 0xf, 3, "the PIC's head 0 answers through widget 15"); + assert_eq!(via_switch.read64(crate::dev::pic::HEAD_OFFSET).data >> 12 & 0xffff, 0xd112); + assert!(Pic::new(15).enable_pxbow(15, clock().0).is_err(), "host and PIC differ"); +} + +/// A link reset on the host's link or the PIC's takes the PIC out of reach +/// for the pulse and brings it back, as the qtest's 20 ns clock step does. +#[test] +fn a_link_reset_makes_the_pic_unreachable_for_the_pulse() { + let (c, now) = clock(); + let pic = Pic::new(15); + let p = pic.enable_pxbow(10, c).unwrap(); + let via = p.route(15); + let wz = p.widget_zero(); + assert!(via.read64(0).is_ok()); + for (widget, reset) in [(10u64, 0x1b4u64), (15, 0x2f4)] { + now.fetch_add(100, Ordering::SeqCst); + assert_eq!(wz.write32(reset, 0), BUS_OK, "widget {widget}'s link reset"); + assert!(!via.read64(0).is_ok(), "down during the pulse"); + assert_eq!(via.write64(0x20, 0), BUS_ERR); + now.fetch_add(20, Ordering::SeqCst); + assert!(via.read64(0).is_ok(), "back after 20 ns"); + assert_eq!(via.read64(0x20).data, 0x0f00_320f, "the PIC kept its registers"); + } +} diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index 26099dd75..68cfce5ee 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -36,13 +36,31 @@ //! error. These follow hw/misc/sgi-xbridge-xbow.c in //! jamesbraid/qemu-sgi-origami, not a published specification. //! +//! # Behind a PIC +//! +//! The chip can also carry a part number and manufacturer, report a +//! revision other than the one whose behaviour it has, and name a source +//! widget in its status reads: what [`Pxbow`](super::pxbow::Pxbow) needs +//! behind a PIC. +//! +//! With a clock ([`Xbow::with_clock`]) a link reset is the two-core-cycle +//! pulse the QEMU model has: the link is down for 20 ns of the clock, and +//! the chip ends the pulse the next time anything reads its registers or +//! asks [`Xbow::carries_timed`], so what the guest sees follows the clock and not +//! host scheduling. Without one the pulse is over at once. [`Xbow::warm_reset`] +//! takes a warm reset from a link through the revision-5 fences. No board in +//! this tree sends one yet: the Hub's link control is the only sender the +//! QEMU model names. +//! //! Ported from hw/misc/sgi-xbow.c in jamesbraid/qemu-sgi-origami. -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}; use std::sync::{Arc, OnceLock}; use parking_lot::Mutex; +use super::heart::Clock; + use crate::irq::InterruptController; use crate::snapshot::{field_bool, field_u32, field_u32s, field_u8, StateTable}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_ERR, BUS_OK, Saveable}; @@ -85,6 +103,15 @@ const FENCE_MASK: u32 = 0xff; /// The first revision code with fences and the lock (Crossbow 2.0 in the /// software tables; an Octane2's code-5 part shows as 1.4 in its inventory). const REVISION_FENCES: u32 = 5; +const CORE_CYCLE_NS: u64 = 10; +/// How long a link reset holds the link down: two core cycles. +const LINK_RESET_NS: u64 = 2 * CORE_CYCLE_NS; +/// A warm reset arriving this many cycles after another is ignored. +const WARM_RESET_QUIET_CYCLES: u64 = 1024; +/// Where a status read carries the widget that asked, when the chip is told +/// to. +const STATUS_SOURCE_SHIFT: u32 = 6; +const STATUS_SOURCE_MASK: u32 = 0xf << STATUS_SOURCE_SHIFT; const LINK_BASE: u32 = 0x100; const LINK_STRIDE: u32 = 0x40; const LINK_END: u32 = LINK_BASE + LINK_STRIDE * LINKS as u32; @@ -166,6 +193,12 @@ struct Regs { fences: [u32; FENCES], /// The resource lock's byte. lock: u32, + /// When each link's reset pulse ends, in clock ns; 0 when none runs. + reset_due: [u64; LINKS], + /// Links that were alive when their pulse began and return alive. + reset_restore: u8, + /// A warm reset before this time is ignored. + warm_quiet_until: u64, } /// Which crossbar a [`Xbow`] is: see the module documentation. @@ -217,6 +250,9 @@ impl Regs { initializing: 0, eight_bit: 0, interrupt_asserted: false, + reset_due: [0; LINKS], + reset_restore: 0, + warm_quiet_until: 0, } } } @@ -226,14 +262,27 @@ impl Regs { struct Out { /// Widget 0's interrupt packets to the host: (address, vector, set). packets: Vec<(u64, u8, bool)>, - /// A link reset's far-end widget. - link_reset: Option, + /// Link resets' far-end widgets. + link_resets: Vec, } pub struct Xbow { regs: Mutex, revision: u32, part: Part, + /// Revision field of the identification word when it differs from the + /// revision that selects behaviour. + reported_revision: Option, + /// Part number and manufacturer in a Crossbow's identification word. + identity: (u32, u32), + /// The widget a status read reports as its source, if the chip does. + status_source: Option, + clock: Option, + /// The earliest running pulse's end, or `u64::MAX`. Written under `regs`. + reset_due: AtomicU64, + /// A write to an error address register completes; see + /// [`Xbow::with_posted_error_address_writes`]. + posted_error_address_writes: bool, /// Links with something attached, bit n for widget 8 + n. present: AtomicU8, /// Links passing traffic, as `present`. Written under `regs`. @@ -279,6 +328,12 @@ impl Xbow { regs: Mutex::new(Regs::reset(part, present)), revision, part, + reported_revision: None, + identity: (0, 0), + status_source: None, + clock: None, + reset_due: AtomicU64::new(u64::MAX), + posted_error_address_writes: false, present: AtomicU8::new(present), alive: AtomicU8::new(present), irq_order: Mutex::new(()), @@ -290,6 +345,47 @@ impl Xbow { } } + /// Complete a write to either error address register instead of + /// refusing it. The registers are read-only and the write still changes + /// nothing, sets the register-access error and is listed in + /// [`Xbow::unsupported`]: the specification makes a write to a read-only + /// register an error condition that widget 0's status reports. What the + /// processor sees is the other half: an uncached store is a write + /// request with no completion response (NEC U10278EJ4V0UM p.104, table + /// 6-1), so no error can come back to it. A stock IP35 PROM writes the + /// lower register on purpose in its crossbar check. QEMU's + /// `complete-posted-error-address-writes` property. + pub fn with_posted_error_address_writes(mut self) -> Self { + self.posted_error_address_writes = true; + self + } + + /// Answer to `part` and `manufacturer` in the identification word + /// instead of the Crossbow's zeros. + pub fn with_identity(mut self, part: u32, manufacturer: u32) -> Self { + self.identity = (part, manufacturer); + self + } + + /// Report `revision` in the identification word while behaviour stays + /// that of the revision given to [`Xbow::new`]. + pub fn with_reported_revision(mut self, revision: u32) -> Self { + self.reported_revision = Some(revision); + self + } + + /// Time link resets and warm resets by `clock`, in nanoseconds. + pub fn with_clock(mut self, clock: Clock) -> Self { + self.clock = Some(clock); + self + } + + /// Report `widget` in bits 9:6 of every status read. + pub fn with_status_source(mut self, widget: u8) -> Self { + self.status_source = Some(widget); + self + } + /// Where widget 0's interrupt packets go: the host chip addressed as /// `host`, whose line is the vector the packet names. Once, before the /// machine runs. @@ -322,7 +418,14 @@ impl Xbow { let order = self.irq_order.lock(); let before = self.alive.load(Ordering::Acquire); let mut out = Out::default(); - let ret = f(&mut self.regs.lock(), &mut out); + let ret = { + let mut r = self.regs.lock(); + self.end_pulses(&mut r, &mut out); + let ret = f(&mut r, &mut out); + let due = r.reset_due.iter().copied().filter(|&d| d != 0).min().unwrap_or(u64::MAX); + self.reset_due.store(due, Ordering::Release); + ret + }; if let Some((_, h)) = self.host.get() { for (address, vector, set) in out.packets { h.packet(0, address, vector as u32, set); @@ -335,8 +438,10 @@ impl Xbow { cb(FIRST_WIDGET + link, self.alive.load(Ordering::Acquire) >> link & 1 != 0); } } - if let (Some(w), Some(cb)) = (out.link_reset, self.on_link_reset.get()) { - cb(w); + if let Some(cb) = self.on_link_reset.get() { + for w in out.link_resets { + cb(w); + } } ret } @@ -352,28 +457,46 @@ impl Xbow { } } - /// Whether the switch passes traffic to and from `widget`. Lock-free. + /// Whether the switch passes traffic to and from `widget`. A link reset + /// pulse ends here only on a register access; a chip with a clock asks + /// [`Self::carries_timed`]. pub fn carries(&self, widget: u8) -> bool { link_bit(widget).is_some_and(|b| self.alive.load(Ordering::Acquire) & b != 0) } + /// [`Self::carries`] on a chip with a clock: a link reset pulse that is + /// over by the clock ends first. Lock-free unless one is. + pub fn carries_timed(&self, widget: u8) -> bool { + if let Some(clock) = &self.clock { + if clock() >= self.reset_due.load(Ordering::Acquire) { + self.locked(|_, _| {}); + } + } + self.carries(widget) + } + pub fn unsupported(&self) -> Vec<(u32, u8, bool)> { self.unsupported.lock().clone() } fn identification(&self) -> u32 { match self.part { - // Part number and manufacturer are zero on Crossbow. - Part::Crossbow => (self.revision << 28) | 1, + // Part number and manufacturer are zero on Crossbow unless the + // board names others. + Part::Crossbow => { + let (part, manufacturer) = self.identity; + (self.reported_revision.unwrap_or(self.revision) << 28) | (part << 12) | (manufacturer << 1) | 1 + } Part::XBridge => XBRIDGE_ID, } } /// The status register as the host reads it: on the XBridge, with the - /// host's port as the requester. + /// host's port as the requester; on a chip told its source, with that. fn status(&self, r: &Regs) -> u32 { - match (self.part, self.host.get()) { - (Part::XBridge, Some((host, _))) => r.status | u32::from(host.widget) << XBRIDGE_REQUESTER_SHIFT, + match (self.part, self.host.get(), self.status_source) { + (Part::XBridge, Some((host, _)), _) => r.status | u32::from(host.widget) << XBRIDGE_REQUESTER_SHIFT, + (Part::Crossbow, _, Some(widget)) => (r.status & !STATUS_SOURCE_MASK) | (u32::from(widget) << STATUS_SOURCE_SHIFT), _ => r.status, } } @@ -571,7 +694,14 @@ impl Xbow { pub fn write(&self, off: u32, size: usize, val: u64) -> bool { let xbridge = self.part == Part::XBridge; self.locked(|r, out| { - let Some(slot) = self.decode(r, out, off, size, true) else { return xbridge }; + let Some(slot) = self.decode(r, out, off, size, true) else { + // Refused and recorded either way; a posted error address + // write completes all the same. + return xbridge + || (self.posted_error_address_writes + && size == 4 + && matches!(off.wrapping_sub(4), ERR_UPPER | ERR_LOWER)); + }; let val = val as u32; match slot { CONTROL => { @@ -620,7 +750,7 @@ impl Xbow { self.link_reset(r, link, out); // After the lock: the widget's reset may ask // the switch about its links. - out.link_reset = Some(FIRST_WIDGET + link as u8); + out.link_resets.push(FIRST_WIDGET + link as u8); } _ => unreachable!(), } @@ -630,13 +760,17 @@ impl Xbow { }) } - /// A link reset. The pulse is two core cycles, over before the + /// A link reset. Without a clock the pulse is over before the /// processor can look again, so it is not kept. A fitted link that was /// alive comes back alive with an alive event, as the Octane's /// restore-alive policy has it; one that was down stays down, still /// initializing; an empty port fails initialization. fn link_reset(&self, r: &mut Regs, link: usize, out: &mut Out) { let bit = 1u8 << link; + if let Some(clock) = &self.clock { + self.begin_pulse(r, link, clock(), out); + return; + } let was_alive = self.alive.load(Ordering::Acquire) & bit != 0; r.eight_bit &= !bit; if self.present.load(Ordering::Acquire) & bit != 0 { @@ -657,6 +791,95 @@ impl Xbow { } self.update_interrupt(r, out); } + + /// A link reset with a clock: the link drops now and the pulse ends + /// two core cycles on. A fitted link that was alive comes back alive + /// then, with an alive event; one that was down comes out of + /// initialization still down; an empty port fails initialization and + /// stays failed. + fn begin_pulse(&self, r: &mut Regs, link: usize, now: u64, out: &mut Out) { + let bit = 1u8 << link; + let present = self.present.load(Ordering::Acquire) & bit != 0; + if present && self.alive.load(Ordering::Acquire) & bit != 0 { + r.reset_restore |= bit; + } else { + r.reset_restore &= !bit; + } + self.alive.fetch_and(!bit, Ordering::AcqRel); + r.eight_bit &= !bit; + if present { + r.initializing |= bit; + } else { + r.initializing &= !bit; + r.came_up &= !bit; + } + r.reset_due[link] = now + LINK_RESET_NS; + self.update_interrupt(r, out); + } + + /// End every pulse the clock says is over. + fn end_pulses(&self, r: &mut Regs, out: &mut Out) { + let Some(clock) = &self.clock else { return }; + if r.reset_due.iter().all(|&d| d == 0) { + return; + } + let now = clock(); + for link in 0..LINKS { + let bit = 1u8 << link; + if r.reset_due[link] == 0 || r.reset_due[link] > now { + continue; + } + r.reset_due[link] = 0; + if self.present.load(Ordering::Acquire) & bit == 0 { + continue; + } + r.initializing &= !bit; + if r.reset_restore & bit != 0 { + self.alive.fetch_or(bit, Ordering::AcqRel); + r.came_up |= bit; + r.link_event[link] |= LINK_ALIVE; + if r.link_control[link] & LINK_EIGHT_BIT_FORCE != 0 { + r.eight_bit |= bit; + } + } + r.reset_restore &= !bit; + self.update_interrupt(r, out); + } + } + + /// A warm reset arriving on the link of `source` widget, at `now` ns. + /// The first in 1024 core cycles is taken and later ones are ignored. + /// On revision 5 and later each destination has a fence naming the + /// sources it admits: the core resets only if its fence admits the + /// source, and each link whose fence does gets a reset pulse out to + /// its far end ([`Self::on_link_reset`]). Fences keep their programmed + /// value; the lock clears with the core. Earlier revisions admit every + /// source. + pub fn warm_reset(&self, source: u8, now: u64) { + let Some(port) = link_bit(source).map(|b| b.trailing_zeros() as usize) else { return }; + let fenced = self.revision >= REVISION_FENCES || self.part == Part::XBridge; + self.locked(|r, out| { + if now < r.warm_quiet_until { + crate::dlog!(crate::devlog::LogModule::Bedrock, "xbow: warm reset from widget {source} inside the quiet window; ignored"); + return; + } + let fences = r.fences; + let admits = |fence: u32| !fenced || fence >> port & 1 != 0; + if admits(fences[0]) { + let present = self.present.load(Ordering::Acquire); + *r = Regs::reset(self.part, present); + self.alive.store(present, Ordering::Release); + r.fences = fences; + self.update_interrupt(r, out); + } + r.warm_quiet_until = now + WARM_RESET_QUIET_CYCLES * CORE_CYCLE_NS; + for (link, &fence) in fences[1..].iter().enumerate() { + if admits(fence) { + out.link_resets.push(FIRST_WIDGET + link as u8); + } + } + }); + } } impl crate::traits::Resettable for Xbow { @@ -841,8 +1064,15 @@ impl Saveable for Xbow { interrupt_asserted: field_bool(v, "interrupt_asserted")?, fences: field_u32s(v, "fences")?, lock: field_u32(v, "lock")?, + // A link reset lasts two core cycles and the warm-reset quiet + // time is counted on the board clock; a snapshot carries + // neither, so a restored crossbow has none in progress. + reset_due: [0; LINKS], + reset_restore: 0, + warm_quiet_until: 0, }; *self.regs.lock() = n; + self.reset_due.store(u64::MAX, Ordering::Release); self.alive.store(field_u8(v, "alive")?, Ordering::Release); Ok(()) } @@ -1105,6 +1335,166 @@ mod tests { assert_eq!(x.write32(0x1000_0000 + IDENTIFICATION as u64 + 4, 0), BUS_ERR); } + /// A write to a read-only error address register is an access error and + /// changes nothing. With posted error + /// address writes the store still completes, as QEMU's crossbar does with + /// `complete-posted-error-address-writes` (hw/misc/sgi-xbow.c, asserted + /// set on the Origin 2000 by sn0-origin2000-test's xio-owner case); other + /// read-only registers are refused as before. + #[test] + fn a_posted_error_address_write_completes_and_still_records_the_access_error() { + let refusing = octane(); + assert_eq!(refusing.write32(ERR_LOWER as u64 + 4, 0xdead_beef), BUS_ERR); + let x = octane().with_posted_error_address_writes(); + assert_eq!(x.write32(ERR_LOWER as u64 + 4, 0xdead_beef), BUS_OK); + assert_eq!(x.read(ERR_LOWER + 4, 4), Some(0), "the register keeps its value"); + assert_eq!(x.read(STATUS + 4, 4).unwrap() as u32 & STATUS_ACCESS_ERROR, STATUS_ACCESS_ERROR); + assert_eq!(x.write32(ERR_UPPER as u64 + 4, 0), BUS_OK); + assert_eq!(x.read(STATUS + 4, 4).unwrap() as u32 & STATUS_MULTIPLE_ERROR, STATUS_MULTIPLE_ERROR); + assert_eq!(x.unsupported(), vec![(ERR_LOWER + 4, 4, true), (ERR_UPPER + 4, 4, true)]); + assert_eq!(x.write32(IDENTIFICATION as u64 + 4, 0), BUS_ERR, "only the error address registers"); + assert_eq!(x.write64(ERR_LOWER as u64, 0), BUS_ERR, "nor an eight-byte write"); + } + + /// A Crossbow built without the posted-write option, as the Octane and + /// any other standalone one is, refuses both error address registers and + /// still records the access error; only the PXBOW completes them. + #[test] + fn a_standalone_crossbow_refuses_both_error_address_writes() { + for revision in [4, 5] { + let x = Xbow::new(revision, 0x8400); + assert_eq!(x.write32(ERR_LOWER as u64 + 4, 0xdead_beef), BUS_ERR, "revision {revision}"); + assert_eq!(x.write32(ERR_UPPER as u64 + 4, 0), BUS_ERR, "revision {revision}"); + assert_eq!(x.read(STATUS + 4, 4).unwrap() as u32 & STATUS_ACCESS_ERROR, STATUS_ACCESS_ERROR); + } + } + + /// The fences, the lock and a non-zero part number belong to revision 5 + /// and later; the Octane's revision 4 leaves them reserved. + #[test] + fn fences_the_lock_and_the_identity_fields_need_the_revision_that_has_them() { + let x = octane(); + for slot in [FENCE_BASE, (FENCE_BASE + 8), LOCK, LOCK_CLEAR] { + assert_eq!(x.read(slot + 4, 4), None, "slot {slot:#x} is reserved on revision 4"); + assert!(!x.write(slot + 4, 4, 1)); + } + assert_eq!(x.read(IDENTIFICATION + 4, 4), Some(0x4000_0001)); + let y = Xbow::new(5, 0x8400).with_identity(0xd100, 0); + assert_eq!(y.read(IDENTIFICATION + 4, 4), Some(0x5d10_0001)); + assert_eq!(y.read(FENCE_BASE + 4, 4), Some(0xff)); + assert!(y.write(FENCE_BASE + 4, 4, 0x1ff)); + assert_eq!(y.read(FENCE_BASE + 4, 4), Some(0xff), "a fence is eight bits"); + } + + fn timed(revision: u32) -> (Xbow, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let clock = now.clone(); + let x = Xbow::new(revision, 0x8100).with_clock(Arc::new(move || clock.load(Ordering::SeqCst))); + (x, now) + } + + /// Link reset is a pulse of two core cycles: the link is down and not + /// initializing-failed while it lasts, then a fitted live link returns + /// with an alive event. (sn0-xbow-test link-reset-pulse) + #[test] + fn a_link_reset_holds_the_link_down_for_twenty_nanoseconds() { + let (x, now) = timed(4); + let status = |link| LINK_BASE + LINK_STRIDE * link + LINK_STATUS + 4; + now.store(1000, Ordering::SeqCst); + assert!(x.write(LINK_BASE + LINK_STRIDE * 3 + LINK_RESET + 4, 4, 0), "widget 11 has nothing fitted"); + assert!(x.write(LINK_BASE + LINK_STRIDE * 7 + LINK_RESET + 4, 4, 0)); + assert!(!x.carries_timed(15)); + assert_eq!(x.read(status(7), 4), Some(0), "initializing shows none of the three bits"); + assert_eq!(x.read(LINK_BASE + LINK_STRIDE * 7 + LINK_AUX_STATUS + 4, 4), Some(AUX_PRESENT as u64)); + now.store(1019, Ordering::SeqCst); + assert!(!x.carries_timed(15)); + now.store(1020, Ordering::SeqCst); + assert!(x.carries_timed(15), "the pulse is over by the clock without any register access"); + assert_eq!(x.read(status(7), 4).unwrap() as u32 & LINK_ALIVE, LINK_ALIVE); + assert!(!x.carries_timed(11), "an empty port stays down"); + assert_eq!(x.read(status(3), 4), Some(LINK_MAX_RETRY as u64)); + assert!(x.carries_timed(8), "other links were not touched"); + } + + /// A register read ends a finished pulse before it answers. + #[test] + fn a_register_read_after_the_pulse_sees_the_link_back() { + let (x, now) = timed(4); + assert!(x.write(LINK_BASE + LINK_STRIDE * 7 + LINK_RESET + 4, 4, 0)); + now.store(20, Ordering::SeqCst); + let aux = x.read(LINK_BASE + LINK_STRIDE * 7 + LINK_AUX_STATUS + 4, 4).unwrap() as u32; + assert_eq!(aux & (AUX_PRESENT | AUX_FAILURE_MODE), AUX_PRESENT); + } + + fn resets(x: &Xbow) -> Arc>> { + let seen = Arc::new(Mutex::new(Vec::new())); + let s = seen.clone(); + x.on_link_reset(Box::new(move |w| s.lock().push(w))); + seen + } + + /// Fences gate a warm reset per destination, an accepted one leaves + /// them as programmed, and a machine reset restores them. + /// (sn0-xbow-test fence-persists-across-warm-reset, + /// fence-independence-link-outlives-core-rejection) + #[test] + fn a_warm_reset_obeys_each_destinations_fence_and_keeps_the_fences() { + let (x, now) = timed(5); + let seen = resets(&x); + assert!(x.write(CONTROL + 4, 4, 0x26)); + assert!(x.write(FENCE_BASE + 4, 4, 0xbf)); + assert!(x.write((FENCE_BASE + 8) + 8 * 7 + 4, 4, 0x3c)); + assert!(x.write((FENCE_BASE + 8) + 4, 4, 0xfd)); + x.warm_reset(15, 0); + assert_eq!(x.read(CONTROL + 4, 4), Some(0), "the core admitted widget 15 and reset"); + assert_eq!(x.read(FENCE_BASE + 4, 4), Some(0xbf)); + assert_eq!(x.read((FENCE_BASE + 8) + 8 * 7 + 4, 4), Some(0x3c)); + // Link 7's fence refuses port 15 (bit 7 clear); the rest admit it. + assert_eq!(*seen.lock(), (8..15).collect::>()); + // A core that refuses the source is left alone while links still admit. + assert!(x.write(CONTROL + 4, 4, 0x26)); + assert!(x.write(FENCE_BASE + 4, 4, 0xbf & !(1 << 7))); + now.store(20_000, Ordering::SeqCst); + x.warm_reset(15, 20_000); + assert_eq!(x.read(CONTROL + 4, 4), Some(0x26)); + crate::traits::Resettable::power_on(&x); + assert_eq!(x.read(FENCE_BASE + 4, 4), Some(0xff)); + assert_eq!(x.read((FENCE_BASE + 8) + 8 * 7 + 4, 4), Some(0xff)); + } + + /// A second warm reset inside 1024 core cycles is the first one's echo. + /// The lock clears with the core only. (sn0-xbow-test + /// lock-clears-with-core-reset-only) + #[test] + fn a_warm_reset_inside_the_quiet_window_is_ignored_and_the_lock_follows_the_core() { + let (x, _) = timed(5); + let seen = resets(&x); + assert!(x.write(FENCE_BASE + 4, 4, 0xff & !(1 << 7))); + assert!(x.write(LOCK + 4, 4, 0x66)); + x.warm_reset(15, 100); + assert_eq!(x.read(LOCK + 4, 4), Some(0x66), "fenced out: the core is untouched"); + assert_eq!(seen.lock().len(), 8, "every link admitted the source"); + assert!(x.write(FENCE_BASE + 4, 4, 0xff)); + x.warm_reset(15, 100 + 10_239); + assert_eq!(x.read(LOCK + 4, 4), Some(0x66), "still inside the window"); + assert_eq!(seen.lock().len(), 8); + x.warm_reset(15, 100 + 10_240); + assert_eq!(x.read(LOCK + 4, 4), Some(0)); + assert_eq!(seen.lock().len(), 16); + } + + /// Revision 4 has no fences, so a warm reset always reaches the core and + /// every link. (sn0-xbow-test warm-reset-revision-4-unaffected-by-fence) + #[test] + fn revision_four_admits_every_warm_reset() { + let (x, _) = timed(4); + let seen = resets(&x); + assert!(x.write(CONTROL + 4, 4, 0x26)); + assert!(!x.write(FENCE_BASE + 4, 4, 0), "reserved on this revision"); + x.warm_reset(15, 0); + assert_eq!(x.read(CONTROL + 4, 4), Some(0)); + assert_eq!(seen.lock().len(), 8); + } #[test] fn link_changes_reach_the_callback_outside_the_lock() { let x = Arc::new(octane()); From 0ebe6c55e6dc3b2761ab32f5612779001000dca7 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sun, 8 Nov 2026 20:23:24 -0700 Subject: [PATCH 23/33] pci: let a function take a whole-dword configuration write A NIC's indirect register and memory windows are configuration dwords the chip computes: a write to the data dword is a register write, and writing the same value twice is two register writes. The header cannot do that, because it stores a value and tells a function about it only when the stored value changed (config_written), and the window dwords have no writable bits to change. config_write is offered a whole-dword write before the header. A function that takes it returns true and the header stores nothing; the default leaves every other function as it was. Narrower writes keep the header's path. The read side already works: a function overrides config_dword. --- docs/pci.md | 5 ++++- src/pci/bus.rs | 39 ++++++++++++++++++++++++++++++++++++++- src/pci/function.rs | 9 +++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/docs/pci.md b/docs/pci.md index 9d0aecd39..7c283c578 100644 --- a/docs/pci.md +++ b/docs/pci.md @@ -15,7 +15,10 @@ header and, once the bus attaches it, its interrupt line and DMA. The bus resets a function's header; a function never resets its own, so the decode table cannot go stale behind a BAR that moved. Headers change only through the bus: a configuration write, a reset or a snapshot load, and each updates the decode -table and the BAR mappings. +table and the BAR mappings. A function whose chip +computes a configuration register (the indirect windows of a NIC) overrides +`config_dword` for the read, and `config_write` to take a whole-dword write +itself, which the header then never sees. `PciBus::attach` gives the function its DMA and, if its header names an interrupt pin, the line that pin drives, then calls `attached` for a function diff --git a/src/pci/bus.rs b/src/pci/bus.rs index 55f116203..b4ae1304f 100644 --- a/src/pci/bus.rs +++ b/src/pci/bus.rs @@ -206,7 +206,8 @@ impl PciBus { } /// A configuration write of `width` bytes at byte `off`: only those bytes - /// change, and the header decides which of their bits stick. False if + /// change, and the header decides which of their bits stick. A function + /// may take a whole-dword write itself ([`PciFunction::config_write`]). False if /// nothing answers on a root bus; behind a bridge the write is discarded by /// master abort, as a read of nothing is (see [`PciBus::config_read`]), and /// counts as answered. The mappings are updated when the command register, @@ -217,6 +218,9 @@ impl PciBus { crate::dlog!(LogModule::Pci, "pci: bus {} {}.{} write {off:#04x}/{width} {val:#x}{}", self.0.number, at.device, at.function, if f.is_some() { "" } else { ", no answer" }); let Some(f) = f else { return self.master_abort() }; + if width == 4 && f.config_write(off / 4, val) { + return true; + } let (cfg, reg, shift) = (f.state().config(), off / 4, (off & 3) * 8); let mask = (u32::MAX >> (32 - 8 * width)) << shift; let old = cfg.read(reg); @@ -544,6 +548,39 @@ mod tests { assert!(b.occupied(3) && !b.occupied(4)); } + /// A function whose dword 0x1e is a window into the chip: a whole-dword + /// write goes to the chip every time, even when it repeats a value, and + /// the header stores nothing; a narrower one takes the header's path. + #[test] + fn a_function_takes_a_whole_dword_configuration_write_itself() { + struct Window(crate::pci::PciFunctionState, Mutex>); + impl PciFunction for Window { + fn state(&self) -> &crate::pci::PciFunctionState { + &self.0 + } + fn bar_read(&self, _: usize, _: u32, _: usize) -> u32 { + 0 + } + fn bar_write(&self, _: usize, _: u32, _: u32, _: usize) {} + fn config_write(&self, reg: usize, val: u32) -> bool { + (reg == 0x1e).then(|| self.1.lock().unwrap().push(val)).is_some() + } + } + let (_h, b) = bus(); + let w = Arc::new(Window(crate::pci::PciFunctionState::new(PciConfig::new(1, 2, 0, 1, bars(), PCI_INTA)), Mutex::new(Vec::new()))); + b.attach(at(1, 0), w.clone()); + assert!(b.config_write(at(1, 0), 0x78, 4, 0x1234_5678)); + assert!(b.config_write(at(1, 0), 0x78, 4, 0x1234_5678)); + assert_eq!(*w.1.lock().unwrap(), [0x1234_5678, 0x1234_5678], "a repeated value reaches the chip again"); + assert_eq!(b.config_read(at(1, 0), 0x78, 4), Some(0), "the header stored nothing"); + assert!(b.config_write(at(1, 0), 0x78, 2, 0xffff)); + assert_eq!(w.1.lock().unwrap().len(), 2, "a halfword write is the header's"); + // Another dword is the header's too. + assert!(b.config_write(at(1, 0), 0x3c, 4, 0x0000_00ab)); + assert_eq!(b.config_read(at(1, 0), 0x3c, 1), Some(0xab)); + assert_eq!(w.1.lock().unwrap().len(), 2); + } + #[test] fn config_size_probe_and_masks() { let (_h, b) = bus(); diff --git a/src/pci/function.rs b/src/pci/function.rs index d239d1e62..a7d12e7d3 100644 --- a/src/pci/function.rs +++ b/src/pci/function.rs @@ -73,6 +73,15 @@ pub trait PciFunction: Send + Sync { /// shared core, and the SCSI controllers drive their pins. fn attached(&self) {} + /// A configuration write of the whole dword `reg`, offered before the + /// header sees it. A function whose chip computes the register (the + /// indirect windows of a NIC) takes it here, returns true, and the header + /// stores nothing; every other function leaves it to the header. Narrower + /// writes always take the header's path. Called with no header lock held. + fn config_write(&self, _reg: usize, _val: u32) -> bool { + false + } + /// The bus wrote configuration dword `reg` of this function's header and /// it changed from `old` to `new`. A PCI-to-PCI bridge acts on bridge /// control here; every other function has nothing to do. From 01a9f879323703adc47f6f9e8d8a056b824dd9bf Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 21:29:21 -0700 Subject: [PATCH 24/33] bcm5701: add the Broadcom gigabit Ethernet controller The Tezro's IO9 board carries a Broadcom BCM5701 gigabit Ethernet controller (PCI 14e4:1645) next to its IOC4 and SCSI controller, and nothing in Iris implemented one. The PROM and the IRIX driver both bring the chip up through its registers and then move frames through its rings. BAR0 is 64 KiB: the register file, whose first 256 bytes alias the PCI configuration registers, and a window onto 32 KiB of on-chip SRAM. A driver reaches the same state three ways: the BAR, an indirect register window and an indirect memory window in configuration space. The last two are registers the chip computes, which a plain configuration header cannot: the function answers them from config_dword and config_write, so every bus that attaches it works the same. A slot that drives REQ64# is part of the board's config; the chip samples it as reset ends. The model has the chip id in MISC_CTL, the global reset with the bootcode's firmware-ready handshake, the self-clearing mode resets, the buffer manager's delayed enable, the station address the bootcode leaves in SRAM and filter slot 0, the interrupt mailbox and global mask with PCISTATE.INTR_NOT_ACTIVE, the MII management path to the copper PHY, and the serial EEPROM controller over a backing the caller supplies. Nothing synthesises an EEPROM image: no complete board image is established. Anything unmodelled (a write to an unmodelled register, an undefined MII opcode, direct EEPROM pin control, a BAR access of a width the chip refuses) is stored or dropped but counted in Bcm5701::refused and logged on first sight. The send ring is walked from the chip's consumer to the producer the driver writes to the doorbell mailbox. A frame's descriptors are gathered, source-address replacement and checksum offload are applied if asked, and a large TCP buffer with a segment size is cut in software. A doorbell that ends inside a frame, or a descriptor or buffer the bus cannot reach, rolls back to the start of that frame so a later doorbell retries it. The standard, jumbo and return rings carry received frames: the ring is chosen by size, a frame with no fetched buffer is discarded and counted when the other ring could still take traffic and otherwise left at the head of the backend's queue. The status block and coalescing follow the register contract: update criteria are a descriptor count or a microsecond budget per direction, an expired budget reports the next event at once, zero is off, and an update the bus refused is retried. A completion is held for a 200 microsecond minimum arrival latency so a reply never overtakes the transmit that provoked it. That is host time, which passes whether or not the guest runs, so the completion also waits until each processor that rang the send doorbell has run 20,000 cycles past it, on the cycle counters the machine hands over. With host time alone, a Tezro guest on one processor of a loaded host lost up to 94% of its pings to the NAT gateway: IRIX's ICMP input counted every reply, but one reported while ping was still inside sendto never reached it. Descriptors, the status block and frame data go through the chip's byte-swap controls as lane permutations; the bridge's own byte order sits behind PciDma. Frames use the shared network link as the IOC3 Ethernet does. The status block's standard and jumbo consumers report how far the chip has fetched descriptors, not how far frames have used them. Broadcom's programmer's guide says the chip copies a quarter of each producer ring into its own memory ahead of use (128 standard and 64 jumbo descriptors, the sizes Linux tg3 uses) and reports the last descriptor index it took in, so on hardware the consumer runs ahead of the received frames; a driver reading it must see the same. A frame takes a buffer the chip has fetched. The chip fetches when the producer is written and when a frame frees a cache entry, never past the producer, and only once the host has posted the ring's replenish threshold of descriptors. The guide gives neither the fetch burst size nor its latency, so a fetch fills the cache at once, and only the index is kept: a frame still reads its descriptor from host memory when it lands. The function reports PCI revision 0x15, the B5 stepping's (chip id 0x105): what an Origin 350's IO9 controller reports under OpenBSD and a Tezro's PROM inventory prints. It has the chip's capability list, at 0x40 PCI-X, then power management, vital product data and MSI, with the reset values and writable bits of the BCM57XX programmer's guide (57XX-PG105-R, pp. 310-322). Only the VPD acts: a read sets the flag at once and returns the four bytes at the address from the VPD area of a fitted EEPROM (offset 0x100), all ones without one; a VPD write is refused and counted; MSI cannot be enabled, so the chip always uses INTA. The configuration alias in BAR0 now takes naturally aligned byte and halfword accesses as a configuration cycle does, since IRIX's tg driver reads the VPD at every attach with halfword accesses to the address register there and polls its flag up to 1000 times per word. What an unprogrammed board's VPD returns is unknown. Timers fire at their own deadlines, earliest first with the clock set to each one's deadline, so a timer that re-arms another counts from its expiry. The PHY settles after 100 ms and the buffer manager enables after 8 ms. A chip or machine reset purges the frames the backend still holds, so one queued before the reset cannot land in rings set up after it. The tests port the plain-memory cases from bcm570x-test.c: registers, descriptors, status block, coalescing, interrupts, receive, the receive queue reset and the checksum report for every malformed header. Cases needing virtio-net headers, QEMU's user-mode network, a TAP, the dual-port BCM5704 or migration have no counterpart; the bridge byte-order cases belong with the PIC. One more case, with no QEMU counterpart, holds a reply until the processor that sent the request has run on. Ported from hw/net/bcm570x.c and hw/net/bcm570x_phy.c in jamesbraid/qemu-sgi-origami. --- src/dev/bcm5701.rs | 734 ++++++++++ src/dev/bcm5701/dma.rs | 1148 ++++++++++++++++ src/dev/bcm5701/eeprom.rs | 113 ++ src/dev/bcm5701/phy.rs | 263 ++++ src/dev/bcm5701/state.rs | 742 +++++++++++ src/dev/bcm5701/tests.rs | 2643 +++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 7 files changed, 5644 insertions(+) create mode 100644 src/dev/bcm5701.rs create mode 100644 src/dev/bcm5701/dma.rs create mode 100644 src/dev/bcm5701/eeprom.rs create mode 100644 src/dev/bcm5701/phy.rs create mode 100644 src/dev/bcm5701/state.rs create mode 100644 src/dev/bcm5701/tests.rs diff --git a/src/dev/bcm5701.rs b/src/dev/bcm5701.rs new file mode 100644 index 000000000..81cd29c8a --- /dev/null +++ b/src/dev/bcm5701.rs @@ -0,0 +1,734 @@ +//! The Broadcom BCM5701 gigabit Ethernet controller (PCI 14e4:1645), as the +//! single-port SGI board carries it: a descriptor-ring MAC with an integrated +//! copper PHY, which the public Linux tg3 and OpenBSD bge drivers and the +//! IRIX tg driver all drive. +//! +//! BAR0 is 64 KiB: a register file at 0x0000-0x7fff (its first 256 bytes also +//! alias the PCI configuration registers) and a 32 KiB window onto the 32 KiB +//! on-chip SRAM at 0x8000-0xffff, whose base the memory-window base register +//! selects. The driver reaches chip state three ways: the BAR directly, an +//! indirect register window through configuration registers 0x78 and 0x80, +//! and an indirect memory window through 0x7c and 0x84. All three land on the +//! same state. The configuration side is the function's own `config_dword` and +//! `config_write`, so it works behind any bridge. +//! +//! Modelled: the PCI identity and revision; the capability list and vital +//! product data reads ([`caps`]); the chip id in MISC_CTL; the global reset with +//! the on-chip firmware-ready handshake in SRAM and the firmware-progress +//! register; the self-clearing mode-register resets; the MII path to the +//! PHY ([`phy`]); the station address the bootcode leaves in SRAM and in +//! address filter slot 0; the interrupt mailbox, the MISC_CTL global mask and +//! PCISTATE.INTR_NOT_ACTIVE; the buffer manager's delayed enable; and the +//! serial EEPROM controller over a caller-supplied backing ([`eeprom`]); the +//! send ring and its doorbell, the standard, jumbo and return receive rings, +//! the status block DMA, host coalescing, the interrupt it drives, the +//! transmit checksum offload and segmentation, and the receive checksum +//! report ([`dma`]). +//! +//! # What is approximated +//! +//! - Iris has no virtual clock, so timing is wall-clock. Deadlines (the PHY's +//! 100 ms settle, the buffer manager's 8 ms startup, the coalescing budgets +//! and the arrival latency) are evaluated when the device is touched and by +//! the receive thread, each at its own expiry and earliest first, as the +//! model's virtual clock would fire them. +//! - Wall-clock time passes whether or not the guest runs, so the arrival +//! latency also waits for each processor that rang the send doorbell to run +//! 20,000 cycles past it, 20 to 29 microseconds at the R16000's 700 MHz to +//! 1 GHz and so within the latency itself. Without it a reply could be +//! reported while the guest was still sending the request (see +//! `dma::HeldCompletion`). +//! - EEPROM transfers complete synchronously; the delay register only reads +//! back, and the 10 ms write spacing is not enforced. Direct serial-pin +//! control is refused and counted. +//! - The management interface ignores the PHY address field; one PHY answers. +//! - A write to a register with no modelled behaviour is stored and reads +//! back, and is recorded in [`Bcm5701::refused`]. +//! - The receive descriptor cache is an index, not a copy: the chip fetches +//! up to a quarter of each producer ring ahead of use and the status block +//! reports how far, but the fetch is instant, fills the cache at once, and +//! a frame reads its descriptor from host memory when it lands. +//! - The BCM5704 dual-port variants are not modelled. +//! - The capability list has the chip's four entries, but only the vital +//! product data reads act: the PCI-X and power-management fields store +//! what is written and change nothing, MSI cannot be enabled, so the chip +//! always interrupts on INTA, and a VPD write is refused and counted. +//! Without a fitted EEPROM the VPD reads all ones; what an unprogrammed +//! board returns is unknown. +//! - Frames reach the host through the shared network link; the chip's +//! virtio-net offload path has no counterpart, so a large TCP buffer is +//! always cut into segments in software. +//! - A transmit is carried out entirely inside the doorbell write, so there +//! is no re-entrancy guard and no deferred flush: the receive thread offers +//! a waiting frame again whenever the guest makes room. +//! +//! Ported from hw/net/bcm570x.c and hw/net/bcm570x_phy.c in +//! jamesbraid/qemu-sgi-origami. + +mod dma; +pub mod eeprom; +pub mod phy; +mod state; + +#[cfg(test)] +mod tests; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; + +use parking_lot::Mutex; + +use crate::irq::IrqLine; +use crate::pci::{Bar, PciConfig, PciConfigPolicy, PciFunction, PciFunctionState, PciRegisterPolicy, PCI_INTA}; +use crate::traits::{Device, Resettable}; +use eeprom::Eeprom; +pub use dma::{DmaWhat, Mailbox, Ring, RxOutcome, MAX_FRAME, RX_ARRIVAL_CYCLES_DEFAULT, RX_ARRIVAL_LATENCY_DEFAULT}; +pub use state::Refused; +use state::{cfg, misc_ctl, pcistate, Header, State}; + +pub const VENDOR_ID: u16 = 0x14e4; +pub const DEVICE_ID: u16 = 0x1645; +/// The SGI board's subsystem identity. +pub const SUBSYSTEM_VENDOR_ID: u16 = 0x10a9; +pub const SUBSYSTEM_ID: u16 = 0x8010; +/// PCI network controller, Ethernet. +const CLASS_ETHERNET: u32 = 0x02_0000; +/// The PCI revision of the B5 stepping (chip id 0x105): what an Origin +/// 350's IO9 controller reports under OpenBSD and a Tezro's PROM inventory +/// prints for its on-board controller. +pub const PCI_REVISION: u8 = 0x15; + +pub const BAR0_SIZE: u32 = 0x1_0000; +/// The low 256 bytes of BAR0 are the PCI configuration registers again. +const CFG_ALIAS: u32 = 0x100; + +/// The capability list at 0x40: PCI-X, power management, vital product data +/// and MSI, chained in that order, with the reset values and writable bits +/// the BCM57XX programmer's guide (57XX-PG105-R, pp. 310-322) gives a +/// conventional-PCI BCM5701 with no auxiliary power. +pub(crate) mod caps { + use crate::pci::PciRegisterPolicy; + + const fn at(offset: usize, initial: u32, writable_mask: u32) -> PciRegisterPolicy { + PciRegisterPolicy { reg_dword: offset / 4, initial, writable_mask } + } + pub const POINTER: PciRegisterPolicy = at(0x34, 0x40, 0); + /// PCI-X: next 0x48; command with relaxed ordering enabled, bits 6:0 + /// writable. + pub const PCIX_COMMAND: PciRegisterPolicy = at(0x40, 0x0002_4807, 0x007f_0000); + /// PCI-X status: 64-bit, 133 MHz capable, bus 0xff, device 0x1f, + /// function 1, as the chip leaves it until a PCI-X configuration cycle. + pub const PCIX_STATUS: PciRegisterPolicy = at(0x44, 0x0003_fff9, 0); + /// Power management: next 0x50, PME from D3hot, version 1.1. + pub const PM_CAPABILITIES: PciRegisterPolicy = at(0x48, 0x4002_5001, 0); + /// Control and status: data scale 1; data select, PME enable and power + /// state writable. + pub const PM_CONTROL: PciRegisterPolicy = at(0x4c, 0x0000_2000, 0x0000_1f03); + /// Vital product data: next 0x58; flag and address writable. + pub const VPD_ADDRESS: PciRegisterPolicy = at(0x50, 0x0000_5803, 0xffff_0000); + pub const VPD_DATA: PciRegisterPolicy = at(0x54, 0, 0xffff_ffff); + /// MSI: last entry, 64-bit, eight messages requested. The enable bit + /// reads zero (not modelled); the messages-allocated field is writable. + pub const MSI_CONTROL: PciRegisterPolicy = at(0x58, 0x0086_0005, 0x0070_0000); + pub const MSI_ADDRESS_LOW: PciRegisterPolicy = at(0x5c, 0, 0xffff_fffc); + pub const MSI_ADDRESS_HIGH: PciRegisterPolicy = at(0x60, 0, 0xffff_ffff); + pub const MSI_DATA: PciRegisterPolicy = at(0x64, 0, 0x0000_ffff); + + pub const VPD_DWORD: usize = 0x50 / 4; + pub const VPD_DATA_DWORD: usize = 0x54 / 4; + /// In the VPD dword: the flag, bit 15 of the address register at 0x52. + pub const VPD_FLAG: u32 = 1 << 31; + pub const VPD_ADDRESS_MASK: u32 = 0x7ffc; + /// Where the VPD lies in the chip's NVRAM. + pub const VPD_BASE: u32 = 0x100; +} + +/// What a board gives the chip: its station address and, optionally, the +/// serial EEPROM the board fits. +pub struct Bcm5701Config { + pub mac: [u8; 6], + pub eeprom: Option, + /// The minimum between a frame arriving and the driver being told, so a + /// completion never beats the arrival it reports. Zero publishes at once. + pub rx_arrival_latency: Duration, + /// The same minimum in cycles of the processor that rang the send + /// doorbell, once [`Bcm5701::set_cpu_cycles`] has wired the counters. + pub rx_arrival_cycles: u64, + /// Whether the slot drives REQ64# as reset ends. The chip samples it to + /// report a 64-bit bus in PCISTATE. + pub req64: bool, +} + +impl Bcm5701Config { + pub fn new(mac: [u8; 6]) -> Self { + Self { + mac, + eeprom: None, + rx_arrival_latency: RX_ARRIVAL_LATENCY_DEFAULT, + rx_arrival_cycles: RX_ARRIVAL_CYCLES_DEFAULT, + req64: false, + } + } +} + +pub struct Bcm5701 { + pci: PciFunctionState, + st: Mutex, + irq: Mutex, + /// Serialises delivery of level changes to the interrupt line. + irq_order: Mutex<()>, + link: Mutex>, + rx_wake: Mutex>>, + /// Between `start` and `stop`; the NAT engine watches it too. + running: Arc, + /// The flag the network backend watches, set and cleared with `running`. + net_running: Mutex>>, + /// The network backend, between runs. A NAT engine keeps its flows across + /// a stop. + backend: Arc>>>, + /// The NAT and receive threads, joined in `stop`. + threads: Mutex>>, + me: Weak, + /// Tests freeze the clock to step it exactly. + #[cfg(test)] + frozen: Mutex>, +} + +impl Bcm5701 { + pub fn new(config: Bcm5701Config) -> Result, String> { + if config.mac[0] & 1 != 0 { + return Err("bcm5701: station address must be unicast".into()); + } + let policy = PciConfigPolicy { + registers: &[ + // The chip id in the high half is read-only; the driver's + // byte-order and interrupt-mask bits in the low half stick. + PciRegisterPolicy { + reg_dword: cfg::MISC_CTL, + initial: state::CHIPID_BCM5701 << 16, + writable_mask: misc_ctl::WRITABLE, + }, + // The chip drives PCISTATE, which `config_dword` reports from + // its state; the guest cannot write it. It runs as + // conventional PCI and starts with the interrupt line down. + PciRegisterPolicy { + reg_dword: cfg::PCISTATE, + initial: State::pcistate_at_reset(config.req64), + writable_mask: 0, + }, + // The indirect windows live in the chip, reached by whole + // dwords through `config_dword` and `config_write`. The header + // bytes behind them are what a narrower write reaches, and + // nothing is stored there. + PciRegisterPolicy { reg_dword: cfg::REG_BASEADDR, initial: 0, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: cfg::MEMWIN_BASEADDR, initial: 0, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: cfg::REG_DATA, initial: 0, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: cfg::MEMWIN_DATA, initial: 0, writable_mask: 0 }, + caps::POINTER, + caps::PCIX_COMMAND, + caps::PCIX_STATUS, + caps::PM_CAPABILITIES, + caps::PM_CONTROL, + caps::VPD_ADDRESS, + caps::VPD_DATA, + caps::MSI_CONTROL, + caps::MSI_ADDRESS_LOW, + caps::MSI_ADDRESS_HIGH, + caps::MSI_DATA, + ], + ..Default::default() + }; + let pci = PciConfig::new_with_policy( + VENDOR_ID, + DEVICE_ID, + CLASS_ETHERNET, + PCI_REVISION, + [Bar::Mem(BAR0_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], + PCI_INTA, + policy, + ); + pci.write(11, u32::from(SUBSYSTEM_ID) << 16 | u32::from(SUBSYSTEM_VENDOR_ID)); + let now = Instant::now(); + let dev = Arc::new_cyclic(|me| Self { + st: Mutex::new(State::new( + Header::new(me.clone()), + config.req64, + config.mac, + config.eeprom, + config.rx_arrival_latency, + config.rx_arrival_cycles, + now, + )), + pci: PciFunctionState::new(pci), + irq: Mutex::new(IrqLine::unconnected()), + irq_order: Mutex::new(()), + link: Mutex::new(None), + rx_wake: Mutex::new(None), + running: Arc::new(AtomicBool::new(false)), + net_running: Mutex::new(None), + backend: Arc::new(Mutex::new(None)), + threads: Mutex::new(Vec::new()), + me: me.clone(), + #[cfg(test)] + frozen: Mutex::new(None), + }); + Ok(dev) + } + + /// The header the bus keeps for the function. + fn config_header(&self) -> &PciConfig { + self.pci.config() + } + + fn now(&self) -> Instant { + #[cfg(test)] + if let Some(t) = *self.frozen.lock() { + return t; + } + Instant::now() + } + + /// Run `f` with the state at `now`, then deliver what it produced: any + /// interrupt level it changed, frames to send, and a wake-up for the + /// receive thread if the guest made room. + fn enter(&self, now: Instant, f: impl FnOnce(&mut State) -> R) -> R { + let (result, wire, wake, purge) = { + let mut st = self.st.lock(); + st.enter(now); + let result = f(&mut st); + ( + result, + std::mem::take(&mut st.wire), + std::mem::take(&mut st.wake_rx), + std::mem::take(&mut st.purge_rx), + ) + }; + if purge { + // A reset drops what the backend was holding for the guest, so a + // frame from before it cannot reach the new rings. + if let Some(link) = self.link.lock().as_mut() { + while link.from_net.pop().is_ok() {} + } + } + self.irq_flush(); + self.send(wire); + if wake { + self.wake_rx(); + } + result + } + + /// Deliver queued interrupt level changes, in order, with no state lock + /// held that the line could need. + fn irq_flush(&self) { + let _order = self.irq_order.lock(); + let events = std::mem::take(&mut self.st.lock().irq_events); + if events.is_empty() { + return; + } + let line = self.irq.lock(); + for level in events { + line.set(level); + } + } + + /// A machine reset: registers, SRAM, PHY and the PCI header as at + /// power-on, the interrupt dropped. The EEPROM keeps its contents. + pub fn reset(&self) { + self.pci.config().reset(); + PciFunction::reset(self); + } + + /// A write of `width` bytes at configuration offset `off` through the + /// BAR's alias of the header, acting as a configuration cycle does: the + /// chip's own registers take a whole-dword write, anything else is the + /// header's, and a change to it is acted on. + fn alias_write(&self, off: u32, val: u32, width: usize) { + let reg = (off / 4) as usize; + if width == 4 && self.config_write(reg, val) { + return; + } + let shift = (off & 3) * 8; + let mask = (u32::MAX >> (32 - 8 * width)) << shift; + let header = self.pci.config(); + let old = header.read(reg); + header.write_masked(reg, val << shift, mask); + let new = header.read(reg); + if new != old { + self.config_written(reg, old, new); + } + } + + /// The vital product data address register was written: a request with + /// the flag clear reads the four bytes there into the data register and + /// sets the flag; one with the flag set is a write, which is refused and + /// completes at once by clearing the flag. + fn vpd_request(&self, dword: u32) { + let header = self.pci.config(); + let address = (dword >> 16) & caps::VPD_ADDRESS_MASK; + if dword & caps::VPD_FLAG != 0 { + self.st.lock().refuse(Refused::VpdWrite); + header.write(caps::VPD_DWORD, dword & !caps::VPD_FLAG); + return; + } + let storage = self.st.lock().eeprom_bytes(); + let byte = |i: u32| { + let at = (caps::VPD_BASE + address + i) as usize; + storage.as_ref().and_then(|b| b.get(at).copied()).unwrap_or(0xff) + }; + header.write(caps::VPD_DATA_DWORD, u32::from_le_bytes([byte(0), byte(1), byte(2), byte(3)])); + header.write(caps::VPD_DWORD, dword | caps::VPD_FLAG); + } + + /// The processors' cycle counters, by CPU number, which hold a + /// completion until the processor that sent last has run on (see + /// `rx_arrival_cycles`). Set before the machine starts. + pub fn set_cpu_cycles(&self, cycles: Vec) { + self.st.lock().cpu_cycles = cycles; + } + + /// The backend's cable state; the PHY reports it as link. + pub fn set_backend_link(&self, up: bool) { + self.enter(self.now(), |st| st.phy.set_link_down(!up)); + } + + fn send(&self, frames: Vec>) { + if frames.is_empty() { + return; + } + if let Some(link) = self.link.lock().as_mut() { + for frame in frames { + let _ = link.to_net.push(frame); + } + link.tx_wake.notify(); + } + } + + fn wake_rx(&self) { + if let Some(wake) = self.rx_wake.lock().as_ref() { + wake.notify(); + } + } + + /// Connect to the backend `[network]` selected: frames sent go on + /// `link.to_net`, frames on `link.from_net` are received, and + /// `link.rx_wake` is notified when the guest does something that can + /// unblock a waiting frame. The backend and receive threads run between + /// `start` and `stop`; a NAT engine, which only trades frames with the + /// host and never reaches the guest, keeps its flows across a stop. + pub fn attach(&self, mut link: crate::net::Link) { + let backend = link.backend(); + let running = link.running(); + let rings = link.into_rings(); + *self.rx_wake.lock() = Some(rings.rx_wake.clone()); + *self.link.lock() = Some(rings); + *self.net_running.lock() = Some(running.clone()); + *self.backend.lock() = backend; + if self.running.load(Ordering::SeqCst) { + running.store(true, Ordering::SeqCst); + self.spawn_nat(); + } + } + + fn spawn_nat(&self) { + let slot = self.backend.clone(); + let Some(mut engine) = slot.lock().take() else { return }; + let t = std::thread::Builder::new() + .name("bcm5701-net".into()) + .spawn(move || { + engine.run(); + *slot.lock() = Some(engine); + }) + .expect("bcm5701 NAT thread"); + self.threads.lock().push(t); + } + + /// Take frames into guest memory as they arrive and as the guest makes + /// room, and run the device's timers, until it stops. The wait ends early + /// for a wake from the guest or the NAT engine. + fn rx_loop(me: Weak, running: Arc, wake: Arc) { + while running.load(Ordering::SeqCst) { + let timeout = { + let Some(this) = me.upgrade() else { return }; + let now = this.now(); + this.enter(now, |_| ()); + let next = this.st.lock().deadline(); + next.and_then(|d| d.checked_duration_since(now)).unwrap_or(RX_IDLE) + }; + wake.wait(timeout); + if !running.load(Ordering::SeqCst) { + return; + } + let Some(this) = me.upgrade() else { return }; + this.poll(); + } + } + + /// Take frames from the backend into the buffers the driver has posted. + /// A frame the engine cannot take yet stays at the head of the backend's + /// queue until the guest makes room; one the filter rejects is discarded. + /// Returns whether any frame was consumed. + /// + /// The link is held only to take one frame at a time, so a transmit on the + /// CPU thread never waits behind a burst of deliveries. + pub fn poll(&self) -> bool { + let mut moved = false; + loop { + let now = self.now(); + { + let mut link = self.link.lock(); + let Some(link) = link.as_mut() else { break }; + let outcome = { + let Ok(frame) = link.from_net.peek() else { break }; + let mut st = self.st.lock(); + st.enter(now); + st.receive(frame) + }; + if outcome == RxOutcome::Deferred { + break; + } + let _ = link.from_net.pop(); + moved = true; + } + self.irq_flush(); + } + moved + } + + /// Everything the model was asked for and could not do, with counts. + pub fn refused(&self) -> Vec<(Refused, u64)> { + self.st.lock().refused() + } + + /// The serial EEPROM backing's current contents, if one is fitted. + pub fn eeprom_bytes(&self) -> Option> { + self.st.lock().eeprom_bytes() + } + + /// Name the long-lived locks in the lock registry, which holds only a + /// weak reference to the device. + pub fn register_locks(self: &Arc) { + for (what, probe) in [ + ("st", (|d: &Bcm5701| d.st.is_locked()) as fn(&Bcm5701) -> bool), + ("link", |d| d.link.is_locked()), + ("irq_order", |d| d.irq_order.is_locked()), + ] { + let me = Arc::downgrade(self); + crate::locks::register_lock(format!("bcm5701::{what}"), move || me.upgrade().is_some_and(|d| probe(&d))); + } + } + + /// BAR0 accepts aligned 32-bit accesses everywhere, byte reads of the + /// first station-address pair, and naturally aligned byte and halfword + /// accesses to the configuration alias, which IRIX's driver uses for the + /// VPD address register. + fn accepts(off: u32, width: usize, write: bool) -> bool { + match width { + 4 => off & 3 == 0, + 2 | 1 if off < CFG_ALIAS => off % width as u32 == 0, + 1 => !write && (state::reg::MAC_ADDR_0_HIGH..state::reg::MAC_ADDR_0_LOW + 4).contains(&off), + _ => false, + } + } +} + +impl PciFunction for Bcm5701 { + fn state(&self) -> &PciFunctionState { + &self.pci + } + + /// The registers the chip computes: the four indirect-window registers + /// and PCISTATE. Everything else is the header's. + fn config_dword(&self, reg: usize) -> u32 { + if reg == cfg::PCISTATE { + return self.st.lock().pcistate; + } + if !(cfg::REG_BASEADDR..=cfg::MEMWIN_DATA).contains(®) { + return self.pci.config().read(reg); + } + self.enter(self.now(), |st| match reg { + cfg::REG_BASEADDR => st.reg_win_base, + cfg::MEMWIN_BASEADDR => st.mem_win_base, + cfg::REG_DATA => st.reg_read(st.reg_win_base), + _ => st.sram_read(st.mem_win_base), + }) + } + + /// A whole-dword write to the indirect-window registers acts on the chip + /// every time; a write to MISC_CTL is stored by the header and then acted + /// on, whether or not its value changed. + fn config_write(&self, reg: usize, val: u32) -> bool { + if reg == cfg::MISC_CTL { + self.pci.config().write(reg, val); + self.enter(self.now(), State::misc_ctl_written); + return true; + } + if !(cfg::REG_BASEADDR..=cfg::MEMWIN_DATA).contains(®) { + return false; + } + self.enter(self.now(), |st| match reg { + cfg::REG_BASEADDR => st.reg_win_base = val, + cfg::MEMWIN_BASEADDR => st.mem_win_base = val, + cfg::REG_DATA => st.reg_write(st.reg_win_base, val), + _ => st.sram_write(st.mem_win_base, val), + }); + true + } + + /// A narrower write to MISC_CTL changed it: act on it as a whole-dword + /// write does. A write to the VPD address register starts a transfer. + fn config_written(&self, reg: usize, _old: u32, new: u32) { + if reg == cfg::MISC_CTL { + self.enter(self.now(), State::misc_ctl_written); + } + if reg == caps::VPD_DWORD { + self.vpd_request(new); + } + } + + fn bar_read(&self, bar: usize, off: u32, width: usize) -> u32 { + // A rejected read returns zero, as the model it is ported from does + // behind its bridge; the refusal is counted. + if bar != 0 { + return 0; + } + let valid = Self::accepts(off, width, false) && off < BAR0_SIZE; + // The configuration alias goes through the function's own dwords, + // which take the state lock themselves. + if valid && off < CFG_ALIAS { + let dword = self.config_dword((off / 4) as usize); + return (dword >> ((off & 3) * 8)) & (u32::MAX >> (32 - 8 * width)); + } + self.enter(self.now(), |st| { + if !valid { + st.refuse(Refused::BarAccess { offset: off, width: width as u8, write: false }); + 0 + } else if width == 1 { + (st.reg_read(off & !3) >> ((off & 3) * 8)) & 0xff + } else if off < state::REG_FILE_SIZE { + st.reg_read(off) + } else { + st.sram_read((st.mem_win_base & !(state::SRAM_SIZE - 1)) + (off - state::REG_FILE_SIZE)) + } + }) + } + + fn bar_write(&self, bar: usize, off: u32, val: u32, width: usize) { + if bar != 0 { + return; + } + let valid = Self::accepts(off, width, true) && off < BAR0_SIZE; + if valid && off < CFG_ALIAS { + // Not the registers the bus built its BAR decode from: they change + // only through its configuration cycles. + let reg = (off / 4) as usize; + if !matches!(reg, 1 | 4..=9) { + self.alias_write(off, val, width); + } + return; + } + self.enter(self.now(), |st| { + if !valid { + st.refuse(Refused::BarAccess { offset: off, width: width as u8, write: true }); + } else if off < state::REG_FILE_SIZE { + st.reg_write(off, val); + } else { + st.sram_write((st.mem_win_base & !(state::SRAM_SIZE - 1)) + (off - state::REG_FILE_SIZE), val); + } + }); + } + + /// RST# has reset the header: the chip returns to power-on state and + /// drops its interrupt. It samples REQ64# again as reset ends. + fn reset(&self) { + self.enter(self.now(), State::reset); + } + + /// The chip's DMA, and its interrupt line, which carries the level the + /// chip already has. + fn attached(&self) { + if let Some(dma) = self.pci.dma() { + self.st.lock().dma = dma.clone(); + self.wake_rx(); + } + if let Some(line) = self.pci.irq() { + *self.irq.lock() = line.clone(); + // Present the current level to the new line. + self.st.lock().update_irq(true); + self.irq_flush(); + } + } +} + +/// How long the receive thread sleeps with no deadline to wait for: it is +/// woken by the guest and the NAT engine, so this is only a bound. +const RX_IDLE: Duration = Duration::from_secs(60); + +impl Device for Bcm5701 { + fn step(&self, _cycles: u64) {} + + /// Start the receive thread, and the NAT engine's if one is attached. + fn start(&self) { + if self.running.swap(true, Ordering::SeqCst) { + return; + } + let wake = self.rx_wake.lock().get_or_insert_with(crate::net::Wake::new).clone(); + let (me, running) = (self.me.clone(), self.running.clone()); + let rx = std::thread::Builder::new() + .name("bcm5701-rx".into()) + .spawn(move || Self::rx_loop(me, running, wake)) + .expect("bcm5701 rx thread"); + self.threads.lock().push(rx); + if let Some(r) = self.net_running.lock().as_ref() { + r.store(true, Ordering::SeqCst); + } + self.spawn_nat(); + // Frames queued while stopped. + self.wake_rx(); + } + + /// Join the threads: no frame reaches guest memory and no line moves + /// afterwards. Frames the NAT engine takes meanwhile queue for `start`. + fn stop(&self) { + if !self.running.swap(false, Ordering::SeqCst) { + return; + } + if let Some(r) = self.net_running.lock().as_ref() { + r.store(false, Ordering::SeqCst); + } + self.wake_rx(); + if let Some(l) = self.link.lock().as_ref() { + l.tx_wake.notify(); + } + // The receive thread can hold the last reference for a moment, and + // then this runs on it; it cannot join itself. + let me = std::thread::current().id(); + let threads: Vec<_> = self.threads.lock().drain(..).collect(); + for t in threads { + if t.thread().id() != me { + let _ = t.join(); + } + } + } + + fn is_running(&self) -> bool { + self.running.load(Ordering::SeqCst) + } + + fn get_clock(&self) -> u64 { + 0 + } +} + +impl Resettable for Bcm5701 { + fn power_on(&self) { + self.reset(); + } +} + +impl Drop for Bcm5701 { + fn drop(&mut self) { + Device::stop(&*self); + } +} diff --git a/src/dev/bcm5701/dma.rs b/src/dev/bcm5701/dma.rs new file mode 100644 index 000000000..378f035bf --- /dev/null +++ b/src/dev/bcm5701/dma.rs @@ -0,0 +1,1148 @@ +//! The descriptor engines: the send ring, the standard and jumbo receive +//! rings and the return ring, the status block, and the host coalescing that +//! decides when the driver is told. +//! +//! Descriptors and the status block are the chip's big-endian internal image, +//! reaching the bus through the MODE_CTL swap controls; frame data uses the +//! frame-data controls. What the host sees of those bytes is the bridge's +//! business ([`PciDma`]). + +use std::time::{Duration, Instant}; + +use super::state::{reg, Refused, State}; +use crate::pci::{PciDma, PciDmaStatus}; + +/// The largest frame the engines carry, transmit or receive. The part does +/// jumbo frames; the figure matches what the other Origin adapters carry so a +/// guest configured for jumbo behaves the same whichever card it has. It is a +/// model limit, not the hardware's. +pub const MAX_FRAME: usize = 9600; + +const SRAM_SEND_RING_RCB: u32 = 0x0100; +const SRAM_RX_RETURN_RCB: u32 = 0x0200; +/// `nicNoMoreRXBDs`, the one statistics counter the model keeps: the number of +/// times the chip ran out of receive descriptors. +const SRAM_STATS_NO_MORE_RXBDS: u32 = 0x0898; +const RCB_HOSTADDR_HI: u32 = 0x00; +const RCB_HOSTADDR_LO: u32 = 0x04; +const RCB_MAXLEN_FLAGS: u32 = 0x08; +const RCB_FLAG_EXT_RX_ENABLE: u32 = 1; +const RCB_FLAG_DISABLE_RING: u32 = 2; + +const TXBD_SIZE: u64 = 16; +// Offsets in the descriptors' little-endian layout; a 16-bit field's +// image offset is its offset xor 2. +const TXBD_FLAGS: usize = 0x08; +const TXBD_LEN: usize = 0x0a; +const TXBD_MSS: usize = 0x0e; +const TXBDFLAG_TCP_UDP_CKSUM: u16 = 0x0001; +const TXBDFLAG_IP_CKSUM: u16 = 0x0002; +const TXBDFLAG_END: u16 = 0x0004; +const TXBDFLAG_INSERT_SRC_ADDR: u16 = 0x1000; +const TXBDFLAG_CHOOSE_SRC_ADDR: u16 = 0x6000; +const TXBDFLAG_SRC_ADDR_SHIFT: u16 = 13; + +const RXBD_SIZE: u64 = 32; +const RXBD_IDX_LEN: usize = 0x08; +const RXBD_TYPE_FLAGS: usize = 0x0c; +const RXBD_CSUM: usize = 0x10; +const RXBD_OPAQUE: usize = 0x1c; +const RXBDFLAG_END: u32 = 0x0004; +const RXBDFLAG_JUMBO_RING: u32 = 0x0020; +const RXBDFLAG_IP_CKSUM_OK: u32 = 0x1000; +const RXBDFLAG_TCP_UDP_CKSUM_OK: u32 = 0x2000; +const RXBDFLAG_TCP: u32 = 0x4000; +const MODE_RX_NO_PSEUDO: u32 = 1 << 23; +const MODE_NO_FRAME_CRACK: u32 = 1 << 9; + +/// The standard ring holds a fixed number of descriptors. Its control block's +/// length is the size of the buffer each descriptor points at, an MTU rather +/// than a count, while the return ring's really is a ring length. +const RX_STD_RING_LEN: u16 = 512; +const RX_JUMBO_RING_LEN: u16 = 256; +/// The chip copies a quarter of each producer ring into its own memory +/// ahead of use [`57XX-PG105-R`, printed page 118]; Linux tg3 sizes the +/// same caches at 128 and 64 descriptors for this chip. +const RX_STD_BD_CACHE: u16 = RX_STD_RING_LEN / 4; +const RX_JUMBO_BD_CACHE: u16 = RX_JUMBO_RING_LEN / 4; +const FCS_LEN: u32 = 4; +const MIN_FRAME: usize = 60; + +const STATUS_BLK_SIZE: usize = 0x50; +const STATFLAG_UPDATED: u32 = 1; +const STATUS_RX_PROD_OFF: usize = 16; +const STATUS_TX_CONS_OFF: usize = 18; +const STATUS_TAG_OFF: usize = 4; +const STATUS_RX_STD_CONS_IMAGE_OFF: usize = 8; +const STATUS_RX_JUMBO_CONS_IMAGE_OFF: usize = 10; +const HCC_MODE_ENABLE: u32 = 2; + +/// Both coalescing tick budgets are programmed in microseconds. +const COAL_TICK: Duration = Duration::from_micros(1); +/// A status DMA the guest's address refused is retried no slower than this. +const COAL_RETRY_MAX: Duration = Duration::from_millis(250); +/// Completions the arrival line can hold; a full line refuses frames. +const RX_LATENCY_DEPTH: usize = 256; +/// The default minimum between a frame arriving and its completion. +pub const RX_ARRIVAL_LATENCY_DEFAULT: Duration = Duration::from_micros(200); +/// The default minimum in processor cycles: how far the processor that rang +/// the send doorbell must have run past it before a frame arriving after it is +/// reported. See [`HeldCompletion`]. +pub const RX_ARRIVAL_CYCLES_DEFAULT: u64 = 20_000; +/// How soon a completion whose host-time deadline has passed looks again at +/// whether the sending processor has run far enough. +const RX_CYCLES_RECHECK: Duration = Duration::from_micros(50); + +/// A completion in guest memory whose report waits for its frame's arrival. +/// +/// The arrival floor stands for the time a reply takes on a real network, so +/// that it never lands before the transmit that provoked it has finished. On +/// the host the reply comes back almost at once, and host time alone does not +/// hold it back far enough: the guest's processor runs slower than the part +/// it models, and while the host has its thread descheduled it does not run +/// at all, so a deadline in host time can pass with the guest still inside +/// the send path that rang the doorbell. An echo reply reported then is +/// counted by IRIX's ICMP input yet never reaches the `ping` still sending +/// the request. So the report also waits until every processor that rang +/// the doorbell before the frame arrived has run `rx_floor_cycles` past its +/// last ring. A processor parked in its idle loop still counts cycles, so +/// nothing waits long on an idle guest. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct HeldCompletion { + /// The host-time deadline. + pub(super) due: Instant, + /// The producer index the completion publishes. + pub(super) prod: u16, + /// Each processor's last send doorbell before the frame arrived. + pub(super) after: Doorbells, +} + +/// Each processor's cycle count when it last rang the send doorbell, by CPU +/// number. +pub(super) type Doorbells = [Option; crate::cpu::smp::MAX_CPUS]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Ring { + Standard, + Jumbo, +} + +/// The mailboxes whose index is range-checked. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Mailbox { + TxProducer, + RxStandardProducer, + RxJumboProducer, + RxReturnConsumer, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum DmaWhat { + TxDescriptor, + TxPayload, + RxDescriptor, + RxPayload, + RxCompletion, + StatusBlock, +} + +/// The timers the engines keep, ordered by when they fire. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Timer { + PhySettle, + BufferManager, + RxCoalescing, + TxCoalescing, + RxArrival, +} + +/// What the receive path did with a frame the backend offered. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RxOutcome { + /// Delivered, filtered or dropped: the backend may discard it. + Consumed, + /// Not taken; the frame stays at the head of the backend's queue until + /// the guest makes room. + Deferred, +} + +/// A bus master with nowhere to go, until the bridge connects one. +pub(super) struct NullDma; +impl PciDma for NullDma { + fn dma_read(&self, _addr: u64, buf: &mut [u8]) -> PciDmaStatus { + buf.fill(0); + PciDmaStatus::MasterAbort + } + fn dma_write(&self, _addr: u64, _data: &[u8]) -> PciDmaStatus { + PciDmaStatus::MasterAbort + } +} + +fn be32(b: &[u8], off: usize) -> u32 { + u32::from_be_bytes(b[off..off + 4].try_into().unwrap()) +} + +/// A 16-bit field of the chip's image sits in the half of its 32-bit word +/// the big-endian structure puts it in. +fn ld16(b: &[u8], off: usize) -> u16 { + let o = off ^ 2; + u16::from_be_bytes([b[o], b[o + 1]]) +} + +fn st32(b: &mut [u8], off: usize, v: u32) { + b[off..off + 4].copy_from_slice(&v.to_be_bytes()); +} + +fn st16(b: &mut [u8], off: usize, v: u16) { + let o = off ^ 2; + b[o..o + 2].copy_from_slice(&v.to_be_bytes()); +} + +/// The one's-complement sum of a span, folded. +fn ones_sum(buf: &[u8], seed: u32) -> u16 { + let mut sum = seed; + for pair in buf.chunks(2) { + sum += u32::from(pair[0]) << 8 | u32::from(*pair.get(1).unwrap_or(&0)); + } + while sum >> 16 != 0 { + sum = (sum & 0xffff) + (sum >> 16); + } + sum as u16 +} + +/// Fill in the IP header and/or TCP and UDP checksums of an untagged IPv4 +/// frame. A fragment keeps its transport checksum. +fn checksum_calculate(frame: &mut [u8], ip: bool, transport: bool) { + if frame.len() < 14 + 20 || frame[14] >> 4 != 4 { + return; + } + let ihl = usize::from(frame[14] & 0xf) * 4; + if ihl < 20 || frame.len() < 14 + ihl { + return; + } + if ip { + frame[24] = 0; + frame[25] = 0; + let sum = !ones_sum(&frame[14..14 + ihl], 0); + frame[24..26].copy_from_slice(&sum.to_be_bytes()); + } + if u16::from_be_bytes([frame[20], frame[21]]) & 0x3fff != 0 { + return; + } + let total = usize::from(u16::from_be_bytes([frame[16], frame[17]])); + if total < ihl { + return; + } + let data_len = (total - ihl).min(frame.len() - 14 - ihl); + let proto = frame[23]; + let at = match proto { + 6 if transport && data_len >= 20 => 16, + 17 if transport && data_len >= 8 => 6, + _ => return, + }; + let start = 14 + ihl; + frame[start + at] = 0; + frame[start + at + 1] = 0; + let pseudo = ones_pseudo(&frame[26..34], proto, data_len); + let sum = !ones_sum(&frame[start..start + data_len], pseudo); + frame[start + at..start + at + 2].copy_from_slice(&sum.to_be_bytes()); +} + +fn ones_pseudo(addresses: &[u8], proto: u8, len: usize) -> u32 { + addresses.chunks(2).map(|p| u32::from(p[0]) << 8 | u32::from(p[1])).sum::() + u32::from(proto) + len as u32 +} + +/// Finish the checksums the driver left to the card. The lengths in the frame +/// were composed by the guest, so a header shorter than its own minimum, or +/// lengths reaching past what arrived, decline the offload rather than walk +/// off the buffer. +fn tx_offload_checksum(frame: &mut [u8], flags: u16) { + if frame.len() < 14 + 20 || frame[12..14] != [0x08, 0x00] || frame[14] >> 4 != 4 { + return; + } + let ihl = usize::from(frame[14] & 0xf) * 4; + let total = usize::from(u16::from_be_bytes([frame[16], frame[17]])); + if ihl < 20 || total < ihl || 14 + total > frame.len() { + return; + } + checksum_calculate(frame, flags & TXBDFLAG_IP_CKSUM != 0, flags & TXBDFLAG_TCP_UDP_CKSUM != 0); +} + +struct LsoLayout { + ihl: usize, + thl: usize, + hdr: usize, + payload: usize, +} + +/// Whether `frame` is an unfragmented IPv4 TCP frame that `mss` can cut up. +fn lso_layout(frame: &[u8], mss: usize) -> Option { + if frame.len() < 14 + 20 + 20 || frame[12..14] != [0x08, 0x00] || frame[14] >> 4 != 4 || frame[23] != 6 { + return None; + } + let ihl = usize::from(frame[14] & 0xf) * 4; + if ihl < 20 || 14 + ihl + 20 > frame.len() { + return None; + } + let ip_len = usize::from(u16::from_be_bytes([frame[16], frame[17]])); + if u16::from_be_bytes([frame[20], frame[21]]) & 0x3fff != 0 || ip_len < ihl + 20 || 14 + ip_len != frame.len() { + return None; + } + let thl = usize::from(frame[14 + ihl + 12] >> 4) * 4; + let hdr = 14 + ihl + thl; + if thl < 20 || ihl + thl > ip_len || mss == 0 || hdr + mss > MAX_FRAME { + return None; + } + let payload = ip_len - ihl - thl; + if payload <= mss { + return None; + } + Some(LsoLayout { ihl, thl, hdr, payload }) +} + +/// The checksums of an arriving frame, as the part reports them: the sums it +/// computed rather than a verdict, a field that checks out summing to 0xffff. +/// Anything not plainly IPv4 TCP or UDP is left unclaimed, because reporting +/// a sum for something never summed would have the stack skip its own pass. +fn rx_offload_checksum(frame: &[u8], mode: u32, flags: &mut u32) -> u32 { + if mode & MODE_NO_FRAME_CRACK != 0 { + return 0; + } + if frame.len() < 34 || frame[12..14] != [0x08, 0x00] || frame[14] >> 4 != 4 { + return 0; + } + let ihl = usize::from(frame[14] & 0xf) * 4; + let total = usize::from(u16::from_be_bytes([frame[16], frame[17]])); + if ihl < 20 || total < ihl || 14 + total > frame.len() { + return 0; + } + let ip_sum = ones_sum(&frame[14..14 + ihl], 0); + if ip_sum == 0 || ip_sum == 0xffff { + *flags |= RXBDFLAG_IP_CKSUM_OK; + } + let ip_only = u32::from(ip_sum) << 16; + let proto = frame[23]; + if proto != 6 && proto != 17 { + return ip_only; + } + if proto == 6 { + *flags |= RXBDFLAG_TCP; + } + if u16::from_be_bytes([frame[20], frame[21]]) & 0x3fff != 0 { + return ip_only; + } + let l4len = total - ihl; + if (proto == 6 && l4len < 20) || (proto == 17 && l4len < 8) { + return ip_only; + } + let mut transport_checksum_present = true; + if proto == 6 { + let tcp_hlen = usize::from(frame[14 + ihl + 12] >> 4) * 4; + if tcp_hlen < 20 || tcp_hlen > l4len { + return ip_only; + } + } else { + let udp_len = usize::from(u16::from_be_bytes([frame[14 + ihl + 4], frame[14 + ihl + 5]])); + if udp_len != l4len { + return ip_only; + } + transport_checksum_present = frame[14 + ihl + 6..14 + ihl + 8] != [0, 0]; + } + let pseudo = if mode & MODE_RX_NO_PSEUDO == 0 { ones_pseudo(&frame[26..34], proto, l4len) } else { 0 }; + let l4_sum = ones_sum(&frame[14 + ihl..14 + ihl + l4len], pseudo); + if l4_sum == 0xffff && transport_checksum_present && mode & MODE_RX_NO_PSEUDO == 0 { + *flags |= RXBDFLAG_TCP_UDP_CKSUM_OK; + } + ip_only | u32::from(l4_sum) +} + +/// Where a producer ring's fetch index stands after the chip fetches what it +/// may. `used` is the next descriptor a frame takes and `fetched` the next +/// the chip copies, so the cache holds those in between. A producer written +/// back into the cached span, which no driver should do, fetches nothing. +fn fetch_ahead(prod: u16, used: u16, fetched: u16, ring_len: u16, cache: u16, thresh: u32) -> u16 { + let posted = (prod + ring_len - used) % ring_len; + let cached = (fetched + ring_len - used) % ring_len; + let available = posted.saturating_sub(cached); + let room = cache.saturating_sub(cached); + if available == 0 || room == 0 || u32::from(available) < thresh { + return fetched; + } + (fetched + available.min(room)) % ring_len +} + +impl State { + /// The engine state a reset returns to zero. + pub(super) fn reset_engines(&mut self) { + self.tx_cons = 0; + self.status_tx_cons = 0; + self.status_rx_prod = 0; + self.rx_prod_staged = 0; + self.rx_std_cons = 0; + self.rx_jumbo_cons = 0; + self.rx_std_fetched = 0; + self.rx_jumbo_fetched = 0; + self.rx_shortage_drops = 0; + self.rx_std_shortage_drops = 0; + self.status_tag = 0; + self.rx_coal_pending = 0; + self.tx_coal_pending = 0; + self.rx_coal_expired = false; + self.tx_coal_expired = false; + self.rx_coal_timer = None; + self.tx_coal_timer = None; + self.rx_latency.clear(); + self.tx_doorbell = [None; crate::cpu::smp::MAX_CPUS]; + self.wire.clear(); + self.wake_rx = true; + self.purge_rx = true; + } + + /// Whatever made the chip ready again, a posted buffer or a consumer + /// index the driver moved, is the moment to offer the backend's waiting + /// frame again. + pub(super) fn retry_rx_flush(&mut self) { + self.wake_rx = true; + } + + // ---- host memory ---- + + /// The 64-bit host address a pair of control-block words names: the high + /// word at the lower offset, the low word at the next. With + /// ENDIAN_WORDSWAP set, a 32-bit target access at +0 of an on-chip + /// eight-byte field reaches its more significant word (Figure 93 of the + /// family programmer's guide), which is the setting the drivers use, so + /// the pair takes no further exchange. The swap-clear 32-bit case (the + /// low word at +0) is not modelled; no driver leaves the swap clear. + fn compose_hostaddr(&self, hi: u32, lo: u32) -> u64 { + u64::from(hi) << 32 | u64::from(lo) + } + + /// The chip's DMA byte-swap controls, MODE_CTL bits 5 and 4 for frame data + /// and bits 2 and 1 for everything else. The chip is big-endian + /// internally with a 64-bit memory: its internal byte at lane l of a + /// doubleword (lane 0 the most significant) goes on PCI byte 7 - l with no + /// swap, and the swaps exchange the halves and reverse bytes within them. + /// The frame-data combinations are established by the programmer's guide; + /// the non-frame single-swap cases are inferred. A transfer's chip-side + /// lane is its PCI address modulo 8, an approximation: the chip's + /// internal buffer alignment is not modelled. + fn lane_xor(&self, frame: bool) -> usize { + let mode = self.rf(reg::MODE_CTL); + let (byte, word) = if frame { (0x10, 0x20) } else { (0x2, 0x4) }; + 7 ^ if mode & byte != 0 { 3 } else { 0 } ^ if mode & word != 0 { 4 } else { 0 } + } + + /// Read `len` bytes of the chip's internal image from `addr`. + fn dma_read_image(&self, addr: u64, len: usize, frame: bool) -> Option> { + if len == 0 { + return Some(Vec::new()); + } + let t = self.lane_xor(frame); + let astart = addr & !7; + let lead = (addr - astart) as usize; + let span = (lead + len + 7) & !7; + let mut raw = vec![0; span]; + if !self.dma.dma_read(astart, &mut raw).is_ok() { + return None; + } + Some( + (0..len) + .map(|j| { + let p = lead + j; + raw[(p & !7) + ((p & 7) ^ t)] + }) + .collect(), + ) + } + + /// Write `data` of the chip's internal image to `addr`, touching only the + /// lanes it covers as the byte enables do: whole doublewords as one + /// transfer, the partial ends a byte at a time. + fn dma_write_image(&self, addr: u64, data: &[u8], frame: bool) -> bool { + let t = self.lane_xor(frame); + let mut i = 0; + while i < data.len() { + let a = addr + i as u64; + if a & 7 == 0 && data.len() - i >= 8 { + let run = (data.len() - i) & !7; + let mut raw = vec![0; run]; + for j in 0..run { + raw[(j & !7) + ((j & 7) ^ t)] = data[i + j]; + } + if !self.dma.dma_write(a, &raw).is_ok() { + return false; + } + i += run; + } else { + let at = (a & !7) + ((a & 7) as usize ^ t) as u64; + if !self.dma.dma_write(at, &data[i..i + 1]).is_ok() { + return false; + } + i += 1; + } + } + true + } + + // ---- status block and coalescing ---- + + /// DMA the status block to the host address the driver programmed, only + /// while host coalescing is enabled. The chip owns the block, so it is + /// built whole each time. `raise` marks an update the driver is meant to + /// be interrupted for. Returns whether it reached the host. + fn post_status_block(&mut self, raise: bool) -> bool { + if self.rf(reg::HCC_MODE) & HCC_MODE_ENABLE == 0 { + return false; + } + let base = self.compose_hostaddr(self.rf(reg::HCC_STATUSBLK_ADDR_HI), self.rf(reg::HCC_STATUSBLK_ADDR_LO)); + let next_tag = self.status_tag.wrapping_add(1); + let mut blk = [0u8; STATUS_BLK_SIZE]; + st32(&mut blk, 0, STATFLAG_UPDATED); + st32(&mut blk, STATUS_TAG_OFF, u32::from(next_tag)); + // A producer ring's consumer is how far the chip has fetched, not + // how far frames have used [`57XX-PG105-R`, printed pages 104, 123 + // and 132]. + blk[STATUS_RX_STD_CONS_IMAGE_OFF..][..2].copy_from_slice(&self.rx_std_fetched.to_be_bytes()); + blk[STATUS_RX_JUMBO_CONS_IMAGE_OFF..][..2].copy_from_slice(&self.rx_jumbo_fetched.to_be_bytes()); + st16(&mut blk, STATUS_TX_CONS_OFF, self.status_tx_cons); + st16(&mut blk, STATUS_RX_PROD_OFF, self.status_rx_prod); + if !self.dma_write_image(base, &blk, false) { + self.refuse(Refused::DmaFault(DmaWhat::StatusBlock)); + return false; + } + self.status_tag = next_tag; + // The block now carries an update the driver has not taken. The line + // asserts while that latch is set and interrupts are unmasked, and + // drops when the driver acknowledges through the interrupt mailbox. + if raise { + self.irq_pending = true; + self.update_irq(false); + } + true + } + + fn coal_arm_one(&mut self, rx: bool) { + let ticks = self.rf(if rx { reg::HCC_RX_COAL_TICKS } else { reg::HCC_TX_COAL_TICKS }); + let deadline = (ticks != 0).then(|| self.now + COAL_TICK * ticks); + if rx { + self.rx_coal_expired = false; + self.rx_coal_timer = deadline; + } else { + self.tx_coal_expired = false; + self.tx_coal_timer = deadline; + } + } + + /// Both budgets restart together: one update reports every ring. + fn coal_arm(&mut self) { + self.coal_arm_one(true); + self.coal_arm_one(false); + } + + /// One host update. All the ring indices are in one status block, so + /// whichever criterion fired reports every ring and restarts every count + /// and timer. + fn status_update(&mut self) -> bool { + if !self.post_status_block(true) { + return false; + } + self.rx_coal_pending = 0; + self.tx_coal_pending = 0; + self.coal_arm(); + true + } + + /// Retry later instead of spinning on an address the guest has not + /// repaired. + fn coal_retry(&mut self, rx: bool) { + let ticks = self.rf(if rx { reg::HCC_RX_COAL_TICKS } else { reg::HCC_TX_COAL_TICKS }); + let wait = (COAL_TICK * ticks.max(1)).min(COAL_RETRY_MAX); + let deadline = Some(self.now + wait); + if rx { + self.rx_coal_timer = deadline; + } else { + self.tx_coal_timer = deadline; + } + } + + fn rx_coal_flush(&mut self) { + if self.rx_coal_pending == 0 || self.status_update() { + return; + } + self.coal_retry(true); + } + + /// The receive budget ran out. It reports whatever arrived since the last + /// update; with nothing to report it stays expired, and the next frame is + /// reported as it lands. + pub(super) fn rx_coal_timer_fired(&mut self) { + self.rx_coal_timer = None; + self.rx_coal_expired = true; + self.rx_coal_flush(); + } + + fn tx_coal_flush(&mut self) { + if self.tx_coal_pending == 0 || self.status_update() { + return; + } + self.coal_retry(false); + } + + pub(super) fn tx_coal_timer_fired(&mut self) { + self.tx_coal_timer = None; + self.tx_coal_expired = true; + self.tx_coal_flush(); + } + + /// Count the descriptors this doorbell completed, and report them once + /// the count criterion is met or the send budget has already run out. + /// + /// A doorbell that completes nothing is reported anyway, which no + /// criterion covers: bring-up leans on that update arriving. This is a + /// known deviation from the coalescing contract, not an established + /// hardware behaviour. + fn tx_coalesce(&mut self, completed: usize) { + let max_bds = self.rf(reg::HCC_TX_MAX_COAL_BDS); + if completed == 0 { + self.status_update(); + return; + } + self.tx_coal_pending = self.tx_coal_pending.wrapping_add(completed as u16); + if (max_bds != 0 && u32::from(self.tx_coal_pending) >= max_bds) || self.tx_coal_expired { + self.tx_coal_flush(); + } + } + + /// A received frame filled a return-ring descriptor. + fn rx_coalesce(&mut self) { + let max_bds = self.rf(reg::HCC_RX_MAX_COAL_BDS); + self.rx_coal_pending = self.rx_coal_pending.wrapping_add(1); + if (max_bds != 0 && u32::from(self.rx_coal_pending) >= max_bds) || self.rx_coal_expired { + self.rx_coal_flush(); + } + } + + pub(super) fn hcc_mode_written(&mut self) { + if self.rf(reg::HCC_MODE) & HCC_MODE_ENABLE != 0 { + // The budgets run from here until the first update reloads them. + self.coal_arm(); + self.retry_rx_flush(); + } else { + self.rx_coal_timer = None; + self.tx_coal_timer = None; + } + } + + /// A budget was reprogrammed. A running timer keeps the value it was + /// loaded with, since the chip reloads one only at a status-block update; + /// one that is not running starts here, because nothing else would. + pub(super) fn coal_ticks_written(&mut self, rx: bool) { + let (expired, running) = if rx { + (self.rx_coal_expired, self.rx_coal_timer.is_some()) + } else { + (self.tx_coal_expired, self.tx_coal_timer.is_some()) + }; + if self.rf(reg::HCC_MODE) & HCC_MODE_ENABLE == 0 || expired || running { + return; + } + self.coal_arm_one(rx); + } + + // ---- transmit ---- + + fn tx_rollback_to_frame(&mut self, frame_start: u16, completed: usize) { + self.tx_cons = frame_start; + if completed != 0 { + self.status_tx_cons = frame_start; + self.tx_coalesce(completed); + } + } + + /// Replace the frame's source address with a filter slot's, as the + /// descriptor asks. + fn tx_replace_source(&self, frame: &mut [u8], flags: u16) { + if flags & TXBDFLAG_INSERT_SRC_ADDR == 0 || frame.len() < 12 { + return; + } + let slot = u32::from((flags & TXBDFLAG_CHOOSE_SRC_ADDR) >> TXBDFLAG_SRC_ADDR_SHIFT); + let hi = self.rf(reg::MAC_ADDR_0_HIGH + slot * reg::MAC_ADDR_STRIDE); + let lo = self.rf(reg::MAC_ADDR_0_LOW + slot * reg::MAC_ADDR_STRIDE); + frame[6] = (hi >> 8) as u8; + frame[7] = hi as u8; + frame[8..12].copy_from_slice(&lo.to_be_bytes()); + } + + /// Cut one oversized TCP buffer into segments the peer will accept. The + /// guest hands down a single frame holding many segments' worth of + /// payload; each piece goes out with its own copy of the headers, its own + /// sequence number, and the finishing flags only on the last. Anything + /// that does not look like the IPv4 TCP frame this is for is sent + /// unchanged rather than guessed at. + fn tx_segment(&mut self, mut frame: Vec, mss: usize, flags: u16) { + let Some(LsoLayout { ihl, thl, hdr, payload }) = lso_layout(&frame, mss) else { + if flags & (TXBDFLAG_IP_CKSUM | TXBDFLAG_TCP_UDP_CKSUM) != 0 { + tx_offload_checksum(&mut frame, flags); + } + self.wire.push(frame); + return; + }; + let seq = be32(&frame, 14 + ihl + 4); + let ip_id = u16::from_be_bytes([frame[18], frame[19]]); + let mut off = 0; + while off < payload { + let piece = mss.min(payload - off); + let mut seg = frame[..hdr].to_vec(); + seg.extend_from_slice(&frame[hdr + off..hdr + off + piece]); + seg[16..18].copy_from_slice(&((ihl + thl + piece) as u16).to_be_bytes()); + seg[18..20].copy_from_slice(&ip_id.wrapping_add((off / mss) as u16).to_be_bytes()); + seg[14 + ihl + 4..14 + ihl + 8].copy_from_slice(&seq.wrapping_add(off as u32).to_be_bytes()); + if off + piece < payload { + // Only the final piece finishes the sender's message. + seg[14 + ihl + 13] &= !0x09; + } + seg[24..26].fill(0); + seg[14 + ihl + 16..14 + ihl + 18].fill(0); + checksum_calculate(&mut seg, true, true); + self.wire.push(seg); + off += piece; + } + } + + /// Ring the transmit doorbell: send every descriptor from the chip's + /// consumer up to the producer index the driver just wrote, then report + /// the new consumer through the status block. + /// + /// The producer is a descriptor index the driver reduces modulo the ring + /// length itself; the failure mode is an index past the ring, which would + /// leave the walk chasing a consumer it never reaches. An out-of-range + /// index is refused, and the walk is bounded by the ring length + /// regardless. A frame whose descriptors are not all there yet, or whose + /// memory cannot be read, is left to retry from its first descriptor. + pub(super) fn tx_kick(&mut self, prod_raw: u32) { + if let Some(cpu) = crate::cpu::smp::on_cpu_thread() { + if let (Some(cycles), Some(mark)) = (self.cpu_cycles.get(cpu), self.tx_doorbell.get_mut(cpu)) { + *mark = Some(cycles.get()); + } + } + let prod = (prod_raw & 0xffff) as u16; + let ring_len = (self.sram_read(SRAM_SEND_RING_RCB + RCB_MAXLEN_FLAGS) >> 16) as u16; + if ring_len == 0 || prod >= ring_len { + self.refuse(Refused::ProducerOutOfRange(Mailbox::TxProducer)); + return; + } + let ring_base = self.compose_hostaddr( + self.sram_read(SRAM_SEND_RING_RCB + RCB_HOSTADDR_HI), + self.sram_read(SRAM_SEND_RING_RCB + RCB_HOSTADDR_LO), + ); + let mut frame: Vec = Vec::new(); + let mut walked = 0usize; + let mut completed = 0usize; + let mut frame_flags = 0u16; + let mut frame_start = self.tx_cons; + let mut frame_mss = 0usize; + let mut dropped = false; + while self.tx_cons != prod && walked < usize::from(ring_len) { + let Some(bd) = self.dma_read_image(ring_base + u64::from(self.tx_cons) * TXBD_SIZE, TXBD_SIZE as usize, false) + else { + self.refuse(Refused::DmaFault(DmaWhat::TxDescriptor)); + self.tx_rollback_to_frame(frame_start, completed); + return; + }; + let buf_addr = u64::from(be32(&bd, 0)) << 32 | u64::from(be32(&bd, 4)); + let len = usize::from(ld16(&bd, TXBD_LEN)); + let flags = ld16(&bd, TXBD_FLAGS); + if !dropped && frame.len() + len <= MAX_FRAME { + let Some(payload) = self.dma_read_image(buf_addr, len, true) else { + self.refuse(Refused::DmaFault(DmaWhat::TxPayload)); + self.tx_rollback_to_frame(frame_start, completed); + return; + }; + frame.extend_from_slice(&payload); + } else if !dropped { + // The frame overran the buffer. Drop the whole frame and stay + // dropping across the remaining fragments so a later one is + // not mistaken for a fresh packet; resume at the next END. + self.refuse(Refused::TxFrameTooLarge); + dropped = true; + } + frame_flags |= flags; + if frame_mss == 0 { + frame_mss = usize::from(ld16(&bd, TXBD_MSS)); + } + if flags & TXBDFLAG_END != 0 { + if !dropped && !frame.is_empty() { + let mut whole = std::mem::take(&mut frame); + if frame_mss == 0 { + self.tx_replace_source(&mut whole, frame_flags); + } + self.tx_segment(whole, frame_mss, frame_flags); + } + frame.clear(); + frame_flags = 0; + frame_mss = 0; + dropped = false; + } + self.tx_cons = (self.tx_cons + 1) % ring_len; + walked += 1; + if flags & TXBDFLAG_END != 0 { + frame_start = self.tx_cons; + completed = walked; + } + } + if self.tx_cons != frame_start { + self.tx_rollback_to_frame(frame_start, completed); + return; + } + self.status_tx_cons = prod; + self.tx_coalesce(completed); + } + + // ---- receive mailboxes ---- + + pub(super) fn rx_mailbox_written(&mut self, off: u32, val: u32) { + let index = (val & 0xffff) as u16; + match off { + reg::MBX_RX_JUMBO_PROD_LO => { + // Posting jumbo-ring buffers; guard the index like the others. + if index >= RX_JUMBO_RING_LEN { + self.refuse(Refused::ProducerOutOfRange(Mailbox::RxJumboProducer)); + return; + } + self.set_rf(off, val); + self.rx_fetch(); + } + reg::MBX_RX_STD_PROD_LO => { + let std_len = self.rf(reg::RX_STD_RCB_MAXLEN_FLAGS) >> 16; + if std_len == 0 || index >= RX_STD_RING_LEN { + self.refuse(Refused::ProducerOutOfRange(Mailbox::RxStandardProducer)); + return; + } + self.set_rf(off, val); + self.rx_fetch(); + } + _ => { + // The driver's return-ring acknowledgement, same range guard. + let ret_len = (self.sram_read(SRAM_RX_RETURN_RCB + RCB_MAXLEN_FLAGS) >> 16) as u16; + if ret_len == 0 || index >= ret_len { + self.refuse(Refused::ProducerOutOfRange(Mailbox::RxReturnConsumer)); + return; + } + self.set_rf(off, val); + } + } + // Room where there was none: take anything the peer had waiting. + self.retry_rx_flush(); + } + + /// A producer ring's control block or replenish threshold changed, which + /// can let a fetch start that the last producer write could not. + pub(super) fn rx_ring_setup_written(&mut self) { + self.rx_fetch(); + self.retry_rx_flush(); + } + + // ---- descriptor fetch ---- + + /// Copy posted descriptors into the chip's cache, on both producer rings. + /// + /// The status block's producer-ring consumers report how far this has + /// gone, which runs ahead of the descriptors frames have used. The chip + /// fetches only while its cache has room, never past the producer, and + /// starts a fetch only once the host has made at least the ring's + /// replenish threshold of descriptors available [`57XX-PG105-R`, printed + /// pages 118, 119 and 443]. A fetch fills the cache as far as both allow, + /// at once: the guide gives neither the chip's burst size nor its fetch + /// latency, so those are approximated. So is when the chip looks again: + /// here, at every producer write and every time a frame frees a cache + /// entry. Only the index is tracked; a frame reads its descriptor from + /// host memory when it uses it, where the chip uses the copy it fetched, + /// which differs only for a driver that rewrites a posted descriptor. + pub(super) fn rx_fetch(&mut self) { + if self.rf(reg::RX_STD_RCB_MAXLEN_FLAGS) >> 16 != 0 { + let prod = (self.rf(reg::MBX_RX_STD_PROD_LO) & 0xffff) as u16; + let thresh = self.rf(reg::RX_STD_REPLENISH_THRESH); + self.rx_std_fetched = + fetch_ahead(prod, self.rx_std_cons, self.rx_std_fetched, RX_STD_RING_LEN, RX_STD_BD_CACHE, thresh); + // A fetch that gave the chip a buffer ends the shortage. + if self.rx_std_fetched != self.rx_std_cons { + self.rx_std_shortage_drops = 0; + } + } + if self.jumbo_ring_enabled() { + let prod = (self.rf(reg::MBX_RX_JUMBO_PROD_LO) & 0xffff) as u16; + let thresh = self.rf(reg::RX_JUMBO_REPLENISH_THRESH); + self.rx_jumbo_fetched = fetch_ahead( + prod, + self.rx_jumbo_cons, + self.rx_jumbo_fetched, + RX_JUMBO_RING_LEN, + RX_JUMBO_BD_CACHE, + thresh, + ); + if self.rx_jumbo_fetched != self.rx_jumbo_cons { + self.rx_shortage_drops = 0; + } + } + } + + // ---- receive ---- + + /// Whether the jumbo ring can take a frame at all, independent of whether + /// one is posted: it needs a host base and must not be disabled. Extended + /// RX Enable asks for the 64-byte descriptor format this model does not + /// parse, so a ring asking for it is treated as disabled. Max_Len plays no + /// part: hardware ignores it for the jumbo ring. + fn jumbo_ring_enabled(&self) -> bool { + let flags = self.rf(reg::RX_JUMBO_RCB_MAXLEN_FLAGS); + let base = self.compose_hostaddr(self.rf(reg::RX_JUMBO_RCB_HADDR_HI), self.rf(reg::RX_JUMBO_RCB_HADDR_LO)); + base != 0 && flags & (RCB_FLAG_DISABLE_RING | RCB_FLAG_EXT_RX_ENABLE) == 0 + } + + /// Whether the engine can take a frame: the rings must be set up and host + /// coalescing enabled, and the chip must hold a fetched buffer it has not + /// yet used. + pub(super) fn rx_ready(&self) -> bool { + let std_len = self.rf(reg::RX_STD_RCB_MAXLEN_FLAGS) >> 16; + let ret_len = (self.sram_read(SRAM_RX_RETURN_RCB + RCB_MAXLEN_FLAGS) >> 16) as u16; + // A completion the driver has not read yet must not be overwritten; + // when the next would land on the entry the driver has consumed up to, + // the ring is full and the frame is refused. + let ret_cons = (self.rf(reg::MBX_RX_CONS0_LO) & 0xffff) as u16; + // Either ring holding a buffer is enough; which one a frame takes + // depends on its size. + let posted = (std_len != 0 && self.rx_std_fetched != self.rx_std_cons) + || (self.jumbo_ring_enabled() && self.rx_jumbo_fetched != self.rx_jumbo_cons); + if self.rx_latency.len() >= RX_LATENCY_DEPTH { + return false; + } + // Against the staged producer, not the published one: a return + // descriptor whose completion is still held has already been written. + ret_len != 0 + && posted + && self.rf(reg::HCC_MODE) & HCC_MODE_ENABLE != 0 + && (self.rx_prod_staged + 1) % ret_len != ret_cons + } + + /// The address the part filters on is MAC address filter slot 0, which the + /// driver programs, not the address the board gave the device: IRIX + /// installs the system Ethernet address over whatever the card came up + /// with. + fn frame_accepted(&self, frame: &[u8]) -> bool { + if frame.len() < 6 { + return false; + } + if frame[0] & 1 != 0 { + return true; + } + let hi = self.rf(reg::MAC_ADDR_0_HIGH); + let lo = self.rf(reg::MAC_ADDR_0_LOW); + frame[..6] == [(hi >> 8) as u8, hi as u8, (lo >> 24) as u8, (lo >> 16) as u8, (lo >> 8) as u8, lo as u8] + } + + fn count_no_more_rxbds(&mut self) { + let n = self.sram_read(SRAM_STATS_NO_MORE_RXBDS).wrapping_add(1); + self.sram_write(SRAM_STATS_NO_MORE_RXBDS, n); + } + + /// A frame from the backend. Not ready, or no buffer fetched, defers it: + /// the only answer that leaves the frame at the peer's queue head. A frame + /// the address filter drops is consumed, never deferred, so the peer is + /// not wedged waiting on a frame that will never be taken. Otherwise the + /// frame is DMA'd into the next posted buffer, a completed descriptor is + /// written into the return ring, and the producer the status block reports + /// advances once the frame's arrival deadline passes. + pub(super) fn receive(&mut self, frame: &[u8]) -> RxOutcome { + // A halted receive MAC is not listening, so the frame is gone rather + // than held: a backend that queued it would hand the driver traffic + // from before the interface existed. + if self.rf(reg::RX_MODE) & 2 == 0 { + return RxOutcome::Consumed; + } + if !self.rx_ready() { + return RxOutcome::Deferred; + } + if !self.frame_accepted(frame) { + return RxOutcome::Consumed; + } + if frame.len() > MAX_FRAME { + // Drop it rather than deliver part of it: a frame cut short looks + // whole, and the failure surfaces wherever the truncated payload + // is finally parsed. + self.refuse(Refused::RxFrameTooLarge); + return RxOutcome::Consumed; + } + // Pad a short frame to the 60-byte Ethernet minimum: a backend can + // hand over a runt, and the driver rejects a descriptor below it. + let mut padded; + let buf: &[u8] = if frame.len() < MIN_FRAME { + padded = frame.to_vec(); + padded.resize(MIN_FRAME, 0); + &padded + } else { + frame + }; + let framelen = buf.len() as u32; + + // Pick the ring by what the frame fits in. The standard ring's Max_Len + // is the driver's statement of how much room its buffers hold; a + // larger frame goes to the jumbo ring when it is enabled, and is + // dropped rather than written past the end of someone's buffer when + // it is not. + let std_size = u32::from((self.rf(reg::RX_STD_RCB_MAXLEN_FLAGS) >> 16) as u16); + // A frame takes a buffer the chip has fetched, never one only posted. + let std_empty = self.rx_std_fetched == self.rx_std_cons; + let jumbo_empty = self.rx_jumbo_fetched == self.rx_jumbo_cons; + let jumbo_enabled = self.jumbo_ring_enabled(); + let (ring, ring_base, idx) = if framelen <= std_size { + if std_empty { + // The frame fits a standard buffer and the chip holds none; it + // cannot take a jumbo one, because a frame's ring is chosen by + // its length against the standard ring's Max_Len. Discard + // rather than refuse: refusing stops the backend reading and + // strands the frames behind it that the jumbo ring can carry. + // Only while the chip holds a jumbo buffer; without one the caller + // is owed the refusal. + if !jumbo_enabled || jumbo_empty { + return RxOutcome::Deferred; + } + if self.rx_std_shortage_drops == 0 { + self.refuse(Refused::RxShortage(Ring::Standard)); + } + self.rx_std_shortage_drops += 1; + self.count_no_more_rxbds(); + return RxOutcome::Consumed; + } + let base = self + .compose_hostaddr(self.rf(reg::RX_STD_RCB_HADDR_HI), self.rf(reg::RX_STD_RCB_HADDR_LO)); + (Ring::Standard, base, self.rx_std_cons) + } else if jumbo_enabled { + if jumbo_empty { + // A frame longer than the standard Max_Len consumes a + // descriptor from the jumbo ring, and the chip holds none. + // Discard rather than refuse, for the reason above, and only + // while it holds a standard buffer. + if std_size == 0 || std_empty { + return RxOutcome::Deferred; + } + if self.rx_shortage_drops == 0 { + self.refuse(Refused::RxShortage(Ring::Jumbo)); + } + self.rx_shortage_drops += 1; + self.count_no_more_rxbds(); + return RxOutcome::Consumed; + } + let base = self + .compose_hostaddr(self.rf(reg::RX_JUMBO_RCB_HADDR_HI), self.rf(reg::RX_JUMBO_RCB_HADDR_LO)); + (Ring::Jumbo, base, self.rx_jumbo_cons) + } else { + self.refuse(Refused::RxNoRoom(Ring::Standard)); + return RxOutcome::Consumed; + }; + + let ret_len = (self.sram_read(SRAM_RX_RETURN_RCB + RCB_MAXLEN_FLAGS) >> 16) as u16; + let ret_base = self.compose_hostaddr( + self.sram_read(SRAM_RX_RETURN_RCB + RCB_HOSTADDR_HI), + self.sram_read(SRAM_RX_RETURN_RCB + RCB_HOSTADDR_LO), + ); + + // The posted buffer's host address, from the chosen ring's descriptor. + let Some(std_bd) = self.dma_read_image(ring_base + u64::from(idx) * RXBD_SIZE, RXBD_SIZE as usize, false) + else { + self.refuse(Refused::DmaFault(DmaWhat::RxDescriptor)); + return RxOutcome::Deferred; + }; + let buf_addr = u64::from(be32(&std_bd, 0)) << 32 | u64::from(be32(&std_bd, 4)); + let mut rx_flags = RXBDFLAG_END; + if ring == Ring::Jumbo { + // The jumbo ring carries no ring-wide buffer size; the driver + // states each buffer's size in the descriptor it posts. + let buf_len = be32(&std_bd, RXBD_IDX_LEN) & 0xffff; + if framelen > buf_len { + self.refuse(Refused::RxNoRoom(Ring::Jumbo)); + return RxOutcome::Consumed; + } + rx_flags |= RXBDFLAG_JUMBO_RING; + } + if !self.dma_write_image(buf_addr, buf, true) { + self.refuse(Refused::DmaFault(DmaWhat::RxPayload)); + return RxOutcome::Deferred; + } + let csum = rx_offload_checksum(buf, self.rf(reg::MODE_CTL), &mut rx_flags); + + let mut ret_bd = [0u8; RXBD_SIZE as usize]; + st32(&mut ret_bd, 0, (buf_addr >> 32) as u32); + st32(&mut ret_bd, 4, buf_addr as u32); + // The length includes the four-byte FCS the driver strips. + st32(&mut ret_bd, RXBD_IDX_LEN, u32::from(idx) << 16 | ((framelen + FCS_LEN) & 0xffff)); + st32(&mut ret_bd, RXBD_TYPE_FLAGS, rx_flags); + st32(&mut ret_bd, RXBD_CSUM, csum); + st32(&mut ret_bd, RXBD_OPAQUE, be32(&std_bd, RXBD_OPAQUE)); + if !self.dma_write_image(ret_base + u64::from(self.rx_prod_staged) * RXBD_SIZE, &ret_bd, false) { + self.refuse(Refused::DmaFault(DmaWhat::RxCompletion)); + return RxOutcome::Deferred; + } + match ring { + Ring::Jumbo => self.rx_jumbo_cons = (self.rx_jumbo_cons + 1) % RX_JUMBO_RING_LEN, + Ring::Standard => self.rx_std_cons = (self.rx_std_cons + 1) % RX_STD_RING_LEN, + } + // The cache entry the frame used is free for the next fetch. + self.rx_fetch(); + self.rx_prod_staged = (self.rx_prod_staged + 1) % ret_len; + // The frame and its return descriptor are in guest memory; the + // producer the status block reports, and the interrupt after it, wait + // for the frame's arrival deadline. + self.rx_hold(self.rx_prod_staged); + RxOutcome::Consumed + } + + // ---- the arrival line ---- + + fn rx_publish(&mut self, status_rx_prod: u16) { + self.status_rx_prod = status_rx_prod; + self.rx_coalesce(); + } + + /// Hold a completion until the frame's minimum arrival latency has passed. + fn rx_hold(&mut self, status_rx_prod: u16) { + let held = HeldCompletion { due: self.now + self.rx_floor, prod: status_rx_prod, after: self.tx_doorbell }; + if self.rx_floor.is_zero() && self.rx_latency.is_empty() && self.senders_ran_past(&held.after) { + self.rx_publish(status_rx_prod); + } else { + self.rx_latency.push_back(held); + } + } + + /// Whether every processor that rang the send doorbell has run the + /// cycle floor past its ring. A counter that went backwards was reset + /// with its processor, which has left the send path too. + fn senders_ran_past(&self, after: &Doorbells) -> bool { + after.iter().zip(&self.cpu_cycles).all(|(mark, cycles)| { + let Some(at) = *mark else { return true }; + let now = cycles.get(); + now < at || now - at >= self.rx_floor_cycles + }) + } + + /// The head completion's deadline passed. One whose sender has not yet + /// run far enough looks again shortly after `entered`, the time the + /// model was entered at. + pub(super) fn rx_arrival_due(&mut self, entered: Instant) { + let mut published = false; + while let Some(held) = self.rx_latency.front().copied() { + if held.due > self.now { + break; + } + if !self.senders_ran_past(&held.after) { + self.rx_latency[0].due = entered.max(self.now) + RX_CYCLES_RECHECK; + break; + } + self.rx_latency.pop_front(); + self.rx_publish(held.prod); + published = true; + } + if published { + self.retry_rx_flush(); + } + } +} diff --git a/src/dev/bcm5701/eeprom.rs b/src/dev/bcm5701/eeprom.rs new file mode 100644 index 000000000..b56868226 --- /dev/null +++ b/src/dev/bcm5701/eeprom.rs @@ -0,0 +1,113 @@ +//! The BCM5701's serial EEPROM, behind the controller at registers 0x6838 +//! (address and command), 0x683c (data) and 0x6840 (direct serial pins). +//! +//! The controller transfers one aligned 32-bit word at a time. The backing is +//! caller-supplied: nothing here synthesises a signature, a bootstrap, a CRC +//! or a station address, because no complete board image is established. A +//! write goes through to the backing file when there is one, so contents +//! persist across runs. Transfers complete synchronously and the serial bus +//! timing, including the 10 ms spacing between writes, is not modelled. +//! +//! Ported from hw/net/bcm570x.c in jamesbraid/qemu-sgi-origami. + +use std::path::PathBuf; + +pub const MIN_SIZE: usize = 4; +pub const MAX_SIZE: usize = 65536; + +/// Why a transfer did not happen. The register file keeps Complete clear and +/// the data register unchanged; the caller counts and logs the reason. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Fault { + /// No backing is configured, whatever the device ID. + NoBacking, + /// A different device ID than the one configured. + WrongDevice(u8), + /// The word lies beyond the backing. + OutOfRange(u32), + ReadOnly, + /// The backing file could not be written. + Io, +} + +#[derive(Debug)] +pub struct Eeprom { + device_id: u8, + data: Vec, + path: Option, + read_only: bool, +} + +impl Eeprom { + /// An in-memory backing. `device_id` is 0..=7; the image is 4 to 65536 + /// bytes in a multiple of four. + pub fn from_image(device_id: u8, data: Vec) -> Result { + if device_id > 7 { + return Err("bcm5701: eeprom requires a device id in 0..=7".into()); + } + if data.len() < MIN_SIZE || data.len() > MAX_SIZE || data.len() % 4 != 0 { + return Err("bcm5701: EEPROM backing must be 4 to 65536 bytes and a multiple of four".into()); + } + Ok(Self { device_id, data, path: None, read_only: false }) + } + + /// A backing read from `path` and written back to it. A file the + /// filesystem marks read-only makes a read-only backing. + pub fn from_file(device_id: u8, path: PathBuf) -> Result { + let data = std::fs::read(&path).map_err(|e| format!("bcm5701: EEPROM {}: {e}", path.display()))?; + let read_only = std::fs::metadata(&path).map(|m| m.permissions().readonly()).unwrap_or(true); + let mut eeprom = Self::from_image(device_id, data)?; + eeprom.read_only = read_only; + eeprom.path = Some(path); + Ok(eeprom) + } + + /// Refuse writes, as a backing opened read-only does. + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } + + pub fn device_id(&self) -> u8 { + self.device_id + } + + pub fn is_read_only(&self) -> bool { + self.read_only + } + + pub fn bytes(&self) -> &[u8] { + &self.data + } + + fn word_offset(&self, device: u8, word: u32) -> Result { + if device != self.device_id { + return Err(Fault::WrongDevice(device)); + } + let offset = word as usize * 4; + if offset + 4 > self.data.len() { + return Err(Fault::OutOfRange(word)); + } + Ok(offset) + } + + /// The word at `word`, least significant byte first: the image bytes + /// `66 99 55 aa` read as `0xaa559966`. + pub fn read_word(&self, device: u8, word: u32) -> Result { + let offset = self.word_offset(device, word)?; + Ok(u32::from_le_bytes(self.data[offset..offset + 4].try_into().unwrap())) + } + + pub fn write_word(&mut self, device: u8, word: u32, value: u32) -> Result<(), Fault> { + let offset = self.word_offset(device, word)?; + if self.read_only { + return Err(Fault::ReadOnly); + } + let mut image = self.data.clone(); + image[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + if let Some(path) = &self.path { + crate::nv_storage::save(&path.to_string_lossy(), &image).map_err(|_| Fault::Io)?; + } + self.data = image; + Ok(()) + } +} diff --git a/src/dev/bcm5701/phy.rs b/src/dev/bcm5701/phy.rs new file mode 100644 index 000000000..f6a37eed8 --- /dev/null +++ b/src/dev/bcm5701/phy.rs @@ -0,0 +1,263 @@ +//! The BCM570x's integrated copper PHY, reached through the MAC's MI_COMM +//! register. +//! +//! The PHY has no bus address of its own: software writes a register number +//! into MI_COMM and reads a word back. The contract is the ordinary MII/GMII +//! one the public brgphy driver drives. Link is modelled without a cable: out +//! of reset the PHY negotiates, and autonegotiation completes a fixed settle +//! later, which restarts when the guest restarts it. Reported link is also +//! gated on the backend's cable state. +//! +//! The identity decodes to Broadcom's organisational identifier and the +//! BCM5701 model, the pair brgphy matches to bind. +//! +//! Ported from hw/net/bcm570x_phy.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +pub mod reg { + pub const BMCR: u8 = 0x00; + pub const BMSR: u8 = 0x01; + pub const IDR1: u8 = 0x02; + pub const IDR2: u8 = 0x03; + pub const ANAR: u8 = 0x04; + pub const ANLPAR: u8 = 0x05; + pub const GTCR: u8 = 0x09; + pub const GTSR: u8 = 0x0a; + pub const EXTSR: u8 = 0x0f; + /// Broadcom auxiliary status summary. + pub const AUXSTS: u8 = 0x19; +} + +pub const IDR1_VALUE: u16 = 0x0020; +pub const IDR2_BCM5701: u16 = 0x6110; + +pub mod bmcr { + pub const RESET: u16 = 0x8000; + pub const AUTOEN: u16 = 0x1000; + pub const ISOLATE: u16 = 0x0400; + pub const STARTNEG: u16 = 0x0200; + pub const FDX: u16 = 0x0100; + pub const SPEED1000: u16 = 0x0040; +} +mod bmsr { + pub const TX100_FDX: u16 = 0x4000; + pub const TX100_HDX: u16 = 0x2000; + pub const T10_FDX: u16 = 0x1000; + pub const T10_HDX: u16 = 0x0800; + pub const EXTSTAT: u16 = 0x0100; + pub const ANEGDONE: u16 = 0x0020; + pub const CANAUTONEG: u16 = 0x0008; + pub const LINK: u16 = 0x0004; + pub const EXTCAP: u16 = 0x0001; +} +mod anar { + pub const TX100_FDX: u16 = 0x0100; + pub const TX100_HDX: u16 = 0x0080; + pub const T10_FDX: u16 = 0x0040; + pub const T10_HDX: u16 = 0x0020; + pub const CSMA: u16 = 0x0001; + pub const ACK: u16 = 0x4000; +} +const GTCR_ADV_1000TFDX: u16 = 0x0200; +const GTSR_LINKED: u16 = 0x4000 | 0x2000 | 0x1000 | 0x0800; +const EXTSR_1000: u16 = 0x2000 | 0x1000; +const AUXSTS_UP: u16 = 0x8000 | 0x0700; + +const BMCR_RESET_VALUE: u16 = bmcr::AUTOEN | bmcr::FDX | bmcr::SPEED1000; +const ANAR_RESET_VALUE: u16 = + anar::CSMA | anar::T10_HDX | anar::T10_FDX | anar::TX100_HDX | anar::TX100_FDX; + +/// How long autonegotiation takes to settle. It has to finish inside the +/// driver's own patience: the IRIX driver gives up its link wait well before +/// a second. +pub const SETTLE: Duration = Duration::from_millis(100); + +/// What the PHY could not carry out, for the owner to count and log. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Refusal { + /// A read of a register the model does not implement; it reads 0, never + /// the all-ones an absent PHY returns. + ReadUnimplemented(u8), + WriteUnimplemented(u8), + /// A write to a read-only register. + WriteReadOnly(u8), +} + +pub struct Phy { + bmcr: u16, + anar: u16, + gtcr: u16, + autoneg_done: bool, + link_down: bool, + settle_armed: bool, + settle: Option, + pub refused: Vec, +} + +impl Phy { + pub fn new(now: Instant) -> Self { + let mut phy = Self { + bmcr: 0, + anar: 0, + gtcr: 0, + autoneg_done: false, + link_down: false, + settle_armed: false, + settle: None, + refused: Vec::new(), + }; + phy.reset(now); + phy + } + + /// The deadline at which the settle completes. + pub fn deadline(&self) -> Option { + self.settle + } + + /// Complete the settle if its time has come. + pub fn sync(&mut self, now: Instant) { + if self.settle.is_some_and(|d| d <= now) { + self.settle = None; + self.autoneg_done = true; + } + } + + pub fn autoneg_done(&self) -> bool { + self.autoneg_done + } + + pub fn link_up(&self) -> bool { + self.autoneg_done && !self.link_down + } + + /// The far end's cable state; not a register, so a PHY reset leaves it. + pub fn set_link_down(&mut self, down: bool) { + self.link_down = down; + } + + pub fn link_down(&self) -> bool { + self.link_down + } + + fn status(&self) -> u16 { + let mut status = bmsr::EXTCAP + | bmsr::CANAUTONEG + | bmsr::EXTSTAT + | bmsr::TX100_FDX + | bmsr::TX100_HDX + | bmsr::T10_FDX + | bmsr::T10_HDX; + if self.link_up() { + status |= bmsr::LINK | bmsr::ANEGDONE; + } + status + } + + /// A real PHY negotiates as soon as autonegotiation is enabled, so the + /// settle runs from reset and from the write that restarts it, not from + /// the driver's first look at link status. + fn arm_settle(&mut self, now: Instant) { + if !self.settle_armed && !self.autoneg_done && self.bmcr & bmcr::AUTOEN != 0 { + self.settle_armed = true; + self.settle = Some(now + SETTLE); + } + } + + fn refuse(&mut self, what: Refusal) { + if self.refused.len() < 64 { + self.refused.push(what); + } + } + + pub fn read(&mut self, register: u8, now: Instant) -> u16 { + self.sync(now); + let up = self.link_up(); + match register { + reg::BMCR => self.bmcr, + reg::BMSR => { + self.arm_settle(now); + self.status() + } + reg::IDR1 => IDR1_VALUE, + reg::IDR2 => IDR2_BCM5701, + reg::ANAR => self.anar, + // The far end advertises exactly what this end does, which puts + // the driver on the route it trusts. + reg::ANLPAR => { + if up { + self.anar | anar::ACK + } else { + 0 + } + } + reg::GTCR => self.gtcr, + reg::GTSR => { + if up { + GTSR_LINKED + } else { + 0 + } + } + reg::EXTSR => EXTSR_1000, + // The register the driver gates its link-up path on, so a poll + // that reads only this one still brings the link up. + reg::AUXSTS => { + self.arm_settle(now); + if self.link_up() { + AUXSTS_UP + } else { + 0 + } + } + other => { + self.refuse(Refusal::ReadUnimplemented(other)); + 0 + } + } + } + + pub fn write(&mut self, register: u8, value: u16, now: Instant) { + self.sync(now); + match register { + reg::BMCR => { + // Reset and restart are self-clearing. + if value & bmcr::RESET != 0 { + self.reset(now); + return; + } + // Only a restart (or isolating the part) restarts + // negotiation: a driver rewrites this register for other + // reasons, and dropping a working link each time would show + // as the carrier coming back again and again. + if value & (bmcr::ISOLATE | bmcr::STARTNEG) != 0 { + self.autoneg_done = false; + self.settle_armed = false; + self.settle = None; + } + self.bmcr = value & !(bmcr::RESET | bmcr::STARTNEG); + if value & bmcr::ISOLATE == 0 { + self.arm_settle(now); + } + } + reg::ANAR => self.anar = value, + reg::GTCR => self.gtcr = value, + reg::BMSR | reg::IDR1 | reg::IDR2 | reg::ANLPAR | reg::GTSR | reg::EXTSR => { + self.refuse(Refusal::WriteReadOnly(register)); + } + other => self.refuse(Refusal::WriteUnimplemented(other)), + } + } + + pub fn reset(&mut self, now: Instant) { + self.bmcr = BMCR_RESET_VALUE; + self.anar = ANAR_RESET_VALUE; + self.gtcr = GTCR_ADV_1000TFDX; + self.autoneg_done = false; + self.settle_armed = false; + self.settle = None; + // Out of reset autonegotiation is enabled, so the part starts it. + self.arm_settle(now); + } +} diff --git a/src/dev/bcm5701/state.rs b/src/dev/bcm5701/state.rs new file mode 100644 index 000000000..98a28dc93 --- /dev/null +++ b/src/dev/bcm5701/state.rs @@ -0,0 +1,742 @@ +//! The chip's register file, on-chip SRAM, interrupt line and the engines +//! that run inside register writes. The PCI side and the threads are in the +//! parent module. + +use std::collections::{BTreeMap, VecDeque}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; + +use super::eeprom::{self, Eeprom}; +use super::phy::{self, Phy}; +use super::dma::{Doorbells, HeldCompletion, Ring, Timer}; +use crate::cpu::smp::MAX_CPUS; +use crate::cpu::mips_core::CyclesPtr; +use crate::pci::PciDma; +use super::Bcm5701; + +pub const REG_FILE_SIZE: u32 = 0x8000; +pub const SRAM_SIZE: u32 = 0x8000; +const REG_WORDS: usize = (REG_FILE_SIZE / 4) as usize; +const SRAM_WORDS: usize = (SRAM_SIZE / 4) as usize; + +/// Dword indices of the PCI configuration registers the chip owns. +pub mod cfg { + pub const MISC_CTL: usize = 0x68 / 4; + pub const PCISTATE: usize = 0x70 / 4; + pub const REG_BASEADDR: usize = 0x78 / 4; + pub const MEMWIN_BASEADDR: usize = 0x7c / 4; + pub const REG_DATA: usize = 0x80 / 4; + pub const MEMWIN_DATA: usize = 0x84 / 4; +} + +pub mod misc_ctl { + pub const CLEAR_INTA: u32 = 0x1; + pub const MASK_PCI_INTR: u32 = 0x2; + pub const ENDIAN_BYTESWAP: u32 = 0x4; + pub const ENDIAN_WORDSWAP: u32 = 0x8; + /// The chip id, bits 31:16, is read-only; the low half is writable. + pub const WRITABLE: u32 = 0xffff; +} + +pub mod pcistate { + pub const INTR_NOT_ACTIVE: u32 = 0x2; + pub const BUSMODE_PCI: u32 = 0x4; + /// The chip did not see REQ64# at the end of reset. + pub const BUS_32BIT: u32 = 0x10; +} + +/// The chip id MISC_CTL[31:16] presents; the driver reads the part from it. +pub const CHIPID_BCM5701: u32 = 0x0105; + +/// Register-file offsets with modelled behaviour. +pub mod reg { + pub const MAC_MODE: u32 = 0x0400; + pub const MAC_STATUS: u32 = 0x0404; + pub const MAC_ADDR_0_HIGH: u32 = 0x0410; + pub const MAC_ADDR_0_LOW: u32 = 0x0414; + pub const MAC_ADDR_STRIDE: u32 = 8; + pub const MI_COMM: u32 = 0x044c; + pub const MI_STS: u32 = 0x0450; + pub const RX_MODE: u32 = 0x0468; + pub const HCC_MODE: u32 = 0x3c00; + pub const BUFMGR_MODE: u32 = 0x4400; + pub const BUFMGR_STATUS: u32 = 0x4404; + pub const FW_PROGRESS: u32 = 0x5034; + pub const MODE_CTL: u32 = 0x6800; + pub const MISC_CFG: u32 = 0x6804; + pub const EEPROM_ADDR: u32 = 0x6838; + pub const EEPROM_DATA: u32 = 0x683c; + pub const EEPROM_PIN: u32 = 0x6840; + pub const EEPROM_DELAY: u32 = 0x6848; + pub const MBX_IRQ0_LO: u32 = 0x0204; + pub const MBX_RX_STD_PROD_LO: u32 = 0x026c; + pub const MBX_RX_JUMBO_PROD_LO: u32 = 0x0274; + pub const MBX_RX_CONS0_LO: u32 = 0x0284; + pub const MBX_TX_PROD_LO: u32 = 0x0304; + pub const HCC_RX_COAL_TICKS: u32 = 0x3c08; + pub const HCC_TX_COAL_TICKS: u32 = 0x3c0c; + pub const HCC_RX_MAX_COAL_BDS: u32 = 0x3c10; + pub const HCC_TX_MAX_COAL_BDS: u32 = 0x3c14; + pub const HCC_STATUSBLK_ADDR_HI: u32 = 0x3c38; + pub const HCC_STATUSBLK_ADDR_LO: u32 = 0x3c3c; + pub const RX_JUMBO_RCB_HADDR_HI: u32 = 0x2440; + pub const RX_JUMBO_RCB_HADDR_LO: u32 = 0x2444; + pub const RX_JUMBO_RCB_MAXLEN_FLAGS: u32 = 0x2448; + pub const RX_STD_RCB_HADDR_HI: u32 = 0x2450; + pub const RX_STD_RCB_HADDR_LO: u32 = 0x2454; + pub const RX_STD_RCB_MAXLEN_FLAGS: u32 = 0x2458; + pub const RX_STD_REPLENISH_THRESH: u32 = 0x2c18; + pub const RX_JUMBO_REPLENISH_THRESH: u32 = 0x2c1c; +} + +/// Bit 0 of a mode register is a self-clearing reset; bit 0 of MISC_CFG +/// resets the whole chip. +const MODE_RESET: u32 = 1; +const MISCCFG_RESET_CORE_CLOCKS: u32 = 1; +const RXMODE_ENABLE: u32 = 2; +const RCB_FLAG_EXT_RX_ENABLE: u32 = 1; + +const BUFMGR_MODE_ENABLE: u32 = 2; +/// The buffer manager's enable reads back only after its startup; a +/// functional approximation inside the driver's 10 ms polling budget. +const BUFMGR_START: Duration = Duration::from_millis(8); + +mod eeprom_cmd { + pub const READ: u32 = 1 << 31; + pub const COMPLETE: u32 = 1 << 30; + pub const RESET: u32 = 1 << 29; + pub const DEVICE_ID: u32 = 7 << 26; + pub const START: u32 = 1 << 25; + pub const WORD_ADDR: u32 = 0x3fff << 2; +} + +/// The firmware-ready handshake. The driver writes the magic to SRAM 0x0b50 +/// and polls for its complement, the on-chip bootcode's ready signal. The +/// mailbox returns the complement of the last value written, so the magic +/// reads back as ready whether the driver writes it before the reset or after. +pub const SRAM_FW_MBOX: u32 = 0x0b50; +pub const FW_MAGIC: u32 = 0x4b65_7654; +/// The firmware-progress register holds the all-ones "no progress yet" +/// sentinel, so a driver polling it sees "unchanged" and keeps polling the +/// mailbox. +const FW_PROGRESS_INIT: u32 = 0xffff_ffff; + +/// The station address the on-chip bootcode leaves where a driver looks for +/// it: SRAM 0x0c14 tagged 0x484b in its high half, and MAC address filter +/// slot 0. +const SRAM_MAC_HI: u32 = 0x0c14; +const SRAM_MAC_LO: u32 = 0x0c18; +const MAC_TAG: u32 = 0x484b; + +mod mi { + pub const DATA: u32 = 0x0000_ffff; + pub const REG: u32 = 0x001f_0000; + pub const REG_SHIFT: u32 = 16; + /// Command and address fields, kept across a read. + pub const READ_FIELDS: u32 = 0x0fff_0000; + pub const WRITE: u32 = 0x0400_0000; + pub const READ: u32 = 0x0800_0000; + pub const BUSY: u32 = 0x2000_0000; + pub const LINK: u32 = 1; +} + +/// What the model could not carry out, counted and logged on first sight so +/// an unexpected access is never silent. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Refused { + /// A write to a register with no modelled behaviour; the value is stored + /// and reads back. + UnmodelledWrite(u32), + /// A write to the read-only buffer-manager status. + BufmgrStatusWrite, + /// A vital product data write; the storage is not written. + VpdWrite, + /// Direct EEPROM serial-pin control. + EepromPins, + EepromFault(eeprom::Fault), + /// An MI_COMM opcode other than read and write. + MiOpcode(u32), + /// A BAR access of a width or alignment the chip does not accept. + BarAccess { offset: u32, width: u8, write: bool }, + Phy(phy::Refusal), + /// A transfer to or from host memory that did not complete. + DmaFault(super::dma::DmaWhat), + /// A ring index written to a mailbox that lies outside the ring. + ProducerOutOfRange(super::dma::Mailbox), + /// A transmit frame larger than the model carries; it is dropped. + TxFrameTooLarge, + /// A received frame larger than the model carries; it is dropped. + RxFrameTooLarge, + /// A frame discarded for want of a posted descriptor, once per drought. + RxShortage(Ring), + /// A frame too big for the ring it would use; it is dropped. + RxNoRoom(Ring), + /// The jumbo ring asks for 64-byte descriptors, which are not modelled; + /// the ring is treated as unavailable. + JumboExtendedDescriptors, +} + +/// The chip's view of its own configuration header, which the bus owns: the +/// device holds it in its function state, and the state holds the device +/// weakly. +pub(super) struct Header(Weak); + +impl Header { + pub(super) fn new(device: Weak) -> Self { + Self(device) + } + + /// MISC_CTL as the guest and the bus have left it. + fn misc_ctl(&self) -> u32 { + self.0.upgrade().map_or(0, |dev| dev.config_header().read(cfg::MISC_CTL)) + } + + /// Clear `bits` of MISC_CTL, which the chip does itself. + fn clear_misc_ctl(&self, bits: u32) { + if let Some(dev) = self.0.upgrade() { + dev.config_header().write_masked(cfg::MISC_CTL, 0, bits); + } + } +} + +pub(super) struct State { + pub cfg: Header, + /// PCISTATE, which the chip drives: the bus mode, whether it saw + /// REQ64# at the end of reset, and whether the interrupt is down. + pub pcistate: u32, + /// Whether the slot drives REQ64# as reset ends. + req64: bool, + pub mac: [u8; 6], + pub(super) regfile: Box<[u32; REG_WORDS]>, + sram: Box<[u32; SRAM_WORDS]>, + /// The two indirect-window base registers (config 0x78 and 0x7c). + pub reg_win_base: u32, + pub mem_win_base: u32, + eeprom: Option, + bufmgr_start: Option, + /// The mask set through the 0x0204 mailbox, and the latch a status-block + /// update sets and the driver clears. The line is the latch with + /// neither mask (this or MISC_CTL's) set. + pub(super) irq_masked: bool, + pub(super) irq_pending: bool, + irq_level: bool, + pub irq_events: Vec, + pub phy: Phy, + pub now: Instant, + refused: BTreeMap, + pub(super) dma: Arc, + /// The send-ring consumer, and what the status block reports. + pub(super) tx_cons: u16, + pub(super) status_tx_cons: u16, + pub(super) status_rx_prod: u16, + /// Where the next return descriptor goes; it runs ahead of + /// `status_rx_prod` while the arrival line holds a completion. + pub(super) rx_prod_staged: u16, + /// The next descriptor a frame takes from each producer ring, and the + /// next one the chip fetches into its descriptor cache; the status block + /// reports the second. + pub(super) rx_std_cons: u16, + pub(super) rx_jumbo_cons: u16, + pub(super) rx_std_fetched: u16, + pub(super) rx_jumbo_fetched: u16, + /// Frames discarded since a ring's shortage began, so a drought is + /// reported at both ends rather than once per frame. + pub(super) rx_shortage_drops: u64, + pub(super) rx_std_shortage_drops: u64, + pub(super) status_tag: u8, + /// Descriptors consumed on each ring since the last status-block update, + /// and whether each coalescing budget has run out. + pub(super) rx_coal_pending: u16, + pub(super) tx_coal_pending: u16, + pub(super) rx_coal_expired: bool, + pub(super) tx_coal_expired: bool, + pub(super) rx_coal_timer: Option, + pub(super) tx_coal_timer: Option, + /// Completions held until their frame's arrival deadline. + pub(super) rx_latency: VecDeque, + pub(super) rx_floor: Duration, + /// The same minimum, counted in cycles of the processor that rang the + /// send doorbell. + pub(super) rx_floor_cycles: u64, + /// Each processor's cycle counter, by CPU number, once the machine has + /// wired them. Without them the floor is host time alone. + pub(super) cpu_cycles: Vec, + /// Each processor's cycle count when it last rang the send doorbell. + pub(super) tx_doorbell: Doorbells, + /// Frames the transmit engine produced, for the owner to send. + pub wire: Vec>, + /// Set when something the guest did can unblock a waiting frame. + pub wake_rx: bool, + /// A reset discards the frames the backend still holds for the guest. + pub purge_rx: bool, +} + +impl State { + pub fn new( + cfg: Header, + req64: bool, + mac: [u8; 6], + eeprom: Option, + rx_floor: Duration, + rx_floor_cycles: u64, + now: Instant, + ) -> Self { + let mut st = Self { + cfg, + pcistate: Self::pcistate_at_reset(req64), + req64, + mac, + regfile: Box::new([0; REG_WORDS]), + sram: Box::new([0; SRAM_WORDS]), + reg_win_base: 0, + mem_win_base: 0, + eeprom, + bufmgr_start: None, + irq_masked: true, + irq_pending: false, + irq_level: false, + irq_events: Vec::new(), + phy: Phy::new(now), + now, + refused: BTreeMap::new(), + dma: Arc::new(super::dma::NullDma), + tx_cons: 0, + status_tx_cons: 0, + status_rx_prod: 0, + rx_prod_staged: 0, + rx_std_cons: 0, + rx_jumbo_cons: 0, + rx_std_fetched: 0, + rx_jumbo_fetched: 0, + rx_shortage_drops: 0, + rx_std_shortage_drops: 0, + status_tag: 0, + rx_coal_pending: 0, + tx_coal_pending: 0, + rx_coal_expired: false, + tx_coal_expired: false, + rx_coal_timer: None, + tx_coal_timer: None, + rx_latency: VecDeque::new(), + rx_floor, + rx_floor_cycles, + cpu_cycles: Vec::new(), + tx_doorbell: [None; MAX_CPUS], + wire: Vec::new(), + wake_rx: false, + purge_rx: false, + }; + st.reset(); + st + } + + /// Enter at `now`: run whatever came due since the last entry. + pub fn enter(&mut self, now: Instant) { + if now > self.now { + self.now = now; + } + self.run_timers(); + } + + /// Run whatever came due, earliest first, each at its own deadline: the + /// virtual clock the model is ported from reaches a timer's expiry before + /// it fires, so a timer that arms another counts from there, not from + /// whenever this entry happened. + fn run_timers(&mut self) { + let target = self.now; + while let Some((deadline, timer)) = self.timers().into_iter().filter(|(d, _)| *d <= target).min() { + self.now = deadline; + match timer { + Timer::PhySettle => self.phy.sync(deadline), + Timer::BufferManager => { + self.bufmgr_start = None; + let mode = self.rf(reg::BUFMGR_MODE); + self.set_rf(reg::BUFMGR_MODE, mode | BUFMGR_MODE_ENABLE); + } + Timer::RxCoalescing => self.rx_coal_timer_fired(), + Timer::TxCoalescing => self.tx_coal_timer_fired(), + Timer::RxArrival => self.rx_arrival_due(target), + } + } + self.now = target; + } + + fn timers(&self) -> Vec<(Instant, Timer)> { + [ + self.phy.deadline().map(|d| (d, Timer::PhySettle)), + self.bufmgr_start.map(|d| (d, Timer::BufferManager)), + self.rx_coal_timer.map(|d| (d, Timer::RxCoalescing)), + self.tx_coal_timer.map(|d| (d, Timer::TxCoalescing)), + self.rx_latency.front().map(|held| (held.due, Timer::RxArrival)), + ] + .into_iter() + .flatten() + .collect() + } + + /// The earliest moment something here needs attention. + pub fn deadline(&self) -> Option { + self.timers().into_iter().map(|(d, _)| d).min() + } + + pub fn refused(&self) -> Vec<(Refused, u64)> { + self.refused.iter().map(|(what, count)| (*what, *count)).collect() + } + + pub fn eeprom_bytes(&self) -> Option> { + self.eeprom.as_ref().map(|e| e.bytes().to_vec()) + } + + pub fn refuse(&mut self, what: Refused) { + let count = self.refused.entry(what).or_insert(0); + *count += 1; + if *count == 1 { + crate::dlog!(crate::devlog::LogModule::Net, "bcm5701: unsupported or unexpected: {what:?}"); + } + } + + fn drain_phy_refusals(&mut self) { + for what in std::mem::take(&mut self.phy.refused) { + self.refuse(Refused::Phy(what)); + } + } + + // ---- storage ---- + + pub(super) fn rf(&self, off: u32) -> u32 { + self.regfile[((off & (REG_FILE_SIZE - 1)) >> 2) as usize] + } + + pub(super) fn set_rf(&mut self, off: u32, val: u32) { + self.regfile[((off & (REG_FILE_SIZE - 1)) >> 2) as usize] = val; + } + + pub fn sram_read(&self, off: u32) -> u32 { + self.sram[((off & (SRAM_SIZE - 1)) >> 2) as usize] + } + + pub fn sram_write(&mut self, off: u32, val: u32) { + let off = off & (SRAM_SIZE - 1); + let val = if off == SRAM_FW_MBOX { !val } else { val }; + self.sram[(off >> 2) as usize] = val; + } + + // ---- resets ---- + + /// The station address, as the on-chip bootcode owns it: every reset + /// puts it back. + fn load_station_address(&mut self) { + let m = self.mac; + let hi = u32::from(m[0]) << 8 | u32::from(m[1]); + let lo = u32::from_be_bytes([m[2], m[3], m[4], m[5]]); + self.sram_write(SRAM_MAC_HI, MAC_TAG << 16 | hi); + self.sram_write(SRAM_MAC_LO, lo); + self.set_rf(reg::MAC_ADDR_0_HIGH, hi); + self.set_rf(reg::MAC_ADDR_0_LOW, lo); + } + + /// The driver's write of MISC_CFG.RESET_CORE_CLOCKS, by either access + /// path: the register file and SRAM return to their reset values, the + /// handshake mailbox re-arms to the firmware-ready complement and the + /// bootcode's station address comes back. PCI configuration, the chip id + /// in MISC_CTL among it, and the indirect-window bases are untouched. + fn global_reset(&mut self) { + self.bufmgr_start = None; + self.regfile.fill(0); + self.sram.fill(0); + self.reset_engines(); + self.irq_masked = true; + self.irq_pending = false; + self.sram_write(SRAM_FW_MBOX, FW_MAGIC); + self.load_station_address(); + self.update_irq(false); + } + + /// A cold reset: as a global reset, plus the window bases, the PHY, and + /// no firmware-ready complement (only the driver-triggered reset arranges + /// that, because it is the one the driver writes the magic for). + pub fn reset(&mut self) { + self.bufmgr_start = None; + self.regfile.fill(0); + self.sram.fill(0); + self.reg_win_base = 0; + self.mem_win_base = 0; + self.pcistate = Self::pcistate_at_reset(self.req64); + self.reset_engines(); + self.irq_masked = true; + self.irq_pending = false; + self.phy.reset(self.now); + self.load_station_address(); + self.update_irq(true); + } + + // ---- interrupt ---- + + pub(super) fn misc_ctl(&self) -> u32 { + self.cfg.misc_ctl() + } + + /// PCISTATE after reset: conventional PCI, the interrupt line down, and + /// a 32-bit bus unless the slot drove REQ64#. + pub fn pcistate_at_reset(req64: bool) -> u32 { + pcistate::BUSMODE_PCI | pcistate::INTR_NOT_ACTIVE | if req64 { 0 } else { pcistate::BUS_32BIT } + } + + /// The interrupt is a level: INTA asserts while a status-block update + /// awaits the driver and neither the mailbox nor MISC_CTL.MASK_PCI_INTR + /// masks it. PCISTATE.INTR_NOT_ACTIVE mirrors the inverse, so the + /// driver's shared-interrupt check reads true exactly when the line is + /// down. `force` delivers the level even if unchanged. + pub fn update_irq(&mut self, force: bool) { + let active = self.irq_pending && !self.irq_masked && self.misc_ctl() & misc_ctl::MASK_PCI_INTR == 0; + if active { + self.pcistate &= !pcistate::INTR_NOT_ACTIVE; + } else { + self.pcistate |= pcistate::INTR_NOT_ACTIVE; + } + if force || active != self.irq_level { + self.irq_level = active; + self.irq_events.push(active); + } + } + + pub fn irq_level(&self) -> bool { + self.irq_level + } + + /// A write to MISC_CTL acknowledges if CLEAR_INTA was set, then re-derives + /// the line. This is how the driver's "set CLEAR_INTA, clear + /// MASK_PCI_INTR" enables the interrupt. + pub fn misc_ctl_written(&mut self) { + if self.misc_ctl() & misc_ctl::CLEAR_INTA != 0 { + self.irq_pending = false; + self.cfg.clear_misc_ctl(misc_ctl::CLEAR_INTA); + } + self.update_irq(false); + } + + // ---- EEPROM ---- + + fn eeprom_access(&mut self, command: u32) -> bool { + let device = ((command & eeprom_cmd::DEVICE_ID) >> 26) as u8; + let word = (command & eeprom_cmd::WORD_ADDR) >> 2; + let result = match self.eeprom.as_mut() { + None => { + self.refuse(Refused::EepromFault(eeprom::Fault::NoBacking)); + return false; + } + Some(e) if command & eeprom_cmd::READ != 0 => e.read_word(device, word).map(Some), + Some(e) => { + let value = self.regfile[(reg::EEPROM_DATA >> 2) as usize]; + e.write_word(device, word, value).map(|()| None) + } + }; + match result { + Ok(Some(value)) => { + self.set_rf(reg::EEPROM_DATA, value); + true + } + Ok(None) => true, + Err(fault) => { + self.refuse(Refused::EepromFault(fault)); + false + } + } + } + + fn eeprom_command_write(&mut self, val: u32) { + let old = self.rf(reg::EEPROM_ADDR); + let mut command = val & !(eeprom_cmd::START | eeprom_cmd::COMPLETE | eeprom_cmd::RESET | 3); + let acknowledge = val & eeprom_cmd::COMPLETE != 0; + if val & eeprom_cmd::RESET != 0 { + self.set_rf(reg::EEPROM_ADDR, eeprom_cmd::RESET); + self.set_rf(reg::EEPROM_DATA, 0); + return; + } + if !acknowledge { + command |= old & eeprom_cmd::COMPLETE; + } + self.set_rf(reg::EEPROM_ADDR, command); + // A pending Complete blocks the next transfer until it is + // acknowledged; acknowledging and starting in one write is allowed. + if val & eeprom_cmd::START == 0 || (old & eeprom_cmd::COMPLETE != 0 && !acknowledge) { + return; + } + if self.eeprom_access(command) { + self.set_rf(reg::EEPROM_ADDR, command | eeprom_cmd::COMPLETE); + } + } + + // ---- register file ---- + + /// Register-file writes, reached identically from the BAR and from the + /// indirect register window, so a side effect fires whichever path the + /// driver uses. The 5700/5701 issue the global reset through the window. + pub fn reg_write(&mut self, off: u32, val: u32) { + let off = off & (REG_FILE_SIZE - 1); + let mut val = val; + match off { + reg::MISC_CFG => { + if val & MISCCFG_RESET_CORE_CLOCKS != 0 { + self.global_reset(); + return; + } + } + reg::EEPROM_ADDR => { + self.eeprom_command_write(val); + return; + } + reg::EEPROM_DATA | reg::EEPROM_DELAY => {} + reg::EEPROM_PIN => { + self.refuse(Refused::EepromPins); + self.set_rf(off, val); + return; + } + // The reset bit self-clears; the rest of the write persists. + reg::MAC_MODE => val &= !MODE_RESET, + reg::RX_MODE => { + // A halt takes effect at once. The chip finishes the frame in + // flight first and reads back ENABLE until it has, which a + // model that takes one frame at a time never needs. + val &= !MODE_RESET; + self.set_rf(off, val); + if val & RXMODE_ENABLE != 0 { + self.retry_rx_flush(); + } + return; + } + // Write one to clear: the bits written are the ones acknowledged. + reg::MAC_STATUS => { + let old = self.rf(off); + self.set_rf(off, old & !val); + return; + } + reg::HCC_MODE => { + val &= !MODE_RESET; + self.set_rf(off, val); + self.hcc_mode_written(); + return; + } + reg::HCC_RX_COAL_TICKS | reg::HCC_TX_COAL_TICKS => { + self.set_rf(off, val); + self.coal_ticks_written(off == reg::HCC_RX_COAL_TICKS); + return; + } + reg::MBX_TX_PROD_LO => { + // Store the producer for read-back, then send up to it. + self.set_rf(off, val); + self.tx_kick(val); + return; + } + reg::RX_JUMBO_RCB_MAXLEN_FLAGS => { + // Max_Len is unused for the jumbo ring; only Disable Ring and + // Extended RX Enable do anything, and both are read from the + // stored word. Flag the unmodelled extended format once, here, + // rather than on every frame that finds the ring unavailable. + if val & RCB_FLAG_EXT_RX_ENABLE != 0 { + self.refuse(Refused::JumboExtendedDescriptors); + } + self.set_rf(off, val); + self.rx_ring_setup_written(); + return; + } + // What the descriptor fetch depends on: a ring that becomes + // usable, or a lower threshold, can start one. + reg::RX_STD_RCB_HADDR_HI + | reg::RX_STD_RCB_HADDR_LO + | reg::RX_STD_RCB_MAXLEN_FLAGS + | reg::RX_JUMBO_RCB_HADDR_HI + | reg::RX_JUMBO_RCB_HADDR_LO + | reg::RX_STD_REPLENISH_THRESH + | reg::RX_JUMBO_REPLENISH_THRESH => { + self.set_rf(off, val); + self.rx_ring_setup_written(); + return; + } + reg::MBX_RX_JUMBO_PROD_LO | reg::MBX_RX_STD_PROD_LO | reg::MBX_RX_CONS0_LO => { + self.rx_mailbox_written(off, val); + return; + } + reg::BUFMGR_MODE => { + if val & MODE_RESET != 0 { + self.bufmgr_start = None; + self.set_rf(off, 0); + } + val &= !MODE_RESET; + if val & BUFMGR_MODE_ENABLE == 0 { + self.bufmgr_start = None; + } else if self.rf(off) & BUFMGR_MODE_ENABLE == 0 { + if self.bufmgr_start.is_none() { + self.bufmgr_start = Some(self.now + BUFMGR_START); + } + val &= !BUFMGR_MODE_ENABLE; + } + } + reg::BUFMGR_STATUS => { + self.refuse(Refused::BufmgrStatusWrite); + return; + } + reg::MBX_IRQ0_LO => { + // Every write clears the pending update; a nonzero value + // masks the line and zero re-enables it. + self.irq_pending = false; + self.irq_masked = val != 0; + self.set_rf(off, val); + if val == 0 { + self.retry_rx_flush(); + } + self.update_irq(false); + return; + } + reg::MI_COMM => { + self.mi_comm_write(val); + return; + } + _ => { + self.refuse(Refused::UnmodelledWrite(off)); + self.set_rf(off, val); + return; + } + } + self.set_rf(off, val); + } + + fn mi_comm_write(&mut self, command: u32) { + let register = ((command & mi::REG) >> mi::REG_SHIFT) as u8; + if command & mi::BUSY == 0 { + self.set_rf(reg::MI_COMM, command); + return; + } + let now = self.now; + let opcode = command & (mi::READ | mi::WRITE); + let val = if opcode == mi::READ { + (command & mi::READ_FIELDS) | u32::from(self.phy.read(register, now)) + } else if opcode == mi::WRITE { + self.phy.write(register, (command & mi::DATA) as u16, now); + command & !mi::BUSY + } else { + self.refuse(Refused::MiOpcode(opcode >> 26)); + command & !mi::BUSY + }; + self.set_rf(reg::MI_COMM, val); + self.drain_phy_refusals(); + } + + /// Register-file reads, reached identically from the BAR and the window. + /// Most read back their stored value; MI_STS is the PHY's live link so + /// the driver's auto-poll path sees the carrier. + pub fn reg_read(&mut self, off: u32) -> u32 { + match off & (REG_FILE_SIZE - 1) { + reg::MI_STS => { + self.phy.sync(self.now); + if self.phy.link_up() { + mi::LINK + } else { + 0 + } + } + reg::FW_PROGRESS => FW_PROGRESS_INIT, + _ => self.rf(off), + } + } +} diff --git a/src/dev/bcm5701/tests.rs b/src/dev/bcm5701/tests.rs new file mode 100644 index 000000000..e723f7824 --- /dev/null +++ b/src/dev/bcm5701/tests.rs @@ -0,0 +1,2643 @@ +//! Register-contract tests for the BCM5701, ported from the QEMU model's +//! qtests. The device is built alone, with a controllable clock and an +//! interrupt probe standing in for the rest of the machine. +//! +//! Ported from tests/qtest/bcm570x-test.c in jamesbraid/qemu-sgi-origami. + +use super::eeprom::{Eeprom, Fault}; +use super::phy::{reg as phy_reg, IDR1_VALUE, IDR2_BCM5701}; +use super::state::{reg, Refused, FW_MAGIC, SRAM_FW_MBOX}; +use super::*; +use crate::irq::LevelProbe; +use crate::pci::{PciDma, PciDmaStatus}; +use std::time::Duration; + +const SRAM_WINDOW: u32 = 0x8000; +const MISC_CFG_RESET: u32 = 1; +const MODE_RESET: u32 = 1; +const RXMODE_ENABLE: u32 = 2; +const HCC_ENABLE: u32 = 2; +const BUFMGR_ENABLE: u32 = 2; +const BUFMGR_ATTN: u32 = 4; +const BUFMGR_START: Duration = Duration::from_millis(8); +const PHY_SETTLE: Duration = Duration::from_millis(100); +const MICOMM_BUSY: u32 = 0x2000_0000; +const MICMD_READ: u32 = 0x0800_0000; +const MICMD_WRITE: u32 = 0x0400_0000; +const MICOMM_READ_FIELDS: u32 = 0x0fff_0000; +const PHY_ADDR: u32 = 1; +const MAC: [u8; 6] = [0x52, 0x54, 0x00, 0xab, 0xcd, 0xef]; + +const EEPROM_READ: u32 = 1 << 31; +const EEPROM_COMPLETE: u32 = 1 << 30; +const EEPROM_RESET: u32 = 1 << 29; +const EEPROM_START: u32 = 1 << 25; + +const MEM_SIZE: usize = 0x90_0000; +const TX_RING: u64 = 0x20_0000; +const TX_BUF: u64 = 0x21_0000; +const STATUS: u64 = 0x22_0000; +const STD_RING: u64 = 0x23_0000; +const RET_RING: u64 = 0x24_0000; +const RX_BUF: u64 = 0x25_0000; +const JUMBO_RING: u64 = 0x26_0000; +const JUMBO_BUF: u64 = 0x27_0000; +const STD_STALE_BUF: u64 = 0x28_0000; +/// Past the memory: nothing backs it. +const UNMAPPED: u64 = 0x400_0000; +const TX_RING_LEN: u32 = 4; +const RX_RING_LEN: u32 = 4; +const STD_BUF_SIZE: u32 = 1536; +const JUMBO_BUF_SIZE: u32 = 9216; +const STATUS_BLK_SIZE: usize = 0x50; +const ARRIVAL: Duration = Duration::from_micros(200); +/// Every DMA swap control set: the chip's image goes on the bus as it is. +const MODE_ALL_SWAPS: u32 = 0x36; + +/// Bus-master memory with no bridge in front of it. +struct Mem(std::sync::Mutex>); + +impl PciDma for Mem { + fn dma_read(&self, addr: u64, buf: &mut [u8]) -> PciDmaStatus { + let m = self.0.lock().unwrap(); + let a = addr as usize; + if a.saturating_add(buf.len()) > m.len() { + buf.fill(0); + return PciDmaStatus::MasterAbort; + } + buf.copy_from_slice(&m[a..a + buf.len()]); + PciDmaStatus::Ok + } + fn dma_write(&self, addr: u64, data: &[u8]) -> PciDmaStatus { + let mut m = self.0.lock().unwrap(); + let a = addr as usize; + if a.saturating_add(data.len()) > m.len() { + return PciDmaStatus::MasterAbort; + } + m[a..a + data.len()].copy_from_slice(data); + PciDmaStatus::Ok + } +} + +struct Rig { + dev: Arc, + irq: Arc, + mem: Arc, + /// Frames the guest sent, and a way to hand frames to the guest. + sent: rtrb::Consumer>, + inject: rtrb::Producer>, + ring_slots: usize, +} + +impl Rig { + fn new() -> Self { + Self::with(Bcm5701Config::new(MAC)) + } + + fn with(config: Bcm5701Config) -> Self { + let dev = Bcm5701::new(config).unwrap(); + *dev.frozen.lock() = Some(Instant::now()); + let (irq, line) = LevelProbe::line(); + let mem = Arc::new(Mem(std::sync::Mutex::new(vec![0; MEM_SIZE]))); + dev.state().connect_irq(line); + dev.state().connect_dma(mem.clone()); + dev.attached(); + let (link, sent, inject) = crate::net::Link::loopback(); + dev.attach(link); + dev.reset(); + let ring_slots = inject.slots(); + Self { dev, irq, mem, sent, inject, ring_slots } + } + + /// Advance the clock and run whatever came due. + fn step(&self, by: Duration) { + { + let mut t = self.dev.frozen.lock(); + *t = Some(t.unwrap() + by); + } + self.dev.enter(self.dev.now(), |_| ()); + } + + fn mem_write(&self, addr: u64, data: &[u8]) { + let mut m = self.mem.0.lock().unwrap(); + m[addr as usize..addr as usize + data.len()].copy_from_slice(data); + } + fn mem_read(&self, addr: u64, len: usize) -> Vec { + self.mem.0.lock().unwrap()[addr as usize..addr as usize + len].to_vec() + } + fn mem_fill(&self, addr: u64, len: usize, byte: u8) { + self.mem.0.lock().unwrap()[addr as usize..addr as usize + len].fill(byte); + } + fn status_block(&self) -> Vec { + self.mem_read(STATUS, STATUS_BLK_SIZE) + } + fn status_tag(&self) -> u32 { + be32(&self.status_block(), 4) + } + fn status_tx_cons(&self) -> u16 { + get16dma(&self.status_block(), 18) + } + fn status_rx_prod(&self) -> u16 { + get16dma(&self.status_block(), 16) + } + /// The producer-ring consumers, the high and low halves of word 0x08. + fn status_std_cons(&self) -> u16 { + let sb = self.status_block(); + u16::from_be_bytes([sb[8], sb[9]]) + } + fn status_jumbo_cons(&self) -> u16 { + let sb = self.status_block(); + u16::from_be_bytes([sb[10], sb[11]]) + } + fn line_up(&self) -> bool { + self.cfg_read(0x70 / 4) & 2 == 0 + } + fn nic_no_more_rxbds(&self) -> u32 { + self.readl(SRAM_WINDOW + 0x898) + } + /// The frames the guest has sent so far. + fn sent_frames(&mut self) -> Vec> { + std::iter::from_fn(|| self.sent.pop().ok()).collect() + } + /// Frames handed to the guest that it has not yet taken. + fn waiting(&self) -> usize { + self.ring_slots - self.inject.slots() + } + /// Offer a frame to the receive path, as the backend would. + fn deliver(&mut self, frame: &[u8]) { + self.inject.push(frame.to_vec()).unwrap(); + self.dev.poll(); + } + + fn readl(&self, off: u32) -> u32 { + self.dev.bar_read(0, off, 4) + } + fn readb(&self, off: u32) -> u32 { + self.dev.bar_read(0, off, 1) + } + fn writel(&self, off: u32, val: u32) { + self.dev.bar_write(0, off, val, 4); + } + fn cfg_read(&self, reg: usize) -> u32 { + self.dev.config_dword(reg) + } + /// A configuration write as the bus makes it: `width` bytes at byte + /// `offset`. + fn cfg_write_sized(&self, offset: usize, width: usize, val: u32) { + let (reg, shift) = (offset / 4, (offset & 3) * 8); + if width == 4 && self.dev.config_write(reg, val) { + return; + } + let config = self.dev.state().config(); + let old = config.read(reg); + config.write_masked(reg, val << shift, (u32::MAX >> (32 - 8 * width)) << shift); + let new = config.read(reg); + if new != old { + self.dev.config_written(reg, old, new); + } + } + fn cfg_write(&self, reg: usize, val: u32) { + self.cfg_write_sized(reg * 4, 4, val); + } + /// A configuration read of `width` bytes at byte `offset`. + fn cfg_read_sized(&self, offset: usize, width: usize) -> u32 { + (self.cfg_read(offset / 4) >> ((offset & 3) * 8)) & (u32::MAX >> (32 - 8 * width)) + } + + /// The indirect register window, config 0x78 and 0x80. + fn ind_reg_write(&self, off: u32, val: u32) { + self.cfg_write(0x78 / 4, off); + self.cfg_write(0x80 / 4, val); + } + fn ind_reg_read(&self, off: u32) -> u32 { + self.cfg_write(0x78 / 4, off); + self.cfg_read(0x80 / 4) + } + /// The indirect memory window, config 0x7c and 0x84. + fn ind_mem_write(&self, off: u32, val: u32) { + self.cfg_write(0x7c / 4, off); + self.cfg_write(0x84 / 4, val); + } + fn ind_mem_read(&self, off: u32) -> u32 { + self.cfg_write(0x7c / 4, off); + self.cfg_read(0x84 / 4) + } + + fn mii_read(&self, phy: u32, register: u8) -> u16 { + self.writel(reg::MI_COMM, MICOMM_BUSY | MICMD_READ | phy << 21 | u32::from(register) << 16); + self.readl(reg::MI_COMM) as u16 + } + fn mii_write(&self, phy: u32, register: u8, val: u16) { + self.writel( + reg::MI_COMM, + MICOMM_BUSY | MICMD_WRITE | phy << 21 | u32::from(register) << 16 | u32::from(val), + ); + } + fn sram_word(&self, off: u32) -> u32 { + self.dev.st.lock().sram_read(off) + } + fn split_addr(&self, hi_reg: u32, lo_reg: u32, addr: u64) { + self.writel(hi_reg, (addr >> 32) as u32); + self.writel(lo_reg, addr as u32); + } + fn refusals(&self, what: Refused) -> u64 { + self.dev.refused().iter().find(|(r, _)| *r == what).map_or(0, |(_, n)| *n) + } +} + +fn temp_file(name: &str, contents: &[u8]) -> std::path::PathBuf { + let path = std::env::temp_dir().join(format!("iris-bcm5701-{}-{name}", std::process::id())); + std::fs::write(&path, contents).unwrap(); + path +} + +/// Dword and sub-dword reads of the identity, so a hidden swap in one half +/// cannot hide behind the other. Ported from test_pci_four_tuple. +#[test] +fn the_function_presents_the_sgi_board_identity() { + let rig = Rig::new(); + assert_eq!(rig.cfg_read(0), 0x1645_14e4); + assert_eq!(rig.cfg_read(0x2c / 4), 0x8010_10a9); + assert_eq!(rig.cfg_read(0x08 / 4) >> 8, 0x02_0000); + assert_eq!(rig.cfg_read(0x08 / 4) & 0xff, 0x15, "the B5 stepping's PCI revision"); + assert_eq!(rig.dev.state().config().interrupt_pin(), PCI_INTA); +} + +/// The capability list is PCI-X at 0x40, power management at 0x48, VPD at +/// 0x50 and MSI at 0x58, in that order, and the status register says there +/// is one. A walk from the pointer visits exactly those. +#[test] +fn the_capability_list_is_pcix_pm_vpd_and_msi() { + let rig = Rig::new(); + assert_ne!(rig.cfg_read_sized(0x06, 2) & 0x10, 0, "status: capability list"); + let mut walk = Vec::new(); + let mut at = rig.cfg_read_sized(0x34, 1) as usize; + while at != 0 { + walk.push((at, rig.cfg_read_sized(at, 1))); + at = rig.cfg_read_sized(at + 1, 1) as usize; + } + assert_eq!(walk, [(0x40, 0x07), (0x48, 0x01), (0x50, 0x03), (0x58, 0x05)]); + assert_eq!(rig.cfg_read_sized(0x42, 2), 0x0002, "PCI-X command: relaxed ordering"); + assert_eq!(rig.cfg_read(0x44 / 4), 0x0003_fff9, "PCI-X status at reset"); + assert_eq!(rig.cfg_read_sized(0x4a, 2), 0x4002, "PM: PME from D3hot, version 1.1"); + assert_eq!(rig.cfg_read_sized(0x5a, 2), 0x0086, "MSI: 64-bit, eight messages, disabled"); + // The driver saves and restores the PCI-X command halfword around a reset. + rig.cfg_write_sized(0x42, 2, 0x0023); + assert_eq!(rig.cfg_read_sized(0x42, 2), 0x0023); + rig.cfg_write_sized(0x40, 2, 0); + assert_eq!(rig.cfg_read_sized(0x40, 2), 0x4807, "identity and link are read-only"); + rig.cfg_write_sized(0x5a, 2, 0x0001); + assert_eq!(rig.cfg_read_sized(0x5a, 2) & 1, 0, "MSI cannot be enabled"); +} + +/// A VPD read sets the flag at once and puts the four bytes at the address, +/// least significant first, in the data register: from the EEPROM's VPD +/// area at 0x100 when one is fitted, all ones without one. IRIX's driver +/// makes these reads through the BAR's alias of the header with halfword +/// accesses at 0x52, and a flag that never set cost it 6.4 s per attach. +#[test] +fn a_vpd_read_completes_at_once_through_config_or_the_bar_alias() { + let bare = Rig::new(); + bare.cfg_write_sized(0x52, 2, 0x0010); + assert_eq!(bare.cfg_read_sized(0x52, 2), 0x8010, "flag set, address kept"); + assert_eq!(bare.cfg_read(0x54 / 4), 0xffff_ffff, "no NVRAM fitted"); + + let mut image = vec![0u8; 0x200]; + image[0x100..0x108].copy_from_slice(&[0x82, 0x05, 0x00, b'T', b'e', b's', b't', b'!']); + let rig = eeprom_rig(&image, 3); + rig.cfg_write_sized(0x52, 2, 0x0004); + assert_eq!(rig.cfg_read(0x54 / 4), u32::from_le_bytes(*b"est!")); + // The same through BAR0's alias, as halfword and word accesses. + rig.dev.bar_write(0, 0x52, 0x0000, 2); + assert_eq!(rig.dev.bar_read(0, 0x52, 2), 0x8000); + assert_eq!(rig.readl(0x54), u32::from_le_bytes([0x82, 0x05, 0x00, b'T'])); + rig.writel(0x50, 0x0004_0000); + assert_eq!(rig.readl(0x50), 0x8004_5803); + // A write request is refused and completes by clearing the flag. + rig.cfg_write_sized(0x52, 2, 0x8008); + assert_eq!(rig.cfg_read_sized(0x52, 2), 0x0008); + assert_eq!(rig.refusals(Refused::VpdWrite), 1); + assert_eq!(rig.dev.eeprom_bytes().unwrap(), image, "storage untouched"); +} + +/// The chip id in MISC_CTL decodes to the BCM5701, ASIC revision 0, and is +/// read-only while the low half sticks. Ported from test_misc_ctl_asic_revision. +#[test] +fn misc_ctl_presents_the_chip_id() { + let rig = Rig::new(); + let misc = rig.cfg_read(0x68 / 4); + assert_eq!(misc >> 16, 0x0105); + assert_eq!((misc >> 16) >> 12, 0); + rig.cfg_write(0x68 / 4, 0xffff_0004); + assert_eq!(rig.cfg_read(0x68 / 4), 0x0105_0004); +} + +/// Ported from test_indirect_register_window: the window aliases the file. +#[test] +fn the_indirect_register_window_aliases_the_register_file() { + let rig = Rig::new(); + rig.ind_reg_write(0x0410, 0x1234_5678); + assert_eq!(rig.readl(0x0410), 0x1234_5678); + assert_eq!(rig.ind_reg_read(0x0410), 0x1234_5678); + assert_eq!(rig.cfg_read(0x78 / 4), 0x0410); +} + +/// Only whole dwords reach the windows. A narrower configuration read sees +/// the header's own bytes, which stay zero because nothing writes them, as +/// in the model this is ported from, where the window state sits outside +/// the header and the header bytes are read-only. +#[test] +fn narrow_configuration_accesses_bypass_the_indirect_windows() { + let rig = Rig::new(); + rig.ind_reg_write(0x0410, 0x1234_5678); + rig.ind_mem_write(0x0c14, 0xdead_beef); + let config = rig.dev.state().config(); + for offset in 0x78..0x88 { + assert_eq!(config.read(offset / 4) >> ((offset & 3) * 8) & 0xff, 0, "byte at {offset:#x}"); + } + for offset in (0x78..0x88).step_by(2) { + assert_eq!(config.read(offset / 4) >> ((offset & 3) * 8) & 0xffff, 0, "halfword at {offset:#x}"); + } + rig.cfg_write_sized(0x80, 1, 0xff); + rig.cfg_write_sized(0x7e, 2, 0xffff); + assert_eq!(config.read(0x80 / 4) & 0xff, 0); + assert_eq!(config.read(0x7c / 4) >> 16, 0); + assert_eq!(rig.cfg_read_sized(0x78, 4), 0x0410); + assert_eq!(rig.cfg_read_sized(0x80, 4), 0x1234_5678); + assert_eq!(rig.cfg_read_sized(0x7c, 4), 0x0c14); + assert_eq!(rig.cfg_read_sized(0x84, 4), 0xdead_beef); +} + +/// Ported from test_indirect_memory_window: SRAM through config and through +/// the BAR window at 0x8000. +#[test] +fn the_indirect_memory_window_aliases_sram() { + let rig = Rig::new(); + rig.ind_mem_write(0x0c14, 0xdead_beef); + assert_eq!(rig.ind_mem_read(0x0c14), 0xdead_beef); + assert_eq!(rig.readl(SRAM_WINDOW + (0x0c14 & 0x7fff)), 0xdead_beef); + // The base register is masked to a 32 KiB boundary for the BAR window, + // and the 32 KiB SRAM has one position. + rig.cfg_write(0x7c / 4, 0x0c14); + assert_eq!(rig.readl(SRAM_WINDOW), rig.sram_word(0)); +} + +/// The magic written before the reset (the bge order) reads back as the +/// complement, and the reset self-clears its trigger and re-arms the mailbox. +/// Ported from test_reset_handshake_via_bar. +#[test] +fn the_reset_handshake_completes_when_the_magic_precedes_the_reset() { + let rig = Rig::new(); + rig.ind_mem_write(SRAM_FW_MBOX, FW_MAGIC); + assert_eq!(rig.ind_mem_read(SRAM_FW_MBOX), !FW_MAGIC); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + assert_eq!(rig.readl(reg::MISC_CFG) & MISC_CFG_RESET, 0); + assert_eq!(rig.ind_mem_read(SRAM_FW_MBOX), !FW_MAGIC); +} + +/// The IRIX order: reset first, then the magic. Ported from +/// test_reset_handshake_magic_after_reset. +#[test] +fn the_reset_handshake_completes_when_the_magic_follows_the_reset() { + let rig = Rig::new(); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + rig.ind_mem_write(SRAM_FW_MBOX, FW_MAGIC); + assert_eq!(rig.ind_mem_read(SRAM_FW_MBOX), 0xb49a_89ab); +} + +/// Ported from test_reset_handshake_via_indirect: the 5700/5701 reset through +/// the configuration window alone. +#[test] +fn the_reset_handshake_completes_through_the_indirect_windows() { + let rig = Rig::new(); + rig.ind_mem_write(SRAM_FW_MBOX, FW_MAGIC); + rig.ind_reg_write(reg::MISC_CFG, MISC_CFG_RESET); + assert_eq!(rig.ind_mem_read(SRAM_FW_MBOX), 0xb49a_89ab); +} + +/// Ported from test_firmware_progress_register: the all-ones "no progress +/// yet" sentinel survives a reset. +#[test] +fn the_firmware_progress_register_holds_its_sentinel() { + let rig = Rig::new(); + assert_eq!(rig.readl(reg::FW_PROGRESS), 0xffff_ffff); + assert_eq!(rig.ind_reg_read(reg::FW_PROGRESS), 0xffff_ffff); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + assert_eq!(rig.readl(reg::FW_PROGRESS), 0xffff_ffff); +} + +/// A global reset keeps the PCI header, the chip id among it, and the +/// window bases, and restores the bootcode's station address. Ported from +/// test_mac_address_registers. +#[test] +fn a_global_reset_restores_the_bootcode_state_only() { + let rig = Rig::new(); + rig.cfg_write(0x68 / 4, 0x0000_0004); + rig.cfg_write(0x7c / 4, 0x1234); + rig.writel(reg::MAC_ADDR_0_HIGH, 0xa1a2); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + assert_eq!(rig.cfg_read(0x68 / 4), 0x0105_0004); + assert_eq!(rig.cfg_read(0x7c / 4), 0x1234); + assert_eq!(rig.readl(reg::MAC_ADDR_0_HIGH), 0x5254); +} + +/// Ported from test_self_clearing_reset_bits. +#[test] +fn mode_register_reset_bits_self_clear_and_the_rest_persists() { + let rig = Rig::new(); + rig.writel(reg::MAC_MODE, MODE_RESET | 0x4); + assert_eq!(rig.readl(reg::MAC_MODE) & MODE_RESET, 0); + assert_eq!(rig.readl(reg::MAC_MODE) & 4, 4); + + rig.writel(reg::HCC_MODE, MODE_RESET); + assert_eq!(rig.readl(reg::HCC_MODE) & MODE_RESET, 0); + + rig.writel(reg::RX_MODE, MODE_RESET | RXMODE_ENABLE); + assert_eq!(rig.readl(reg::RX_MODE) & MODE_RESET, 0); + assert_eq!(rig.readl(reg::RX_MODE) & RXMODE_ENABLE, RXMODE_ENABLE); + // The IRIX sequence: reset on its own leaves the receiver halted. + rig.writel(reg::RX_MODE, MODE_RESET); + assert_eq!(rig.readl(reg::RX_MODE), 0); +} + +/// The driver's idle sequence writes ENABLE, then 0, and polls for the whole +/// register to read zero. Ported from test_hcc_mode_idle_poll and +/// test_hcc_mode_clears_to_zero. +#[test] +fn host_coalescing_mode_stores_and_clears() { + let rig = Rig::new(); + rig.writel(reg::HCC_MODE, HCC_ENABLE); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + assert_eq!(rig.readl(reg::HCC_MODE) & HCC_ENABLE, HCC_ENABLE); + rig.writel(reg::HCC_MODE, 0); + assert_eq!(rig.readl(reg::HCC_MODE) & HCC_ENABLE, 0); + + rig.writel(reg::HCC_MODE, 0x13e); + rig.writel(reg::HCC_MODE, 0); + assert_eq!(rig.readl(reg::HCC_MODE), 0); +} + +/// ENABLE reads back only after the buffer manager's startup, by either +/// access path. Ported from test_bufmgr_registers. +#[test] +fn the_buffer_manager_enable_reads_back_after_its_startup() { + for indirect in [false, true] { + let rig = Rig::new(); + let read = |off| if indirect { rig.ind_reg_read(off) } else { rig.readl(off) }; + let write = |off, val| if indirect { rig.ind_reg_write(off, val) } else { rig.writel(off, val) }; + let on = BUFMGR_ENABLE | BUFMGR_ATTN; + + write(reg::BUFMGR_MODE, BUFMGR_ATTN); + write(reg::BUFMGR_MODE, on); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + rig.step(Duration::from_millis(7)); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + // A repeated request does not restart the interval. + write(reg::BUFMGR_MODE, on); + rig.step(BUFMGR_START - Duration::from_millis(7) - Duration::from_nanos(1)); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + rig.step(Duration::from_nanos(1)); + assert_eq!(read(reg::BUFMGR_MODE), on); + + // Reset cancels startup; the new request starts a fresh interval. + write(reg::BUFMGR_MODE, MODE_RESET | on); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + rig.step(BUFMGR_START); + assert_eq!(read(reg::BUFMGR_MODE), on); + + // Halting is immediate and cancels a pending start. + write(reg::BUFMGR_MODE, BUFMGR_ATTN); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + write(reg::BUFMGR_MODE, on); + write(reg::BUFMGR_MODE, BUFMGR_ATTN); + rig.step(BUFMGR_START); + assert_eq!(read(reg::BUFMGR_MODE), BUFMGR_ATTN); + + // The status register is read-only, and the refusal is counted. + write(reg::BUFMGR_STATUS, on); + assert_eq!(read(reg::BUFMGR_STATUS), 0); + assert!(rig.refusals(Refused::BufmgrStatusWrite) >= 1); + + // A global reset cancels a pending startup. + rig.writel(reg::BUFMGR_MODE, on); + rig.step(BUFMGR_START / 2); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + rig.step(BUFMGR_START); + assert_eq!(rig.readl(reg::BUFMGR_MODE), 0); + } +} + +/// MAC_STATUS bits clear on a write of one, and the write is never readable +/// back as state. Ported from test_mac_status_write_one_clears. +#[test] +fn mac_status_clears_on_write_one() { + let rig = Rig::new(); + assert_eq!(rig.readl(reg::MAC_STATUS), 0); + rig.writel(reg::MAC_STATUS, 0x50); + assert_eq!(rig.readl(reg::MAC_STATUS), 0); + rig.writel(reg::MAC_STATUS, 0xffff_ffff); + assert_eq!(rig.readl(reg::MAC_STATUS), 0); +} + +/// MI_COMM completes at once; a successful read keeps the command and address +/// fields, replaces the data and clears Start and ReadFailed, whatever +/// reserved bits the request carried; a write without Start only stores; an +/// undefined opcode touches no PHY register and is counted. Ported from +/// test_mi_busy_clears. +#[test] +fn mi_comm_completes_reads_and_writes_by_either_path() { + for indirect in [false, true] { + let rig = Rig::new(); + let read = |off| if indirect { rig.ind_reg_read(off) } else { rig.readl(off) }; + let write = |off, val| if indirect { rig.ind_reg_write(off, val) } else { rig.writel(off, val) }; + let anar = |rig: &Rig| { + let command = MICOMM_BUSY | MICMD_READ | PHY_ADDR << 21 | 4 << 16; + if indirect { + rig.ind_reg_write(reg::MI_COMM, command); + rig.ind_reg_read(reg::MI_COMM) + } else { + rig.writel(reg::MI_COMM, command); + rig.readl(reg::MI_COMM) + } + }; + + // The discriminating full-word case: a PHY ID read keeps its fields. + for (command, result) in [(0x2822_ffff, 0x0822_0020), (0xf822_ffff, 0x0822_0020)] { + write(reg::MI_COMM, command); + assert_eq!(read(reg::MI_COMM), result); + } + + let comm = anar(&rig); + assert_eq!(comm & MICOMM_BUSY, 0); + let baseline = comm & 0xffff; + assert_eq!(comm, (MICOMM_BUSY | MICMD_READ | PHY_ADDR << 21 | 4 << 16) & MICOMM_READ_FIELDS | baseline); + + // The 10baseT half-duplex advertisement toggles. + let value = baseline ^ 0x20; + let write_command = MICMD_WRITE | PHY_ADDR << 21 | 4 << 16 | value; + write(reg::MI_COMM, write_command); + assert_eq!(read(reg::MI_COMM), write_command); + write(reg::MI_COMM, write_command); + assert_eq!(read(reg::MI_COMM), write_command); + + // Without Start nothing reaches the PHY. + assert_eq!(anar(&rig) & 0xffff, baseline); + + let id_command = MICMD_READ | PHY_ADDR << 21 | 2 << 16; + write(reg::MI_COMM, id_command); + assert_eq!(read(reg::MI_COMM), id_command); + write(reg::MI_COMM, id_command | MICOMM_BUSY); + assert_eq!(read(reg::MI_COMM), (id_command & MICOMM_READ_FIELDS) | u32::from(IDR1_VALUE)); + + write(reg::MI_COMM, write_command | MICOMM_BUSY); + assert_eq!(read(reg::MI_COMM) & MICOMM_BUSY, 0); + assert_eq!(anar(&rig) & 0xffff, value); + + // Undefined opcodes complete without a PHY access and are counted. + for opcode in [0, MICMD_READ | MICMD_WRITE] { + let command = MICOMM_BUSY | opcode | PHY_ADDR << 21 | 4 << 16 | 0x5a5a; + write(reg::MI_COMM, command); + assert_eq!(read(reg::MI_COMM), command & !MICOMM_BUSY); + assert_eq!(anar(&rig) & 0xffff, value); + } + assert_eq!(rig.refusals(Refused::MiOpcode(0)), 1); + assert_eq!(rig.refusals(Refused::MiOpcode(3)), 1); + } +} + +/// Ported from test_phy_readback: identity, a link that is down while +/// negotiating, a settle that must elapse, and MI_STS following the carrier. +#[test] +fn the_phy_negotiates_and_the_link_follows_the_settle() { + let rig = Rig::new(); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::IDR1), IDR1_VALUE); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::IDR2), IDR2_BCM5701); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::BMSR) & 4, 0); + assert_eq!(rig.readl(reg::MI_STS) & 1, 0); + + // Restarting autonegotiation negotiates again. + rig.mii_write(PHY_ADDR, phy_reg::BMCR, 0x1000 | 0x0200); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::BMSR) & 4, 0); + + rig.step(PHY_SETTLE + Duration::from_micros(1)); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::BMSR) & 4, 4); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::BMSR) & 0x20, 0x20); + assert_eq!(rig.readl(reg::MI_STS) & 1, 1); + + // A cable pulled drops the carrier without touching negotiation. + rig.dev.set_backend_link(false); + assert_eq!(rig.readl(reg::MI_STS) & 1, 0); + rig.dev.set_backend_link(true); + assert_eq!(rig.readl(reg::MI_STS) & 1, 1); +} + +/// Ported from test_phy_aux_status_link_up: never all-ones, ACOMP and the +/// 1000baseT full-duplex resolution once settled. +#[test] +fn the_phy_auxiliary_status_gates_the_link_up_path() { + let rig = Rig::new(); + let aux = rig.mii_read(PHY_ADDR, phy_reg::AUXSTS); + assert_eq!(aux & 0x8000, 0); + assert_ne!(aux, 0xffff); + rig.mii_write(PHY_ADDR, phy_reg::BMCR, 0x1000 | 0x0200); + rig.step(PHY_SETTLE + Duration::from_micros(1)); + let aux = rig.mii_read(PHY_ADDR, phy_reg::AUXSTS); + assert_eq!(aux & 0x8000, 0x8000); + assert_eq!(aux & 0x0700, 0x0700); + assert_ne!(aux, 0xffff); + // The partner advertises what this end does, and gigabit full duplex. + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::GTSR) & 0x0800, 0x0800); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::ANLPAR), rig.mii_read(PHY_ADDR, phy_reg::ANAR) | 0x4000); +} + +/// A PHY register the model lacks reads zero, never the all-ones an absent +/// PHY returns, and the access is counted. +#[test] +fn unimplemented_phy_registers_read_zero_and_are_counted() { + let rig = Rig::new(); + assert_eq!(rig.mii_read(PHY_ADDR, 0x1c), 0); + rig.mii_write(PHY_ADDR, 0x1c, 0x1234); + rig.mii_write(PHY_ADDR, phy_reg::IDR1, 0); + let refused = rig.dev.refused(); + assert!(refused.contains(&(Refused::Phy(phy::Refusal::ReadUnimplemented(0x1c)), 1))); + assert!(refused.contains(&(Refused::Phy(phy::Refusal::WriteUnimplemented(0x1c)), 1))); + assert!(refused.contains(&(Refused::Phy(phy::Refusal::WriteReadOnly(phy_reg::IDR1)), 1))); +} + +/// The station address lands in the SRAM mailbox the driver reads first. +/// Ported from test_mac_address_mailbox. +#[test] +fn the_station_address_is_in_the_sram_mailbox() { + let rig = Rig::new(); + assert_eq!(rig.ind_mem_read(0x0c14), 0x484b_5254); + assert_eq!(rig.ind_mem_read(0x0c18), 0x00ab_cdef); +} + +/// The station address is also in filter slot 0, readable a byte at a time +/// from that pair alone. Ported from test_mac_address_registers. +#[test] +fn the_station_address_is_in_filter_slot_zero_with_narrow_reads_there_only() { + let rig = Rig::new(); + assert_eq!(rig.readl(reg::MAC_ADDR_0_HIGH), 0x0000_5254); + assert_eq!(rig.readl(reg::MAC_ADDR_0_LOW), 0x00ab_cdef); + let bytes: Vec = (0..8).map(|i| rig.readb(reg::MAC_ADDR_0_HIGH + i)).collect(); + assert_eq!(bytes, [0x54, 0x52, 0, 0, 0xef, 0xcd, 0xab, 0]); + + rig.writel(reg::MAC_ADDR_0_HIGH - 4, 0x5566_7788); + rig.writel(reg::MAC_ADDR_0_LOW + 4, 0x1122_3344); + // Adjacent byte reads and halfword accesses are rejected and counted. + assert_eq!(rig.readb(reg::MAC_ADDR_0_HIGH - 1), 0); + assert_eq!(rig.readb(reg::MAC_ADDR_0_LOW + 4), 0); + assert_eq!(rig.dev.bar_read(0, reg::MAC_ADDR_0_HIGH, 2), 0); + // Narrow writes are not part of the byte-read contract. + rig.dev.bar_write(0, reg::MAC_ADDR_0_HIGH, 0, 1); + rig.dev.bar_write(0, reg::MAC_ADDR_0_LOW, 0, 2); + assert_eq!(rig.readl(reg::MAC_ADDR_0_HIGH), 0x5254); + assert_eq!(rig.readl(reg::MAC_ADDR_0_LOW), 0x00ab_cdef); + assert_eq!(rig.readl(reg::MAC_ADDR_0_HIGH - 4), 0x5566_7788); + assert_eq!(rig.readl(reg::MAC_ADDR_0_LOW + 4), 0x1122_3344); + assert!(rig.refusals(Refused::BarAccess { offset: reg::MAC_ADDR_0_HIGH, width: 1, write: true }) >= 1); + assert!(rig.refusals(Refused::BarAccess { offset: reg::MAC_ADDR_0_HIGH, width: 2, write: false }) >= 1); +} + +/// A write to a register with no modelled behaviour reads back and is counted. +#[test] +fn an_unmodelled_register_stores_and_is_counted() { + let rig = Rig::new(); + rig.writel(0x1000, 0xcafe_f00d); + assert_eq!(rig.readl(0x1000), 0xcafe_f00d); + assert_eq!(rig.refusals(Refused::UnmodelledWrite(0x1000)), 1); +} + +/// The first 256 bytes of BAR0 are the configuration registers again; a +/// MISC_CTL acknowledge there lands on the same state. +#[test] +fn bar0_aliases_the_configuration_registers() { + let rig = Rig::new(); + assert_eq!(rig.readl(0x68), 0x0105_0000); + rig.writel(0x68, 0x0000_0008); + assert_eq!(rig.cfg_read(0x68 / 4), 0x0105_0008); + rig.writel(0x7c, 0x0c14); + assert_eq!(rig.readl(0x84), rig.ind_mem_read(0x0c14)); +} + +/// PCISTATE reports conventional PCI and a quiet interrupt line, is chip +/// driven, and CLEAR_INTA self-clears. Ported from the interrupt cases of +/// test_interrupt_mailbox that need no status block. +#[test] +fn pcistate_and_the_interrupt_mailbox_start_quiet() { + let rig = Rig::new(); + assert_eq!(rig.cfg_read(0x70 / 4), 0x16); + rig.cfg_write(0x70 / 4, 0); + assert_eq!(rig.cfg_read(0x70 / 4), 0x16); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.cfg_write(0x68 / 4, 1); + assert_eq!(rig.cfg_read(0x68 / 4) & 1, 0); + assert!(!rig.irq.level()); +} + +/// A slot that drives REQ64# makes the chip report a 64-bit bus, at attach +/// and after each reset. +#[test] +fn pcistate_reports_a_64_bit_bus_in_a_req64_slot() { + let rig = Rig::with(Bcm5701Config { req64: true, ..Bcm5701Config::new(MAC) }); + assert_eq!(rig.cfg_read(0x70 / 4), 0x06); + rig.dev.state().config().reset(); + PciFunction::reset(&*rig.dev); + assert_eq!(rig.cfg_read(0x70 / 4), 0x06); + let narrow = Rig::new(); + assert_eq!(narrow.cfg_read(0x70 / 4), 0x16); +} + +fn eeprom_rig(image: &[u8], device_id: u8) -> Rig { + Rig::with(Bcm5701Config { eeprom: Some(Eeprom::from_image(device_id, image.to_vec()).unwrap()), ..Bcm5701Config::new(MAC) }) +} + +/// Ported from test_eeprom_bar_read_write: Start cannot replace an +/// unacknowledged completion, acknowledge-and-start works in one write, +/// transfers stay word aligned, writes reach only their word, and Reset wins +/// over Start. +#[test] +fn the_eeprom_controller_reads_and_writes_aligned_words() { + let path = temp_file("bar-rw", &[0x66, 0x99, 0x55, 0xaa, 0x12, 0x34, 0x56, 0x78]); + let rig = Rig::with(Bcm5701Config { eeprom: Some(Eeprom::from_file(3, path.clone()).unwrap()), ..Bcm5701Config::new(MAC) }); + let done = |rig: &Rig| rig.readl(reg::EEPROM_ADDR) & (EEPROM_COMPLETE | EEPROM_START); + + rig.writel(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START | 3 << 26); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0xaa55_9966); + assert_eq!(done(&rig), EEPROM_COMPLETE); + + // Start cannot replace an unacknowledged completion. + rig.ind_reg_write(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START | 3 << 26 | 4); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0xaa55_9966); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & EEPROM_COMPLETE, EEPROM_COMPLETE); + + // OpenBSD acknowledges the old completion with the next Start. + rig.ind_reg_write(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_COMPLETE | EEPROM_START | 3 << 26 | 4); + assert_eq!(rig.ind_reg_read(reg::EEPROM_DATA), 0x7856_3412); + assert_eq!(done(&rig), EEPROM_COMPLETE); + + // Address bits 1:0 are ignored: this acknowledges and starts nothing. + rig.writel(reg::EEPROM_DATA, 0x1357_9bdf); + rig.writel(reg::EEPROM_ADDR, EEPROM_COMPLETE); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x1357_9bdf); + assert_eq!(done(&rig), 0); + assert_eq!(std::fs::read(&path).unwrap()[4..], [0x12, 0x34, 0x56, 0x78]); + + rig.writel(reg::EEPROM_DATA, 0x89ab_cdef); + rig.ind_reg_write(reg::EEPROM_ADDR, EEPROM_START | 3 << 26 | 7); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & EEPROM_COMPLETE, EEPROM_COMPLETE); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & 3, 0); + assert_eq!(rig.readl(reg::EEPROM_DELAY), 0); + assert_eq!(std::fs::read(&path).unwrap(), [0x66, 0x99, 0x55, 0xaa, 0xef, 0xcd, 0xab, 0x89]); + + // Block reset clears the volatile state; the delay register only reads back. + rig.writel(reg::EEPROM_DELAY, 0x000a_2c2a); + rig.writel(reg::EEPROM_ADDR, EEPROM_RESET); + assert_eq!(rig.readl(reg::EEPROM_ADDR), EEPROM_RESET); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0); + assert_eq!(rig.readl(reg::EEPROM_DELAY), 0x000a_2c2a); + + // Reset wins over Start, including while the level remains set. + for data in [0x1122_3344, 0x5566_7788] { + rig.writel(reg::EEPROM_DATA, data); + rig.writel(reg::EEPROM_ADDR, EEPROM_RESET | EEPROM_START | 3 << 26 | 4); + assert_eq!(rig.readl(reg::EEPROM_ADDR), EEPROM_RESET); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0); + } + + // Chip and machine resets do not replace the external bytes. + rig.writel(reg::EEPROM_ADDR, 0); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + assert_eq!(rig.readl(reg::EEPROM_DELAY), 0); + rig.writel(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START | 3 << 26 | 4); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x89ab_cdef); + rig.dev.reset(); + rig.writel(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START | 3 << 26 | 4); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x89ab_cdef); + + // A fresh process sees the guest write in the same file. + let again = Eeprom::from_file(3, path.clone()).unwrap(); + assert_eq!(again.read_word(3, 1), Ok(0x89ab_cdef)); + std::fs::remove_file(path).unwrap(); +} + +/// Ported from test_eeprom_missing_backing: no backing means no transfer and +/// the data register is untouched. +#[test] +fn an_eeprom_command_with_no_backing_does_not_complete() { + let rig = Rig::new(); + rig.writel(reg::EEPROM_DATA, 0x7654_3210); + rig.writel(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x7654_3210); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & (EEPROM_COMPLETE | EEPROM_START), 0); + assert_eq!(rig.refusals(Refused::EepromFault(Fault::NoBacking)), 1); +} + +/// Ported from test_eeprom_read_only_backing. +#[test] +fn a_read_only_eeprom_refuses_writes() { + let mut eeprom = Eeprom::from_image(3, vec![0x66, 0x99, 0x55, 0xaa]).unwrap(); + eeprom.set_read_only(true); + assert!(eeprom.is_read_only()); + let rig = Rig::with(Bcm5701Config { eeprom: Some(eeprom), ..Bcm5701Config::new(MAC) }); + rig.writel(reg::EEPROM_DATA, 0x1234_5678); + rig.writel(reg::EEPROM_ADDR, EEPROM_START | 3 << 26); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x1234_5678); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & (EEPROM_COMPLETE | EEPROM_START), 0); + assert_eq!(rig.dev.eeprom_bytes().unwrap(), [0x66, 0x99, 0x55, 0xaa]); + assert_eq!(rig.refusals(Refused::EepromFault(Fault::ReadOnly)), 1); +} + +/// Ported from test_eeprom_bounds_and_device_id. +#[test] +fn eeprom_words_past_the_backing_and_other_device_ids_fail() { + let rig = eeprom_rig(&[0x66, 0x99, 0x55, 0xaa], 3); + rig.writel(reg::EEPROM_DATA, 0x1234_5678); + for command in [EEPROM_READ | EEPROM_START | 3 << 26 | 4, EEPROM_READ | EEPROM_START | 4 << 26] { + rig.writel(reg::EEPROM_ADDR, command); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0x1234_5678); + assert_eq!(rig.readl(reg::EEPROM_ADDR) & (EEPROM_COMPLETE | EEPROM_START), 0); + } + assert_eq!(rig.refusals(Refused::EepromFault(Fault::OutOfRange(1))), 1); + assert_eq!(rig.refusals(Refused::EepromFault(Fault::WrongDevice(4))), 1); +} + +/// Ported from test_eeprom_last_interface_word: the 64 KiB reach of the +/// address field. +#[test] +fn the_last_eeprom_interface_word_is_reachable() { + let mut image = vec![0u8; 65536]; + image[65532..].copy_from_slice(&[0x66, 0x99, 0x55, 0xaa]); + let rig = eeprom_rig(&image, 3); + rig.writel(reg::EEPROM_ADDR, EEPROM_READ | EEPROM_START | 3 << 26 | 0x3fff << 2); + assert_eq!(rig.readl(reg::EEPROM_DATA), 0xaa55_9966); + rig.writel(reg::EEPROM_DATA, 0x89ab_cdef); + rig.writel(reg::EEPROM_ADDR, EEPROM_COMPLETE | EEPROM_START | 3 << 26 | 0x3fff << 2); + assert_eq!(rig.dev.eeprom_bytes().unwrap()[65532..], [0xef, 0xcd, 0xab, 0x89]); +} + +/// Ported from test_eeprom_rejects_non_word_backing and +/// test_eeprom_id_requires_backing: the configuration is validated up front. +#[test] +fn eeprom_backings_are_validated() { + assert!(Eeprom::from_image(3, vec![1, 2, 3, 4, 5]).unwrap_err().contains("multiple of four")); + assert!(Eeprom::from_image(3, vec![]).is_err()); + assert!(Eeprom::from_image(3, vec![0; 65540]).is_err()); + assert!(Eeprom::from_image(8, vec![0; 4]).unwrap_err().contains("0..=7")); + assert!(Bcm5701::new(Bcm5701Config::new([1, 0, 0, 0, 0, 0])).is_err()); +} + +/// Direct serial-pin control is unsupported and observable, not faked. +#[test] +fn direct_eeprom_pin_control_is_refused() { + let rig = Rig::new(); + rig.writel(reg::EEPROM_PIN, 0x15); + assert_eq!(rig.readl(reg::EEPROM_PIN), 0x15); + assert_eq!(rig.refusals(Refused::EepromPins), 1); +} + +// ---- the descriptor engines ---- + +fn be32(b: &[u8], off: usize) -> u32 { + u32::from_be_bytes(b[off..off + 4].try_into().unwrap()) +} +fn put32(b: &mut [u8], off: usize, v: u32) { + b[off..off + 4].copy_from_slice(&v.to_be_bytes()); +} +/// A 16-bit descriptor field sits in the other half of its word, big-endian. +fn put16dma(b: &mut [u8], off: usize, v: u16) { + b[off ^ 2..(off ^ 2) + 2].copy_from_slice(&v.to_be_bytes()); +} +fn get16dma(b: &[u8], off: usize) -> u16 { + u16::from_be_bytes([b[off ^ 2], b[(off ^ 2) + 1]]) +} + +fn tx_bd(buf: u64, len: u16, flags: u16, mss: u16) -> [u8; 16] { + let mut bd = [0; 16]; + put32(&mut bd, 0, (buf >> 32) as u32); + put32(&mut bd, 4, buf as u32); + put16dma(&mut bd, 8, flags); + put16dma(&mut bd, 10, len); + put16dma(&mut bd, 14, mss); + bd +} + +const TXF_TCP_UDP_CKSUM: u16 = 1; +const TXF_IP_CKSUM: u16 = 2; +const TXF_END: u16 = 4; +const TXF_INSERT_SRC: u16 = 0x1000; +const TXF_CHOOSE_SRC: fn(u16) -> u16 = |n| n << 13; + +fn rx_bd(buf: u64, buf_len: u16) -> [u8; 32] { + let mut bd = [0; 32]; + put32(&mut bd, 0, (buf >> 32) as u32); + put32(&mut bd, 4, buf as u32); + put16dma(&mut bd, 8, buf_len); + bd +} + +impl Rig { + /// Send ring in memory, status block address, coalescing and the MAC on, + /// the way a driver brings the transmit side up. + fn tx_open(&self, ring_len: u32, coal_bds: u32, coal_ticks: u32) { + self.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + self.ind_mem_write(0x100, 0); + self.ind_mem_write(0x104, TX_RING as u32); + self.ind_mem_write(0x108, ring_len << 16); + self.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + self.writel(reg::HCC_TX_MAX_COAL_BDS, coal_bds); + self.writel(reg::HCC_TX_COAL_TICKS, coal_ticks); + self.writel(reg::HCC_MODE, HCC_ENABLE); + self.writel(reg::RX_MODE, RXMODE_ENABLE); + } + + fn tx_post(&self, slot: u64, bd: [u8; 16]) { + self.mem_write(TX_RING + slot * 16, &bd); + } + + /// One whole frame in one descriptor at `slot`, then its doorbell. + fn tx_send_one(&self, slot: u64, frame: &[u8], flags: u16) { + let buf = TX_BUF + slot * 0x800; + self.mem_write(buf, frame); + self.tx_post(slot, tx_bd(buf, frame.len() as u16, flags | TXF_END, 0)); + self.writel(reg::MBX_TX_PROD_LO, slot as u32 + 1); + } + + /// Receive side up: standard ring of `STD_BUF_SIZE` buffers, a return + /// ring of `ret_len`, one completion per update. + fn rx_open(&self, ret_len: u32, coal_bds: u32, coal_ticks: u32) { + self.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + self.split_addr(reg::RX_STD_RCB_HADDR_HI, reg::RX_STD_RCB_HADDR_LO, STD_RING); + self.writel(reg::RX_STD_RCB_MAXLEN_FLAGS, STD_BUF_SIZE << 16); + self.ind_mem_write(0x200, 0); + self.ind_mem_write(0x204, RET_RING as u32); + self.ind_mem_write(0x208, ret_len << 16); + self.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + self.writel(reg::HCC_RX_MAX_COAL_BDS, coal_bds); + self.writel(reg::HCC_RX_COAL_TICKS, coal_ticks); + self.writel(reg::HCC_MODE, HCC_ENABLE); + self.writel(reg::RX_MODE, RXMODE_ENABLE); + self.writel(reg::MBX_IRQ0_LO, 0); + } + + /// Post `count` standard buffers, all at `RX_BUF`. + fn rx_post_std(&self, count: u32) { + for i in 0..u64::from(count) { + self.mem_write(STD_RING + i * 32, &rx_bd(RX_BUF, 0)); + } + self.writel(reg::MBX_RX_STD_PROD_LO, count); + } + + fn rx_open_jumbo(&self) { + self.split_addr(reg::RX_JUMBO_RCB_HADDR_HI, reg::RX_JUMBO_RCB_HADDR_LO, JUMBO_RING); + self.writel(reg::RX_JUMBO_RCB_MAXLEN_FLAGS, JUMBO_BUF_SIZE << 16); + self.mem_write(JUMBO_RING, &rx_bd(JUMBO_BUF, JUMBO_BUF_SIZE as u16)); + self.writel(reg::MBX_RX_JUMBO_PROD_LO, 1); + } + + fn return_descriptor(&self, slot: u64) -> Vec { + self.mem_read(RET_RING + slot * 32, 32) + } +} + +fn broadcast(len: usize, seed: u8) -> Vec { + let mut f: Vec = (0..len).map(|i| (i as u8).wrapping_mul(seed).wrapping_add(1)).collect(); + f[..6].fill(0xff); + f +} + +/// A doorbell posts the status block with UPDATED set, big-endian, once the +/// coalescing block is enabled; disabled it posts nothing and the line stays +/// down. Ported from test_status_block_byte_order and +/// test_status_block_gated_by_hcc. +#[test] +fn the_status_block_is_posted_only_while_host_coalescing_is_enabled() { + let rig = Rig::new(); + rig.ind_mem_write(0x108, TX_RING_LEN << 16); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + rig.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + rig.writel(reg::MBX_IRQ0_LO, 0); + + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert_eq!(be32(&rig.status_block(), 0), 0); + assert!(!rig.line_up()); + + rig.writel(reg::HCC_MODE, HCC_ENABLE); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert_eq!(rig.status_block()[..4], [0, 0, 0, 1]); + assert!(rig.line_up()); + assert!(rig.irq.level()); +} + +/// With ENDIAN_WORDSWAP set, a 32-bit access at +0 of an eight-byte field +/// reaches its more significant word, so the big-endian driver writes each +/// host address high word first and the pair composes with no exchange. An +/// exchange on top sends the status block to the low word shifted into the +/// high half, and IRIX's tg0 then never sees a transmit completion or a +/// received frame. Ported from test_status_block_hostaddr_wordswap. +#[test] +fn host_addresses_compose_high_word_first_under_the_word_swap() { + let intended = 0x80_0000u64; + let rig = Rig::new(); + rig.ind_mem_write(0x108, TX_RING_LEN << 16); + rig.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + rig.cfg_write(0x68 / 4, rig.cfg_read(0x68 / 4) | misc_ctl::ENDIAN_WORDSWAP); + rig.writel(reg::HCC_STATUSBLK_ADDR_HI, (intended >> 32) as u32); + rig.writel(reg::HCC_STATUSBLK_ADDR_LO, intended as u32); + rig.writel(reg::HCC_MODE, HCC_ENABLE); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert_eq!(be32(&rig.mem_read(intended, STATUS_BLK_SIZE), 0), 1); +} + +/// Ported from test_transmit_round_trip: the descriptor and its buffer are +/// read, the frame reaches the backend byte for byte, and the status block +/// reports the consumer advanced, in the chip's image order. +#[test] +fn a_transmitted_frame_reaches_the_backend_and_the_status_block_follows() { + let mut rig = Rig::new(); + let frame: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + rig.tx_open(TX_RING_LEN, 1, 0); + rig.tx_send_one(0, &frame, 0); + assert_eq!(rig.sent_frames(), [frame]); + let sb = rig.status_block(); + assert_eq!(be32(&sb, 0), 1); + assert_eq!(get16dma(&sb, 18), 1); + // A consumer of 1 at the field's offset xor 2, big-endian. + assert_eq!(sb[16..18], [0, 1]); +} + +/// With no swap controls the chip's image reaches the bus permuted within each +/// doubleword; the frame still arrives intact. Derived from the lane rule the +/// model documents (PG105 Tables 102-109), not a port. +#[test] +fn the_swap_controls_permute_the_bytes_within_each_doubleword() { + for (mode, xor) in [(0u32, 7usize), (0x10, 4), (0x20, 3), (0x30, 0)] { + let mut rig = Rig::new(); + rig.tx_open(TX_RING_LEN, 1, 0); + rig.ind_reg_write(reg::MODE_CTL, mode); + let frame: Vec = (0..64u8).map(|i| i.wrapping_mul(5) ^ 0x21).collect(); + let lanes = |bytes: &[u8], xor: usize| { + let mut out = vec![0; bytes.len()]; + for (i, b) in bytes.iter().enumerate() { + out[(i & !7) + ((i & 7) ^ xor)] = *b; + } + out + }; + // Descriptors take the non-frame controls, which are off here. + rig.mem_write(TX_RING, &lanes(&tx_bd(TX_BUF, 64, TXF_END, 0), 7)); + rig.mem_write(TX_BUF, &lanes(&frame, xor)); + rig.writel(reg::MBX_TX_PROD_LO, 1); + assert_eq!(rig.sent_frames(), [frame], "frame-data control {mode:#x}"); + // The status block is written through the same non-frame lanes. + let sb = lanes(&rig.status_block(), 7); + assert_eq!(be32(&sb, 0), 1); + } +} + +/// A transmit descriptor that cannot be read leaves the ring where it was and +/// the doorbell is retried later. Ported from test_transmit_descriptor_dma_error. +#[test] +fn an_unreadable_send_ring_leaves_the_frame_for_a_retry() { + let mut rig = Rig::new(); + let frame: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + rig.tx_open(TX_RING_LEN, 0, 0); + rig.ind_mem_write(0x100, (UNMAPPED >> 32) as u32); + rig.ind_mem_write(0x104, UNMAPPED as u32); + rig.writel(reg::MBX_TX_PROD_LO, 1); + assert!(rig.sent_frames().is_empty()); + assert_eq!(rig.status_tag(), 0); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::TxDescriptor)), 1); + + rig.ind_mem_write(0x104, TX_RING as u32); + rig.mem_write(TX_BUF, &frame); + rig.tx_post(0, tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 1); + assert_eq!(rig.sent_frames(), [frame]); +} + +/// A later fragment's unreadable payload rolls the whole frame back, and an +/// earlier completed frame stays completed. Ported from +/// test_transmit_payload_dma_error and test_transmit_complete_then_fragment_dma_error_retry. +#[test] +fn an_unreadable_fragment_rolls_back_to_the_start_of_its_frame() { + let mut rig = Rig::new(); + let first: Vec = (0..60u8).map(|i| i + 1).collect(); + let second: Vec = (0..60u8).map(|i| i + 100).collect(); + rig.tx_open(8, 1, 0); + rig.mem_write(TX_BUF, &first); + rig.tx_post(0, tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.mem_write(TX_BUF + 0x100, &second[..28]); + rig.tx_post(1, tx_bd(TX_BUF + 0x100, 28, 0, 0)); + rig.tx_post(2, tx_bd(UNMAPPED, 32, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 3); + // The first frame went and is reported; the second waits. + assert_eq!(rig.sent_frames(), [first]); + assert_eq!(rig.status_tx_cons(), 1); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::TxPayload)), 1); + + rig.mem_write(TX_BUF + 0x200, &second[28..]); + rig.tx_post(2, tx_bd(TX_BUF + 0x200, 32, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 3); + assert_eq!(rig.sent_frames(), [second]); + assert_eq!(rig.status_tx_cons(), 3); +} + +/// Ported from test_tx_producer_out_of_range: an index past the ring is +/// refused, observably, and nothing is walked. +#[test] +fn a_transmit_producer_past_the_ring_is_refused() { + let rig = Rig::new(); + rig.ind_mem_write(0x108, TX_RING_LEN << 16); + rig.writel(reg::MBX_TX_PROD_LO, TX_RING_LEN); + rig.writel(reg::MBX_TX_PROD_LO, 0xffff); + assert_eq!(rig.refusals(Refused::ProducerOutOfRange(Mailbox::TxProducer)), 2); + assert_eq!(rig.mii_read(PHY_ADDR, phy_reg::IDR1), IDR1_VALUE); +} + +/// Ported from test_transmit_zero_length_descriptor: nothing goes out, the +/// descriptor still completes. +#[test] +fn a_zero_length_descriptor_completes_without_a_frame() { + let mut rig = Rig::new(); + rig.tx_open(TX_RING_LEN, 1, 0); + rig.tx_post(0, tx_bd(TX_BUF, 0, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 1); + assert_eq!(be32(&rig.status_block(), 0), 1); + assert_eq!(rig.status_tx_cons(), 1); + assert!(rig.sent_frames().is_empty()); +} + +/// Ported from test_transmit_source_address: the descriptor picks one of four +/// filter slots to replace the frame's source, only when it asks to. +#[test] +fn a_descriptor_can_replace_the_source_address_from_a_filter_slot() { + let mut rig = Rig::new(); + rig.tx_open(8, 0, 0); + let slots = [(0x210u32, 0x2030_4050u32), (0x211, 0x2131_4151), (0x212, 0x2232_4252), (0x213, 0x2333_4353)]; + for (i, (hi, lo)) in slots.iter().enumerate() { + rig.writel(reg::MAC_ADDR_0_HIGH + i as u32 * reg::MAC_ADDR_STRIDE, *hi); + rig.writel(reg::MAC_ADDR_0_LOW + i as u32 * reg::MAC_ADDR_STRIDE, *lo); + } + let frame: Vec = (0..60u8).map(|i| 0x80 + i).collect(); + let with_source = |slot: usize| { + let mut f = frame.clone(); + f[6..8].copy_from_slice(&(slots[slot].0 as u16).to_be_bytes()); + f[8..12].copy_from_slice(&slots[slot].1.to_be_bytes()); + f + }; + for (i, selector) in [2u16, 0, 3, 1].into_iter().enumerate() { + rig.tx_send_one(i as u64, &frame, TXF_INSERT_SRC | TXF_CHOOSE_SRC(selector)); + assert_eq!(rig.sent_frames(), [with_source(usize::from(selector))]); + } + // A selector without INSERT leaves the frame untouched. + rig.tx_send_one(4, &frame, TXF_CHOOSE_SRC(3)); + assert_eq!(rig.sent_frames(), std::slice::from_ref(&frame)); + // Split after the first source bytes: both fragments carry the flags. + rig.mem_write(TX_BUF + 5 * 0x800, &frame[..8]); + rig.mem_write(TX_BUF + 6 * 0x800, &frame[8..]); + let flags = TXF_INSERT_SRC | TXF_CHOOSE_SRC(2); + rig.tx_post(5, tx_bd(TX_BUF + 5 * 0x800, 8, flags, 0)); + rig.tx_post(6, tx_bd(TX_BUF + 6 * 0x800, 52, flags | TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 7); + assert_eq!(rig.sent_frames(), [with_source(2)]); +} + +fn lso_frame(payload: usize) -> Vec { + let mut f = vec![0u8; 54 + payload]; + f[..14].copy_from_slice(&[0x52, 0x54, 0, 0x11, 0x22, 0x33, 0x52, 0x54, 0, 0xbc, 0x57, 1, 8, 0]); + f[14] = 0x45; + f[16..18].copy_from_slice(&((40 + payload) as u16).to_be_bytes()); + f[18..20].copy_from_slice(&0x1234u16.to_be_bytes()); + f[22] = 64; + f[23] = 6; + f[26..30].copy_from_slice(&[10, 0, 2, 15]); + f[30..34].copy_from_slice(&[10, 0, 2, 2]); + f[34..36].copy_from_slice(&0x3039u16.to_be_bytes()); + f[36..38].copy_from_slice(&0x0017u16.to_be_bytes()); + f[38..42].copy_from_slice(&0x1000u32.to_be_bytes()); + f[46] = 0x50; + f[47] = 0x19; + f[48..50].copy_from_slice(&0x2000u16.to_be_bytes()); + for (i, b) in f[54..].iter_mut().enumerate() { + *b = i as u8; + } + f +} + +fn inet_sum(buf: &[u8], seed: u32) -> u16 { + let mut sum = seed; + for pair in buf.chunks(2) { + sum += u32::from(pair[0]) << 8 | u32::from(*pair.get(1).unwrap_or(&0)); + } + while sum >> 16 != 0 { + sum = (sum & 0xffff) + (sum >> 16); + } + !(sum as u16) +} + +fn tcp_pseudo(frame: &[u8], tcp_len: usize) -> u32 { + frame[26..34].chunks(2).map(|p| u32::from(p[0]) << 8 | u32::from(p[1])).sum::() + 6 + tcp_len as u32 +} + +/// Ported from test_transmit_segmentation: one buffer of three segments goes +/// out as three frames with their own headers, sequence numbers, identifiers +/// and checksums, and the finishing flags only on the last. +#[test] +fn an_oversized_tcp_buffer_is_cut_into_segments() { + const MSS: usize = 512; + let mut rig = Rig::new(); + let frame = lso_frame(MSS * 3); + rig.tx_open(TX_RING_LEN, 0, 0); + rig.mem_write(TX_BUF, &frame); + rig.tx_post(0, tx_bd(TX_BUF, frame.len() as u16, 0x7000 | TXF_END, MSS as u16)); + rig.writel(reg::MBX_TX_PROD_LO, 1); + let sent = rig.sent_frames(); + assert_eq!(sent.len(), 3); + for (i, got) in sent.iter().enumerate() { + let mut expected = frame[..54].to_vec(); + expected.extend_from_slice(&frame[54 + i * MSS..54 + (i + 1) * MSS]); + expected[16..18].copy_from_slice(&((40 + MSS) as u16).to_be_bytes()); + expected[18..20].copy_from_slice(&(0x1234 + i as u16).to_be_bytes()); + expected[38..42].copy_from_slice(&(0x1000 + (i * MSS) as u32).to_be_bytes()); + if i < 2 { + expected[47] &= !0x09; + } + expected[24..26].fill(0); + expected[50..52].fill(0); + let ip = inet_sum(&expected[14..34], 0); + expected[24..26].copy_from_slice(&ip.to_be_bytes()); + let tcp = inet_sum(&expected[34..], tcp_pseudo(&expected, 20 + MSS)); + expected[50..52].copy_from_slice(&tcp.to_be_bytes()); + assert_eq!(got, &expected, "segment {i}"); + } +} + +/// A frame no larger than one segment, or one that is not plain IPv4 TCP, is +/// sent whole. Ported from test_transmit_no_segmentation. +#[test] +fn a_frame_that_needs_no_segmentation_is_sent_whole() { + let mut rig = Rig::new(); + rig.tx_open(TX_RING_LEN, 0, 0); + let small = lso_frame(100); + rig.mem_write(TX_BUF, &small); + rig.tx_post(0, tx_bd(TX_BUF, small.len() as u16, TXF_END, 512)); + rig.writel(reg::MBX_TX_PROD_LO, 1); + assert_eq!(rig.sent_frames(), [small]); +} + +fn cksum_frame(ihl_byte: u8) -> Vec { + let mut f = vec![0u8; 62]; + f[..14].copy_from_slice(&[0x52, 0x54, 0, 0x11, 0x22, 0x33, 0x52, 0x54, 0, 0xbc, 0x57, 1, 8, 0]); + f[14] = ihl_byte; + f[17] = 48; + f[22] = 64; + f[23] = 6; + f[26..30].copy_from_slice(&[10, 0, 2, 15]); + f[30..34].copy_from_slice(&[10, 0, 2, 2]); + f[34..36].copy_from_slice(&[0x30, 0x39]); + f[37] = 0x17; + f[46] = 0x50; + f[47] = 0x02; + f[48] = 0x20; + for (i, b) in f.iter_mut().enumerate().skip(54) { + *b = i as u8; + } + f +} + +/// Ported from test_transmit_checksum_offload: a driver that leaves the +/// checksum fields for the card gets them filled. +#[test] +fn the_card_fills_in_the_checksums_the_driver_asked_for() { + let mut rig = Rig::new(); + let frame = cksum_frame(0x45); + rig.tx_open(TX_RING_LEN, 0, 0); + rig.tx_send_one(0, &frame, TXF_IP_CKSUM | TXF_TCP_UDP_CKSUM); + let got = rig.sent_frames().remove(0); + let ip = inet_sum(&frame[14..34], 0); + let tcp = inet_sum(&frame[34..], tcp_pseudo(&frame, 28)); + assert_eq!(u16::from_be_bytes([got[24], got[25]]), ip); + assert_eq!(u16::from_be_bytes([got[50], got[51]]), tcp); + assert_ne!(ip, 0); + assert_ne!(tcp, 0); + assert_eq!(got[..24], frame[..24]); +} + +/// A header the offload cannot trust is declined, not walked. Ported from +/// test_transmit_checksum_bad_header. +#[test] +fn a_checksum_offload_on_a_malformed_header_is_declined() { + let mut rig = Rig::new(); + let frame = cksum_frame(0x44); + rig.tx_open(TX_RING_LEN, 0, 0); + rig.tx_send_one(0, &frame, TXF_IP_CKSUM | TXF_TCP_UDP_CKSUM); + assert_eq!(rig.sent_frames(), [frame]); +} + +/// One update per group of descriptors, the tick budget reporting a short +/// group, a zero budget never doing so. Ported from test_transmit_coalescing, +/// test_transmit_coalescing_timer and test_transmit_coalescing_zero_ticks. +#[test] +fn transmit_updates_follow_the_count_and_the_tick_budget() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + rig.tx_open(64, 4, 0x1000_0000); + for i in 0..8 { + rig.tx_send_one(i, &frame, 0); + } + assert_eq!(rig.sent_frames().len(), 8); + // Eight frames in groups of four are two updates, not eight. + assert_eq!(rig.status_tag(), 2); + + // A short group waits for the budget. + let rig = Rig::new(); + rig.tx_open(64, 8, 50); + for i in 0..3 { + rig.tx_send_one(i, &frame, 0); + } + assert_eq!(rig.status_tag(), 0); + rig.step(Duration::from_micros(49)); + assert_eq!(rig.status_tag(), 0); + rig.step(Duration::from_micros(1)); + assert_eq!(rig.status_tag(), 1); + assert_eq!(rig.status_tx_cons(), 3); + + // Zero disables the timer: only the count reports. + let mut rig = Rig::new(); + rig.tx_open(64, 2, 0); + rig.tx_send_one(0, &frame, 0); + rig.step(Duration::from_secs(1)); + assert_eq!(rig.status_tag(), 0); + rig.tx_send_one(1, &frame, 0); + assert_eq!(rig.status_tag(), 1); + assert_eq!(rig.status_tx_cons(), 2); + assert_eq!(rig.sent_frames().len(), 2); +} + +/// Empty and dropped frames still complete every descriptor they consume. +/// Ported from test_transmit_coalescing_empty_and_dropped. +#[test] +fn empty_and_oversize_frames_still_complete_their_descriptors() { + let mut rig = Rig::new(); + let frame = broadcast(64, 5); + rig.tx_open(64, 4, 0x1000_0000); + rig.mem_write(TX_BUF, &frame); + rig.mem_fill(TX_BUF + 0x1000, 9600, 0xa5); + rig.tx_post(0, tx_bd(TX_BUF, 64, TXF_END, 0)); + rig.tx_post(1, tx_bd(0, 0, TXF_END, 0)); + rig.tx_post(2, tx_bd(TX_BUF + 0x1000, 9600, 0, 0)); + rig.tx_post(3, tx_bd(TX_BUF + 0x4000, 1, TXF_END, 0)); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.writel(reg::MBX_TX_PROD_LO, 4); + // Only the first is a frame: the oversize pair is dropped whole. + assert_eq!(rig.sent_frames(), [frame]); + assert_eq!(rig.status_tx_cons(), 4); + assert_eq!(rig.status_tag(), 1); + assert!(rig.line_up()); + assert_eq!(rig.refusals(Refused::TxFrameTooLarge), 1); +} + +/// A doorbell that ends inside a frame leaves every fragment to retry; a +/// completed frame earlier is not rolled back. Ported from +/// test_transmit_split_doorbell and test_transmit_complete_then_split_doorbell. +#[test] +fn a_doorbell_that_ends_inside_a_frame_waits_for_the_rest() { + let mut rig = Rig::new(); + let first: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + let second: Vec = (0..60u8).map(|i| i.wrapping_mul(3).wrapping_add(9)).collect(); + rig.tx_open(8, 1, 0x1000_0000); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(TX_BUF, &first); + rig.tx_post(0, tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.mem_write(TX_BUF + 0x100, &second[..28]); + rig.mem_write(TX_BUF + 0x200, &second[28..]); + rig.tx_post(1, tx_bd(TX_BUF + 0x100, 28, 0, 0)); + rig.tx_post(2, tx_bd(TX_BUF + 0x200, 32, TXF_END, 0)); + // The doorbell stops after the first fragment of the second frame. + rig.writel(reg::MBX_TX_PROD_LO, 2); + assert_eq!(rig.sent_frames(), [first]); + assert_eq!(rig.status_tx_cons(), 1); + let tag = rig.status_tag(); + rig.writel(reg::MBX_TX_PROD_LO, 3); + assert_eq!(rig.sent_frames(), [second]); + assert_eq!(rig.status_tx_cons(), 3); + assert_eq!(rig.status_tag(), tag + 1); +} + +/// A status DMA the guest's address refused is retried when the address is +/// repaired. Ported from test_transmit_coalescing_status_dma_retry. +#[test] +fn a_refused_status_update_is_retried_by_the_budget_timer() { + let mut rig = Rig::new(); + let frame = broadcast(64, 9); + rig.tx_open(64, 1, 50); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, UNMAPPED); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.tx_send_one(0, &frame, 0); + assert_eq!(rig.sent_frames(), [frame]); + assert_eq!(rig.status_tx_cons(), 0); + assert!(!rig.line_up()); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::StatusBlock)), 1); + + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + rig.step(Duration::from_micros(51)); + assert_eq!(rig.status_tx_cons(), 1); + assert_eq!(rig.status_tag(), 1); + assert!(rig.line_up()); +} + +/// Ported from test_interrupt_mailbox and test_mailbox_nonzero_masks: the +/// mailbox masks and acknowledges, MISC_CTL's global mask holds the line down +/// without losing the update, and any nonzero value masks. +#[test] +fn the_interrupt_line_follows_the_mailbox_and_the_global_mask() { + let rig = Rig::new(); + rig.ind_mem_write(0x108, TX_RING_LEN << 16); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + rig.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + rig.writel(reg::HCC_MODE, HCC_ENABLE); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + + // Masked out of reset: an update posts, but the line stays down. + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(!rig.line_up()); + // Enable interrupts; the next update asserts the line. + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(rig.line_up() && rig.irq.level()); + // Write 1: masks and acknowledges. + rig.writel(reg::MBX_IRQ0_LO, 1); + assert!(!rig.line_up() && !rig.irq.level()); + // An update posted while masked is discarded by the zero write. + rig.writel(reg::MBX_TX_PROD_LO, 0); + rig.writel(reg::MBX_IRQ0_LO, 0); + assert!(!rig.line_up()); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(rig.line_up()); + rig.writel(reg::MBX_IRQ0_LO, 0); + assert!(!rig.line_up()); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(rig.line_up()); + + // The global mask suppresses the line; the update was never taken. + let misc = rig.cfg_read(0x68 / 4); + rig.cfg_write(0x68 / 4, misc | 2); + assert!(!rig.line_up() && !rig.irq.level()); + rig.cfg_write(0x68 / 4, misc); + assert!(rig.line_up() && rig.irq.level()); + // CLEAR_INTA acknowledges and self-clears. + rig.cfg_write(0x68 / 4, misc | 1); + assert!(!rig.line_up()); + assert_eq!(rig.cfg_read(0x68 / 4), misc); + + // Any nonzero In_ISR value masks while the handler runs. + rig.writel(reg::MBX_IRQ0_LO, 2); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(!rig.line_up()); + rig.writel(reg::MBX_IRQ0_LO, 0); + assert!(!rig.line_up()); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(rig.line_up()); +} + +/// A failed status write advances neither its tag nor the interrupt state. +/// Ported from test_status_dma_error. +#[test] +fn a_failed_status_write_advances_nothing() { + let rig = Rig::new(); + rig.ind_mem_write(0x108, TX_RING_LEN << 16); + rig.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, UNMAPPED); + rig.writel(reg::HCC_MODE, HCC_ENABLE); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert!(!rig.line_up()); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + rig.writel(reg::MBX_TX_PROD_LO, 0); + assert_eq!(rig.status_tag(), 1); + assert!(rig.line_up()); +} + +/// Ported from test_receive_round_trip: the frame lands in the posted buffer, +/// the return descriptor and status block describe it in the chip's image +/// order, and a second buffer exposes the next consumer. +#[test] +fn a_received_frame_lands_in_the_posted_buffer_with_its_completion() { + let mut rig = Rig::new(); + let frame = broadcast(64, 5); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + let ret = rig.return_descriptor(0); + assert_eq!(get16dma(&ret, 0x0a), 0); + assert_eq!(get16dma(&ret, 0x08), 64 + 4); + assert_eq!(get16dma(&ret, 0x0c) & 4, 4); + assert_eq!(ret[0x0a..0x0c], [0, 0x44]); + let sb = rig.status_block(); + assert_eq!(get16dma(&sb, 16), 1); + assert_eq!(sb[8..12], [0, 1, 0, 0]); + assert_eq!(sb[18..20], [0, 1]); + + // The return-ring acknowledgement is accepted; reposting exposes the + // next consumer. + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.mem_write(STD_RING + 32, &rx_bd(RX_BUF + 0x1000, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 2); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.status_block()[8..10], [0, 2]); +} + +/// The return descriptor is written when the frame lands; the producer the +/// status block reports, and the interrupt, wait for the arrival deadline. +/// Ported from test_receive_waits_for_its_arrival_deadline. +#[test] +fn a_completion_waits_for_its_arrival_deadline() { + let mut rig = Rig::new(); + let frame = broadcast(64, 7); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.deliver(&frame); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + assert_eq!(get16dma(&rig.return_descriptor(0), 0x0c) & 4, 4); + assert_eq!(rig.status_rx_prod(), 0); + assert!(!rig.line_up()); + rig.step(ARRIVAL - Duration::from_nanos(1)); + assert_eq!(rig.status_rx_prod(), 0); + assert!(!rig.line_up()); + rig.step(Duration::from_nanos(1)); + assert_eq!(rig.status_rx_prod(), 1); + assert!(rig.line_up()); +} + +/// A frame that arrives after a doorbell is not reported until the +/// processor that rang it has run the cycle floor past it, however much host +/// time has gone by: host time passes while the guest is descheduled or slow, +/// and a reply reported while its sender is still in the send path is lost to +/// it. A frame from before any doorbell waits only for its deadline. +#[test] +fn a_reply_waits_for_the_sending_processor_to_run_on() { + const FLOOR: u64 = 1_000; + // The processor's cycle counter, which outlives the device. + let word: *mut u64 = Box::leak(Box::new(5_000u64)); + let counter = crate::cpu::mips_core::CyclesPtr::new(word); + let mut rig = Rig::with(Bcm5701Config { rx_arrival_cycles: FLOOR, ..Bcm5701Config::new(MAC) }); + rig.dev.set_cpu_cycles(vec![counter]); + rig.tx_open(TX_RING_LEN, 1, 0); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(2); + + rig.deliver(&broadcast(64, 3)); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1, "no doorbell yet: host time alone"); + + crate::cpu::smp::set_current_cpu(0); + rig.tx_send_one(0, &broadcast(64, 5), 0); + crate::cpu::smp::set_current_cpu(u8::MAX); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.deliver(&broadcast(64, 7)); + rig.step(ARRIVAL * 10); + assert_eq!(rig.status_rx_prod(), 1, "the sender has not run on"); + // SAFETY: the leaked word is live for the process and only this thread + // touches it. + unsafe { word.write_volatile(5_000 + FLOOR - 1) }; + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1, "one cycle short"); + unsafe { word.write_volatile(5_000 + FLOOR) }; + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); +} + +/// Ported from test_receive_jumbo_frame: a frame beyond the standard buffer +/// goes to the jumbo ring and is marked so. +#[test] +fn a_frame_too_big_for_the_standard_buffer_takes_the_jumbo_ring() { + let mut rig = Rig::new(); + let frame = broadcast(9000, 7); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.rx_open_jumbo(); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + let ret = rig.return_descriptor(0); + assert_eq!(get16dma(&ret, 0x08), 9004); + assert_eq!(get16dma(&ret, 0x0c) & 0x20, 0x20); + assert_eq!(rig.mem_read(JUMBO_BUF, 9000), frame); + // Both consumers report what the chip fetched: the unused standard + // buffer as well as the used jumbo one. + let sb = rig.status_block(); + assert_eq!(sb[8..12], [0, 1, 0, 1]); +} + +/// Post more standard buffers than the chip's cache holds, the way a driver +/// fills its ring at start. +fn rx_post_std_many(rig: &Rig, count: u32) { + for i in 0..u64::from(count) { + rig.mem_write(STD_RING + i * 32, &rx_bd(RX_BUF + (i % 8) * 0x800, 0)); + } + rig.writel(reg::MBX_RX_STD_PROD_LO, count); +} + +/// The status block's standard consumer is the next descriptor the chip +/// fetches, not the next a frame uses: it runs ahead of the frames by as +/// much as the chip's 128-descriptor cache, and never past the producer. +#[test] +fn the_standard_consumer_reports_the_fetch_not_the_frames() { + let mut rig = Rig::new(); + let frame = broadcast(64, 9); + rig.rx_open(16, 1, 0); + rx_post_std_many(&rig, 300); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + // One used, its cache entry refilled: 1 + 128. + assert_eq!(rig.status_std_cons(), 129); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_std_cons(), 130); + + // Fewer posted than the cache holds: all of them, before any frame. + let mut rig = Rig::new(); + rig.rx_open(16, 1, 0); + rx_post_std_many(&rig, 10); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_std_cons(), 10); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); +} + +/// The jumbo consumer is the fetch index as well, against a 64-descriptor +/// cache. +#[test] +fn the_jumbo_consumer_reports_the_fetch_not_the_frames() { + let mut rig = Rig::new(); + let frame = broadcast(9000, 7); + rig.rx_open(16, 1, 0); + rig.split_addr(reg::RX_JUMBO_RCB_HADDR_HI, reg::RX_JUMBO_RCB_HADDR_LO, JUMBO_RING); + rig.writel(reg::RX_JUMBO_RCB_MAXLEN_FLAGS, JUMBO_BUF_SIZE << 16); + for i in 0..100 { + rig.mem_write(JUMBO_RING + i * 32, &rx_bd(JUMBO_BUF, JUMBO_BUF_SIZE as u16)); + } + rig.writel(reg::MBX_RX_JUMBO_PROD_LO, 100); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.status_jumbo_cons(), 65); + assert_eq!(rig.mem_read(JUMBO_BUF, 9000), frame); +} + +/// The chip starts a fetch only once the host has made the replenish +/// threshold's worth of descriptors available; until then a frame finds no +/// buffer and waits, though some are posted. +#[test] +fn a_fetch_waits_for_the_replenish_threshold() { + let mut rig = Rig::new(); + let frame = broadcast(64, 21); + rig.rx_open(16, 1, 0); + rig.writel(reg::RX_STD_REPLENISH_THRESH, 25); + assert_eq!(rig.refusals(Refused::UnmodelledWrite(reg::RX_STD_REPLENISH_THRESH)), 0); + rx_post_std_many(&rig, 20); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.waiting(), 1); + rx_post_std_many(&rig, 25); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + assert_eq!(rig.status_std_cons(), 25); + + // Lowering the threshold lets the posted buffers in without another + // producer write. + let mut rig = Rig::new(); + rig.rx_open(16, 1, 0); + rig.writel(reg::RX_STD_REPLENISH_THRESH, 25); + rx_post_std_many(&rig, 20); + rig.deliver(&frame); + assert_eq!(rig.waiting(), 1); + rig.writel(reg::RX_STD_REPLENISH_THRESH, 1); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.status_std_cons(), 20); +} + +/// With the standard ring empty, a standard-size frame is discarded and +/// counted rather than refused, while a jumbo buffer is posted; the next jumbo +/// frame still gets through. Ported from +/// test_receive_drops_standard_with_empty_standard_ring. +#[test] +fn a_short_frame_with_no_standard_buffer_is_discarded_and_counted() { + let mut rig = Rig::new(); + let first = broadcast(64, 3); + let second = broadcast(64, 11); + let big = broadcast(9000, 13); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.rx_open_jumbo(); + rig.mem_write(STD_RING + 32, &rx_bd(STD_STALE_BUF, 0)); + assert_eq!(rig.nic_no_more_rxbds(), 0); + rig.deliver(&first); + rig.step(ARRIVAL); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.mem_fill(STD_STALE_BUF, 64, 0xa5); + rig.mem_fill(JUMBO_BUF, 64, 0xa5); + rig.deliver(&second); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(STD_STALE_BUF, 64), vec![0xa5; 64]); + assert_eq!(rig.mem_read(JUMBO_BUF, 64), vec![0xa5; 64]); + assert_eq!(rig.nic_no_more_rxbds(), 1); + assert_eq!(rig.refusals(Refused::RxShortage(Ring::Standard)), 1); + // The jumbo ring still takes a jumbo frame. + rig.deliver(&big); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.mem_read(JUMBO_BUF, 9000), big); + // Posting the standard buffer afterwards does not resurrect the frame. + rig.writel(reg::MBX_RX_STD_PROD_LO, 2); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.nic_no_more_rxbds(), 1); +} + +/// The mirror: a jumbo frame with the jumbo ring empty. Ported from +/// test_receive_drops_oversize_with_empty_jumbo_ring. +#[test] +fn a_jumbo_frame_with_no_jumbo_buffer_is_discarded_and_counted() { + let mut rig = Rig::new(); + let first = broadcast(9000, 3); + let second = broadcast(9000, 11); + let small = broadcast(64, 15); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(2); + rig.rx_open_jumbo(); + rig.mem_write(JUMBO_RING + 32, &rx_bd(0x28_0000, JUMBO_BUF_SIZE as u16)); + rig.deliver(&first); + rig.step(ARRIVAL); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.mem_fill(0x28_0000, 9000, 0xa5); + rig.deliver(&second); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(0x28_0000, 9000), vec![0xa5; 9000]); + assert_eq!(rig.nic_no_more_rxbds(), 1); + assert_eq!(rig.refusals(Refused::RxShortage(Ring::Jumbo)), 1); + rig.deliver(&small); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.mem_read(RX_BUF, 64), small); + assert_eq!(get16dma(&rig.return_descriptor(1), 0x0c) & 0x20, 0); +} + +/// A frame too big for a standard buffer with the jumbo ring disabled is +/// dropped, observably. Ported from test_receive_oversize_dropped. +#[test] +fn an_oversize_frame_with_no_jumbo_ring_is_dropped_and_counted() { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.mem_fill(RX_BUF, 2048, 0xa5); + rig.deliver(&broadcast(2000, 3)); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.mem_read(RX_BUF, 2048), vec![0xa5; 2048]); + assert_eq!(rig.refusals(Refused::RxNoRoom(Ring::Standard)), 1); + // The frame was consumed, not left to wedge the queue. + assert_eq!(rig.waiting(), 0); + // And one over the model's limit is dropped too. + rig.deliver(&broadcast(9700, 3)); + assert_eq!(rig.refusals(Refused::RxFrameTooLarge), 1); +} + +/// Ported from test_receive_mac_enable: a halted receiver drops what it is +/// offered; with no buffer the frame waits for the guest to make room. +#[test] +fn a_halted_receiver_drops_and_a_full_one_waits() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.writel(reg::RX_MODE, 0); + rig.deliver(&frame); + assert_eq!(rig.waiting(), 0); + assert_eq!(rig.status_rx_prod(), 0); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.deliver(&frame); + // The one posted buffer is used; the next frame waits at the head. + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.waiting(), 1); + rig.mem_write(STD_RING + 32, &rx_bd(RX_BUF + 0x1000, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 2); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.dev.poll(); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.mem_read(RX_BUF + 0x1000, 64), frame); +} + +/// The address filter is slot 0, which the driver programs: other unicast +/// frames are consumed without a completion. Ported from the filter cases of +/// test_receive_checksum_case's setup and test_receive_round_trip. +#[test] +fn the_receive_filter_follows_the_programmed_station_address() { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + let mut other = broadcast(64, 3); + other[..6].copy_from_slice(&[2, 0, 0, 0, 0, 9]); + rig.deliver(&other); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.waiting(), 0); + let mut mine = other.clone(); + mine[..6].copy_from_slice(&MAC); + rig.deliver(&mine); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + // IRIX installs its own address over the card's. + rig.writel(reg::MAC_ADDR_0_HIGH, 0x0200); + rig.writel(reg::MAC_ADDR_0_LOW, 9); + rig.mem_write(STD_RING + 32, &rx_bd(RX_BUF + 0x1000, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 2); + rig.writel(reg::MBX_RX_CONS0_LO, 1); + rig.deliver(&other); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); +} + +/// A runt is padded to the Ethernet minimum before it is delivered. +#[test] +fn a_short_frame_is_padded_to_the_ethernet_minimum() { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + let mut runt = vec![0xff; 6]; + runt.extend_from_slice(&[1, 2, 3, 4]); + rig.deliver(&runt); + rig.step(ARRIVAL); + let mut padded = runt.clone(); + padded.resize(60, 0); + assert_eq!(rig.mem_read(RX_BUF, 60), padded); + assert_eq!(get16dma(&rig.return_descriptor(0), 0x08), 64); +} + +fn rx_frame(proto: u8, payload: &[u8], fix: impl Fn(&mut Vec)) -> Vec { + let mut f = vec![0xff; 6]; + f.extend_from_slice(&[0x52, 0x54, 0, 0xbc, 0x57, 1, 8, 0]); + let total = 20 + payload.len(); + f.extend_from_slice(&[0x45, 0]); + f.extend_from_slice(&(total as u16).to_be_bytes()); + f.extend_from_slice(&[0, 1, 0, 0, 64, proto, 0, 0, 10, 0, 2, 2, 10, 0, 2, 15]); + f.extend_from_slice(payload); + f.resize(f.len().max(60), 0); + fix(&mut f); + let ip = inet_sum(&f[14..34], 0); + f[24..26].copy_from_slice(&ip.to_be_bytes()); + f +} + +/// The receive checksum report: an intact TCP segment claims both sums +/// (0xffff) and the OK flags; a corrupt one claims the IP sum only; a frame +/// that is not IPv4 TCP or UDP claims nothing. Ported from +/// test_receive_checksum_offload, test_receive_checksum_bad_transport and +/// test_receive_checksum_not_ip. +#[test] +fn the_receive_report_claims_only_the_sums_it_computed() { + let mut tcp = vec![0u8; 20]; + tcp[0..4].copy_from_slice(&[0x30, 0x39, 0, 0x17]); + tcp[12] = 0x50; + let seal = |f: &mut Vec| { + f[14 + 20 + 16..14 + 20 + 18].fill(0); + let sum = inet_sum(&f[34..54], tcp_pseudo(f, 20)); + f[50..52].copy_from_slice(&sum.to_be_bytes()); + }; + for (label, frame, ip_ok, l4_ok, ip_report, l4_report) in [ + ("intact", rx_frame(6, &tcp, seal), true, true, 0xffffu32, 0xffffu32), + ("corrupt", rx_frame(6, &tcp, |_| ()), true, false, 0xffff, 0), + ] { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.deliver(&frame); + rig.step(ARRIVAL); + let ret = rig.return_descriptor(0); + let flags = get16dma(&ret, 0x0c); + assert_eq!(flags & 0x1000 != 0, ip_ok, "{label} ip flag"); + assert_eq!(flags & 0x2000 != 0, l4_ok, "{label} transport flag"); + assert_eq!(flags & 0x4000, 0x4000, "{label} tcp flag"); + assert_eq!(u32::from(get16dma(&ret, 0x12)), ip_report, "{label} ip sum"); + let l4 = u32::from(get16dma(&ret, 0x10)); + if l4_ok { + assert_eq!(l4, l4_report); + } else { + assert_ne!(l4, 0xffff); + } + } + // Not IPv4: nothing is claimed. + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + let mut arp = broadcast(60, 3); + arp[12..14].copy_from_slice(&[0x08, 0x06]); + rig.deliver(&arp); + rig.step(ARRIVAL); + let ret = rig.return_descriptor(0); + assert_eq!(get16dma(&ret, 0x0c) & 0x7000, 0); + assert_eq!(be32(&ret, 0x10), 0); +} + +/// Receive coalescing: with a count of two, two frames make one update; the +/// tick budget reports a short group. Ported from +/// test_receive_interrupt_coalescing and test_receive_coalescing_timer. +#[test] +fn receive_updates_follow_the_count_and_the_tick_budget() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + rig.rx_open(16, 2, 0x1000_0000); + rig.rx_post_std(4); + for _ in 0..4 { + rig.deliver(&frame); + } + rig.step(ARRIVAL); + assert_eq!(rig.status_tag(), 2); + assert_eq!(rig.status_rx_prod(), 4); + + let mut rig = Rig::new(); + rig.rx_open(16, 8, 1000); + rig.rx_post_std(4); + rig.deliver(&frame); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_tag(), 0); + rig.step(Duration::from_micros(800)); + assert_eq!(rig.status_tag(), 1); + assert_eq!(rig.status_rx_prod(), 2); + + // Neither criterion armed: the guest has asked to be told nothing. + let mut rig = Rig::new(); + rig.rx_open(16, 0, 0); + rig.rx_post_std(2); + rig.deliver(&frame); + rig.step(Duration::from_secs(1)); + assert_eq!(rig.status_tag(), 0); +} + +/// Ported from test_rx_producer_out_of_range and the ring-length guards: +/// out-of-range indexes are refused, observably, and not stored. +#[test] +fn receive_mailbox_indexes_past_their_rings_are_refused() { + let rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.writel(reg::MBX_RX_STD_PROD_LO, 512); + rig.writel(reg::MBX_RX_JUMBO_PROD_LO, 256); + rig.writel(reg::MBX_RX_CONS0_LO, RX_RING_LEN); + assert_eq!(rig.readl(reg::MBX_RX_STD_PROD_LO), 0); + assert_eq!(rig.refusals(Refused::ProducerOutOfRange(Mailbox::RxStandardProducer)), 1); + assert_eq!(rig.refusals(Refused::ProducerOutOfRange(Mailbox::RxJumboProducer)), 1); + assert_eq!(rig.refusals(Refused::ProducerOutOfRange(Mailbox::RxReturnConsumer)), 1); +} + +/// A return-ring descriptor that cannot be read or written leaves the frame +/// queued; repairing the address delivers it. Ported from +/// test_receive_descriptor_dma_error and test_receive_completion_dma_error. +#[test] +fn an_unreachable_ring_defers_the_frame_until_it_is_repaired() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.split_addr(reg::RX_STD_RCB_HADDR_HI, reg::RX_STD_RCB_HADDR_LO, UNMAPPED); + rig.mem_write(STD_RING, &rx_bd(RX_BUF, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.deliver(&frame); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.waiting(), 1); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::RxDescriptor)), 1); + rig.split_addr(reg::RX_STD_RCB_HADDR_HI, reg::RX_STD_RCB_HADDR_LO, STD_RING); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.dev.poll(); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + + // The same for the return ring. + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.ind_mem_write(0x204, UNMAPPED as u32); + rig.ind_mem_write(0x200, (UNMAPPED >> 32) as u32); + rig.rx_post_std(1); + rig.deliver(&frame); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::RxCompletion)), 1); + rig.ind_mem_write(0x200, 0); + rig.ind_mem_write(0x204, RET_RING as u32); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.dev.poll(); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); +} + +/// A global reset cancels pending coalescing work and the held completions. +/// Ported from test_global_reset_cancels_receive_coalescing and +/// test_system_reset_cancels_transmit_coalescing. +#[test] +fn a_reset_cancels_coalescing_timers_and_held_completions() { + let rig = Rig::new(); + let frame = broadcast(64, 3); + rig.tx_open(64, 8, 50); + rig.tx_send_one(0, &frame, 0); + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + rig.step(Duration::from_millis(1)); + assert_eq!(rig.status_tag(), 0); + + let mut rig = Rig::new(); + rig.rx_open(16, 1, 0); + rig.rx_post_std(1); + rig.deliver(&frame); + rig.dev.reset(); + rig.step(Duration::from_millis(1)); + assert_eq!(rig.status_rx_prod(), 0); + assert!(!rig.irq.level()); +} + +/// The receive thread takes a frame when the guest posts a buffer and +/// publishes its completion on the clock, with nothing polling by hand; stop +/// joins it. Exercises `Device::start` and `stop`, which the cases above +/// replace with explicit steps. +#[test] +fn the_receive_thread_delivers_and_publishes_on_its_own() { + let mut rig = Rig::new(); + *rig.dev.frozen.lock() = None; + let frame = broadcast(64, 3); + rig.rx_open(RX_RING_LEN, 1, 0); + Device::start(&*rig.dev); + assert!(Device::is_running(&*rig.dev)); + // A frame offered before any buffer is posted waits at the queue head. + rig.inject.push(frame.clone()).unwrap(); + rig.dev.wake_rx(); + rig.rx_post_std(1); + let deadline = Instant::now() + Duration::from_secs(5); + while rig.status_rx_prod() != 1 && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(2)); + } + Device::stop(&*rig.dev); + assert!(!Device::is_running(&*rig.dev)); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + assert!(rig.line_up()); +} + +// ---- the rest of the plain-memory qtests ---- + +impl Rig { + /// What the receive thread does: take what it can, let the arrival + /// deadline pass. + fn pump(&mut self) { + self.dev.poll(); + self.step(ARRIVAL); + } + fn ret_flags_end(&self, slot: u64) -> u16 { + get16dma(&self.return_descriptor(slot), 0x0c) & 4 + } +} + +/// A completed frame ahead of a descriptor read failure stays completed; the +/// failed frame retries from its first descriptor once the address is +/// repaired. Ported from test_transmit_complete_then_fragment_dma_error_retry +/// (both its empty-first and 60-byte-first variants). +#[test] +fn a_frame_with_an_unreadable_second_fragment_retries_after_the_first_completes() { + for empty_first in [false, true] { + let mut rig = Rig::new(); + let first: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + let second: Vec = (0..60u8).map(|i| i.wrapping_mul(5).wrapping_add(3)).collect(); + rig.tx_open(TX_RING_LEN, 1, 0); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(TX_BUF, &first); + rig.mem_write(TX_BUF + 0x100, &second[..28]); + rig.tx_post(0, tx_bd(TX_BUF, if empty_first { 0 } else { 60 }, TXF_END, 0)); + rig.tx_post(1, tx_bd(TX_BUF + 0x100, 28, 0, 0)); + rig.tx_post(2, tx_bd(UNMAPPED, 32, TXF_END, 0)); + let tag = rig.status_tag(); + rig.writel(reg::MBX_TX_PROD_LO, 3); + let expect_first = if empty_first { vec![] } else { vec![first.clone()] }; + assert_eq!(rig.sent_frames(), expect_first); + assert_eq!(rig.status_tx_cons(), 1); + assert_eq!(rig.status_tag(), tag + 1); + assert!(rig.line_up()); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::TxPayload)), 1); + + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(TX_BUF + 0x100 + 28, &second[28..]); + rig.tx_post(2, tx_bd(TX_BUF + 0x100 + 28, 32, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 3); + assert_eq!(rig.sent_frames(), [second]); + assert_eq!(rig.status_tx_cons(), 3); + assert_eq!(rig.status_tag(), tag + 2); + assert!(rig.line_up()); + } +} + +/// The same when it is the next descriptor that cannot be read: the ring's +/// last 16 bytes sit at the end of memory. Ported from +/// test_transmit_complete_then_descriptor_dma_error_retry. +#[test] +fn a_frame_with_an_unreadable_next_descriptor_retries_after_the_first_completes() { + let mut rig = Rig::new(); + let first: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + let second: Vec = (0..60u8).map(|i| i.wrapping_mul(5).wrapping_add(3)).collect(); + let initial_ring = MEM_SIZE as u64 - 16; + let repaired_ring = TX_RING - 16; + rig.tx_open(TX_RING_LEN, 1, 0); + rig.ind_mem_write(0x104, initial_ring as u32); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(initial_ring, &tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.mem_write(TX_BUF, &first); + let tag = rig.status_tag(); + rig.writel(reg::MBX_TX_PROD_LO, 2); + assert_eq!(rig.sent_frames(), [first]); + assert_eq!(rig.status_tx_cons(), 1); + assert_eq!(rig.status_tag(), tag + 1); + assert!(rig.line_up()); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::TxDescriptor)), 1); + + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(repaired_ring, &tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.mem_write(TX_RING, &tx_bd(TX_BUF + 0x100, 60, TXF_END, 0)); + rig.mem_write(TX_BUF + 0x100, &second); + rig.ind_mem_write(0x104, repaired_ring as u32); + rig.writel(reg::MBX_TX_PROD_LO, 2); + assert_eq!(rig.sent_frames(), [second]); + assert_eq!(rig.status_tx_cons(), 2); + assert_eq!(rig.status_tag(), tag + 2); +} + +/// A count of two with a completed frame, a failed one and a repaired retry: +/// the retried frame's two descriptors report at once, and the budget timer +/// has nothing left to say. Ported from +/// test_transmit_partial_error_descriptor_coalescing. +#[test] +fn a_partly_failed_doorbell_coalesces_by_the_descriptors_that_completed() { + let mut rig = Rig::new(); + let first: Vec = (0..60u8).map(|i| i.wrapping_mul(7).wrapping_add(1)).collect(); + let second: Vec = (0..60u8).map(|i| i.wrapping_mul(5).wrapping_add(3)).collect(); + rig.tx_open(TX_RING_LEN, 2, 50); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(TX_BUF, &first); + rig.tx_post(0, tx_bd(TX_BUF, 60, TXF_END, 0)); + rig.tx_post(1, tx_bd(0, 0, TXF_END, 0)); + rig.tx_post(2, tx_bd(UNMAPPED, 60, TXF_END, 0)); + let tag = rig.status_tag(); + rig.writel(reg::MBX_TX_PROD_LO, 3); + assert_eq!(rig.sent_frames(), [first]); + assert_eq!(rig.status_tx_cons(), 2); + assert_eq!(rig.status_tag(), tag + 1); + assert!(rig.line_up()); + // The qtest's wait for the log line steps the clock a millisecond at a + // time, so the budget has run out by the retry. + rig.step(Duration::from_millis(1)); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.mem_write(TX_BUF + 0x100, &second); + rig.tx_post(2, tx_bd(TX_BUF + 0x100, 60, TXF_END, 0)); + rig.writel(reg::MBX_TX_PROD_LO, 3); + assert_eq!(rig.sent_frames(), [second]); + assert_eq!(rig.status_tx_cons(), 3); + assert_eq!(rig.status_tag(), tag + 2); + assert!(rig.line_up()); + // The budget gives up on a group that already reported. + rig.step(Duration::from_micros(1050)); + assert_eq!(rig.status_tx_cons(), 3); + assert_eq!(rig.status_tag(), tag + 2); +} + +/// A reply handed back straight after the transmit sees the transmit +/// completion already published. The qtest runs an ARP exchange through +/// QEMU's user-mode network; here the backend queue plays the gateway. Adapted +/// from test_user_backend_reply_waits_for_transmit. +#[test] +fn a_reply_to_a_transmit_finds_the_transmit_already_reported() { + let mut rig = Rig::new(); + let mut arp_request = vec![0xff; 6]; + arp_request.extend_from_slice(&[0x52, 0x54, 0, 0x12, 0x34, 0x56, 8, 6, 0, 1, 8, 0, 6, 4, 0, 1]); + arp_request.extend_from_slice(&[0x52, 0x54, 0, 0x12, 0x34, 0x56, 10, 0, 2, 15, 0, 0, 0, 0, 0, 0, 10, 0, 2, 2]); + assert_eq!(arp_request.len(), 42); + rig.writel(reg::MAC_ADDR_0_HIGH, 0x5254); + rig.writel(reg::MAC_ADDR_0_LOW, 0x0012_3456); + rig.tx_open(TX_RING_LEN, 2, 1_000_000); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.tx_send_one(0, &arp_request, 0); + assert_eq!(rig.sent_frames(), [arp_request.clone()]); + // The gateway's reply, to the address just programmed. + let mut reply = arp_request[6..12].to_vec(); + reply.extend_from_slice(&[0x52, 0x55, 10, 0, 2, 2, 8, 6, 0, 1, 8, 0, 6, 4, 0, 2]); + reply.extend_from_slice(&[0x52, 0x55, 10, 0, 2, 2, 10, 0, 2, 2]); + reply.extend_from_slice(&arp_request[22..32]); + reply.resize(60, 0); + rig.deliver(&reply); + rig.step(ARRIVAL); + assert_eq!(rig.status_tx_cons(), 1); + assert_eq!(rig.status_rx_prod(), 1); + assert!(rig.line_up()); + assert_eq!(rig.mem_read(RX_BUF, 60), reply); +} + +/// A frame bigger than the standard buffer, with no jumbo ring, must not +/// write past the buffer. Ported from test_receive_overruns_nothing. +#[test] +fn an_oversize_frame_never_writes_past_the_posted_buffer() { + let mut rig = Rig::new(); + let frame = broadcast(4096, 7); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.mem_fill(RX_BUF, 4096, 0xa5); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.mem_read(RX_BUF + u64::from(STD_BUF_SIZE), 4096 - STD_BUF_SIZE as usize), vec![0xa5; 4096 - 1536]); +} + +fn lap_open(rig: &Rig, coal_bds: u32) { + rig.rx_open(8, coal_bds, 0); + rig.writel(reg::RX_STD_RCB_MAXLEN_FLAGS, 1518 << 16); + rig.rx_post_std(64); +} + +/// Five trips round an eight-entry return ring, each completion visible in +/// its slot before the driver acknowledges it. Ported from +/// test_receive_ring_laps. +#[test] +fn the_return_ring_laps_cleanly() { + let mut rig = Rig::new(); + let frame = broadcast(64, 2); + lap_open(&rig, 0); + for i in 0..40u32 { + let idx = u64::from(i % 8); + rig.mem_fill(RET_RING + idx * 32, 32, 0); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.ret_flags_end(idx), 4, "frame {i}"); + assert_eq!(get16dma(&rig.return_descriptor(idx), 0x08), 68); + rig.writel(reg::MBX_RX_CONS0_LO, (i + 1) % 8); + rig.writel(reg::MBX_RX_STD_PROD_LO, (64 + i + 1) % 512); + } +} + +/// A full return ring holds frames back rather than overwrite completions the +/// driver owes; freeing space with a consumer write releases them, without +/// another interrupt acknowledgement. Ported from +/// test_receive_ring_full_recovers. +#[test] +fn a_full_return_ring_holds_frames_until_the_driver_frees_it() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + lap_open(&rig, 1); + for _ in 0..20 { + rig.deliver(&frame); + } + rig.step(ARRIVAL * 4); + // Seven fit; the last slot stays clear and the rest wait in the backend. + assert_eq!(rig.status_rx_prod(), 7); + assert_eq!(rig.ret_flags_end(7), 0); + assert_eq!(rig.waiting(), 13); + rig.writel(reg::MBX_IRQ0_LO, 0); + assert!(!rig.line_up()); + rig.step(ARRIVAL); + assert_eq!(rig.ret_flags_end(7), 0); + for i in 0..8 { + rig.mem_fill(RET_RING + i * 32, 32, 0); + } + rig.writel(reg::MBX_RX_CONS0_LO, 7); + rig.pump(); + // The retry fills the ring a second time, wrapping to slot 6. + assert_eq!(rig.status_rx_prod(), 6); + assert_eq!(rig.ret_flags_end(6), 0); + for i in 0..8 { + rig.mem_fill(RET_RING + i * 32, 32, 0); + } + rig.writel(reg::MBX_RX_CONS0_LO, 6); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 4); +} + +/// A halted receiver takes nothing and keeps nothing: frames from before it +/// is enabled are gone. Ported from test_receive_mac_enable. +#[test] +fn frames_offered_to_a_halted_receiver_are_not_kept() { + let mut rig = Rig::new(); + let frame = broadcast(64, 11); + rig.rx_open(8, 1, 0); + rig.writel(reg::RX_MODE, 0); + rig.rx_post_std(8); + assert_eq!(rig.readl(reg::RX_MODE) & RXMODE_ENABLE, 0); + for _ in 0..4 { + rig.deliver(&frame); + } + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.waiting(), 0); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.ret_flags_end(0), 0); + let fresh = broadcast(64, 17); + rig.deliver(&fresh); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert!(rig.line_up()); + assert_eq!(get16dma(&rig.return_descriptor(0), 0x08), 68); + // Halting again is not a bring-up special case. + rig.writel(reg::RX_MODE, 0); + rig.deliver(&broadcast(64, 19)); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + rig.writel(reg::RX_MODE, RXMODE_ENABLE); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 1); + rig.deliver(&broadcast(64, 23)); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 2); + assert_eq!(rig.ret_flags_end(1), 4); +} + +/// A frame offered before any buffer is posted waits; posting one releases +/// it. Ported from test_queued_receive_released_by_producer. +#[test] +fn a_waiting_frame_is_released_by_the_producer_mailbox() { + let mut rig = Rig::new(); + let frame = broadcast(64, 29); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.mem_write(STD_RING, &rx_bd(RX_BUF, 0)); + rig.deliver(&frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.ret_flags_end(0), 0); + assert_eq!(rig.waiting(), 1); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + assert_eq!(get16dma(&rig.return_descriptor(0), 0x0a), 0); +} + +/// A frame queued before a reset must not reach the rings brought up after +/// it, by chip reset or machine reset. Ported from test_receive_queue_reset. +#[test] +fn a_reset_discards_frames_the_backend_was_holding() { + for machine_reset in [false, true] { + let mut rig = Rig::new(); + let stale = broadcast(64, 0x51); + let fresh = broadcast(64, 0xa7); + // Nothing is posted yet, so the stale frame waits at the head. + rig.rx_open(RX_RING_LEN, 1, 0); + rig.deliver(&stale); + assert_eq!(rig.waiting(), 1); + if machine_reset { + rig.dev.reset(); + } else { + rig.writel(reg::MISC_CFG, MISC_CFG_RESET); + } + assert_eq!(rig.waiting(), 0); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.deliver(&fresh); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), fresh); + } +} + +/// An unreadable posted buffer defers the frame, leaves the descriptor +/// unconsumed, and the repaired buffer delivers it. Derived from the receive +/// payload read in the model; the qtest of the same subject is +/// test_receive_payload_dma_error, whose log-marker checks are not ported. +#[test] +fn an_unreachable_receive_buffer_defers_the_frame() { + let mut rig = Rig::new(); + let frame = broadcast(64, 3); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.mem_write(STD_RING, &rx_bd(UNMAPPED, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.deliver(&frame); + assert_eq!(rig.waiting(), 1); + assert_eq!(rig.status_rx_prod(), 0); + assert_eq!(rig.refusals(Refused::DmaFault(DmaWhat::RxPayload)), 1); + rig.mem_write(STD_RING, &rx_bd(RX_BUF, 0)); + rig.writel(reg::MBX_RX_STD_PROD_LO, 1); + rig.pump(); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.mem_read(RX_BUF, 64), frame); + assert_eq!(get16dma(&rig.return_descriptor(0), 0x0a), 0); +} + +/// With neither criterion armed the receive side stays silent however long +/// the ring is idle, and so does the transmit side. Ported from +/// test_receive_without_a_criterion_is_silent and +/// test_transmit_without_a_criterion_is_silent. +#[test] +fn without_a_criterion_no_update_is_ever_posted() { + let mut rig = Rig::new(); + rig.rx_open(64, 0, 0); + rig.rx_post_std(1); + rig.deliver(&broadcast(64, 3)); + rig.step(Duration::from_secs(1)); + assert_eq!(rig.ret_flags_end(0), 4); + assert_eq!(be32(&rig.status_block(), 0), 0); + assert_eq!(rig.status_rx_prod(), 0); + assert!(!rig.line_up()); + + let mut rig = Rig::new(); + let frame = broadcast(64, 5); + rig.tx_open(64, 0, 0); + rig.writel(reg::MBX_IRQ0_LO, 0); + rig.tx_send_one(0, &frame, 0); + assert_eq!(rig.sent_frames().len(), 1); + rig.step(Duration::from_secs(1)); + assert_eq!(be32(&rig.status_block(), 0), 0); + assert_eq!(rig.status_tx_cons(), 0); + assert!(!rig.line_up()); +} + +/// A budget that has run out reports the next event as it happens, in either +/// direction. Ported from test_receive_expired_timer_reports_at_once and +/// test_transmit_expired_timer_reports_at_once. +#[test] +fn an_expired_budget_reports_the_next_event_at_once() { + let mut rig = Rig::new(); + rig.rx_open(64, 8, 50); + rig.rx_post_std(2); + rig.step(Duration::from_micros(200)); + assert_eq!(rig.status_tag(), 0); + rig.deliver(&broadcast(64, 3)); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.status_tag(), 1); + + let mut rig = Rig::new(); + rig.tx_open(64, 8, 50); + rig.step(Duration::from_micros(200)); + assert_eq!(rig.status_tag(), 0); + rig.tx_send_one(0, &broadcast(64, 3), 0); + assert_eq!(rig.sent_frames().len(), 1); + assert_eq!(rig.status_tag(), 1); +} + +/// The producer the status block names follows its completion: the +/// descriptor is written, the next slot is still empty. Ported from +/// test_receive_producer_follows_its_completion. +#[test] +fn the_receive_producer_names_only_completed_descriptors() { + let mut rig = Rig::new(); + rig.rx_open(64, 1, 0); + rig.rx_post_std(3); + for i in 0..3u64 { + rig.deliver(&broadcast(64, 0x55)); + rig.step(ARRIVAL); + assert_eq!(u64::from(rig.status_rx_prod()), i + 1); + let bd = rig.return_descriptor(i); + assert_eq!(get16dma(&bd, 0x0c) & 4, 4); + assert_eq!(get16dma(&bd, 0x08), 68); + let next = rig.return_descriptor(i + 1); + assert_eq!(get16dma(&next, 0x08), 0); + assert_eq!(get16dma(&next, 0x0c), 0); + } +} + +/// A receive update the bus refused is retried by the budget timer once the +/// address is repaired. Ported from test_receive_coalescing_status_dma_retry. +#[test] +fn a_refused_receive_update_is_retried_by_the_budget_timer() { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 50); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, UNMAPPED); + rig.rx_post_std(1); + rig.deliver(&broadcast(64, 0x6d)); + rig.step(ARRIVAL); + assert_eq!(rig.ret_flags_end(0), 4); + assert_eq!(rig.status_rx_prod(), 0); + assert!(!rig.line_up()); + rig.split_addr(reg::HCC_STATUSBLK_ADDR_HI, reg::HCC_STATUSBLK_ADDR_LO, STATUS); + rig.step(Duration::from_micros(1051)); + assert_eq!(rig.status_rx_prod(), 1); + assert_eq!(rig.status_tag(), 1); + assert!(rig.line_up()); +} + +fn cksum_rx_frame() -> Vec { + let mut f = cksum_frame(0x45); + f[..6].fill(0xff); + seal_tcp(&mut f); + f +} +fn seal_tcp(f: &mut [u8]) { + f[24..26].fill(0); + let ip = inet_sum(&f[14..34], 0); + f[24..26].copy_from_slice(&ip.to_be_bytes()); + f[50..52].fill(0); + let tcp = inet_sum(&f[34..62], tcp_pseudo(f, 28)); + f[50..52].copy_from_slice(&tcp.to_be_bytes()); +} +fn seal_udp(f: &mut [u8]) { + f[23] = 17; + f[16] = 0; + f[17] = 48; + f[38] = 0; + f[39] = 28; + f[40..42].fill(0); + f[24..26].fill(0); + let ip = inet_sum(&f[14..34], 0); + f[24..26].copy_from_slice(&ip.to_be_bytes()); + let pseudo: u32 = f[26..34].chunks(2).map(|p| u32::from(p[0]) << 8 | u32::from(p[1])).sum::() + 17 + 28; + let udp = inet_sum(&f[34..62], pseudo); + f[40..42].copy_from_slice(&udp.to_be_bytes()); +} +fn inet_fix_ip(f: &mut [u8]) { + f[24..26].fill(0); + let ip = inet_sum(&f[14..34], 0); + f[24..26].copy_from_slice(&ip.to_be_bytes()); +} + +/// One row of test_receive_checksum_case: the flags, the IP sum and the +/// transport sum a frame earns under a MODE_CTL setting. +fn rx_checksum_case(frame: &[u8], mode: u32, flags: u16, ip: u16, l4: u16, label: &str) { + let mut rig = Rig::new(); + rig.rx_open(RX_RING_LEN, 1, 0); + rig.rx_post_std(1); + rig.ind_reg_write(reg::MODE_CTL, MODE_ALL_SWAPS | mode); + rig.deliver(frame); + rig.step(ARRIVAL); + assert_eq!(rig.status_rx_prod(), 1, "{label}"); + let ret = rig.return_descriptor(0); + let got = get16dma(&ret, 0x0c); + assert_eq!(got, flags | 4, "{label} flags"); + assert_eq!(got & 3, 0, "{label}"); + assert_eq!(get16dma(&ret, 0x12), ip, "{label} ip sum"); + assert_eq!(get16dma(&ret, 0x10), l4, "{label} transport sum"); +} + +/// Ported from test_receive_checksum_offload, _udp, _udp_zero, +/// _malformed_udp_length, _bad_ip, _bad_transport, _no_pseudo, _padding, +/// _fragment, _later_fragment, _malformed_tcp_header, _malformed_length and +/// _raw_mode. +#[test] +fn the_receive_checksum_report_covers_every_malformed_case() { + let tcp = cksum_rx_frame(); + rx_checksum_case(&tcp, 0, 0x7000, 0xffff, 0xffff, "tcp"); + + let mut udp = cksum_frame(0x45); + udp[..6].fill(0xff); + seal_udp(&mut udp); + rx_checksum_case(&udp, 0, 0x3000, 0xffff, 0xffff, "udp"); + + // UDP with the checksum omitted: IP only, and the sum is 0xffff. + let mut udp_zero = udp.clone(); + udp_zero[42..44].copy_from_slice(&[0x62, 0x6a]); + udp_zero[40..42].fill(0); + rx_checksum_case(&udp_zero, 0, 0x1000, 0xffff, 0xffff, "udp, no checksum"); + + let mut bad_udp_len = udp.clone(); + bad_udp_len[39] = 27; + rx_checksum_case(&bad_udp_len, 0, 0x1000, 0xffff, 0, "udp length"); + + let mut bad_ip = tcp.clone(); + bad_ip[22] -= 1; + rx_checksum_case(&bad_ip, 0, 0x6000, 0xfeff, 0xffff, "bad ip"); + + let mut bad_tcp = tcp.clone(); + bad_tcp[51] += 1; + rx_checksum_case(&bad_tcp, 0, 0x5000, 0xffff, 0x0001, "bad transport"); + + rx_checksum_case(&tcp, 1 << 23, 0x5000, 0xffff, !inet_sum(&tcp[34..62], 0), "no pseudo header"); + + let mut padded = tcp.clone(); + padded.extend_from_slice(&[0xa5; 8]); + rx_checksum_case(&padded, 0, 0x7000, 0xffff, 0xffff, "padding"); + + let mut fragment = tcp.clone(); + fragment[20] = 0x20; + inet_fix_ip(&mut fragment); + rx_checksum_case(&fragment, 0, 0x5000, 0xffff, 0, "more fragments"); + + let mut later = tcp.clone(); + later[21] = 1; + inet_fix_ip(&mut later); + rx_checksum_case(&later, 0, 0x5000, 0xffff, 0, "later fragment"); + + let mut short_header = tcp.clone(); + short_header[46] = 0x40; + rx_checksum_case(&short_header, 0, 0x5000, 0xffff, 0, "tcp data offset"); + + let mut bad_length = tcp.clone(); + bad_length[16..18].copy_from_slice(&[1, 0]); + rx_checksum_case(&bad_length, 0, 0, 0, 0, "ip length"); + + rx_checksum_case(&tcp, 1 << 9, 0, 0, 0, "raw mode"); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 5111f9626..c2990d6f6 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -82,6 +82,7 @@ pub mod hub_pi; pub mod dp83840; pub mod bedrock; pub mod bedrock_flash; +pub mod bcm5701; pub mod dp83843; pub mod ics1890; pub mod isp12160; From 4bc0255fcd0916f1ff1698fa2545d980e45679ab Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 18:50:26 -0700 Subject: [PATCH 25/33] pci: let a PCI-to-PCI bridge report its chip's revision A PCI-to-PCI bridge reports revision 0 whatever chip it stands for. A machine that fits a named bridge, such as the TI part on the Tezro's IO9 board, prints that part's revision in its inventories, so the bridge needs to carry it. Add PciToPciBridge::with_revision, which puts the revision byte in the type 1 header; new() keeps revision 0. The byte is read-only and survives RST#, as the rest of the identity does. --- src/pci/pci_to_pci_bridge.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pci/pci_to_pci_bridge.rs b/src/pci/pci_to_pci_bridge.rs index 43fa0fe5f..d2b59735c 100644 --- a/src/pci/pci_to_pci_bridge.rs +++ b/src/pci/pci_to_pci_bridge.rs @@ -25,8 +25,13 @@ impl PciToPciBridge { /// `parent.attach(at, bridge)`. Only the type 1 header is modelled, not /// the registers of any chip, so the identity is the machine's to name. pub fn new(parent: &PciBus, at: DevFn, vendor: u16, device: u16) -> Arc { + Self::with_revision(parent, at, vendor, device, 0) + } + + /// As [`Self::new`], with the revision byte the chip reports at 0x08. + pub fn with_revision(parent: &PciBus, at: DevFn, vendor: u16, device: u16, revision: u8) -> Arc { Arc::new(Self { - pci: PciFunctionState::new(PciConfig::type1(vendor, device, 0)), + pci: PciFunctionState::new(PciConfig::type1(vendor, device, revision)), sec: PciBus::secondary(parent.clone(), at), }) } @@ -144,6 +149,7 @@ mod tests { let read = |off, w| root.config_read(at(3, 0), off, w).unwrap(); assert_eq!(read(0x0e, 1), 1, "header type 1"); assert_eq!(read(0x09, 3), 0x06_0400, "a PCI-to-PCI bridge"); + assert_eq!(read(0x08, 1), 0, "revision 0 unless the chip names one"); for (off, probe, stuck) in [(0x18, 0xffff_ffffu32, 0xffff_ffffu32), (0x1c, !0, 0xf0f0), (0x20, !0, 0xfff0_fff0), (0x24, !0, 0xfff0_fff0), (0x28, !0, 0), (0x2c, !0, 0), (0x30, !0, 0), (0x38, !0, 0)] { root.config_write(at(3, 0), off, 4, probe); assert_eq!(read(off, 4), stuck, "register {off:#x}"); @@ -154,6 +160,17 @@ mod tests { assert_eq!(read(0x19, 1), 0x05, "the secondary bus number is a byte"); } + #[test] + fn a_bridge_reports_the_revision_it_is_given() { + let host = Arc::new(TestHost::default()); + let up: Weak = Arc::downgrade(&(host.clone() as Arc)); + let root = PciBus::new(0, &W, up); + root.attach(at(3, 0), PciToPciBridge::with_revision(&root, at(3, 0), 0x1234, 0x5678, 0x21)); + assert_eq!(root.config_read(at(3, 0), 0x08, 4), Some(0x0604_0021)); + root.reset(); + assert_eq!(root.config_read(at(3, 0), 0x08, 1), Some(0x21), "and keeps it across RST#"); + } + #[test] fn a_child_bar_inside_the_bridge_window_gets_a_run_with_the_parents_window_index() { let r = rig(); From 18cf121a9a25a70745c44e7992a1c474d7c0f817 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 08:20:04 -0700 Subject: [PATCH 26/33] tezro: add the machine Iris had every Tezro device and the Fuel's Bedrock node but no machine that put them together. Add the tezro_ip53 profile, configured by the shared keys and a [tezro] section, with the boards the QEMU Tezro compositions describe. The node is the Fuel's, given the Tezro's inputs: an R16000 with the Config its chip's flash record implies (0x6c9bad85 at 700 MHz), that record, Bedrock board word 0x2001, memory from 512 and 1024 MiB banks with the 4 GiB layout of 1024, 1024 and four 512 MiB banks as the default, and an L1 that reports brick type 0x39, revision 1.48.1, the first port-speed integer 1, the router brick reply and, in the node board's record, the flash record's mode word and clocks. Without that configuration the PROM checks the flash against the 400 MHz table row board word 0x2001 selects and prints "CPU configuration values mismatch" on every boot. The board word's class, 2, is inferred: a real IP53 or IP59 board's class is not 4, 8 or 0xc, because Tezro and Origin 350 PROM banners lack the lines the PROM prints for those classes (established), and OpenBSD names class 2 for this board family. Its low nibble, 1, is a software choice that only the PROM's last-resort table fallback reads. The node board's record names the board inventories print for the chip and the processor count, not the count alone: - 700 MHz: IP53_4CPU 030-1868-001 revision C (established: Tezro and Origin 350 inventories), IP53_2CPU 030-1869-001 revision C (inferred, from inventories outside the captures kept beside the model), and IP53_1CPU with no part or revision, a name the PROM's table holds but no captured board prints; - 800 MHz: IP53_4CPU 030-1956-002 revision C, the part both 800 MHz Tezro inventories found print with four 2.2 processors at 4 MB (they show revisions B and C; which a given board carries is unknown); - 900 MHz: IP53_4CPU with no part or revision, since no 900 MHz IP53 or IP59 board is captured; - 1 GHz: IP59_4CPU 030-1989-003 revision C and IP59_2CPU 030-2059-002 revision A. That part carries the 1 GHz chip in every capture of it, but those are Onyx 350 modules and no 1 GHz Tezro capture prints its node board, so the names are inferred. The PROM's socket masks for the IP59 names equal the IP53 ones, and nothing in the PROM or the L1 reads the part or revision, so the names change what inventories print, not the boot. A board refuses a processor count its chip is not fitted with. The PIC at widget 15 carries IO9's IOC4 at device 1, a TI PCI-to-PCI bridge (104c:ac28, revision 1) at device 2, the ISP12160 at device 3 and the BCM5701 at device 4 behind its PXBOW, with the DS1744 clock in the IOC4's BAR and the IOC4's four serial ports beside the L1's. Every Tezro capture that prints widget 15 shows the TI bridge at device 2, and no Origin 350 or Onyx 350 IO9 capture does, for a reason not known; no capture shows a device behind it, so its secondary bus is empty (inferred), and only its type 1 header is modelled. Graphics is not attached. The profile lists the four R16000 chips the Tezro shipped with, the 700 MHz one first, so a bare cpu = "r16000" and a config with no cpu both select it, and a chip the Tezro lacks is refused by the generic message that names the machine and its chips. The board keeps what is its own beside the chip: the flash record, the node boards, and the processor counts the chip is fitted with, from the boards captured with each chip (the 700 MHz chip one, two or four; the 1 GHz chip two or four; the 800 and 900 MHz chips four only). The comment on that table grades each value as established, inferred or approximated and names the evidence. The cache type comes from with_cpu!, so the board adds no run-time size field or branch. Memory is refused when the two banks of a pair differ in size: Bedrock keeps one size code for both. An odd last bank is still accepted, although the board takes DIMMs in pairs. [machine] cpus takes 1, 2 or 4, with 4 the default on a Tezro; unset, it is written back only when set, so a saved config keeps following its profile's default. Every fitted processor runs from reset, each on its own host thread, in the population's order (CPU 1 in PI1 slot A, CPUs 2 and 3 in the B slots), with one compile pool and page set between them under jitv2. Two or four processors bring the second PIC at widget 11, with nothing on its buses: it is an endpoint of the IO9 PIC's PXBOW, since the PROM and IRIX find XIO widgets only through widget 0's link status. The board's L1 record names the node board, and the PROM brings up only the positions that name allows, so a one- or two-processor board's first boot runs straight through: with the Fuel's placeholder name, which matches none of the PROM's rows, it tried all four positions and power-cycled the module three times for each empty one. Not changed, and why: the brick serial stays EMUN00. A real tower answers with its interface board's serial, three letters and three digits, but the PROM turns that reply into the node's identity, so its shape is left to the machine-identity settings. The L1 serves no record for the tower's other boards: which component number each takes is not known and their bytes cannot be authored from captures. Board composition ported from hw/mips/sgi/sn-machine.c and hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. --- CHANGELOG.md | 10 + FEATURES.md | 26 +- HACKING.md | 30 +- HELP.md | 4 +- docs/tezro-ip53.md | 201 ++++++ iris-gui/src/config_ui.rs | 4 +- src/config.rs | 450 +++++++++++-- src/ip30_machine.rs | 2 +- src/ip53_machine.rs | 1259 +++++++++++++++++++++++++++++++++++++ src/ip53_machine/tests.rs | 683 ++++++++++++++++++++ src/lib.rs | 1 + src/machine.rs | 8 +- src/workstation.rs | 1 + tezro.toml.example | 43 ++ 14 files changed, 2660 insertions(+), 62 deletions(-) create mode 100644 docs/tezro-ip53.md create mode 100644 src/ip53_machine.rs create mode 100644 src/ip53_machine/tests.rs create mode 100644 tezro.toml.example diff --git a/CHANGELOG.md b/CHANGELOG.md index fd64d0693..d3f78a3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -546,6 +546,16 @@ where they differed: the cost follows what the guest wrote. Reflinking is unchanged. On Windows the copy is marked sparse first. +### SGI Tezro (IP53 board, IP35 software platform) + +- Added a headless Tezro profile with the 700 MHz R16000 (`r16000_700`, the default; `cpu = "r16000"` takes it too), 4 MiB cache, 4 GiB RAM, a PIC with PXBOW, and the IO9's IOC4, ISP12160 and BCM5701. The 800, 900 and 1000 MHz chips (`r16000_800`, `r16000_900`, `r16000_1000`) are listed and take four processors; see the [Tezro guide](docs/tezro-ip53.md). +- A Tezro fits one, two or four processors, four by default, and two or four bring a second PIC at widget 11 on the PXBOW's link 11. Every fitted processor runs, each on its own host thread, so a default four-processor Tezro runs four. +- The Tezro's L1 names its node board for the chip and the processor count, as inventories print it (`IP53_4CPU` 030-1868-001 at 700 MHz, 030-1956-002 at 800 MHz, `IP59_4CPU` and `IP59_2CPU` at 1 GHz, `IP53_2CPU` and `IP53_1CPU` for the smaller 700 MHz boards), so the PROM tries only the fitted processor positions and a one- or two-processor Tezro's first boot does not power-cycle the module for its empty ones. +- The Tezro's IO9 bus has the TI PCI-to-PCI bridge every Tezro capture shows at device 2, with nothing behind it. +- A Tezro refuses a `banks` list whose two banks of a pair differ in size: Bedrock keeps one size code for each pair. +- The Tezro's BCM5701 reports PCI revision 0x15 and has the chip's capability list with vital product data reads, so IRIX's driver no longer waits out its VPD poll at every attach. +- The Tezro's PIC fails a configuration read of an empty slot as a master timeout, so the PROM no longer records two phantom devices per empty slot. + ### `IRIS_GUI_GL` works in iris-gui - The GUI's GL capture path (`IRIS_GUI_GL=1`, or `[debug] gui_gl_capture`) never diff --git a/FEATURES.md b/FEATURES.md index 445d3f4c4..c026c131a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -176,9 +176,9 @@ the first `[section]`, because TOML tables continue until the next header. | Key | Default | Values and behavior | |---|---|---| -| `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)), origin200_ip29 (configured by `[origin200]`, see [the Origin 200 guide](docs/origin200-ip29.md)), origin2000_ip27 (configured by `[origin2000]`, see [the Origin 2000 guide](docs/origin2000-ip27.md)), onyx2_ip27 (also configured by `[origin2000]`, see [the Onyx2 guide](docs/onyx2-ip27.md)), fuel_ip34 (configured by `[fuel]`, see [the Fuel guide](docs/fuel-ip34.md)). | -| `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000; Origin 200, Origin 2000 and Onyx2: r10000 (`r10000_180`), r12000 (`r12000_270`); Fuel: r14000 (`r14000_500`). The first listed is the default. | -| `machine.cpus` | `1` | Processors fitted. 2 only on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). | +| `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)), origin200_ip29 (configured by `[origin200]`, see [the Origin 200 guide](docs/origin200-ip29.md)), origin2000_ip27 (configured by `[origin2000]`, see [the Origin 2000 guide](docs/origin2000-ip27.md)), onyx2_ip27 (also configured by `[origin2000]`, see [the Onyx2 guide](docs/onyx2-ip27.md)), fuel_ip34 (configured by `[fuel]`, see [the Fuel guide](docs/fuel-ip34.md)), tezro_ip53 (configured by `[tezro]`, see [the Tezro guide](docs/tezro-ip53.md)). | +| `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000; Origin 200, Origin 2000 and Onyx2: r10000 (`r10000_180`), r12000 (`r12000_270`); Fuel: r14000 (`r14000_500`); Tezro: r16000 (a bare `r16000` is `r16000_700`; `r16000_800`, `r16000_900` and `r16000_1000` are the other parts it ships with). The first listed is the default. | +| `machine.cpus` | `1`; `4` on the Tezro | Processors fitted. 2 on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). 1, 2 or 4 on the Tezro, each run on its own host thread; one selects the one-processor board with one PIC, two or four the board with two PICs. | ### [origin2000] @@ -212,6 +212,26 @@ banks, and runs on a serial console. A `[fuel]` table is written to a saved config only when one of these keys is set. See the [Fuel guide](docs/fuel-ip34.md). +### [tezro] + +Tezro-specific settings apply when `machine.profile = "tezro_ip53"`. + +| Key | Default | Values and behavior | +|---|---|---| +| `flash` | unset | Persistent 16 MiB boot-flash file. Unset, the flash is volatile. | +| `console` | `l1` | `l1`, `ioc4_a`, `ioc4_b`, `ioc4_c` or `ioc4_d`. Selects the guest console connection. | +| `l1_port` | `8882` | Host TCP port for the L1 console. | +| `l1_log` | unset | Optional serial log path for L1 output when an IOC4 port is selected as console. | +| `serial_port_c`, `serial_port_d` | `8883`, `8884` | Host TCP ports of IOC4 ports 2 and 3; ports 0 and 1 use `serial_port_a` and `serial_port_b`. | + +The machine runs the 700 MHz R16000 (`r16000_700`, the default), or the 800, 900 +or 1000 MHz one (`r16000_800`, `r16000_900`, `r16000_1000`, with 4, 8 and 16 MiB +secondary caches; those take four processors). `banks` +takes 512 or 1024 MiB banks from bank 0; left unset, it is the 4 GiB layout of +1024, 1024 and four 512 MiB banks. A `[tezro]` table is written to a saved +config only when one of these keys is set. See the +[Tezro guide](docs/tezro-ip53.md), which lists its limits. + ### [graphics] | Key | Default | Values and behavior | diff --git a/HACKING.md b/HACKING.md index f09ef9687..437c9b813 100644 --- a/HACKING.md +++ b/HACKING.md @@ -16,15 +16,15 @@ Other documents: ## 1. What IRIS is -IRIS emulates six SGI workstations: the Indy (IP24), the Indigo2 (IP22), the -Indigo2 IMPACT (IP28), the O2 (IP32), the Octane (IP30) and the Fuel (IP34 -board, IP35 software platform). The CPU is an -R4400, R5000, R10000, R12000 or R14000, chosen at runtime from the parts the -machine shipped with (`src/cpu/parts.rs`). -Graphics is Newport (REX3), GR2 (XZ/Extreme) or IMPACT on the Indy and -Indigo2, GBE on the O2 and Solid IMPACT on the Octane; the Fuel takes a VPro -or runs headless on its serial console. It boots the real PROMs and IRIX 5.3 -and 6.5 to a desktop with networking. +IRIS emulates seven SGI workstations: the Indy (IP24), the Indigo2 (IP22), +the Indigo2 IMPACT (IP28), the O2 (IP32), the Octane (IP30), the Fuel (IP34 +board, IP35 software platform) and the Tezro (IP53 board, IP35 software +platform). The CPU is an R4400, R5000, R10000, R12000, R14000 or R16000, +chosen at runtime from the parts the machine shipped with +(`src/cpu/parts.rs`). Graphics is Newport (REX3), GR2 (XZ/Extreme) or IMPACT +on the Indy and Indigo2, GBE on the O2 and Solid IMPACT on the Octane; the +Fuel and the Tezro take a VPro or run headless on their serial consoles. It +boots the real PROMs and IRIX 5.3 and 6.5 to a desktop with networking. It is **not cycle-accurate**, anywhere. IRIX is interrupt-driven and does not care how many cycles an instruction takes, so IRIS models behaviour @@ -318,7 +318,9 @@ Every device that has real work to do runs on its own thread: | `ISP1040-0`, `ISP1040-1` | Octane SCSI request rings and disk transfers | | `ISP12160-0` | Fuel SCSI request ring for both ISP12160 channels | | `IOC3-RX-A`, `IOC3-RX-B`, `IOC3-NVRAM` | Octane and Fuel serial input per port; the DS1685 NVRAM file writer (IOC3's other deadlines are `hptimer` one-shots) | -| `l1-serial` | Fuel L1 console UART worker | +| `l1-serial` | Fuel and Tezro L1 console UART worker | +| `IOC4-RX-0` to `IOC4-RX-3` | Tezro IOC4 serial input | +| `bcm5701-rx`, `bcm5701-net` | Tezro BCM5701 receive pump and network backend | | `ip30-eth-rx`, `ip30-eth-net` | Octane ethernet receive pump; the network backend | | `odyssey-work`, `odyssey-raster`, `odyssey-pool-N`, `odyssey-display` | VPro: the front end decodes the queued command-FIFO stores (the processor's stores only queue); the raster unit sets each draw up and walks its primitives into 2x2 quads; a pool of workers (`[graphics] threads`, default 2) walks, shades and stores filled triangles and stores every other fragment, one screen tile at a time; and the display composes each whole frame from board memory (64-bit words, as main memory is: a field is a bit range of its word, read by shift and mask with no byte order, `vram.rs`) as the scan-out registers describe it, counts the retrace and presents the window at 60 Hz. No board-wide lock: the front end owns its state, an access that needs the drawing is answered busy until it is done, the display FIFO (PB&J's register file) has its own lock and never waits for the drawing (`display_fifo.rs`), and the display reads board memory while it is drawn, as the board's own scan-out does; nobody tracks which rows changed. The raster thread sets primitives up and writes each as one command in a ring of 64-byte slots (`pipe/ring.rs`): a triangle's record and bounding box, a quad it shaded itself (lines, points), or a clear's rectangle; the slot's index goes on the queue of every screen tile (`[graphics] tile`, default 32 pixels) it touches. A worker takes a ready tile and owns it until its queue is empty, walking each triangle within the tile and shading and storing quad by quad, so every pixel sees its fragments in command order with no other ordering, and the depth pre-test reads depth its own tile stored. A slot is retired once all its tiles have run it. The raster thread drains the ring before any other work on board memory (a texture upload waits for the shader alone). A completion (a fence's token, a query's words) does not drain: it rides the ring and runs once the ring retires past the quads queued before it. A quad's four pixels are lanes: the walk's depth, the texture unit's positions and the colour weights run on the host's SIMD unit (`fearless_simd`, `pipe/lanes.rs`, the level detected at start-up), bit for bit the scalar arithmetic. When its queue runs dry the raster thread does not drain: it keeps one count of the raster unit's pending jobs for the quads still in flight, and a front-end wait or a refused processor read asks it to store them. `IRIS_ODSY_SHADER=inline` makes the raster thread shade every quad itself, and `IRIS_ODSY_BACKEND=inline` stores on the raster thread with no pool. The texture registers a draw reads live in an array of draw state slots (`pipe/slot.rs`): the front end writes the current slot, a draw latches it and holds it until its job is given back, and a write while a queued draw holds the current slot first copies it to a free one, so the stages read their draw's registers with no lock | | `ioc3-eth-rx`, `ioc3-eth-net` | Fuel and Origin 200 IOC3 Ethernet receive pump and network backend | @@ -825,6 +827,14 @@ introspection commands are in [debug.md](debug.md#jit-v2). access returns what it changed elsewhere for the chip to act on. The node around it (the processor bus, flash, L1 and Hub-special page) is `src/ip35/node.rs`. Snapshots are unsupported. See the [Fuel guide](docs/fuel-ip34.md). +- **IP53 board, IP35 software platform (Tezro)**: its own board + (`src/ip53_machine.rs`) on the Bedrock node it shares with the Fuel + (`ip35::node`): one, two or four R16000s (700 MHz by default), each run on its own host + thread, memory from 512 and 1024 MiB banks (4 GiB by default), and a PIC + whose PXBOW is widget 0, carrying the IO9's IOC4, ISP12160 and BCM5701; + the two- and four-processor board adds a second PIC at widget 11. It runs + headless on the L1 or an IOC4 serial console. See the + [Tezro guide](docs/tezro-ip53.md). The Indy and Indigo2 profiles each have an embedded PROM fallback; `src/platform.rs` and `src/machine.rs` wire their differences, and diff --git a/HELP.md b/HELP.md index 77ace5234..b523a5e0e 100644 --- a/HELP.md +++ b/HELP.md @@ -1,7 +1,7 @@ # IRIS — SGI workstation emulator The complete core/GUI feature and configuration inventory is in -[FEATURES.md](FEATURES.md). For the Fuel, see the [Fuel guide](docs/fuel-ip34.md). +[FEATURES.md](FEATURES.md). For the Fuel, see the [Fuel guide](docs/fuel-ip34.md); for the Tezro, the [Tezro guide](docs/tezro-ip53.md). This guide explains everyday use. The monitor's full command reference is in [debug.md](debug.md). @@ -632,7 +632,7 @@ scsi_deferred_int = true # needed by OpenBSD/NetBSD; see --no-scsi-deferr [machine] profile = "indy_ip24" # "indigo2_ip22" (--ip22), "indigo2_ip28", or "o2_ip32" (an [o2] section: docs/o2-ip32.md); --machine NAME picks any -cpu = "r4400" # "r5000" (--cpu); IP28: "r10000", Octane: r10000/r12000/r14000, O2: r5000/r10000/r12000; or a part, "r14000_600" +cpu = "r4400" # "r5000" (--cpu); IP28: "r10000", Octane: r10000/r12000/r14000, O2: r5000/r10000/r12000, Tezro: r16000; or a part, "r14000_600", "r16000_800" # cpus = 2 # Octane only: a second processor (docs/octane-ip30-multiprocessor.md) [graphics] diff --git a/docs/tezro-ip53.md b/docs/tezro-ip53.md new file mode 100644 index 000000000..16ae48819 --- /dev/null +++ b/docs/tezro-ip53.md @@ -0,0 +1,201 @@ +# SGI Tezro (IP53 board, IP35 software platform) + +The Tezro is the IP53 board running the IP35 software platform, the same +Bedrock node as the [Fuel](fuel-ip34.md) with a different interface board. +The emulated configuration has R16000 processors (700, 800, 900 or 1000 MHz) with +secondary caches of 4, 4, 8 or 16 MiB, a Bedrock node with 4 GiB of RAM by default, and an IO9 +board behind a PIC PCI host bridge: an IOC4 with four serial ports, a +DS1744-compatible clock and a PS/2 keyboard and mouse, an ISP12160 with two +SCSI channels, and a BCM5701 gigabit Ethernet controller. The machine runs +headless on its L1 or IOC4 serial console. + +## Configuration + +Copy [`tezro.toml.example`](../tezro.toml.example) to `tezro.toml`, provide +the PROM and disk files named there, then run `iris --config tezro.toml` from +the repository root. The sample selects `machine.profile = "tezro_ip53"` and +`machine.cpu = "r16000_700"`, the 700 MHz R16000 (revision 2.1, the part +table's `R16000_700_EARLY`), which is +also what the machine runs with no `cpu` line or with the family name +`cpu = "r16000"`. The model takes four chips: `r16000_700`, `r16000_800` +(revision 2.2), `r16000_900` (revision 3.0, 8 MiB secondary cache) and +`r16000_1000` (revision 3.0, 16 MiB). Each revision is the one inventories +print. The 1 GHz part's PRId 0x0f50 is established (a PROM banner prints +`f50` beside hinv's 3.0 on one machine); 0x0f41, 0x0f42 and the 900 MHz +part's 0x0f50 are inferred: the register's major is the printed one plus 2 +for an R16000, the rule the Linux and OpenBSD CPU probes and PROM 6.210's +printers apply, and no raw read of a 2.x part is on record. No IP53 board at +900 MHz is captured (the 900 MHz parts on record are on Fuel boards), so the +900 MHz chip rests on the PROM's speed table alone. A chip the Tezro lacks +gets one error that names the machine and these four. The processor counts +follow the boards captured with each chip. The 700 MHz chip is fitted as +one, two or four. The 800 MHz chip is fitted as four only: every captured +board with it is an `IP53_4CPU`, and the two-processor 800 MHz board +(030-2029-001) carries revision 3.0 processors, another PRId. The 900 MHz +chip is fitted as four only, a model choice, since no board at that clock +is captured. The 1 GHz chip is fitted as two or four, as the captured +`IP59_4CPU` and `IP59_2CPU` boards are. Every chip boots the PROM and IRIX +to login with four processors. The 1 GHz chip's mode word is computed, so +its clean boot shows the model is self-consistent, not that hardware uses +that word. An 800 MHz part with an 8 MB cache (PROM row 0x549caf85) is on +Onyx 350 inventories but on no Tezro capture found, and is not modelled. The +Tezro tower guide gives every processor 4 MB of cache; the captures, not the +manual, back the 8 and 16 MB caches. The processor clock in the flash record and the count rate (half +the clock) follow the chip; `[clock] fixed_mhz` still sets the count rate and +does not select a chip. The secondary cache clock and the cache tap (0xc, 0xa, +0xb and 0x15 from the 700 MHz chip up) are not modelled; the tap appears only +as part of the flash record's mode word. The board's table of chips, with how +well each value is established, is in `src/ip53_machine.rs`; the parts +themselves, with the PRId rule, are in `src/cpu/parts.rs`. + +The L1 names the node board for the chip and the processor count, as +inventories print it: + +| Chip | Processors | Board | Grade | +|---|---|---|---| +| 700 MHz | 4 | `IP53_4CPU` 030-1868-001 -C | established (Tezro and Origin 350 inventories) | +| 700 MHz | 2 | `IP53_2CPU` 030-1869-001 -C | inferred (inventories outside the captures kept here) | +| 700 MHz | 1 | `IP53_1CPU`, no part or revision | inferred for the name (the PROM's table); no board captured | +| 800 MHz | 4 | `IP53_4CPU` 030-1956-002 -C | established for the part (both 800 MHz Tezro captures; they print -B and -C) | +| 900 MHz | 4 | `IP53_4CPU`, no part or revision | approximated: no 900 MHz IP53 or IP59 board captured | +| 1 GHz | 4 | `IP59_4CPU` 030-1989-003 -C | inferred: the part is captured with this chip on Onyx 350s, no 1 GHz Tezro capture names its board; revisions -A to -D are seen | +| 1 GHz | 2 | `IP59_2CPU` 030-2059-002 -A | inferred (one rack Tezro record) | + +The PROM's socket masks for the IP59 names equal the IP53 ones, and nothing +in the PROM or the L1 reads the part or revision, so the names change what +inventories print and not how the machine boots. + +`[machine] cpus` is the number of processors fitted: 1, 2 or 4, with 4 the +default, as in the Tezros whose inventories have been seen. The count selects +the interface board. The one-processor board has one PIC at XIO widget 15 +carrying the IO9. The two- and four-processor board adds a second PIC at +widget 11, with nothing on its buses. It sits on link 11 of the IO9 PIC's +PXBOW, beside the hub's link 10 and the IO9's link 15, so the PROM and IRIX +find it through that crossbar's link status. Whether it has a crossbar of +its own in front of it is not known. + +The IO9's PCI bus, head 0 of the PIC at widget 15, holds the IOC4 at device +1, a TI PCI2050 PCI-to-PCI bridge (`104c:ac28`, revision 1) at device 2, the +ISP12160 at device 3 and the BCM5701 at device 4. Every Tezro capture that +prints widget 15 shows the TI bridge there, and no Origin 350 or Onyx 350 IO9 +capture does; why is not known. No capture shows anything behind it, so its +secondary bus is empty (inferred), and only its type 1 header is modelled. +IRIX lists it as an unknown adapter, as on a real Tezro. + +Memory comes from `banks`: one 512 or 1024 MiB bank per locational bank, from +bank 0 with no gaps, up to eight. The IP53 board takes 512 MB and 1 GB DIMMs +only (Origin 350 guide 007-4566-001, Table 3-3: established for the board; +inferred for the Tezro, whose guide names no size), and each DIMM pair +provides two banks of the DIMM's size. Bedrock's memory controller holds one +size code for each pair of banks (0 and 1, 2 and 3, and so on), so a +population whose pair differs in size is refused. The model still accepts +an odd last bank, which the controller can enable but the board, fitted in +DIMM pairs, cannot hold. Left unset, the machine fits +the 4 GiB layout of 1024, 1024 and four 512 MiB banks, which follows QEMU and +matches a captured Tezro. One processor needs at least 512 MiB, two or four at least +1 GiB. + +The `[tezro]` settings also select the flash backing file, the console and +the L1 port and log. The console options are `l1` (the default) and `ioc4_a` +to `ioc4_d`, the IOC4's ports 0 to 3. IOC4 ports 0 and 1 listen on +`serial_port_a` and `serial_port_b`, ports 2 and 3 on `tezro.serial_port_c` +and `tezro.serial_port_d`. `nvram` names the 32 KiB DS1744 store; its clock +reference is kept beside it with a `.clock` suffix. + +## Firmware inputs + +The PROM is the IP35 PROM 6.210 payload, given raw or in a supported +version-six, single-segment container, as for the Fuel. A fresh flash is +formatted with the payload in the first 8 MiB position and its configuration +block replaced by the chip's: the PROM's own 0x50-byte layout with the chip's +mode word and clock, machine type 0 (the class PROM 6.210 derives from the +modelled board word 0x2001; approximated), an edit counter of one, and the +compensation byte the PROM's editor would choose, so the block sums to what the +block it replaced did and the whole image still sums to zero. The volume label +goes in the second position. A flash store that already exists keeps the +configuration block it holds, so a block the PROM rewrote survives the next +start. The processor resets with the matching Config, `0x6c9bad85` for the 700 +MHz chip. + +The PROM brings up only the processor positions the node board's product +name allows (IP53_1CPU position A, IP53_2CPU A and C, IP53_4CPU all four), +and all four when the name matches none of its rows. The L1 names the board +for the processor count, so the PROM tries no empty position and a fresh +flash boots straight through. With a name that matches no row, such as the +Fuel's placeholder, a one- or two-processor board power-cycles the module +three times for each empty position it tries before it writes a +`Disable` record and carries on. + +The mode word is a different field from the processor's Config: its low 25 +bits are the Config reset bits, and the bits above carry, among others, the +cache tap. The mode words of the 700, 800 and 900 MHz chips (`0x589bad85`, +`0x549baf85`, `0x569cb185`) are rows of the stock PROM's CPU speed table. The +table stops at 900 MHz, so the 1 GHz word `0x4aedb385` is built from the +formulas the PROM's editor uses for the other rows and the cache clock and tap +its inventories print; it is approximated. A flash store keeps the block it was +formatted with, and the PROM checks its mode word against the processor's, so +give a different chip a fresh `[tezro] flash` file. + +## Limits + +Every fitted processor runs, each on its own host thread. The IP35 PROM +assumes its processors left reset together and run at hardware speed. Its +reset barrier on LB scratch 1 depends on that: every processor clears the +word before the boot master stores its release there. Bedrock's LB orders +those stores itself, by who stored in what order, so a late clear does not +land on the release (`src/dev/bedrock/lb.rs`); hardware drops no store, so +this is a model rule. An NMI, and a write that clears a running processor's +enable bit, are logged by the Bedrock PI and not carried out: the CPU has no +NMI entry. + +The R16000 model leaves out three R16000-specific choices that QEMU's model +makes: the CP0 diagnostic register's mask and reset value, a trap on a +subnormal operand while FCSR.FS is clear, and the limit on CVT.L results. +Those cases follow the behaviour the rest of the R10000 family has in Iris. +No R16000 FIR was found in the retained evidence for any chip; the model +reports `0x0900`, the one R14000 value on record, as a model choice. + +Bedrock board word `0x2001` has class 2: a real IP53 or IP59 board's class +is not 4, 8 or 0xc, because Tezro and Origin 350 PROM banners lack the lines +the PROM prints for those classes (established), and OpenBSD names class 2 +for this board family (inferred; no raw read). Its low nibble is a software +choice that only the PROM's last-resort table fallback reads. The L1 brick +type `0x39` (what the L1 firmware gives a tower interface board; inferred +that a real Tezro answers it) with revision 1.48.1 and the router `brick` +reply, the PIC's revision 3 profile and REQ64# on the BCM5701's slot are +software choices of the QEMU Tezro profiles. The PCI devices and their +revisions (IOC4 0x53, TI bridge 1, ISP12160 6, BCM5701 0x15) are as Tezro +consoles print them. Device d's INTA reaching PIC input d - 1 is what +IRIX's pin rule assigns and what an Origin 350 reports under OpenBSD; the +board wiring, and INTB to INTD, are unknown. No EEPROM image is fitted for +the IO9 board, the node board or the BCM5701, because no published source +gives their contents. The BCM5701 still answers vital product data reads +through its capability list, all ones without an EEPROM, so IRIX's driver +does not wait out its poll at every attach. The L1 gives the node board's +record the flash block's CPU configuration, which the PROM checks the flash +against, and the board identity in the table above; no real board's record +has been read. The brick serial is `EMUN00`, a stand-in: a real tower +answers with its interface board's six-character serial, three letters and +three digits, but the PROM converts the reply into the node's identity, so +its shape is left to the machine-identity settings and a measurement. The +L1 serves no records for the tower's other boards (interface, IO9, +graphics, DIMMs): which component number each takes is not known, and the +bytes cannot be authored from the captures. +PIC DMA to any widget other than Bedrock IO is refused. Where two PCI +functions decode the same range, as a probing PROM leaves them, the lower +device answers; what silicon does there is not defined. A configuration read +of an empty PCI slot fails as a master timeout; the error enable gating and +the treatment of device 0 there are inferred. + +The IRIX 6.5.30 disks tested so far were installed on an Origin 300, not a +Tezro. On them the IOC4 serial ports appear as `tty3` and `tty4`, as on +real single-node IP59 inventories. + +Graphics is not attached. Machine snapshots are unsupported. + +## Source basis + +The board composition follows the project's BSD-licensed QEMU SGI models in +`hw/mips/sgi/sn-machine.c` and `hw/mips/sgi/sn1-node.c` and their Tezro +profile layers. That source is an implementation reference, not proof of +hardware behavior. diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 5743d842a..47675f0d2 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -2031,10 +2031,10 @@ fn show_performance(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { ui.horizontal(|ui| { ui.add_enabled_ui(octane, |ui| { ComboBox::from_id_salt("machine_cpus") - .selected_text(format!("{}", cfg.machine.cpus)) + .selected_text(format!("{}", cfg.machine.cpus())) .show_ui(ui, |ui| { for n in [1u8, 2] { - ui.selectable_value(&mut cfg.machine.cpus, n, format!("{n}")); + ui.selectable_value(&mut cfg.machine.cpus, Some(n), format!("{n}")); } }); }); diff --git a/src/config.rs b/src/config.rs index ecfdbc1cf..5591724ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -679,6 +679,10 @@ pub enum MachineProfile { /// SGI Fuel: the IP34 board on the IP35 software platform, headless or /// with a VPro in its graphics slot. See `src/ip34_machine.rs`. FuelIp34, + /// Serial-only Tezro: the IP53 board on the IP35 software platform, + /// configured by the shared keys and the `[tezro]` section; see + /// `src/ip53_machine.rs`. + TezroIp53, } impl MachineProfile { @@ -703,6 +707,7 @@ impl MachineProfile { Self::O2Ip32 => &[0, 32, 64, 128, 256], Self::Indigo2Ip28 => MC_BANK_SIZES, Self::FuelIp34 => &[0, 256], + Self::TezroIp53 => &[0, 512, 1024], _ => &[0, 8, 16, 32, 64, 128], } } @@ -727,7 +732,7 @@ impl MachineProfile { /// Every profile, in the order `workstation::build` lists them. `ALL` is /// only the ones the GUI offers. - pub const EVERY: [Self; 9] = [ + pub const EVERY: [Self; 10] = [ Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28, @@ -737,6 +742,7 @@ impl MachineProfile { Self::Origin2000Ip27, Self::Onyx2Ip27, Self::FuelIp34, + Self::TezroIp53, ]; /// The name `[machine] profile` and `--machine` give this profile. @@ -751,6 +757,7 @@ impl MachineProfile { Self::Origin2000Ip27 => "origin2000_ip27", Self::Onyx2Ip27 => "onyx2_ip27", Self::FuelIp34 => "fuel_ip34", + Self::TezroIp53 => "tezro_ip53", } } @@ -767,16 +774,21 @@ impl MachineProfile { Self::EVERY.iter().map(|p| format!("{:<14}{}\n", p.name(), p.label())).collect() } - /// The processors fitted at most: two on the Octane, one elsewhere. + /// The processors fitted at most: four on the Tezro, two on the Octane, + /// one elsewhere. pub fn max_cpus(self) -> u8 { - if self == Self::OctaneIp30 { 2 } else { 1 } + match self { + Self::TezroIp53 => 4, + Self::OctaneIp30 => 2, + _ => 1, + } } /// Whether this profile can have `cpus` processors, and why not if it /// cannot. pub fn accepts_cpus(self, cpus: u8) -> Result<(), String> { if cpus != 1 && self.max_cpus() == 1 { - Err(format!("[machine] cpus = {cpus}: only the Octane has more than one processor")) + Err(format!("[machine] cpus = {cpus}: only the Octane and the Tezro have more than one processor")) } else if !(1..=self.max_cpus()).contains(&cpus) { Err(format!("[machine] cpus = {cpus}: IRIX runs an Octane with 1 or 2")) } else { @@ -809,6 +821,7 @@ impl MachineProfile { Self::Origin2000Ip27 => "SGI Origin 2000 (IP27)", Self::Onyx2Ip27 => "SGI Onyx2 (IP27)", Self::FuelIp34 => "SGI Fuel (IP34)", + Self::TezroIp53 => "SGI Tezro (IP53)", } } @@ -818,7 +831,7 @@ impl MachineProfile { /// connector), one on the Indy. pub fn scsi_controllers(self) -> u8 { match self { - Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 | Self::FuelIp34 => 2, + Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 | Self::FuelIp34 | Self::TezroIp53 => 2, _ => 1, } } @@ -827,19 +840,23 @@ impl MachineProfile { /// buses are wide, with ids 0 to 15 and the controller on 0. The Indy's /// and Indigo2's WD33C93A chips are narrow, with ids 0 to 7. The O2's /// Adaptec buses are wide too, but stay at 7 until they are widened on - /// purpose. The Fuel's ISP12160 buses are wide. + /// purpose. The Fuel's and the Tezro's ISP12160 buses are wide. pub fn max_scsi_id(self) -> u8 { match self { - Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 | Self::FuelIp34 => 15, + Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 | Self::FuelIp34 | Self::TezroIp53 => 15, _ => 7, } } /// How many memory banks the machine has: four on the MC, HEART and O2 - /// (the O2's four DIMM pairs), two on Fuel. `banks` has room for more; - /// unused entries must be zero. + /// (the O2's four DIMM pairs), two on Fuel, eight on Tezro. `banks` has + /// room for more; unused entries must be zero. pub fn memory_banks(self) -> usize { - if self == Self::FuelIp34 { 2 } else { 4 } + match self { + Self::FuelIp34 => 2, + Self::TezroIp53 => MAX_BANKS, + _ => 4, + } } pub fn guinness(self) -> bool { @@ -855,6 +872,7 @@ impl MachineProfile { Self::O2Ip32 => crate::ip32_machine::CPUS, Self::Indigo2Ip28 => crate::machine::IP28_CPUS, Self::FuelIp34 => crate::ip34_machine::CPUS, + Self::TezroIp53 => crate::ip53_machine::CPUS, Self::IndyIp24 | Self::Indigo2Ip22 => crate::machine::CPUS, } } @@ -969,6 +987,7 @@ impl GraphicsBoard { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => self == Self::None, MachineProfile::FuelIp34 => matches!(self, Self::V10 | Self::V12 | Self::None), + MachineProfile::TezroIp53 => self == Self::None, MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -979,7 +998,7 @@ impl GraphicsBoard { match profile { MachineProfile::OctaneIp30 => Self::SolidImpact, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => Self::None, - MachineProfile::FuelIp34 => Self::None, + MachineProfile::FuelIp34 | MachineProfile::TezroIp53 => Self::None, _ => Self::Newport, } } @@ -1198,18 +1217,28 @@ pub struct MachineSection { pub cpu: Option, #[serde(default)] pub cache: CacheModel, - /// Processors fitted. More than one only on the Octane. - #[serde(default = "default_cpus")] - pub cpus: u8, + /// Processors fitted, or the machine's default when unset: read it with + /// [`MachineSection::cpus`]. More than one only on the Octane and the + /// Tezro. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cpus: Option, } -fn default_cpus() -> u8 { - 1 +impl MachineSection { + /// The processors fitted: `cpus` as set, or the profile's default. A + /// machine owns its own default (the Tezro's is + /// `ip53_machine::DEFAULT_CPUS`); every other profile fits one. + pub fn cpus(&self) -> u8 { + self.cpus.unwrap_or(match self.profile { + MachineProfile::TezroIp53 => crate::ip53_machine::DEFAULT_CPUS, + _ => 1, + }) + } } impl Default for MachineSection { fn default() -> Self { - Self { profile: MachineProfile::default(), cpu: None, cache: CacheModel::default(), cpus: default_cpus() } + Self { profile: MachineProfile::default(), cpu: None, cache: CacheModel::default(), cpus: None } } } @@ -1877,6 +1906,76 @@ impl FuelSection { } } +/// The Tezro's serial connections: the L1 and the IOC4's four ports. The +/// selected console serves CI and serial_log. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TezroConsole { + #[default] + L1, + Ioc4A, + Ioc4B, + Ioc4C, + Ioc4D, +} + +impl TezroConsole { + /// The connection's index: 0 for the L1, then the IOC4's ports. + pub fn index(self) -> usize { + self as usize + } +} + +/// The processor counts a Tezro board fits. +pub const TEZRO_CPUS: [u8; 3] = [1, 2, 4]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TezroSection { + /// Persistent 16 MiB boot flash store; absent means volatile flash. + #[serde(default)] + pub flash: Option, + #[serde(default)] + pub console: TezroConsole, + /// L1 host console TCP port; IOC4 ports 0 and 1 keep serial_port_a and + /// serial_port_b. + #[serde(default)] + pub l1_port: Option, + /// Separate L1 log when an IOC4 port is selected as console. + #[serde(default)] + pub l1_log: Option, + /// Host TCP ports of IOC4 ports 2 and 3. + #[serde(default)] + pub serial_port_c: Option, + #[serde(default)] + pub serial_port_d: Option, +} + +impl Default for TezroSection { + fn default() -> Self { + Self { + flash: None, + console: TezroConsole::default(), + l1_port: None, + l1_log: None, + serial_port_c: None, + serial_port_d: None, + } + } +} + +impl TezroSection { + /// Left out of a written config when nothing in it was set. + fn is_default(&self) -> bool { + self.flash.is_none() + && self.console == TezroConsole::default() + && self.l1_port.is_none() + && self.l1_log.is_none() + && self.serial_port_c.is_none() + && self.serial_port_d.is_none() + } +} + /// Top-level machine configuration. /// /// Field order matters for TOML export: the `toml` serializer requires every @@ -1922,6 +2021,12 @@ pub struct MachineConfig { #[serde(default = "default_banks", with = "banks_serde")] pub banks: [u32; MAX_BANKS], + /// Whether `banks` was given (in the config file or by `--bank0..3`), as + /// opposed to left at the generic default, so that a value equal to the + /// default still counts as chosen. Only the Tezro reads it. + #[serde(skip)] + pub banks_set: bool, + /// Window scale factor (1 = native, 2 = 2× for HiDPI/4K). CLI --2x overrides this. #[serde(default = "default_scale")] pub scale: u32, @@ -2135,6 +2240,10 @@ pub struct MachineConfig { #[serde(default, skip_serializing_if = "FuelSection::is_default")] pub fuel: FuelSection, + /// Processors, serial console and flash storage for the Tezro. + #[serde(default, skip_serializing_if = "TezroSection::is_default")] + pub tezro: TezroSection, + /// Host performance tuning (`[perf]` section). #[serde(default)] pub perf: PerfConfig, @@ -2329,6 +2438,7 @@ impl Default for MachineConfig { nvram: default_nvram(), nveeprom: default_nveeprom(), banks: default_banks(), + banks_set: false, scsi: default_scsi(), scale: default_scale(), nfs: None, @@ -2374,6 +2484,7 @@ impl Default for MachineConfig { origin200: Origin200Section::default(), origin2000: Origin2000Section::default(), fuel: FuelSection::default(), + tezro: TezroSection::default(), perf: PerfConfig::default(), clock: ClockConfig::default(), rtc_offset: RtcOffset::default(), @@ -2471,6 +2582,9 @@ impl MachineConfig { MachineProfile::FuelIp34 => Err( "[virtio_serial] is not wired on the Fuel: its XBridge has no virtio-pci attachment".into(), ), + MachineProfile::TezroIp53 => Err( + "[virtio_serial] is not wired on the Tezro: its PIC has no virtio-pci attachment".into(), + ), MachineProfile::O2Ip32 => Err( "[virtio_serial] is not wired on the O2 yet: the virtio-pci transport exists, but the \ MACE PCI bridge has no interrupt route for a third slot (see docs/dev/virtio-serial.md)" @@ -2520,6 +2634,16 @@ impl MachineConfig { [self.serial_port_a.unwrap_or(a), self.serial_port_b.unwrap_or(b)] } + /// Parse a config file's text, remembering whether it listed `banks`, and + /// folding in the spellings older versions wrote. + fn parse_toml(text: &str) -> Result { + let mut cfg = toml::from_str::(text)?; + let table = toml::from_str::(text)?; + cfg.banks_set = table.contains_key("banks"); + cfg.fold_deprecated(); + Ok(cfg) + } + /// Load from `iris.toml` if it exists, otherwise return defaults. /// /// A *missing* file is fine (defaults are used). A file that exists but @@ -2543,11 +2667,8 @@ impl MachineConfig { let Ok(text) = std::fs::read_to_string(path) else { return Ok(Self::default()); }; - match toml::from_str::(&text) { - Ok(mut cfg) => { - cfg.fold_deprecated(); - Ok(cfg) - } + match Self::parse_toml(&text) { + Ok(cfg) => Ok(cfg), Err(e) => { let mut msg = format!("failed to parse {path}:\n{e}"); // Common footgun: backslashes in a double-quoted string (Windows @@ -2653,6 +2774,22 @@ impl MachineConfig { self.banks[..n].to_vec() } + /// The Tezro's memory: `banks` from locational bank 0, or the board's + /// 4 GiB default when `banks` was never set (the generic default names a + /// bank size no Tezro takes, so a `banks` equal to it is a mistake to + /// report, not a request for the default). + pub fn tezro_memory(&self) -> Result { + use crate::ip53_machine::MemoryPopulation; + if !self.banks_set && self.banks == default_banks() { + return Ok(MemoryPopulation::default()); + } + let n = self.banks.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1); + if let Some(gap) = self.banks[..n].iter().position(|&b| b == 0) { + return Err(format!("banks: bank {gap} is empty but a later bank is populated; fill the banks from 0")); + } + MemoryPopulation::from_mib(&self.banks[..n]).map_err(|e| format!("banks: {e}")) + } + /// Validate bank sizes, returns a description of any errors. /// The graphics board: `[graphics] board`, or the machine's default /// (Solid IMPACT on Octane, none on Fuel, Newport elsewhere). @@ -2687,7 +2824,7 @@ impl MachineConfig { self.xio_widgets()?; self.validate_nvme()?; self.cpu_part()?; - self.machine.profile.accepts_cpus(self.machine.cpus)?; + self.machine.profile.accepts_cpus(self.machine.cpus())?; if self.graphics.heads != 1 && self.graphics.heads != 2 { return Err(format!( "graphics.heads {} is invalid (valid: 1, 2)", @@ -2754,7 +2891,7 @@ impl MachineConfig { "scsi.{} and scsi.{} both name {}", other, key, scsi_label(controller, id) )); } - if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 | MachineProfile::FuelIp34) { + if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 | MachineProfile::FuelIp34 | MachineProfile::TezroIp53) { return Err(format!( "scsi.{}: kind = \"daynaport\" is not available on {} (its Ethernet is built in)", key, self.machine.profile.label() )); @@ -2831,6 +2968,15 @@ impl MachineConfig { if self.fuel.brick_serial.as_ref().is_some_and(|s| s.len() > 37 || s.contains('\0')) { return Err("fuel.brick_serial must be at most 37 bytes with no NUL".into()); } + } else if self.machine.profile == MachineProfile::TezroIp53 { + if !TEZRO_CPUS.contains(&self.machine.cpus()) { + return Err(format!("[machine] cpus = {} is not a Tezro board's population (valid: {TEZRO_CPUS:?})", self.machine.cpus())); + } + crate::ip53_machine::chip(self.cpu()).accepts_cpus(self.machine.cpus())?; + self.tezro_memory()?.check_for_cpus(usize::from(self.machine.cpus())).map_err(|e| format!("banks: {e}"))?; + if board != GraphicsBoard::None { + return Err("Tezro currently supports graphics.board = \"none\" only".into()); + } } else if self.machine.profile == MachineProfile::OctaneIp30 { // A HEART bank is a matched DIMM pair: 64 MB to 2 GB, filled from // bank 0 without a gap, 8 GB at most (SGI's memory upgrade guide). @@ -2876,8 +3022,8 @@ impl MachineConfig { } } match (self.network.mode, self.machine.profile) { - (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::Origin200Ip29 && p != MachineProfile::Origin2000Ip27 && p != MachineProfile::Onyx2Ip27 && p != MachineProfile::FuelIp34 => { - return Err("network.mode = \"off\" is only available on the O2, the Octane, the Fuel and the Origin 200 and 2000 for now; the Indy and Indigo2 always have their Ethernet".into()); + (NetMode::Off, p) if !matches!(p, MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 | MachineProfile::FuelIp34 | MachineProfile::TezroIp53) => { + return Err("network.mode = \"off\" is only available on the O2, the Octane, the Fuel, the Tezro and the Origin 200 and 2000 for now; the Indy and Indigo2 always have their Ethernet".into()); } (NetMode::Stream, _) => { let ep = self.network.stream.as_deref() @@ -2958,7 +3104,7 @@ impl MachineConfig { if cfg!(feature = "lightning") { return Err("jitv2.compile = \"sync\" is not available in a `lightning` build, which always compiles on the pool".into()); } - if self.machine.cpus > 1 { + if self.machine.cpus() > 1 { return Err("jitv2.compile = \"sync\" needs one CPU: several share the compile pool's threads".into()); } } @@ -3350,10 +3496,10 @@ impl Cli { if let Some(p) = &self.prom { cfg.prom = p.clone(); } if let Some(p) = &self.nvram { cfg.nvram = p.clone(); } if let Some(p) = &self.nveeprom { cfg.nveeprom = p.clone(); } - if let Some(v) = self.bank0 { cfg.banks[0] = v; } - if let Some(v) = self.bank1 { cfg.banks[1] = v; } - if let Some(v) = self.bank2 { cfg.banks[2] = v; } - if let Some(v) = self.bank3 { cfg.banks[3] = v; } + if let Some(v) = self.bank0 { cfg.banks[0] = v; cfg.banks_set = true; } + if let Some(v) = self.bank1 { cfg.banks[1] = v; cfg.banks_set = true; } + if let Some(v) = self.bank2 { cfg.banks[2] = v; cfg.banks_set = true; } + if let Some(v) = self.bank3 { cfg.banks[3] = v; cfg.banks_set = true; } if let Some(n) = self.jitv2_threads { cfg.jitv2.threads = n; } if let Some(m) = self.jitv2_compile { cfg.jitv2.compile = m; } @@ -3833,7 +3979,7 @@ mod export_tests { cfg.validate().expect("sync compile on the default machine"); cfg.machine.profile = MachineProfile::OctaneIp30; cfg.machine.cpu = Some("r10000".into()); - cfg.machine.cpus = 2; + cfg.machine.cpus = Some(2); assert!(cfg.validate().unwrap_err().contains("one CPU")); } @@ -4005,7 +4151,7 @@ mod export_tests { assert_eq!(apply(&["--ip22"]).machine.profile, MachineProfile::Indigo2Ip22); assert!(Cli::try_parse_from(["iris", "--ip22", "--machine", "o2_ip32"]).is_err()); let e = MachineProfile::from_name("vax").unwrap_err(); - assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, origin200_ip29, origin2000_ip27, onyx2_ip27, fuel_ip34)"); + assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, origin200_ip29, origin2000_ip27, onyx2_ip27, fuel_ip34, tezro_ip53)"); let list = MachineProfile::list(); assert_eq!(list.lines().count(), MachineProfile::EVERY.len()); assert!(list.starts_with("indy_ip24 SGI Indy (IP24)\n"), "{list}"); @@ -4014,7 +4160,8 @@ mod export_tests { for p in MachineProfile::EVERY { match p { MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 - | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 | MachineProfile::FuelIp34 => {} + | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 | MachineProfile::FuelIp34 + | MachineProfile::TezroIp53 => {} } let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"{}\"", p.name())).unwrap(); assert_eq!(cfg.machine.profile, p); @@ -4030,19 +4177,24 @@ mod export_tests { assert!(accepts(MachineProfile::OctaneIp30, "r14000") && !accepts(MachineProfile::OctaneIp30, "r5000")); assert!(accepts(MachineProfile::IndyIp24, "r5000") && !accepts(MachineProfile::IndyIp24, "r10000")); assert!(accepts(MachineProfile::FuelIp34, "r14000") && !accepts(MachineProfile::FuelIp34, "r10000")); + assert!(accepts(MachineProfile::TezroIp53, "r16000") && !accepts(MachineProfile::TezroIp53, "r14000")); // The counts are what the checks in validate() and the Octane // constructor allowed before they moved here: one on every machine // but the Octane, one or two on the Octane. - for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::TezroIp53] { for cpus in 0..=4u8 { - let old_ok = if p == MachineProfile::OctaneIp30 { (1..=2).contains(&cpus) } else { cpus == 1 }; + let old_ok = match p { + MachineProfile::OctaneIp30 => (1..=2).contains(&cpus), + MachineProfile::TezroIp53 => (1..=4).contains(&cpus), + _ => cpus == 1, + }; assert_eq!(p.accepts_cpus(cpus).is_ok(), old_ok, "{p:?} with {cpus}"); } - if p != MachineProfile::OctaneIp30 { + if !matches!(p, MachineProfile::OctaneIp30 | MachineProfile::TezroIp53) { assert_eq!( p.accepts_cpus(2).unwrap_err(), - "[machine] cpus = 2: only the Octane has more than one processor" + "[machine] cpus = 2: only the Octane and the Tezro have more than one processor" ); } } @@ -4051,11 +4203,11 @@ mod export_tests { octane.machine.profile = MachineProfile::OctaneIp30; octane.machine.cpu = Some("r10000".into()); octane.banks = banks_from4([128, 0, 0, 0]); - octane.machine.cpus = 2; + octane.machine.cpus = Some(2); octane.validate().expect("two processors"); - octane.machine.cpus = 3; + octane.machine.cpus = Some(3); assert!(octane.validate().unwrap_err().contains("1 or 2")); - octane.machine.cpus = 1; + octane.machine.cpus = Some(1); octane.machine.cpu = Some("r4400".into()); assert!(octane.validate().unwrap_err().contains("takes [machine] cpu = r10000_195")); @@ -4588,3 +4740,219 @@ mod fuel_input_tests { assert!(cfg.validate().unwrap_err().contains("out of range")); } } + +#[cfg(test)] +mod tezro_config_tests { + use super::*; + + fn tezro() -> MachineConfig { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::TezroIp53; + cfg.set_cpu(&parts::R16000_700_EARLY); + cfg.scsi.clear(); + cfg + } + + /// Left at the generic default, `banks` gives the Tezro its 4 GiB layout; + /// a listed population is taken bank by bank. + #[test] + fn tezro_memory_defaults_to_the_4_gib_layout_and_takes_listed_banks() { + let mut cfg = tezro(); + cfg.validate().unwrap(); + assert_eq!(cfg.tezro_memory().unwrap(), crate::ip53_machine::MemoryPopulation::default()); + cfg.banks = [1024, 1024, 512, 0, 0, 0, 0, 0]; + cfg.validate().unwrap(); + assert_eq!(cfg.tezro_memory().unwrap().banks_mib(), [1024, 1024, 512]); + cfg.banks = [1024, 512, 0, 0, 0, 0, 0, 0]; + assert!(cfg.validate().unwrap_err().contains("share one size")); + cfg.banks = [1024, 0, 512, 0, 0, 0, 0, 0]; + assert!(cfg.validate().unwrap_err().contains("fill the banks")); + cfg.banks = [256, 256, 0, 0, 0, 0, 0, 0]; + assert!(cfg.validate().unwrap_err().contains("512 or 1024")); + } + + /// A `[machine]` with no `cpu` gets the Tezro's first part, the 700 MHz + /// R16000, and so does the bare family name and its full name (`r16000_700` + /// is the early part: it is the only 700 MHz one listed); a part name + /// selects that part. `--cpu` overrides all of it. + #[test] + fn an_unset_cpu_defaults_to_the_first_part_and_a_bare_family_name_takes_it() { + let load = |text: &str| MachineConfig::parse_toml(text).unwrap(); + let tezro = load("[machine]\nprofile = \"tezro_ip53\"\n"); + assert_eq!(tezro.cpu(), &parts::R16000_700_EARLY); + tezro.validate().unwrap(); + for name in ["r16000", "r16000_700"] { + let first = load(&format!("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"{name}\"\n")); + assert_eq!(first.cpu_part(), Ok(&parts::R16000_700_EARLY), "{name} takes the first part"); + } + let fast = load("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"r16000_800\"\n"); + assert_eq!(fast.cpu(), &parts::R16000_800); + fast.validate().unwrap(); + let mut two = fast.clone(); + two.machine.cpus = Some(2); + assert!(two.validate().unwrap_err().contains("4 processors only"), "the 800 MHz part with two"); + let cli = Cli::parse_from(["iris", "--cpu", "r16000_800"]); + assert_eq!(cli.apply(tezro).cpu(), &parts::R16000_800, "--cpu names a part"); + let fuel = load("banks = [256, 256]\n[machine]\nprofile = \"fuel_ip34\"\n"); + assert_eq!(fuel.cpu(), &parts::R14000_500); + } + + /// A part the Tezro lacks is one generic error that names the machine and + /// the parts it takes, and so is a name that is not a part. + #[test] + fn a_part_the_tezro_lacks_is_refused_naming_its_parts() { + let load = |cpu: &str| MachineConfig::parse_toml(&format!("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"{cpu}\"\n")).unwrap(); + let parts = "SGI Tezro (IP53) takes [machine] cpu = r16000_700 or r16000_800 or r16000_900 or r16000_1000"; + for cpu in ["r4400", "r10000", "r12000_360", "r14000", "r16000_700_4mb", "r16000_800_8mb", "r16000_1000_16mb", "r16000_600"] { + assert_eq!(load(cpu).validate().unwrap_err(), parts, "{cpu}"); + } + } + + /// The file loader applies the same defaults as the text parser, and + /// refuses an unknown CPU value when the config is checked. + #[test] + fn try_load_applies_tezro_defaults_and_cpu_errors() { + let dir = std::env::temp_dir().join(format!("iris-tezro-try-load-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("tezro.toml"); + let load = |text: &str| { + std::fs::write(&path, text).unwrap(); + MachineConfig::try_load(path.to_str().unwrap()).unwrap() + }; + let cfg = load("[machine]\nprofile = \"tezro_ip53\"\n"); + assert_eq!(cfg.cpu(), &parts::R16000_700_EARLY); + assert!(!cfg.banks_set); + assert_eq!(cfg.tezro_memory().unwrap().total_mib(), 4096); + let e = load("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"not_a_cpu\"\n").validate().unwrap_err(); + assert!(e.contains("SGI Tezro (IP53) takes"), "{e}"); + let _ = std::fs::remove_dir_all(&dir); + } + + /// A `banks` equal to the generic default is a choice, not "unset": the + /// Tezro takes no 128 MB bank, so `--bank0 128 --bank1 128` and a + /// `banks = [128, 128, 0, 0]` line are refused rather than read as the + /// board's 4 GiB default. + #[test] + fn tezro_honours_banks_that_equal_the_generic_default() { + let cli = Cli::parse_from(["iris", "--bank0", "128", "--bank1", "128"]); + let cfg = cli.apply(tezro()); + assert!(cfg.validate().unwrap_err().contains("512 or 1024")); + + let text = "banks = [128, 128, 0, 0]\n[machine]\nprofile = \"tezro_ip53\"\n"; + let cfg = MachineConfig::parse_toml(text).unwrap(); + assert!(cfg.validate().unwrap_err().contains("512 or 1024")); + + let cfg = MachineConfig::parse_toml("[machine]\nprofile = \"tezro_ip53\"\n").unwrap(); + assert_eq!(cfg.tezro_memory().unwrap(), crate::ip53_machine::MemoryPopulation::default()); + } + + /// The processor is one of the Tezro's R16000 chips and graphics is + /// absent. + #[test] + fn tezro_requires_an_r16000_and_no_graphics() { + let mut cfg = tezro(); + cfg.set_cpu(&parts::R16000_700_EARLY); + cfg.validate().unwrap(); + for cpu in [&parts::R4400_150, &parts::R5000_180, &parts::R12000_360, &parts::R14000_600] { + cfg.set_cpu(cpu); + let e = cfg.validate().unwrap_err(); + assert!(e.contains("SGI Tezro (IP53) takes") && e.contains("r16000_700"), "{}: {e}", cpu.name()); + } + assert_eq!(MachineProfile::TezroIp53.cpus(), [&parts::R16000_700_EARLY, &parts::R16000_800, &parts::R16000_900, &parts::R16000_1000]); + let mut cfg = tezro(); + assert_eq!(cfg.graphics_board(), GraphicsBoard::None); + cfg.graphics.board = Some(GraphicsBoard::Newport); + assert!(cfg.validate().is_err()); + cfg.graphics.board = Some(GraphicsBoard::V12); + assert!(cfg.validate().is_err()); + let mut cfg = tezro(); + cfg.network.mode = NetMode::Off; + cfg.validate().unwrap(); + } + + /// `[machine] cpus` is the processors fitted: four on a Tezro unless + /// set, one elsewhere, and only the Octane and the Tezro take more. + #[test] + fn machine_cpus_defaults_to_four_on_the_tezro_and_one_elsewhere() { + let mut cfg = tezro(); + assert_eq!(cfg.machine.cpus(), 4); + cfg.validate().unwrap(); + for cpus in [1, 2] { + cfg.machine.cpus = Some(cpus); + cfg.validate().unwrap(); + assert_eq!(cfg.machine.cpus(), cpus); + } + assert_eq!(MachineConfig::default().machine.cpus(), 1); + let loaded: MachineConfig = toml::from_str("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"r10000\"\ncpus = 2\n").unwrap(); + assert_eq!(loaded.machine.cpus(), 2); + let mut fuel = MachineConfig::default(); + fuel.machine.profile = MachineProfile::FuelIp34; + fuel.set_cpu(&parts::R14000_500); + fuel.machine.cpus = Some(2); + assert!(fuel.validate().unwrap_err().contains("only the Octane and the Tezro")); + } + + /// The 700 MHz chip is fitted as one, two or four processors, the 1000 + /// MHz chip as two or four, the 800 and 900 MHz chips as four only, and + /// the refusal says so. + #[test] + fn each_chip_is_fitted_with_the_counts_its_board_data_allows() { + for &chip in crate::ip53_machine::CPUS { + for cpus in 0..=5u8 { + let mut cfg = tezro(); + cfg.set_cpu(chip); + cfg.machine.cpus = Some(cpus); + let counts: &[u8] = match chip { + c if c == &parts::R16000_700_EARLY => &[1, 2, 4], + c if c == &parts::R16000_1000 => &[2, 4], + _ => &[4], + }; + let fits = counts.contains(&cpus); + assert_eq!(cfg.validate().is_ok(), fits, "{} with {cpus}", chip.name()); + } + if chip != &parts::R16000_700_EARLY { + let mut cfg = tezro(); + cfg.set_cpu(chip); + cfg.machine.cpus = Some(1); + let e = cfg.validate().unwrap_err(); + let only = if chip == &parts::R16000_1000 { "2, 4 processors only" } else { "4 processors only" }; + assert!(e.contains(only) && e.contains(&chip.label()), "{e}"); + } + } + } + + #[test] + fn tezro_refuses_a_processor_count_its_board_does_not_fit() { + for cpus in [0, 3, 5] { + let mut cfg = tezro(); + cfg.machine.cpus = Some(cpus); + assert!(cfg.validate().unwrap_err().contains("[machine] cpus"), "{cpus}"); + } + } + + /// The sample configuration in the repository root is a valid Tezro. + #[test] + fn the_sample_tezro_configuration_validates() { + let cfg: MachineConfig = toml::from_str(include_str!("../tezro.toml.example")).unwrap(); + assert_eq!(cfg.machine.profile, MachineProfile::TezroIp53); + cfg.validate().unwrap(); + assert_eq!(cfg.tezro_memory().unwrap().total_mib(), 4096); + } + + /// A `[tezro]` table is written only when something in it was set, and + /// what was set survives a save and reload. + #[test] + fn only_a_configured_tezro_section_is_saved_and_it_round_trips() { + let saved = toml::to_string(&MachineConfig::default()).unwrap(); + assert!(!saved.contains("[tezro]"), "{saved}"); + let text = "[machine]\nprofile = \"tezro_ip53\"\ncpu = \"r16000\"\n[tezro]\nconsole = \"ioc4_b\"\nflash = \"flash.bin\"\nl1_port = 18882\nserial_port_c = 18883\n"; + let loaded: MachineConfig = toml::from_str(text).unwrap(); + assert_eq!(loaded.machine.profile, MachineProfile::TezroIp53); + assert_eq!(loaded.tezro.console, TezroConsole::Ioc4B); + assert_eq!(loaded.tezro.console.index(), 2); + let again: MachineConfig = toml::from_str(&toml::to_string(&loaded).unwrap()).unwrap(); + assert_eq!(again.tezro.flash.as_deref(), Some("flash.bin")); + assert_eq!((again.tezro.l1_port, again.tezro.serial_port_c), (Some(18882), Some(18883))); + assert!(!MachineProfile::ALL.contains(&MachineProfile::TezroIp53)); + } +} diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 8f6d75c63..d257c062a 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -175,7 +175,7 @@ impl OctaneMachine { if prom.len() > FLASH_SIZE { return Err(format!("Octane PROM {prom_path} is {} bytes; the flash holds {FLASH_SIZE}", prom.len())); } - let cpu_count = cfg.machine.cpus; + let cpu_count = cfg.machine.cpus(); MachineProfile::OctaneIp30.accepts_cpus(cpu_count)?; let banks = heart_banks(cfg); let kind = cfg.graphics_board(); diff --git a/src/ip53_machine.rs b/src/ip53_machine.rs new file mode 100644 index 000000000..886962639 --- /dev/null +++ b/src/ip53_machine.rs @@ -0,0 +1,1259 @@ +//! The Tezro (IP53 board, IP35 software platform): R16000s on one Bedrock +//! node, a PIC host bridge and the IO9 board's IOC4, ISP12160 and BCM5701, +//! with Iris's CPU thread, monitor and serial console. +//! +//! Selected with `[machine] profile = "tezro_ip53"`. Shared keys select the PROM, +//! NVRAM, memory and SCSI devices; `[tezro]` selects the processor +//! population, the serial console and writable flash. The node itself is +//! the one the Fuel uses ([`Node`]); this board adds its widgets and its +//! inputs. Graphics is not attached. +//! +//! The board follows the QEMU Tezro compositions: +//! +//! - The one-processor board has one PIC at XIO widget 15; the two- and +//! four-processor board adds a second at widget 11, with nothing on its +//! buses, and a V12 slot at widget 12 that is left empty. The PIC at +//! widget 15 is the IO9's: its PXBOW answers as widget 0 and forwards +//! the processor's accesses to widget 15. On the larger board it carries +//! widget 11 too, and shows that link present and alive (unlike QEMU's +//! composition, below). Its head 0 carries the IO9 PCI bus: the IOC4 at +//! device 1 (input 0), a TI PCI-to-PCI bridge with nothing behind it at +//! device 2, the ISP12160 at device 3 (input 2) and the BCM5701 at device +//! 4 (input 3), with REQ64# wired on the BCM5701's slot. The PIC's interrupts and DMA go to Bedrock IO, the host widget +//! 10. +//! - The IOC4 carries the DS1744-compatible clock and NVRAM in its BAR and +//! the PS/2 keyboard and mouse; its four ports are serial streams beside +//! the L1's, and the L1 is the console by default. +//! - Bedrock board word 0x2001, the Tezro firmware record (the chip's clock, +//! an authored configuration block), and an L1 that reports +//! brick type 0x39, revision 1.48.1, the first port-speed integer 1, the +//! router `brick` reply and, in the node board's record, the CPU +//! configuration the flash block holds. +//! +//! # What is approximated here +//! +//! - The board word's class, 2, is inferred: Tezro PROM banners rule out +//! classes 4, 8 and 0xc, OpenBSD names class 2 for this board family, and +//! no raw read is on record. Its low nibble, 1, is a software choice: only +//! the PROM's last-resort table fallback reads it, which a board whose L1 +//! record carries the CPU configuration never reaches. The L1 type and +//! revision, the PIC revision 3 profile and REQ64# on the BCM5701's slot +//! are software inputs the QEMU profiles choose, not values measured on a +//! Tezro. +//! - The PCI devices at 1 to 4 and the revisions of the IOC4 (0x53), the +//! ISP12160 (6) and the BCM5701 (0x15) are as Tezro and Origin 350 +//! consoles print them (established for those captures). Device d's INTA +//! reaching PIC input d - 1 is what IRIX's pin rule assigns and what an +//! Origin 350 under OpenBSD reports (established as software +//! expectation); the board wiring itself, and INTB to INTD, are unknown. +//! - The one-processor board has one PIC. The tower guide gives that board +//! two PCI-X buses and one XIO connector; that it is one PIC at widget 15 +//! is inferred from them. +//! - Every fitted processor runs from reset, each on its own thread, as in +//! QEMU's IP35 node; CPU enable is stored and starts or stops nothing. The +//! PROM's reset barrier is ordered by Bedrock's LB, an approximation with +//! no hardware counterpart. +//! - The second PIC is a port of the IO9 PIC's PXBOW, beside the host's +//! and the IO9 PIC's links: the PROM and IRIX learn which widgets exist +//! only from widget 0's link status and auxiliary status, so one +//! crossbar per node carries both PICs. That is a modelling consequence +//! of the discovery path, not a known route: whether the second PIC has +//! a crossbar of its own in front of it, and its part (0xd102 or 0xd112) +//! and link reset values beyond alive and present, are unknown. QEMU's +//! composition still gives the second PIC its own PXBOW, whose widget 0 +//! nothing maps, and leaves link 11 absent on the IO9 PIC's. The second +//! PIC's interrupts and DMA reach Bedrock IO directly, as the IO9 PIC's +//! do, rather than through the crossbar. +//! - PIC DMA aimed at a widget other than Bedrock IO is refused and logged, +//! as the Fuel's XBridge path refuses it. +//! - No EEPROM image is fitted for the IO9, the node board or the BCM5701: +//! no published source gives their contents. The L1's component 0 record +//! stands in for the node board's: it names the board inventories print +//! for the chip and processor count ([`node_board`]), and its +//! CPU configuration is the flash record's, in the encoding QEMU gives (a +//! software input the stock PROM accepts, not a read record). No I/O +//! board record is given, as in QEMU. A BCM5701 EEPROM transfer does not +//! complete. +//! +//! Board composition ported from hw/mips/sgi/sn-machine.c and +//! hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. + +use crate::config::{scsi_label, MachineConfig, TezroConsole}; +use crate::cpu::mips_cache_v2::{CpuModel as CacheCpuModel, MipsCache}; +use crate::cpu::parts::{self, Part}; +use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; +use crate::cpu::cpu_monitor::CpuMonitor; +use crate::cpu::mips_tlb::MipsTlb; +use crate::cpu::smp::World; +use crate::dev::bcm5701::{Bcm5701, Bcm5701Config}; +use crate::dev::bedrock::{Bedrock, Population, SLOTS}; +use crate::dev::bedrock_flash::{FirmwareRecord, Flash}; +use crate::dev::ds1744::{Ds1744, YearBase}; +use crate::dev::heart::{host_clock, Clock}; +use crate::dev::ioc4::{Ioc4, Ioc4Config}; +use crate::dev::isp12160::Isp12160; +use crate::dev::l1::{BoardIdentity, CpuContext, L1Config}; +use crate::dev::pic::{Function, Pic, PicPeer, Refused}; +use crate::dev::prom_image; +use crate::dev::ps2::Ps2Controller; +use crate::dev::serial_host::{null_serial_backend, open_log, tcp_serial_backend, CiSerialBackend, LogMode, SerialBackend, TeeBackend}; +use crate::dev::l1::L1; +use crate::devlog::{init_devlog, LogModule}; +use crate::gdb_stub::CpuDebug; +use crate::gfx_display::GfxDisplay; +use crate::hptimer::TimerManager; +use crate::ip35::{Ip35Bus, CPU_PHYS_MASK}; +use crate::ip35::node::{connect_cpus, Node, NodeConfig}; +use crate::machine_state::GuestState; +use crate::monitor::{Monitor, DEFAULT_PORT as DEFAULT_MONITOR_PORT}; +use crate::net::{GatewayConfig, Link, NatControl}; +use crate::perf_monitor::PerfMonitor; +use crate::pci::{DevFn, PciToPciBridge}; +use crate::scsi::{attach_from_config, ScsiBus}; +use crate::traits::{BusDevice, Device, MachineEvent, Resettable, BUS_OK}; +use crate::workstation::{register_system_controller, DiscChanger, MachineRef, MachineSlot, Workstation}; +use parking_lot::Mutex; +use std::net::{Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Weak}; + +/// The Config bits 24:0 a chip's reset word defines (K0, DN, CT, PE, PM, EC, +/// SB, SK, BE, SS, SC and the bits above them). The primary-cache sizes above +/// them are the family's, fixed by the cache type. +const CONFIG0_MODE_MASK: u32 = 0x01ff_ffff; + +/// The Bedrock board word the QEMU Tezro profiles select. +const BOARD_ID: u64 = 0x2001; +/// The IO9's PIC, the second PIC of the two- and four-processor board, and +/// the widget Bedrock IO answers as. Widget 12 on that board is the V12's +/// XIO slot, which is left empty. +const IO9_WIDGET: u8 = 15; +const SECOND_PIC_WIDGET: u8 = 11; +const HOST_WIDGET: u8 = 10; +/// IO9's PCI functions: configuration device on head 0. +const IOC4_DEVICE: u8 = 1; +const SCSI_DEVICE: u8 = 3; +const ETH_DEVICE: u8 = 4; +/// The TI PCI2050 PCI-to-PCI bridge every Tezro capture that prints widget +/// 15 shows at its device 2 (`104c:ac28`, revision 1; established for those +/// captures, and absent from every Origin 350 and Onyx 350 IO9 capture +/// found, for a reason not known). No capture shows anything behind it, so +/// its secondary bus is empty (inferred). Only the type 1 header is +/// modelled, not the chip's own registers or capability list. +const TI_BRIDGE_DEVICE: u8 = 2; +const TI_BRIDGE_VENDOR: u16 = 0x104c; +const TI_BRIDGE_ID: u16 = 0xac28; +const TI_BRIDGE_REVISION: u8 = 1; + +/// A DIMM size the Tezro's IP53 board takes, which is also the size of each +/// locational bank it provides: each DIMM pair provides two banks of the +/// DIMM's size (Origin 350 guide 007-4566-001, Figure 3-4, p. 118). +/// +/// 512 MB and 1 GB DIMMs only: established for the IP53 node board (Origin +/// 350 guide 007-4566-001, Table 3-3, p. 118); inferred for the Tezro, whose +/// guide (007-4564-001 pp. 27-28) names no size but whose inventories show +/// the same IP53 boards and only 512 and 1024 MB banks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BankSize { + Mib512, + Mib1024, +} +impl BankSize { + pub const fn mib(self) -> u32 { + match self { + Self::Mib512 => 512, + Self::Mib1024 => 1024, + } + } + fn from_mib(mib: u32) -> Result { + match mib { + 512 => Ok(Self::Mib512), + 1024 => Ok(Self::Mib1024), + _ => Err(format!("a Tezro bank is 512 or 1024 MiB, not {mib} MiB")), + } + } +} + +/// The banks a Tezro fits, locational bank 0 upward: up to eight banks of 512 +/// or 1024 MiB with none left out between them. What the board needs in total +/// depends on its processors; see [`Self::check_for_cpus`]. +/// +/// The minimums come from the Tezro manuals as the QEMU assembly layer +/// records them. The default, the 4 GiB layout of 1024, 1024 and four +/// 512 MiB banks, follows QEMU and matches a captured Tezro. +/// +/// Banks 2k and 2k+1 share one size code in Bedrock's memory configuration +/// word (established from the stock IP35 PROM's decode of it), so a +/// population with a lane's two banks at different sizes is refused. +/// Approximated: the board takes DIMMs in pairs (Origin 350 guide p. 116; +/// Tezro guide p. 28), so banks 2k and 2k+1 are present together; the model +/// also accepts a last lane with one bank, which the controller can enable +/// but the board cannot hold. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MemoryPopulation(Vec); +impl Default for MemoryPopulation { + fn default() -> Self { + Self::four_gib() + } +} +impl MemoryPopulation { + pub fn new(banks: &[BankSize]) -> Result { + if banks.is_empty() || banks.len() > SLOTS { + return Err(format!("memory: {} banks; a Tezro fits 1 to {SLOTS}", banks.len())); + } + if let Some(lane) = banks.chunks(2).position(|pair| pair.len() == 2 && pair[0] != pair[1]) { + return Err(format!( + "memory: banks {} and {} are {} and {} MiB; the two banks of a lane share one size", + 2 * lane, 2 * lane + 1, banks[2 * lane].mib(), banks[2 * lane + 1].mib())); + } + Ok(Self(banks.to_vec())) + } + /// From sizes in MiB, rejecting any a Tezro does not take. + pub fn from_mib(banks: &[u32]) -> Result { + let sizes = banks.iter().map(|&mib| BankSize::from_mib(mib)).collect::, _>>()?; + Self::new(&sizes) + } + /// 1 GiB as two banks of 512 MiB. The manual's minimum of 1 GB for two or + /// four processors permits it, but no real Tezro has been observed with + /// less than 4 GiB, and nothing here claims the PROM or IRIX boot at it. + pub fn one_gib() -> Self { + Self(vec![BankSize::Mib512; 2]) + } + /// 4 GiB: 1024, 1024 and four 512 MiB banks, the default. Banks 4 and 5 + /// sit at 4 to 6 GiB. + pub fn four_gib() -> Self { + let mut banks = vec![BankSize::Mib1024; 2]; + banks.extend([BankSize::Mib512; 4]); + Self(banks) + } + pub fn banks(&self) -> &[BankSize] { + &self.0 + } + /// The sizes in MiB, as [`BedrockConfig::banks_mib`](crate::dev::bedrock::BedrockConfig) takes them. + pub fn banks_mib(&self) -> Vec { + self.0.iter().map(|bank| bank.mib()).collect() + } + pub fn total_mib(&self) -> u32 { + self.0.iter().map(|bank| bank.mib()).sum() + } + /// One processor needs at least 512 MB; two or four need at least 1 GB. + pub fn check_for_cpus(&self, cpus: usize) -> Result<(), String> { + let needed = match cpus { + 1 => 512, + 2..=4 => 1024, + _ => return Err(format!("{cpus} processors; a Tezro fits 1 to 4")), + }; + if self.total_mib() < needed { + return Err(format!( + "{} MiB of memory; {cpus} processor{} need at least {needed} MiB", + self.total_mib(), if cpus == 1 { "" } else { "s" })); + } + Ok(()) + } +} + +/// The processors a Tezro takes, the default first: the 700 MHz R16000 a bare +/// `cpu = "r16000"` or `"r16000_700"` selects. Each part's clock, revision, +/// PRId and secondary-cache size are the `Part`'s; `with_cpu!` picks the +/// matching cache type (4 MB for 700 and 800 MHz, 8 MB for 900, 16 MB for 1 GHz). +pub const CPUS: &[&Part] = + &[&parts::R16000_700_EARLY, &parts::R16000_800, &parts::R16000_900, &parts::R16000_1000]; + +/// Processors fitted when `[machine] cpus` is unset: the four-processor tower, +/// as in the Tezro inventories seen. +pub const DEFAULT_CPUS: u8 = 4; + +/// What the Tezro board knows about one of its chips beyond the chip's own +/// identity: the flash record the PROM checks the processor against (which +/// also gives the low 25 bits of the Config word the processor resets with), +/// the processor counts the chip is fitted with, and the cache clock its +/// inventory prints. +/// +/// What is established (E), inferred (I), approximated (A) or unknown (U), +/// by the kind of capture behind it: +/// +/// - PRId 0x0f50 for the 1 GHz part: E (PROM banner `f50` and hinv 3.0 on +/// the same machine). 0x0f41 (700 MHz, rev 2.1), 0x0f42 (800 MHz, rev 2.2) +/// and the 900 MHz part's 0x0f50: I. The displayed revisions are printed +/// by inventories and the raw major follows the display rule, which Linux, +/// OpenBSD and PROM 6.210's printers apply (`Part::prid`). No raw read of a +/// 2.x part is on record. +/// +/// - Config words 0x009bad85 (700 MHz), 0x009baf85 (800 MHz) and 0x009cb185 +/// (900 MHz): E. They are the low 25 bits of the mode words in rows of the +/// stock IP35 PROM's CPU speed table, which the PROM writes to Config at +/// reset and reads back (bits 15:0 and 24:16). That the R16000 lays Config +/// out as the PROM's editor does is E for the PROM and I for the chip: no +/// R16000 manual is retained. SB=1 (128-byte lines) is a model choice the +/// table agrees with. Which row a given machine's flash holds is inferred; +/// Tezro inventories' cache taps (0xc, 0xa, 0xb) agree with these rows. +/// - Config word 0x00edb385 (1 GHz): A. The table stops at 900 MHz and no +/// capture records the word; see `FirmwareRecord::TEZRO_1000`. +/// - FIR 0x0900, every chip: U. It is the one R14000 value on record; a model +/// choice. The PROM never reads or prints it and no R16000 FIR was found +/// in the retained evidence. +/// - Config bit 23 and the HSTL bits of the mode word: their meaning is U. +/// Bit 23 is set in every row of 500 MHz and up, and the model copies it. +/// - Processor counts: the PROM has no count field. It lists IP53_4CPU, +/// IP53_2CPU and IP53_1CPU assembly names with no clock paired to any (E +/// that the names exist), which neither forbids nor shows a two-processor +/// part at any clock. The counts follow the boards captured with each +/// processor (retained inventories of real IP53 and IP59 boards). The +/// 700 MHz chip is fitted as one, two or four processors, from the board's +/// population and those names. The 800 MHz chip (revision 2.2, 4 MB) as +/// four only: E, both 800 MHz Tezro inventories found print four 2.2 +/// processors on `IP53_4CPU` 030-1956-002; the two-processor 800 MHz board (`IP53_2CPU` 030-2029-001, on +/// an Origin 350) carries revision 3.0 processors, another PRId. The 900 +/// MHz chip as four only: A, a model choice: no IP53 or IP59 board at 900 +/// MHz is captured (the 900 MHz parts on record are on Fuel boards). The +/// 1 GHz chip as two or four: E for the counts, from `IP59_4CPU` +/// (030-1989-003) and `IP59_2CPU` (030-2059-002, in a rack Tezro). +/// - The 900 MHz chip: the PROM's speed table has its row, but no IP53 board +/// at 900 MHz is captured, so that a Tezro was fitted with it is not +/// established. +/// - The 1 GHz chip: captured 1 GHz boards are IP59 node boards +/// (`IP59_4CPU`, `IP59_2CPU`), not IP53, and the L1 names them so +/// ([`node_board`]); the rest of the board is the IP53's, and the PROM's +/// socket masks for IP59 equal IP53's. +/// - Cache clock (350, 400, 450 and 333 MHz) is as the inventory prints it +/// and not modelled; the cache taps are only part of the flash records' +/// mode words. +/// +/// An 800 MHz part with an 8 MB cache (PROM row 21, 0x549caf85) is on Onyx +/// 350 inventories; no Tezro capture found shows it, and it is not +/// modelled. +pub(crate) struct TezroChip { + pub chip: &'static Part, + pub firmware: FirmwareRecord, + /// Secondary-cache clock in MHz, as the chip is inventoried. + pub scache_mhz: u32, + pub cpus: &'static [u8], + /// The node board the L1 names for each processor count in `cpus`; see + /// [`node_board`]. + pub boards: &'static [(u8, BoardIdentity)], + /// Why `cpus` is what it is when that is not established: a clause for + /// the refusal message. + pub cpus_basis: &'static str, +} + +pub(crate) const CHIPS: [TezroChip; 4] = [ + TezroChip { + chip: &parts::R16000_700_EARLY, + firmware: FirmwareRecord::TEZRO, + scache_mhz: 350, + cpus: &[1, 2, 4], + boards: &[ + (1, board("IP53_1CPU", "", "")), + (2, board("IP53_2CPU", "030-1869-001", "-C")), + (4, board("IP53_4CPU", "030-1868-001", "-C")), + ], + cpus_basis: ", from the board's population and the PROM's assembly names, which pair with no clock (inferred)", + }, + TezroChip { + chip: &parts::R16000_800, + firmware: FirmwareRecord::TEZRO_800, + scache_mhz: 400, + cpus: &[4], + boards: &[(4, board("IP53_4CPU", "030-1956-002", "-C"))], + cpus_basis: ", as on every captured board with it; the two-processor 800 MHz board carries revision 3.0 processors", + }, + TezroChip { + chip: &parts::R16000_900, + firmware: FirmwareRecord::TEZRO_900, + scache_mhz: 450, + cpus: &[4], + boards: &[(4, board("IP53_4CPU", "", ""))], + cpus_basis: ", a model choice: no IP53 or IP59 board at 900 MHz is captured (approximated)", + }, + TezroChip { + chip: &parts::R16000_1000, + firmware: FirmwareRecord::TEZRO_1000, + scache_mhz: 333, + cpus: &[2, 4], + boards: &[(2, board("IP59_2CPU", "030-2059-002", "-A")), (4, board("IP59_4CPU", "030-1989-003", "-C"))], + cpus_basis: ", as the captured IP59_4CPU and IP59_2CPU boards are", + }, +]; + +/// The board's data for `chip`. Panics for a part the Tezro does not take; +/// `MachineProfile::part` has refused one before a board is built. +pub(crate) fn chip(chip: &Part) -> &'static TezroChip { + CHIPS.iter().find(|c| *c.chip == *chip).expect("a Tezro takes only its own R16000 parts") +} + +impl TezroChip { + /// Whether the chip is fitted as `cpus` processors, and the refusal if + /// not. + pub fn accepts_cpus(&self, cpus: u8) -> Result<(), String> { + if self.cpus.contains(&cpus) { + return Ok(()); + } + let list: Vec = self.cpus.iter().map(u8::to_string).collect(); + Err(format!( + "cpus = {cpus}: the {} is fitted with {} processors only{}", + self.chip.label(), + list.join(", "), + self.cpus_basis + )) + } +} + +/// The CPU configuration the node board's L1 record carries: the flash +/// record's mode word and processor clock and the 200 MHz Bedrock. The PROM +/// checks the flash block against it at every start; without it the PROM +/// checks against the table row it picks for board word 0x2001, a 400 MHz +/// part, and prints "CPU configuration values mismatch". A real board's +/// record has not been read, so this is approximated: what QEMU's Tezro gives. +pub(crate) fn cpu_context(firmware: &FirmwareRecord) -> CpuContext { + CpuContext { mode_word: firmware.cpu_mode_word, cpu_mhz: firmware.cpu_clock_hz / 1_000_000, hub_mhz: 200 } +} + +/// The node board the L1's component 0 record names for `chip` fitted as +/// `cpus` processors: the product name, part and revision that board's +/// inventory line prints. Tezro inventories key the board by processor count +/// and clock, and the 800 MHz and 1 GHz chips sit on other parts than the 700 +/// MHz one, so the name follows the chip. What is behind each entry: +/// +/// - 700 MHz, four processors: `IP53_4CPU` 030-1868-001 revision -C, with +/// 2.1 processors at 4 MB and tap 0xc: established (Tezro and Origin 350 +/// inventories; every board of that part prints the same tuple). +/// - 700 MHz, two processors: `IP53_2CPU` 030-1869-001 revision -C: inferred +/// from Tezro inventories outside the captures retained beside this +/// model. +/// - 700 MHz, one processor: `IP53_1CPU` with its part and revision empty. +/// The PROM's socket-mask table holds the name; no captured board prints +/// it (inferred for the name, unknown for the rest). +/// - 800 MHz, four processors: `IP53_4CPU` 030-1956-002 revision -C: +/// established for that part, four 2.2 processors at 4 MB and tap 0xa, by +/// both 800 MHz Tezro inventories found (one prints revision -B, one -C; +/// which letter a given board carries is unknown, and -C is the model's). +/// - 900 MHz: `IP53_4CPU` with its part and revision empty. No board at 900 +/// MHz is captured on an IP53 or IP59 node (approximated). +/// - 1 GHz, four processors: `IP59_4CPU` 030-1989-003 revision -C: +/// inferred. Every captured board of that part carries four 1.0 GHz +/// revision 3.0 processors at 16 MB and tap 0x15, but on Onyx 350 modules; +/// no 1 GHz Tezro capture prints its node board. Revisions -A to -D are +/// captured; -C is the model's. +/// - 1 GHz, two processors: `IP59_2CPU` 030-2059-002 revision -A, the one +/// two-processor 1 GHz board on record (a rack Tezro): inferred. +/// +/// QEMU's Tezro profiles name the 700 MHz boards the same way +/// (hw/mips/sgi/sn-machine.c and the Tezro product layers in +/// jamesbraid/qemu-sgi-origami). The record's other fields are the model's, +/// not read from a board. +/// +/// The name matters to the PROM: it tries only the processor sockets in +/// the mask of the product-name row that matches (`IP53_4CPU` and +/// `IP59_4CPU` 0xf, `IP53_2CPU` and `IP59_2CPU` 0x5, `IP53_1CPU` 0x1) and all +/// four when no row matches, power-cycling the module three times for each +/// empty socket it tries (read from the PROM's per-socket bring-up code). +/// Nothing in the PROM or the L1 reads the part or revision. +fn node_board(chip: &TezroChip, cpus: usize) -> BoardIdentity { + chip.boards + .iter() + .find(|(count, _)| usize::from(*count) == cpus) + .map(|(_, board)| *board) + .expect("a Tezro chip names a board for each processor count it takes") +} + +const fn board(name: &'static str, part: &'static str, revision: &'static str) -> BoardIdentity { + BoardIdentity { name, part, revision } +} + +/// The serial connections: the L1, then IOC4 ports 0 to 3. +const SERIAL_PORTS: usize = 5; + +pub struct TezroMachine { + board: Arc, + /// The processors, CPU 0 first, in the board's slot order. + cpus: Vec>, + timers: Arc, + monitor: Arc, + monitor_port: u16, + ci_serial: Option>, + console: usize, + ci: bool, + scratch: Option, + nvram: String, + frames: Option>, + nat: Option<(Arc, (Ipv4Addr, Ipv4Addr, Ipv4Addr))>, + events: Option>, + slot: Option, + lifecycle: GuestState, +} + +impl TezroMachine { + pub fn new(cfg: &MachineConfig) -> Result { + cfg.validate()?; + let prom = prom_image::load(Path::new(&cfg.prom), 8 * 1024 * 1024).map_err(|e| format!("Tezro {e}"))?; + let clock = Arc::new(Ds1744::new(YearBase::From(1968))?); + clock.set_nvram_file(Path::new(&cfg.nvram))?; + let memory = cfg.tezro_memory()?; + let chip = chip(cfg.cpu()); + let board = Arc::new(TezroBoard::with_config(TezroBoardConfig { + payload: &prom, + clock: host_clock(), + flash_path: cfg.tezro.flash.as_deref().map(Path::new), + station_mac: cfg.network().mac, + timekeeper: clock, + memory: &memory, + cpus: usize::from(cfg.machine.cpus()), + chip, + })?); + let timers = Arc::new(TimerManager::new()); + board.ioc4.set_timer_manager(timers.clone()); + let count_hz = cfg.clock.fixed_mhz.map_or(chip.chip.count_hz(), |mhz| (mhz * 1_000_000.0) as u64).max(1); + let cpus = build_cpus(&board, &timers, count_hz); + let cpu = cpus[0].clone(); + board.status.set_cpu_cycles(cpu.cycles_ptr()); + board.status.set_count_hz(cpu.count_hz_atomic()); + for c in cpus.iter().skip(1) { + board.status.add_cpu_stats(c.cycles_ptr(), c.fasttick_count()); + } + // The Ethernet holds a reply until the processor that sent the request + // has run on past its doorbell. + board.eth.set_cpu_cycles(cpus.iter().map(|c| c.cycles_ptr()).collect()); + // One page pool and one body of compiled code, reaching each + // processor's state through the core it is called with. + let world = Arc::new(World::new()); + world.set_slots(cpus.iter().map(|c| c.park_slot()).collect()); + #[cfg(feature = "jitv2")] + if cpus.len() > 1 { + if cfg!(feature = "jitv2_lockstep") { + return Err("[machine] cpus above 1 cannot run with jitv2_lockstep".into()); + } + let jit = cpu.jitv2(); + for c in &cpus { + c.share_jitv2(jit.clone(), world.clone()); + } + } + connect_cpus("Tezro", &board.bedrock, &board.l1, &board.events, &cpus)?; + board.bedrock.set_timer_manager(&timers); + let rx = board.receiver.lock().take().expect("the board's events are taken once"); + // After connect_cpu set the windows: each compile worker takes the + // cache's geometry and the CPU model's policies once, as it starts. + // Workers read plain physical code addresses through the view the CPU + // fetched each page by: the cached plane for cached pages, the + // uncached attribute-0 view for the reset PROM's uncached fetches. + #[cfg(feature = "jitv2")] + { + let cpu_device: Arc = cpu.clone().as_device(); + let jit = cpu.jitv2(); + let mut g = jit.lock(); + g.compile_queue.set_thread_count(cfg.jitv2.threads.max(1)); + g.compile_queue.set_cpu(Arc::downgrade(&cpu_device)); + g.compile_queue.set_owner(Arc::downgrade(&jit)); + if cpus.len() > 1 { + g.compile_queue.set_world(world.clone()); + } + #[cfg(not(feature = "jitv2_lockstep"))] + { + let stats = g.stats.clone(); + g.compile_queue.start(board.code_bus(), stats); + } + // The compile pool keeps only a weak handle on the CPU. + std::mem::forget(cpu_device); + } + let scratch = Self::attach_scsi(cfg, &board)?; + let console = cfg.tezro.console.index(); + let ci_serial = cfg.ci.then(|| board.serial[console].clone()); + Self::connect_serial(cfg, &board, console)?; + let monitor = Monitor::new(); + monitor.register_device(init_devlog()); + monitor.register_device(Arc::new(CpuMonitor::new(cpus.clone()))); + monitor.register_device(PerfMonitor::new( + cpu.running_flag(), cpu.cycles_ptr(), cpu.fasttick_count(), None, None, + )); + for device in board.monitor_devices() { + monitor.register_device(device); + } + for c in &cpus { + c.register_locks(); + } + board.scsi.register_locks(); + board.ioc4.register_locks(); + board.eth.register_locks(); + for pic in &board.pics { + pic.register_locks(); + } + board.flash.register_locks(); + board.bedrock.register_locks(); + let (mut nat, mut frames) = (None, None); + let net = GatewayConfig::from_network(&cfg.network()); + if let Some(link) = Link::with_gateway(net.clone())? { + frames = Some(link.frames()); + nat = Some((link.nat(), (net.client_ip, net.gateway_ip, net.netmask))); + board.eth.attach(link); + } + eprintln!( + "iris: Tezro IP53, {} R16000 {} MHz, {} MiB, serial console {:?}", + cpus.len(), + chip.chip.mhz, + memory.total_mib(), + cfg.tezro.console + ); + Ok(Self { + board, + cpus, + timers, + monitor: Arc::new(monitor), + monitor_port: cfg.monitor_port.unwrap_or(DEFAULT_MONITOR_PORT), + ci_serial, + console, + ci: cfg.ci, + scratch, + nvram: cfg.nvram.clone(), + nat, + frames, + events: Some(rx), + slot: None, + lifecycle: GuestState::Idle, + }) + } + + fn attach_scsi(cfg: &MachineConfig, board: &TezroBoard) -> Result, String> { + let attached = attach_from_config(cfg, Some("tezro"), &|channel| { + board.scsi.bus_for_channel(channel) + .ok_or_else(|| format!("controller {channel} (a Tezro has channels 0 and 1)")) + })?; + if let Some(device) = attached.daynaports.first() { + return Err(format!("{}: kind = \"daynaport\" is not available on the Tezro", + scsi_label(device.controller, device.id))); + } + Ok(attached.scratch) + } + + fn connect_serial(cfg: &MachineConfig, board: &TezroBoard, console: usize) -> Result<(), String> { + let [a, b] = cfg.serial_ports(); + let ioc4 = |port: usize| -> Box) + '_> { + Box::new(move |backend| board.ioc4.set_serial_backend(port, backend)) + }; + let ports = [ + SerialPort { + label: "L1", + port: cfg.tezro.l1_port.unwrap_or(8882), + setting: "tezro.l1_port", + ci: &board.serial[0], + log: cfg.tezro.l1_log.clone(), + connect: Box::new(|backend| board.l1.set_serial_backend(backend)), + }, + SerialPort { label: "IOC4 0", port: a, setting: "serial_port_a", ci: &board.serial[1], log: None, connect: ioc4(0) }, + SerialPort { label: "IOC4 1", port: b, setting: "serial_port_b", ci: &board.serial[2], log: None, connect: ioc4(1) }, + SerialPort { + label: "IOC4 2", + port: cfg.tezro.serial_port_c.unwrap_or(8883), + setting: "tezro.serial_port_c", + ci: &board.serial[3], + log: None, + connect: ioc4(2), + }, + SerialPort { + label: "IOC4 3", + port: cfg.tezro.serial_port_d.unwrap_or(8884), + setting: "tezro.serial_port_d", + ci: &board.serial[4], + log: None, + connect: ioc4(3), + }, + ]; + connect_serial_ports(cfg, "Tezro", &ports, console) + } +} + +/// One of the board's serial connections: what it is called, the TCP port +/// and the setting that names the port, the CI backend it uses when it is +/// the console, the log it keeps when it is not, and where the host end goes. +struct SerialPort<'a> { + label: &'static str, + port: u16, + setting: &'static str, + ci: &'a Arc, + log: Option, + connect: Box) + 'a>, +} + +/// Connect the board's serial ports to the host. The console takes +/// `serial_log` and, under CI, the in-process backend; the others listen on +/// their TCP ports, and a port that is taken leaves its stream unconnected +/// rather than stopping the machine. +fn connect_serial_ports(cfg: &MachineConfig, name: &str, ports: &[SerialPort<'_>], console: usize) -> Result<(), String> { + for (i, a) in ports.iter().enumerate() { + if let Some(b) = ports[i + 1..].iter().find(|b| b.port == a.port) { + return Err(format!("{name} {} and {} serial ports must be distinct", a.label, b.label)); + } + } + let mut log_config = cfg.clone(); + for (index, port) in ports.iter().enumerate() { + log_config.serial_log = if index == console { cfg.serial_log.clone() } else { port.log.clone() }; + let tag = format!("{name} {}", port.label); + let log = open_log(&log_config, &tag, LogMode::Fresh); + let backend: Arc = if cfg.ci && index == console { + if let Some(log) = log { + port.ci.set_log(log); + } + port.ci.clone() + } else { + let mut backend = null_serial_backend(); + match tcp_serial_backend(port.port) { + Some((socket, actual_port)) => { + eprintln!("iris: {tag} on 127.0.0.1:{actual_port}"); + backend = socket; + } + None => eprintln!( + "iris: {tag}: port {} unavailable; is another iris using it? ({})", + port.port, port.setting + ), + } + match log { + Some(log) => Arc::new(TeeBackend::new(backend, log)), + None => backend, + } + }; + (port.connect)(backend); + } + Ok(()) +} + +impl Workstation for TezroMachine { + fn start(&mut self) { + self.lifecycle = GuestState::Starting; + self.monitor.clone().start_server(format!("127.0.0.1:{}", self.monitor_port)); + self.restart_peripherals(); + if !self.ci { + self.cpu_start(); + } + } + fn stop(&mut self) { + for c in &self.cpus { + c.clone().as_device().stop(); + } + self.board.stop(); + self.board.timekeeper.flush(); + if self.lifecycle != GuestState::Idle { + self.lifecycle = GuestState::Halted; + } + } + fn reset(&mut self) { + self.stop(); + for c in &self.cpus { + c.power_on(); + } + self.board.power_on(); + self.restart_peripherals(); + } + fn restart_peripherals(&mut self) { + self.board.start(); + } + /// Every processor starts at once, as from the node's reset. + fn cpu_start(&self) { + for c in &self.cpus { + c.clone().as_device().start(); + } + } + fn cpu_is_running(&self) -> bool { + self.cpus.iter().any(|c| c.clone().as_device().is_running()) + } + fn machine_slot(&mut self) -> MachineSlot { + let ptr = self as *mut Self as *mut dyn Workstation; + self.slot.get_or_insert_with(|| Arc::new(Mutex::new(MachineRef(ptr)))).clone() + } + fn register_system_controller(&mut self) { + let slot = self.machine_slot(); + let events = self.events.take().expect("Tezro system controller registered twice"); + register_system_controller(&self.monitor, slot, Some(events)); + } + fn monitor_addr(&self) -> Option { + self.monitor.bound_addr() + } + fn lifecycle(&self) -> GuestState { + self.lifecycle + } + fn set_lifecycle(&mut self, state: GuestState) { + self.lifecycle = state; + } + fn get_display(&self) -> Option> { + None + } + fn register_monitor_device(&self, device: Arc) { + self.monitor.register_device(device); + } + fn get_ps2(&self) -> Arc { + self.board.ioc4.ps2() + } + fn get_timer_manager(&self) -> Arc { + self.timers.clone() + } + fn get_cpu_debug(&self) -> Arc { + self.cpu().clone().debug_adapter() + } + fn fasttick_count(&self) -> Arc { + self.cpu().fasttick_count() + } + fn cpu_counters(&self) -> Vec<(crate::cpu::mips_core::CyclesPtr, Arc)> { + self.cpus.iter().map(|c| (c.cycles_ptr(), c.fasttick_count())).collect() + } + fn get_ci_serial(&self) -> Option> { + self.ci_serial.clone() + } + fn inject_serial_console(&self, bytes: &[u8]) { + self.board.serial[self.console].push_host(bytes); + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn save_nvram(&self, path: Option<&str>) -> Result { + if path.is_some_and(|p| p != self.nvram) { + return Err("Tezro NVRAM save uses the configured path".into()); + } + self.board.timekeeper.flush(); + Ok(self.nvram.clone()) + } + fn scratch_path(&self) -> Option<&Path> { + self.scratch.as_deref() + } + fn disc_changer(&self) -> Option> { + Some(self.board.scsi.clone() as Arc) + } + fn pending_chd_sync_count(&self) -> usize { + self.scsi_buses().iter().map(|bus| bus.pending_chd_sync_count()).sum() + } + fn flush_disks(&self) -> std::io::Result<()> { + self.board.flush_disks() + } + fn sync_chd_disks( + &self, + only: Option, + progress: &mut dyn FnMut(usize, usize, f32), + cancel: &dyn Fn() -> bool, + ) -> std::io::Result { + ScsiBus::sync_chd_disks(&self.scsi_buses(), only, progress, cancel) + } + fn net_frames(&self) -> Option> { + self.frames.clone() + } + fn nat_control(&self) -> Option> { + self.nat.as_ref().map(|(n, _)| n.clone()) + } + fn nat_expected(&self) -> Option<(Ipv4Addr, Ipv4Addr, Ipv4Addr)> { + self.nat.as_ref().map(|(_, a)| *a) + } +} + +impl TezroMachine { + /// CPU 0, the one the front ends drive. + fn cpu(&self) -> &Arc { + &self.cpus[0] + } + + fn scsi_buses(&self) -> Vec<&ScsiBus> { + (0..2).filter_map(|channel| self.board.scsi.bus_for_channel(channel)).collect() + } +} + +impl Drop for TezroMachine { + fn drop(&mut self) { + if let Some(slot) = self.slot.take() { + slot.lock().0 = MachineRef::null(); + } + self.stop(); + self.monitor.shutdown(); + } +} + +/// The processors the machine runs, of the board's chip: one for each +/// fitted slot, CPU 0 first, in the board's slot order. +fn build_cpus(board: &Arc, timers: &Arc, count_hz: u64) -> Vec> { + let count = board.processors.count(); + crate::with_cpu!(board.chip.chip, true, false, C => { + (0..count).map(|id| start_cpu::(board, timers, count_hz, id as u8, count)).collect() + }) +} + +/// Processor `id` of `count`, of type `C`, with its timers; CPU 0 also +/// runs the board's poll. A chip with a secondary cache of another size is +/// another type, instantiated here. +fn start_cpu(board: &Arc, timers: &Arc, count_hz: u64, id: u8, count: usize) -> Arc +where + C: CacheCpuModel + From> + 'static, + MipsCpu: CpuDevice, +{ + let mut exec = build_cpu::(board); + // CPU 0's Compare matches blink the graphics status bar's LED. + exec.core.fasttick_count = if id == 0 { board.fasttick.clone() } else { Arc::new(AtomicU64::new(0)) }; + exec.core.set_timer_manager(timers.clone()); + exec.core.set_count_hz(count_hz); + exec.core.count_write_samples = true; + exec.core.cpu_id = id; + exec.core.smp = count > 1; + if id == 0 { + let poll_board = board.clone(); + exec.batch_hook = Some(Box::new(move |_| poll_board.poll())); + } + Arc::new(MipsCpu::new(*exec)) +} + +/// A processor of type `C` with the board chip's identity and reset-mode +/// inputs, on the board's bus. +fn build_cpu>>(board: &TezroBoard) -> Box> { + let chip = board.chip.chip; + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip35::memory_map(&board.bedrock.ram_extent()))) + .with_prid(chip.prid()); + let mut cpu = Box::new(MipsExecutor::new( + board.bus.clone() as Arc, + MipsTlb::new(cfg.tlb_entries), + &cfg, + )); + cpu.phys_mask = CPU_PHYS_MASK; + // The Config word of the chip's flash record over the low 25 bits the + // cache type builds (FIR is the type's). The flash record's mode word and + // the processor's reset mode must agree: on a mismatch the PROM's early + // check calls a routine that clears the processor's presence and enable + // bits and returns into the normal start-up, but it runs before GP is + // set, so what it does on silicon is unknown; on the model it stops in + // that routine. + cpu.core.cp0_config = (cpu.core.cp0_config & !CONFIG0_MODE_MASK) | board.chip.firmware.reset_mode(); + cpu.cache = C::from(board.cached_bus()); + // The PROM fetches its own code uncached; with memory over its window + // the cached plane answers RAM there, so the fetch is presented at the + // PROM's own address and compiled from the view that holds it. + cpu.set_uncached_fetch_alias(crate::ip35::RESET_WINDOW, crate::ip35::RESET_WINDOW_SIZE, crate::ip35::RESET_WINDOW_FETCH); + #[cfg(feature = "jitv2")] + cpu.set_cached_bus(board.code_bus()); + #[cfg(not(feature = "jitv2"))] + cpu.set_cached_bus(board.cached_bus()); + cpu +} + +pub(crate) struct TezroBoardConfig<'a> { + pub payload: &'a [u8], + pub clock: Clock, + pub flash_path: Option<&'a Path>, + pub station_mac: [u8; 6], + /// The DS1744-compatible clock and NVRAM in the IOC4's BAR. + pub timekeeper: Arc, + pub memory: &'a MemoryPopulation, + /// Processors fitted. + pub cpus: usize, + /// The R16000 chip fitted. + pub chip: &'static TezroChip, +} + +/// The Tezro's devices around its Bedrock node. +pub(crate) struct TezroBoard { + pub bus: Arc, + pub bedrock: Arc, + pub l1: Arc, + pub flash: Arc, + /// Cacheable memory at plain physical addresses: what the caches fill + /// from. + cached: Arc, + /// What the JIT compiles code through: `cached`, and the reset PROM's + /// uncached-fetch window ([`Ip35Bus::code_view`]). + #[cfg(feature = "jitv2")] + code: Arc, + /// The node's resets and power events, and their receiver until the + /// system controller takes it. + events: Sender, + receiver: Mutex>>, + /// The slots whose processors run: every fitted one. + pub processors: Population, + /// The PICs in QEMU's host order, which numbers their PI level sources: + /// widget 11's first when the board has it, then the IO9's at widget 15. + pub pics: Vec>, + pub ioc4: Arc, + pub scsi: Arc, + pub eth: Arc, + pub timekeeper: Arc, + pub serial: [Arc; SERIAL_PORTS], + /// The R16000 chip fitted, whose flash record the node holds. + pub chip: &'static TezroChip, + /// The status bar's activity bits and the CPU's Compare matches, which + /// the graphics board shows. + pub heartbeat: Arc, + pub fasttick: Arc, + /// What the graphics board's status bar shows: the activity bits, the + /// Compare matches and the CPU's cycle count and Count rate. + pub status: Arc, + clock: Clock, + last_clock: AtomicU64, + running: AtomicBool, +} + +impl TezroBoard { + /// The board's devices that take monitor commands: the physical map, the + /// L1, the SCSI controller, the keyboard and mouse (`ps2 type` and + /// friends). + pub fn monitor_devices(&self) -> Vec> { + vec![self.bedrock.physical().clone(), self.l1.clone(), self.scsi.clone(), self.ioc4.ps2()] + } + + /// Flush the disks on both channels of the storage controller. + pub fn flush_disks(&self) -> std::io::Result<()> { + (0..2).try_for_each(|channel| self.scsi.bus_for_channel(channel).unwrap().flush()) + } + + #[cfg(test)] + pub fn new(cpus: usize) -> Result { + Self::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968))?), + memory: &MemoryPopulation::default(), + cpus, + chip: &CHIPS[0], + }) + } + + pub fn with_config(config: TezroBoardConfig<'_>) -> Result { + let TezroBoardConfig { payload, clock, flash_path, station_mac, timekeeper, memory, cpus, chip } = config; + if ![1, 2, 4].contains(&cpus) { + return Err(format!("{cpus} processors; a Tezro board fits 1, 2 or 4")); + } + chip.accepts_cpus(cpus as u8)?; + memory.check_for_cpus(cpus)?; + let running = Population::fitted_cpus(cpus)?; + let node = Node::build(NodeConfig { + name: "Tezro", + payload, + clock: clock.clone(), + flash_path, + banks_mib: &memory.banks_mib(), + processors: running, + firmware: chip.firmware, + board_id: BOARD_ID, + l1: L1Config { + type_code: 0x39, + revision: Some([1, 48, 1]), + mac: station_mac, + port_speed_first: 1, + router: true, + cpu_context: Some(cpu_context(&chip.firmware)), + node_board: Some(node_board(chip, cpus)), + ..Default::default() + }, + })?; + let Node { + bus, + bedrock, + l1, + flash, + cached, + #[cfg(feature = "jitv2")] + code, + events, + receiver, + } = node; + let serial: [Arc; SERIAL_PORTS] = std::array::from_fn(|_| Arc::new(CiSerialBackend::new())); + l1.set_serial_backend(serial[0].clone()); + + let io9 = Pic::new(IO9_WIDGET); + let pxbow = io9.enable_pxbow(HOST_WIDGET, clock.clone()).map_err(|e| format!("Tezro PXBOW: {e:?}"))?; + bedrock.connect_widget(0, pxbow.widget_zero()); + bedrock.connect_widget(IO9_WIDGET, pxbow.route(IO9_WIDGET)); + let heartbeat = Arc::new(AtomicU64::new(0)); + let fasttick = Arc::new(AtomicU64::new(0)); + let status = crate::disp::StatusSource::new(heartbeat.clone(), fasttick.clone()); + let pics = if cpus == 1 { + vec![io9.clone()] + } else { + // The PROM finds widget 11 only as a live link of widget 0's + // crossbar, so the second PIC is a port of the same switch. + let second = Pic::new(SECOND_PIC_WIDGET); + pxbow + .attach_endpoint(SECOND_PIC_WIDGET, second.widget()) + .map_err(|e| format!("Tezro: second PIC on the PXBOW: {e:?}"))?; + bedrock.connect_widget(SECOND_PIC_WIDGET, pxbow.route(SECOND_PIC_WIDGET)); + vec![second, io9.clone()] + }; + for pic in &pics { + pic.connect_peer(Arc::new(BedrockPeer(Arc::downgrade(&bedrock)))) + .map_err(|_| "Tezro: PIC peer connected twice".to_string())?; + } + + let ioc4 = Arc::new(Ioc4::new(Ioc4Config::tezro())); + ioc4.attach_clock(timekeeper.clone()); + for (port, backend) in serial[1..].iter().enumerate() { + ioc4.set_serial_backend(port, backend.clone()); + } + let scsi = Arc::new(Isp12160::new(Isp12160::new_bus(), Isp12160::new_bus())); + scsi.set_heartbeat(heartbeat.clone()); + // The Ethernet slot drives REQ64#, which the chip samples as reset ends. + let eth = Bcm5701::new(Bcm5701Config { req64: true, ..Bcm5701Config::new(l1.station_mac()) })?; + io9.attach(0, IOC4_DEVICE, ioc4.clone(), Function { self_ordered_pio: true, req64: false }); + let ti = PciToPciBridge::with_revision( + io9.bus(0), + DevFn { device: TI_BRIDGE_DEVICE, function: 0 }, + TI_BRIDGE_VENDOR, + TI_BRIDGE_ID, + TI_BRIDGE_REVISION, + ); + io9.attach(0, TI_BRIDGE_DEVICE, ti, Function::default()); + io9.attach(0, SCSI_DEVICE, scsi.clone(), Function::default()); + io9.attach(0, ETH_DEVICE, eth.clone(), Function { self_ordered_pio: false, req64: true }); + Ok(Self { + bus, + bedrock, + l1, + flash, + cached, + #[cfg(feature = "jitv2")] + code, + events, + receiver: Mutex::new(Some(receiver)), + processors: running, + pics, + ioc4, + scsi, + eth, + timekeeper, + serial, + chip, + heartbeat, + fasttick, + status, + last_clock: AtomicU64::new(clock()), + clock, + running: AtomicBool::new(false), + }) + } + + pub fn cached_bus(&self) -> Arc { + self.cached.clone() + } + + /// The bus the JIT compiles code through; see [`Ip35Bus::code_view`]. + #[cfg(feature = "jitv2")] + pub fn code_bus(&self) -> Arc { + self.code.clone() + } + + /// Everything back to its power-on state. + pub fn power_on(&self) { + self.bedrock.power_on(); + self.l1.power_on(); + self.flash.power_on(); + self.ioc4.power_on(); + self.scsi.power_on(); + self.eth.power_on(); + for pic in &self.pics { + pic.power_on(); + } + } + + pub fn start(&self) { + self.last_clock.store((self.clock)(), Ordering::Release); + self.running.store(true, Ordering::Release); + self.bedrock.start(); + self.l1.start(); + self.ioc4.start(); + self.eth.start(); + self.scsi.start(); + } + + /// The boot flash's command timing, on CPU 0's batch boundary. + pub fn poll(&self) { + if self.running.load(Ordering::Acquire) { + let now = (self.clock)(); + let previous = self.last_clock.swap(now, Ordering::AcqRel); + self.flash.advance_ns(now.saturating_sub(previous)); + } + } + + pub fn stop(&self) { + self.poll(); + self.running.store(false, Ordering::Release); + self.scsi.stop(); + self.eth.stop(); + self.ioc4.stop(); + self.l1.stop(); + self.bedrock.stop(); + } +} + +impl Drop for TezroBoard { + fn drop(&mut self) { + self.stop(); + } +} + +/// The far end of a PIC's link: Bedrock IO. Interrupt packets and DMA arrive +/// there as inbound packets from the PIC's widget, device 0, as QEMU's +/// Bedrock IO takes them by default. DMA aimed at another widget is refused: +/// peer-to-peer XIO is not modelled. +struct BedrockPeer(Weak); + +const INBOUND_DEVICE: u8 = 0; + +impl BedrockPeer { + fn io(&self) -> Result, Refused> { + self.0.upgrade().ok_or(Refused) + } + + fn host(widget: u8) -> Result<(), Refused> { + if widget == HOST_WIDGET { + Ok(()) + } else { + dlog!(LogModule::Bedrock, "Tezro: PIC DMA to widget {widget} refused; only Bedrock IO is reachable"); + Err(Refused) + } + } + + /// The legs of a transfer: each the largest naturally aligned size of + /// eight bytes or fewer that fits, so word accesses keep their width. + fn legs(address: u64, len: usize) -> impl Iterator { + let mut offset = 0; + std::iter::from_fn(move || { + if offset == len { + return None; + } + let at = address + offset as u64; + let size = [8usize, 4, 2, 1] + .into_iter() + .find(|&n| at.is_multiple_of(n as u64) && n <= len - offset) + .unwrap_or(1); + let leg = (offset, size); + offset += size; + Some(leg) + }) + } +} + +impl PicPeer for BedrockPeer { + fn interrupt(&self, source_widget: u8, address: u64, payload: u64) -> Result<(), Refused> { + let io = self.io()?; + (io.inbound_write(source_widget, Some(INBOUND_DEVICE), address, 8, payload) == BUS_OK) + .then_some(()) + .ok_or(Refused) + } + + fn dma_read(&self, source_widget: u8, widget: u8, address: u64, data: &mut [u8]) -> Result<(), Refused> { + Self::host(widget)?; + let io = self.io()?; + for (offset, size) in Self::legs(address, data.len()) { + let value = io.inbound_read(source_widget, Some(INBOUND_DEVICE), address + offset as u64, size as u8); + if value.status != BUS_OK { + return Err(Refused); + } + data[offset..offset + size].copy_from_slice(&value.data.to_be_bytes()[8 - size..]); + } + Ok(()) + } + + fn dma_write(&self, source_widget: u8, widget: u8, address: u64, data: &[u8]) -> Result<(), Refused> { + Self::host(widget)?; + let io = self.io()?; + for (offset, size) in Self::legs(address, data.len()) { + let value = data[offset..offset + size].iter().fold(0u64, |v, b| (v << 8) | u64::from(*b)); + if io.inbound_write(source_widget, Some(INBOUND_DEVICE), address + offset as u64, size as u8, value) != BUS_OK { + return Err(Refused); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip53_machine/tests.rs b/src/ip53_machine/tests.rs new file mode 100644 index 000000000..7aa4edef3 --- /dev/null +++ b/src/ip53_machine/tests.rs @@ -0,0 +1,683 @@ +use super::*; +use crate::traits::{BUS_ERR, BUS_OK}; +use std::sync::atomic::AtomicU64; + +/// A PIO offset in Bedrock IO's small window: widget in bits 27:24. +/// The II's PIO face as the processor reaches it, a `size`-byte access at +/// a time. +struct Pio(Arc); +impl Pio { + fn pio_read(&self, a: u64, size: u8) -> crate::traits::BusRead64 { + match size { + 8 => self.0.read64(a), + 4 => { + let r = self.0.read32(a); + crate::traits::BusRead64 { data: u64::from(r.data), status: r.status } + } + _ => unreachable!("tests read words and doublewords"), + } + } + fn pio_write(&self, a: u64, size: u8, value: u64) -> u32 { + match size { + 8 => self.0.write64(a, value), + 4 => self.0.write32(a, value as u32), + _ => unreachable!("tests write words and doublewords"), + } + } +} +fn pio_read(bedrock: &Arc, a: u64, size: u8) -> crate::traits::BusRead64 { + Pio(bedrock.pio()).pio_read(a, size) +} + +fn widget(widget: u64, offset: u64) -> u64 { + (widget << 24) | offset +} + +/// Configuration register 0 of `device` on the IO9 PIC's head 0. +fn config_id(device: u64) -> u64 { + widget(15, 0x2_0000 + (device << 12)) +} + +/// The processor finds the PXBOW at widget 0 and the IO9's PIC at widget +/// 15, as the QEMU profile maps them; the one-processor board has no widget +/// 11 and no other widget answers. +#[test] +fn widget_0_is_the_pxbow_and_widget_15_the_io9_pic() { + let board = TezroBoard::new(1).unwrap(); + let io = Pio(board.bedrock.pio()); + let pxbow = io.pio_read(widget(0, 4), 4); + assert_eq!(pxbow.status, BUS_OK); + assert_eq!((pxbow.data >> 12) & 0xffff, u64::from(crate::dev::pxbow::PART)); + let pic = io.pio_read(widget(15, 0), 8); + assert_eq!(pic.status, BUS_OK); + assert_eq!(pic.data, u64::from(crate::dev::pic::identity(0))); + for absent in [8, 9, 11, 12, 13, 14] { + assert_eq!(io.pio_read(widget(absent, 0), 8).status, BUS_ERR, "widget {absent}"); + } + assert_eq!(board.pics.len(), 1); +} + +/// IO9's functions sit at PCI devices 1, 3 and 4 of head 0 with the +/// revisions Tezro consoles print (the TI bridge at device 2 has its own +/// test). +#[test] +fn the_io9_bus_holds_the_ioc4_the_isp12160_and_the_bcm5701() { + let board = TezroBoard::new(1).unwrap(); + let io = Pio(board.bedrock.pio()); + for (device, id, revision) in [(1, 0x100a_10a9, 0x53), (3, 0x1216_1077, 0x06), (4, 0x1645_14e4, 0x15)] { + assert_eq!(io.pio_read(config_id(device), 4).data, id, "device {device}"); + assert_eq!(io.pio_read(config_id(device) + 8, 4).data & 0xff, revision, "device {device}"); + } +} + +/// The BCM5701's slot drives REQ64#, so the chip reports a 64-bit bus, +/// and still does after the board is reset. +#[test] +fn the_bcm5701_sees_req64_before_and_after_a_reset() { + use crate::pci::PciFunction; + const PCISTATE: usize = 0x70 / 4; + const BUS_32BIT: u32 = 1 << 4; + let board = TezroBoard::new(1).unwrap(); + assert_eq!(board.eth.config_dword(PCISTATE) & BUS_32BIT, 0); + board.power_on(); + assert_eq!(board.eth.config_dword(PCISTATE) & BUS_32BIT, 0); +} + +/// The board identity the PROM reads: Bedrock board word 0x2001 and the +/// L1's Tezro station address. +#[test] +fn the_board_reports_the_tezro_board_word() { + let board = TezroBoard::new(1).unwrap(); + let id = board.bus.read64(0x1000_0000 + 0x400_0000); + assert_eq!((id.status, id.data), (BUS_OK, BOARD_ID)); + assert_eq!(board.l1.station_mac(), L1Config::default().mac); +} + +/// The fields of an L1 EEPROM area after its six-byte header, up to its +/// end mark: each a descriptor byte whose low six bits give the length. +fn area_fields(area: &[u8]) -> Vec<&[u8]> { + let mut fields = Vec::new(); + let mut at = 6; + while area[at] != 0xc1 { + let len = usize::from(area[at] & 0x3f); + fields.push(&area[at + 1..at + 1 + len]); + at += 1 + len; + } + fields +} + +/// The PROM's start-up check compares the boot flash's configuration block +/// with the CPU configuration in the node board's L1 record (component 0's +/// board area): the mode word at +0x04 with Config0, the clocks at +0x10 and +/// +0x18 with the CPU and Hub MHz, and the word at +0x30 with the class it +/// derives from the board word's top nibble (0x4 gives 2, 0x8 gives 1, any +/// other 0). Any difference prints "CPU configuration values mismatch". +#[test] +fn the_node_board_record_agrees_with_the_flash_configuration() { + let class = match BOARD_ID & 0xf000 { + 0x4000 => 2u32, + 0x8000 => 1, + _ => 0, + }; + for cpus in [1, 2, 4] { + let board = TezroBoard::new(cpus).unwrap(); + let area = board.l1.record(0, 2).expect("component 0's board area"); + assert_eq!(area.iter().fold(0u8, |s, b| s.wrapping_add(*b)), 0, "the area's checksum"); + assert_eq!(area[0], 2, "the format that carries CPU fields"); + let fields = area_fields(&area); + let identity = node_board(&CHIPS[0], cpus); + assert_eq!([fields[1], fields[3], fields[5]], [identity.name, identity.part, identity.revision].map(str::as_bytes), + "{cpus} CPUs: the node board's name, part and revision"); + assert_eq!(board.l1.record(1, 2), None, "{cpus} CPUs: no I/O board record"); + assert_eq!(fields[8], [3], "the CPU-field selector"); + let word = |f: &[u8]| u32::from_be_bytes(f.try_into().unwrap()); + let (config0, hub_mhz, cpu_mhz) = (word(fields[10]), word(fields[14]), word(fields[15])); + for empty in [9, 11, 12, 13, 16, 17, 18] { + assert!(fields[empty].is_empty(), "field {empty} is unknown and left empty"); + } + let block = &board.flash.contents()[0x60..0xb0]; + let at = |o: usize, n: usize| block[o..o + n].iter().fold(0u64, |v, b| v << 8 | u64::from(*b)); + assert_eq!(at(0x04, 4), u64::from(config0), "{cpus} CPUs: the mode word"); + assert_eq!(at(0x10, 8), u64::from(cpu_mhz) * 1_000_000, "{cpus} CPUs: the CPU clock"); + assert_eq!(at(0x18, 8), u64::from(hub_mhz) * 1_000_000, "{cpus} CPUs: the Hub clock"); + assert_eq!(at(0x30, 4), u64::from(class), "{cpus} CPUs: the board word's class"); + } + for chip in &CHIPS { + let context = cpu_context(&chip.firmware); + assert_eq!(context.mode_word, chip.firmware.cpu_mode_word, "{}", chip.chip.label()); + assert_eq!(context.cpu_mhz * 1_000_000, chip.firmware.cpu_clock_hz, "{}", chip.chip.label()); + assert_eq!(chip.firmware.machine_type, class, "{}", chip.chip.label()); + } +} + +/// A board of `cpus` processors of `chip`, with the defaults otherwise. +fn board_with_chip(cpus: usize, chip: &'static TezroChip) -> TezroBoard { + TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::default(), + cpus, + chip, + }) + .unwrap() +} + +/// The node board the L1 names follows the chip as well as the processor +/// count: the 800 MHz chip sits on 030-1956-002 and the 1 GHz chip on the +/// IP59 boards, as their inventories print, and every name is one the +/// PROM's socket-mask table holds. +#[test] +fn the_node_board_record_names_the_board_for_the_chip_and_count() { + let expected: [(&str, usize, [&str; 3]); 7] = [ + ("700", 1, ["IP53_1CPU", "", ""]), + ("700", 2, ["IP53_2CPU", "030-1869-001", "-C"]), + ("700", 4, ["IP53_4CPU", "030-1868-001", "-C"]), + ("800", 4, ["IP53_4CPU", "030-1956-002", "-C"]), + ("900", 4, ["IP53_4CPU", "", ""]), + ("1000", 2, ["IP59_2CPU", "030-2059-002", "-A"]), + ("1000", 4, ["IP59_4CPU", "030-1989-003", "-C"]), + ]; + for (mhz, cpus, [name, part, revision]) in expected { + let chip = CHIPS.iter().find(|c| c.chip.mhz.to_string() == mhz).unwrap(); + let board = board_with_chip(cpus, chip); + let area = board.l1.record(0, 2).expect("component 0's board area"); + let fields = area_fields(&area); + assert_eq!([fields[1], fields[3], fields[5]], [name, part, revision].map(str::as_bytes), "{mhz} MHz, {cpus} CPUs"); + } + // Every (chip, count) the board accepts has a board, and no other. + for chip in &CHIPS { + assert_eq!(chip.boards.iter().map(|(n, _)| *n).collect::>(), chip.cpus, "{}", chip.chip.label()); + } +} + +/// The IO9's head 0 carries a TI PCI-to-PCI bridge at device 2, revision +/// 1, with a type 1 header and no interrupt pin, on every board; nothing +/// answers on the bus behind it. +#[test] +fn the_io9_has_a_ti_bridge_with_an_empty_bus_at_device_2() { + for cpus in [1, 2, 4] { + let board = TezroBoard::new(cpus).unwrap(); + let io = Pio(board.bedrock.pio()); + let config = |reg: u64| io.pio_read(config_id(2) + reg, 4); + assert_eq!(config(0).data, 0xac28_104c, "{cpus} CPUs: vendor and device"); + assert_eq!(config(8).data, 0x0604_0001, "{cpus} CPUs: a PCI-to-PCI bridge, revision 1"); + assert_eq!((config(0xc).data >> 16) & 0x7f, 1, "{cpus} CPUs: header type 1"); + assert_eq!((config(0x3c).data >> 8) & 0xff, 0, "{cpus} CPUs: no interrupt pin"); + let io9 = board.pics.last().unwrap().bus(0); + let at = DevFn { device: TI_BRIDGE_DEVICE, function: 0 }; + assert!(io9.config_write(at, 0x18, 4, 0x0001_0100), "number the bus behind it 1"); + let behind = io9.find_bus(1).expect("the bridge's secondary bus"); + // An unclaimed read behind a bridge ends in master abort: all ones. + for device in 0..32 { + assert_eq!(behind.config_read(DevFn { device, function: 0 }, 0, 4), Some(u32::MAX), "device {device} behind the bridge"); + } + } +} + +/// PIC DMA reaches memory through Bedrock IO in naturally aligned legs, and +/// DMA aimed at any widget but Bedrock IO is refused. +#[test] +fn pic_dma_reaches_memory_through_bedrock_io_only() { + let board = TezroBoard::new(1).unwrap(); + // Enable bank pair 0, as the Fuel's DMA tests do. + assert_eq!(board.bedrock.registers().write64(crate::dev::bedrock::reg::MD_BASE + 8, 0x33), BUS_OK); + let peer = BedrockPeer(Arc::downgrade(&board.bedrock)); + let data: Vec = (1..=19).collect(); + assert_eq!(peer.dma_write(15, HOST_WIDGET, 0x1003, &data), Ok(())); + let mut back = vec![0; data.len()]; + assert_eq!(peer.dma_read(15, HOST_WIDGET, 0x1003, &mut back), Ok(())); + assert_eq!(back, data); + let memory = board.bedrock.dma_view(); + assert_eq!(memory.read64(0x1000).data, 0x0000_0001_0203_0405); + assert_eq!(peer.dma_write(15, 12, 0x1003, &data), Err(Refused)); + assert_eq!(peer.dma_read(15, 0, 0x1003, &mut back), Err(Refused)); +} + +#[test] +fn transfer_legs_keep_aligned_widths() { + let legs: Vec<_> = BedrockPeer::legs(0x1003, 19).collect(); + assert_eq!(legs, [(0, 1), (1, 4), (5, 8), (13, 4), (17, 2)]); + assert_eq!(BedrockPeer::legs(0x2000, 16).collect::>(), [(0, 8), (8, 8)]); + assert_eq!(BedrockPeer::legs(0x2000, 0).count(), 0); +} + +/// The two- and four-processor board adds a second PIC at widget 11 with +/// nothing on its buses, keeps the IO9 on widget 15 behind widget 0, and +/// leaves the V12's widget 12 empty. +#[test] +fn two_or_four_processors_bring_the_second_pic_at_widget_11() { + for cpus in [2, 4] { + let board = TezroBoard::new(cpus).unwrap(); + let io = Pio(board.bedrock.pio()); + assert_eq!(board.pics.iter().map(|p| p.widget_number()).collect::>(), [11, 15]); + assert_eq!(io.pio_read(widget(11, 0), 8).data, u64::from(crate::dev::pic::identity(0))); + assert_eq!(io.pio_read(widget(15, 0), 8).data, u64::from(crate::dev::pic::identity(0))); + assert_eq!(io.pio_read(widget(12, 0), 8).status, BUS_ERR, "no V12"); + assert_eq!((io.pio_read(widget(0, 4), 4).data >> 12) & 0xffff, u64::from(crate::dev::pxbow::PART)); + for device in 1..=4 { + assert_eq!(io.pio_read(widget(11, 0x2_0000 + (device << 12)), 4).status, BUS_ERR, "widget 11 device {device}"); + } + assert_eq!(io.pio_read(config_id(1), 4).data, 0x100a_10a9); + } +} + +// Widget 0's crossbar as PROM 6.210 and the IP35 kernel walk it, from how the PROM finds +// widgets 0xb and 0xf. +// QEMU's sn/builder/tezro-two-four-processor-board in +// tests/qtest/sn-builder-test.c still expects link 11 absent; this board +// follows the PROM's walk instead. + +/// Link `n`'s register block in widget 0, as the 32-bit word address the +/// PROM issues: the low word of each doubleword. +fn link_word(n: u64, register: u64) -> u64 { + widget(0, 0x100 + (n - 8) * 0x40 + register + 4) +} +const LINK_CONTROL: u64 = 0x08; +const LINK_STATUS: u64 = 0x10; +const LINK_ARB_UPPER: u64 = 0x18; +const LINK_ARB_LOWER: u64 = 0x20; +const LINK_STATUS_CLEAR: u64 = 0x28; +const LINK_RESET: u64 = 0x30; +const LINK_AUX: u64 = 0x38; +const ALIVE: u64 = 1 << 31; +const PRESENT: u64 = 1 << 5; +const FAILURE_MODE: u64 = 1 << 6; + +fn read_word(board: &TezroBoard, address: u64) -> u64 { + let r = pio_read(&board.bedrock, address, 4); + assert_eq!(r.status, BUS_OK, "{address:#x}"); + r.data +} + +/// Widget 0's part and manufacturer, from its identity word. +fn widget_zero_part(board: &TezroBoard) -> (u64, u64) { + let id = read_word(board, widget(0, 4)); + ((id >> 12) & 0xffff, (id >> 1) & 0x7ff) +} + +fn alive_and_present(board: &TezroBoard, n: u64) -> (bool, bool) { + (read_word(board, link_word(n, LINK_STATUS)) & ALIVE != 0, read_word(board, link_word(n, LINK_AUX)) & PRESENT != 0) +} + +/// The PROM's per-link test: alive and present, except that link 15 is +/// taken regardless when widget 0 is part 0xd100. +fn prom_uses(board: &TezroBoard, n: u64) -> bool { + let (alive, present) = alive_and_present(board, n); + (alive && present) || (n == 15 && widget_zero_part(board).0 == 0xd100) +} + +/// The kernel's: alive, and present unless widget 0 is part 0xd100 with +/// manufacturer 0 and the link is 15. +fn kernel_uses(board: &TezroBoard, n: u64) -> bool { + let (alive, present) = alive_and_present(board, n); + alive && (present || (n == 15 && widget_zero_part(board) == (0xd100, 0))) +} + +/// Links 8 to 15 that pass both tests on their own bits, without either +/// guest's link-15 exception, as a widget mask. +fn live_links(board: &TezroBoard) -> u16 { + (8..16).filter(|&n| alive_and_present(board, n) == (true, true)).fold(0, |m, n| m | 1 << n) +} + +/// A board whose clock the test advances. +fn board_with_clock(cpus: usize) -> (TezroBoard, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let clock = now.clone(); + let board = TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(move || clock.load(Ordering::SeqCst)), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::default(), + cpus, + chip: &CHIPS[0], + }) + .unwrap(); + (board, now) +} + +/// On the two- and four-processor board widget 0's crossbar shows the +/// hub's link 10 and both PICs' links 11 and 15 alive and present, with no +/// failure mode and arbitration bits 0 and 2 clear, so the PROM and the +/// kernel both take 11 and 15 without leaning on the link-15 exception. +/// Every other link reads empty. A power-on keeps the same picture. +#[test] +fn widget_0_shows_both_pic_links_live_on_the_larger_board() { + for cpus in [2, 4] { + let board = TezroBoard::new(cpus).unwrap(); + for pass in ["built", "after power-on"] { + assert_eq!(widget_zero_part(&board), (0xd100, 0), "{cpus} CPUs, {pass}"); + assert_eq!(read_word(&board, widget(0, 4)) & 1, 1); + assert_eq!(live_links(&board), 1 << 10 | 1 << 11 | 1 << 15, "{cpus} CPUs, {pass}"); + for n in [11, 15] { + assert!(prom_uses(&board, n) && kernel_uses(&board, n), "link {n}, {cpus} CPUs, {pass}"); + assert_eq!(read_word(&board, link_word(n, LINK_AUX)) & FAILURE_MODE, 0, "link {n}"); + assert_eq!(read_word(&board, link_word(n, LINK_ARB_UPPER)) & 0b101, 0, "link {n}: unclaimed"); + } + for n in [8, 9, 12, 13, 14] { + assert_eq!(alive_and_present(&board, n), (false, false), "link {n}, {cpus} CPUs, {pass}"); + assert!(!prom_uses(&board, n) && !kernel_uses(&board, n)); + } + board.power_on(); + } + } +} + +/// The one-processor board has no second PIC: link 11 reads empty and +/// widget 11 does not answer, while link 15 stays usable. +#[test] +fn the_one_processor_board_shows_no_link_11() { + let board = TezroBoard::new(1).unwrap(); + assert_eq!(live_links(&board), 1 << 10 | 1 << 15); + assert_eq!(alive_and_present(&board, 11), (false, false)); + assert!(!prom_uses(&board, 11) && !kernel_uses(&board, 11)); + assert!(prom_uses(&board, 15) && kernel_uses(&board, 15)); + assert_eq!(pio_read(&board.bedrock, widget(11, 4), 4).status, BUS_ERR); +} + +/// Widget 11 answers as a bridge-class PIC with both heads, the identity +/// the PROM's "Found PIC" path needs (manufacturer 0x24, bit 0 set, part +/// 0xd102 on head 0), and takes the PROM's control-word writes on both +/// heads. Widget 15 keeps its own. +#[test] +fn widget_11_answers_as_a_pic_with_both_heads() { + use crate::dev::pic::HEAD_OFFSET; + let board = TezroBoard::new(4).unwrap(); + let io = Pio(board.bedrock.pio()); + for (head, part) in [(0, 0xd102), (HEAD_OFFSET, 0xd112)] { + let id = read_word(&board, widget(11, head + 4)); + assert_eq!((id & 1, (id >> 1) & 0x7ff, (id >> 12) & 0xffff), (1, 0x24, part), "head at {head:#x}"); + // Widget control, as the PROM's table-driven field writes it. + let control = widget(11, head + 0x24); + let before = read_word(&board, control); + assert_eq!(io.pio_write(control, 4, (before & !0xf000) | 0x4000), BUS_OK); + assert_eq!(read_word(&board, control) & 0xf000, 0x4000, "head at {head:#x}"); + } + assert_eq!(read_word(&board, widget(15, 4)) >> 12 & 0xffff, 0xd102); +} + +/// The PROM's bring-up of link 11 (read the status clear, write credit 2, +/// clear both arbitration registers, then the 0xd102 credit 3) leaves the +/// link alive and present and the credit field reading back. +#[test] +fn the_proms_link_bring_up_keeps_link_11_alive() { + let board = TezroBoard::new(4).unwrap(); + let io = Pio(board.bedrock.pio()); + read_word(&board, link_word(11, LINK_STATUS_CLEAR)); + assert_eq!(io.pio_write(link_word(11, LINK_CONTROL), 4, 0x0008_0000), BUS_OK); + assert_eq!(io.pio_write(link_word(11, LINK_ARB_UPPER), 4, 0), BUS_OK); + assert_eq!(io.pio_write(link_word(11, LINK_ARB_LOWER), 4, 0), BUS_OK); + assert_eq!(alive_and_present(&board, 11), (true, true)); + let control = read_word(&board, link_word(11, LINK_CONTROL)); + assert_eq!((control >> 18) & 0x1f, 2); + assert_eq!(io.pio_write(link_word(11, LINK_CONTROL), 4, (control & !(0x1f << 18)) | 3 << 18), BUS_OK); + assert_eq!((read_word(&board, link_word(11, LINK_CONTROL)) >> 18) & 0x1f, 3); + assert_eq!(alive_and_present(&board, 11), (true, true)); + assert_eq!(io.pio_read(widget(11, 4), 4).status, BUS_OK); +} + +/// The processor reaches widget 11 through the crossbar, not around it: a +/// reset of link 11 or of the hub's link 10 takes widget 11 out of reach +/// for the pulse, and it answers again, registers kept, once it ends. A +/// reset of link 15 leaves widget 11 alone. +#[test] +fn widget_11_is_routed_through_widget_0s_crossbar() { + let (board, now) = board_with_clock(4); + let io = Pio(board.bedrock.pio()); + let route = widget(11, 0x130); + assert_eq!(io.pio_write(route, 8, 0x1122_3344_5566_7788), BUS_OK); + for link in [11, 10] { + now.fetch_add(100, Ordering::SeqCst); + assert_eq!(io.pio_write(link_word(link, LINK_RESET), 4, 0), BUS_OK); + assert_eq!(io.pio_read(route, 8).status, BUS_ERR, "down during link {link}'s pulse"); + now.fetch_add(20, Ordering::SeqCst); + let r = io.pio_read(route, 8); + assert_eq!((r.status, r.data), (BUS_OK, 0x1122_3344_5566_7788), "after link {link}'s pulse"); + assert_eq!(alive_and_present(&board, 11), (true, true)); + } + now.fetch_add(100, Ordering::SeqCst); + assert_eq!(io.pio_write(link_word(15, LINK_RESET), 4, 0), BUS_OK); + assert_eq!(io.pio_read(route, 8).status, BUS_OK, "link 15's pulse"); + assert_eq!(io.pio_read(widget(15, 4), 4).status, BUS_ERR); + assert_eq!(board.pics[1].pxbow().unwrap().endpoint_widgets(), 1 << 11 | 1 << 15); + assert!(board.pics[0].pxbow().is_none(), "the second PIC has no crossbar of its own"); +} + +/// Every fitted processor slot reads present, in the board's order (CPU 1 +/// in PI1/A, CPUs 2 and 3 in the B slots), and every fitted processor runs +/// from reset with its enable set, as QEMU's node grants them. +#[test] +fn fitted_processors_read_present_and_start_enabled() { + use crate::dev::bedrock::reg::{pi::{CPU_A_ENABLE, CPU_B_ENABLE, INTERFACE_SLOT_A, INTERFACE_SLOT_B}, PI1_BASE}; + for (cpus, present) in [(1, [[1, 0], [0, 0]]), (2, [[1, 0], [1, 0]]), (4, [[1, 1], [1, 1]])] { + let board = TezroBoard::new(cpus).unwrap(); + assert_eq!(board.processors.count(), cpus); + let regs = board.bedrock.registers(); + for interface in 0..2u64 { + let pi = |at: u64| regs.read64(interface * PI1_BASE + at); + let slots = [pi(INTERFACE_SLOT_A).data, pi(INTERFACE_SLOT_B).data]; + assert_eq!(slots, present[interface as usize], "{cpus} CPUs, PI{interface}"); + let enabled = [pi(CPU_A_ENABLE).data & 1, pi(CPU_B_ENABLE).data & 1]; + assert_eq!(enabled, present[interface as usize], "{cpus} CPUs, PI{interface}"); + } + } +} + +/// Only the counts a board fits are accepted, and only those its chip is +/// fitted with. +#[test] +fn the_board_refuses_a_population_it_cannot_fit() { + assert!(TezroBoard::new(3).is_err()); + assert!(TezroBoard::new(0).is_err()); + assert!(TezroBoard::new(5).is_err()); + let one_ghz = CHIPS.iter().find(|c| c.chip.mhz == 1000).unwrap(); + let refused = TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::default(), + cpus: 1, + chip: one_ghz, + }); + assert!(refused.err().is_some_and(|e| e.contains("fitted with 2, 4 processors only"))); +} + +/// A board of one GiB of memory with `chip` fitted, as the fewest +/// processors it takes. +fn board_with(chip: &Part) -> TezroBoard { + TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::one_gib(), + cpus: usize::from(self::chip(chip).cpus[0]), + chip: self::chip(chip), + }) + .unwrap() +} + +/// Run `body` on a thread with the stack a debug build's processor needs. +fn on_big_stack(body: impl FnOnce() + Send + 'static) { + if let Err(panic) = std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join() { + std::panic::resume_unwind(panic); + } +} + +/// Each chip resets with its own PRId, FIR and Config word, and keeps them +/// across a CPU reset, which reloads PRId and FIR from the reset values. The +/// board's flash record and Count rate follow the chip's clock. These are the +/// facts the chips grade as inferred or approximated, so each is pinned here. +#[test] +fn each_tezro_chip_resets_with_its_identity_and_clock() { + fn check>>(model: &Part, prid: u32, config0: u32, mhz: u32) { + let board = board_with(model); + let mut cpu = build_cpu::(&board); + let name = model.name(); + assert_eq!((cpu.core.cp0_prid, cpu.core.reset_prid), (prid, prid), "{name}"); + assert_eq!(model.prid(), prid, "{name}: the chip's PRId"); + assert_eq!((cpu.core.fpu_fir, cpu.core.reset_fir), (0x900, 0x900), "{name}"); + assert_eq!(cpu.core.cp0_config & CONFIG0_MODE_MASK, config0, "{name}"); + assert_eq!(cpu.core.cp0_config >> 26, 0b011_011, "{name}: 32 KB primary caches"); + let config = cpu.core.cp0_config; + cpu.core.fpu_fir = 0; + cpu.core.cp0_prid = 0; + cpu.core.reset(false); + assert_eq!((cpu.core.cp0_prid, cpu.core.fpu_fir, cpu.core.cp0_config), (prid, 0x900, config), "{name}: after reset"); + assert_eq!(model.mhz, mhz); + assert_eq!(model.count_hz(), u64::from(mhz) * 500_000, "{name}: Count at half the clock"); + assert_eq!(chip(model).firmware.cpu_clock_hz, mhz * 1_000_000, "{name}: the PROM's clock record"); + } + use crate::cpu::mips_cache_shadow::{R16000Scache16ShadowCache, R16000Scache8ShadowCache, R16000ShadowCache}; + on_big_stack(|| { + check::(&parts::R16000_700_EARLY, 0x0f41, 0x009b_ad85, 700); + check::(&parts::R16000_800, 0x0f42, 0x009b_af85, 800); + check::(&parts::R16000_900, 0x0f50, 0x009c_b185, 900); + check::(&parts::R16000_1000, 0x0f50, 0x00ed_b385, 1000); + }); +} + +/// Every chip's Config word decodes, by the PROM editor's formulas, to the +/// fields its inventory states: SS gives the cache size, SC the printed cache +/// clock (2 * CPU / (SC + 1)) and EC the 200 MHz Bedrock (code 2 * CPU / +/// Bedrock - 1), with K0 5, PM 3, SB and BE set. The 1 GHz word is the only +/// approximated one, and this is what it is built from: SS 5, SC 5, EC 9. +#[test] +fn each_tezro_configs_fields_decode_to_the_stated_sizes_and_clocks() { + for c in &CHIPS { + let word = c.firmware.reset_mode(); + let name = c.chip.name(); + let field = |lo: u32, bits: u32| (word >> lo) & ((1 << bits) - 1); + let mhz = c.chip.mhz; + let bytes = crate::with_cpu!(c.chip, true, false, C => ::L2_SIZE as u32); + assert_eq!((field(0, 3), field(7, 2), field(13, 1), field(15, 1)), (5, 3, 1, 1), "{name}: K0 PM SB BE"); + assert_eq!(field(9, 4), 2 * mhz / 200 - 1, "{name}: EC"); + assert_eq!(2 * mhz / (field(19, 3) + 1), c.scache_mhz, "{name}: SC gives the cache clock"); + assert_eq!(512u32 << 10 << field(16, 3), bytes, "{name}: SS gives the cache size"); + assert_eq!((word >> 23) & 1, 1, "{name}: bit 23 is set in every row of 500 MHz and up"); + assert_eq!(word >> 24, 0, "{name}: bit 24 clear"); + } + let word = chip(&parts::R16000_1000).firmware.reset_mode(); + assert_eq!(((word >> 16) & 7, (word >> 19) & 7, (word >> 9) & 15, (word >> 22) & 1), (5, 5, 9, 1), "1 GHz SS SC EC and bit 22"); +} + +/// A part's revision, as hinv prints it, and the PRId register's raw one +/// differ for an R16000: the register's major is the displayed one plus 2 (the +/// rule Linux and OpenBSD apply). The raw bytes are written out here, not +/// derived, so this ties the Tezro's "2.1", "2.2", "3.0" and "3.0" to 0x0f41, +/// 0x0f42 and 0x0f50, the 1 GHz one also printed by the PROM banner as `f50`. +#[test] +fn every_parts_revision_is_the_one_its_prid_displays() { + for (part, shown, raw) in + CPUS.iter().zip(["2.1", "2.2", "3.0", "3.0"]).zip([0x41, 0x42, 0x50, 0x50]).map(|((p, s), r)| (p, s, r)) + { + assert_eq!(format!("{}.{}", part.rev.0, part.rev.1), shown, "{}", part.label()); + assert_eq!(part.prid(), 0x0f00 | raw, "{}", part.label()); + } +} + +/// A chip and its flash record are two statements of one chip: the clock, +/// and the Config word the PROM compares against the processor's, must agree. +#[test] +fn each_tezro_chip_agrees_with_its_flash_record() { + for c in &CHIPS { + let record = c.firmware; + let name = c.chip.name(); + assert_eq!(u64::from(record.cpu_clock_hz), u64::from(c.chip.mhz) * 1_000_000, "{name}"); + assert_eq!(record.cpu_mode_word & CONFIG0_MODE_MASK, record.reset_mode(), "{name}"); + assert_eq!(board_with(c.chip).chip.chip, c.chip); + assert!(c.cpus.iter().all(|&n| n <= 4) && !c.cpus.is_empty(), "{name}"); + } + assert_eq!(CHIPS.len(), CPUS.len()); + assert!(CHIPS.iter().zip(CPUS).all(|(c, &model)| c.chip == model), "CPUS lists the chips in table order"); +} + +/// The cache type `with_cpu!` builds follows the chip: the secondary-cache +/// size the chip is inventoried with, and nothing else about the type is the +/// chip's. +#[test] +fn the_processors_take_the_cache_type_of_the_chips_size() { + for (model, mib) in [ + (&parts::R16000_700_EARLY, 4u32), + (&parts::R16000_800, 4), + (&parts::R16000_900, 8), + (&parts::R16000_1000, 16), + ] { + let (name, bytes) = crate::with_cpu!(model, true, false, C => (::NAME, ::L2_SIZE as u32)); + assert_eq!((name, bytes >> 20), ("R16000", mib), "{}", model.name()); + } +} + +/// A bare family name builds the 700 MHz chip, and a chip name its own: +/// PRId, flash record and Count rate follow, end to end from a configuration. +#[test] +fn the_family_name_builds_the_700_mhz_chip_and_a_chip_name_its_own() { + on_big_stack(|| { + for (cpu, prid, mhz, mib) in [("r16000", 0x0f41, 700, 4u32), ("r16000_800", 0x0f42, 800, 4), ("r16000_900", 0x0f50, 900, 8), ("r16000_1000", 0x0f50, 1000, 16)] { + let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"tezro_ip53\"\ncpu = \"{cpu}\"\n")).unwrap(); + let part = cfg.cpu_part().unwrap(); + let board = board_with(part); + let (core_prid, core_config, l2) = crate::with_cpu!(part, true, false, C => { + let cpu_core = build_cpu::(&board); + (cpu_core.core.cp0_prid, cpu_core.core.cp0_config, ::L2_SIZE as u32) + }); + assert_eq!(core_prid, prid, "{cpu}"); + assert_eq!(l2 >> 20, mib, "{cpu}: secondary cache"); + assert_eq!((core_config >> 16) & 7, 1 + mib.trailing_zeros(), "{cpu}: Config SS"); + assert_eq!(board.chip.firmware.cpu_clock_hz, mhz * 1_000_000, "{cpu}: the flash record"); + assert_eq!(board.chip.firmware.reset_config0(), core_config, "{cpu}: record and processor agree"); + } + }); +} + +/// IOC4 reads its 32-bit registers in the processor's byte order through +/// the IO9 PIC's self-ordered slot: what the processor writes is the +/// register's value, as on the QEMU Tezro, where PROM 6.210 writes the +/// serial DMA control 0x100201ff and the TX producer 0x3f8. +#[test] +fn the_processor_reads_and_writes_ioc4_registers_in_its_own_byte_order() { + let board = TezroBoard::new(1).unwrap(); + let io = Pio(board.bedrock.pio()); + // IOC4's BAR at PCI memory 0x200000, decoding memory, reached through + // head 0's first short window. + assert_eq!(io.pio_write(config_id(1) + 0x10, 4, 0x20_0000), BUS_OK); + assert_eq!(io.pio_write(config_id(1) + 4, 4, 2), BUS_OK); + assert_eq!(io.pio_write(widget(15, 0x200), 8, (1 << 12) | 2), BUS_OK); + let reg = |offset: u64| widget(15, 0x20_0000 + offset); + assert_eq!(io.pio_read(reg(0x24), 4).data, 1, "MISC: the 66 MHz clock"); + assert_eq!(io.pio_write(reg(0x314), 4, 0x3f8), BUS_OK); + assert_eq!(io.pio_read(reg(0x314), 4).data, 0x3f8, "port 0 TX producer"); +} + +/// The cached view the processors' caches sit on swaps RAM above 4 GiB, so +/// SC stays atomic there too. +#[test] +fn the_cached_view_swaps_ram_above_4_gib() { + let board = TezroBoard::new(4).unwrap(); + // Banks 0 to 5: two 1 GiB banks and four of 512 MiB, as MD's tests set them. + assert_eq!(board.bedrock.registers().write64(crate::dev::bedrock::reg::MD_BASE + 8, 0x53 | (0x43 << 8) | (0x43 << 16)), BUS_OK); + let cached = board.cached_bus(); + let at = (5u64 << 30) + 0x80; + assert_eq!(cached.write64(at, 1), BUS_OK); + assert_eq!(cached.cas64(at, 1, 2), Some(true)); + assert_eq!(cached.cas64(at, 1, 3), Some(false)); + assert_eq!(cached.read64(at).data, 2); +} + +/// A fresh flash holds no environment records: with the node board named +/// for its processor count the PROM tries only the fitted positions, so no +/// board needs a Disable record to boot straight through. +#[test] +fn a_fresh_flash_starts_with_no_environment_records() { + for cpus in [1, 2, 4] { + let bytes = TezroBoard::new(cpus).unwrap().flash.contents(); + assert!(bytes[0x9e0100..0x9e0200].iter().all(|&b| b == 0xff), "{cpus} CPUs"); + } +} diff --git a/src/lib.rs b/src/lib.rs index c6945d729..5772763c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,7 @@ pub mod ip27_origin2000; /// Fuel has an IP34 board and uses the IP35 software platform and processor views. pub mod ip35; pub mod ip34_machine; +pub mod ip53_machine; pub mod machine_state; pub mod platform; pub mod physical; diff --git a/src/machine.rs b/src/machine.rs index d343154b1..0cb6cf311 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -53,7 +53,7 @@ pub fn set_emulator_profile(profile: MachineProfile) { fn emulator_profile() -> Option { let raw = EMULATOR_PROFILE.load(std::sync::atomic::Ordering::Relaxed); - [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27, MachineProfile::FuelIp34] + [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27, MachineProfile::FuelIp34, MachineProfile::TezroIp53] .into_iter().find(|p| *p as u8 == raw) } @@ -72,6 +72,7 @@ fn emulator_name_thirds(profile: Option) -> &'static [&'static s Some(MachineProfile::Origin2000Ip27) => &["IRIX", "Iris", "IPsomething", "IP27"], Some(MachineProfile::Onyx2Ip27) => &["IRIX", "Iris", "IPsomething", "IP27"], Some(MachineProfile::FuelIp34) => &["IRIX", "Iris", "IPsomething", "IP34"], + Some(MachineProfile::TezroIp53) => &["IRIX", "Iris", "IP53"], None => &["IRIX", "Iris", "IPsomething"], } } @@ -2168,11 +2169,12 @@ mod emulator_name_tests { Some(MachineProfile::Origin2000Ip27) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], Some(MachineProfile::Onyx2Ip27) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], Some(MachineProfile::FuelIp34) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], + Some(MachineProfile::TezroIp53) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "IP34", "O2"], None => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], } }; for p in [None, Some(MachineProfile::IndyIp24), Some(MachineProfile::Indigo2Ip22), - Some(MachineProfile::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::Origin200Ip29), Some(MachineProfile::Origin2000Ip27), Some(MachineProfile::Onyx2Ip27), Some(MachineProfile::FuelIp34)] { + Some(MachineProfile::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::Origin200Ip29), Some(MachineProfile::Origin2000Ip27), Some(MachineProfile::Onyx2Ip27), Some(MachineProfile::FuelIp34), Some(MachineProfile::TezroIp53)] { let thirds = emulator_name_thirds(p); assert!(!thirds.is_empty()); for w in thirds { @@ -2184,7 +2186,7 @@ mod emulator_name_tests { #[test] fn profile_round_trips_through_the_global() { - for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27, MachineProfile::FuelIp34] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27, MachineProfile::FuelIp34, MachineProfile::TezroIp53] { set_emulator_profile(p); assert_eq!(emulator_profile(), Some(p)); } diff --git a/src/workstation.rs b/src/workstation.rs index 6194c1b31..63cc1472e 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -41,6 +41,7 @@ pub fn build(cfg: MachineConfig) -> Result, String> { crate::machine::set_emulator_profile(cfg.machine.profile); match cfg.machine.profile { crate::config::MachineProfile::FuelIp34 => Ok(Box::new(crate::ip34_machine::FuelMachine::new(&cfg)?)), + crate::config::MachineProfile::TezroIp53 => Ok(Box::new(crate::ip53_machine::TezroMachine::new(&cfg)?)), // The O2 is its own machine (CRIME, MACE, GBE), not an MC/HPC3 one. crate::config::MachineProfile::O2Ip32 => Ok(Box::new(crate::ip32_machine::O2Machine::new(&cfg)?)), // So is the Octane (HEART, Xbow, Bridge). diff --git a/tezro.toml.example b/tezro.toml.example new file mode 100644 index 000000000..e3829b156 --- /dev/null +++ b/tezro.toml.example @@ -0,0 +1,43 @@ +# SGI Tezro (IP53 board, IP35 software platform), serial-only setup. +# Copy to tezro.toml and run from the repository root: iris --config tezro.toml +# Create ignore/tezro/ and supply your decoded raw IP35 PROM 6.210 payload +# and an IRIX disk installed on a Tezro: a disk installed on another machine +# carries that machine's device numbering. +# No PROM or disk images are included. Convert QCOW2 disks to raw first. + +prom = "ignore/tezro/ip35prom.payload.bin" +nvram = "ignore/tezro/nvram.bin" +# banks left unset: 4 GiB as 1024, 1024 and four 512 MiB banks. +headless = true +no_audio = true +monitor_port = 18888 +serial_port_a = 18880 +serial_port_b = 18881 + +[machine] +profile = "tezro_ip53" +# Optional; left out, or as the family "r16000", it is the 700 MHz R16000, +# "r16000_700". "r16000_800", "r16000_900" and "r16000_1000" are the others (four +# processors only); other values are refused. +cpu = "r16000_700" +# Processors fitted: 1, 2 or 4 (the default). Each runs on its own host thread. +cpus = 4 + +[graphics] +board = "none" + +[tezro] +flash = "ignore/tezro/flash.bin" +console = "l1" +l1_port = 18882 + +[network] +# Disconnected cable. For NAT, use mode = "nat". +mode = "off" + +[scsi.1] +path = "ignore/tezro/irix.raw" +cdrom = false +# Keep the base disk unchanged. Guest writes go to irix.raw.overlay. +overlay = true +# write_cache = false # default true. false stores every write before it completes, like SGI's drives (slower); true can lose ~5 s of writes in a host crash. See STORAGE.md From b774b9ba7e91db179c0b8e0b59063af9992309cf Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 15:40:32 -0700 Subject: [PATCH 27/33] bedrock: park the other processors for a bank configuration write A write to MD's bank configuration register remaps memory in ppmem's window and in every processor's cached view. ppmem keeps that state without a lock: its contract is that a remap runs with every other processor parked, which HEART's MEMCFG write does through World::stop_others. Bedrock's MD did not, so on a Tezro with several processors the remap ran while the others executed. A cache instruction on another processor adds or drops a page in that processor's view's held-page map, and the remap walks every view's map to save and restore those pages. Walking a map another thread is changing is a data race. In one four-processor PROM boot it ended with a BTreeMap iterator panicking on a None in PpMemSpace::save_held, below map_range, below Md::access, and iris exited. Park the others through the machine's World before the write takes the remap lock. A processor that is waiting for its own turn to write the register counts as parked, so it cannot deadlock the remap. Other MD registers, and machines with no World (the Fuel), are unchanged. --- src/dev/bedrock/md.rs | 99 +++++++++++++++++++++++++++++++++++++++++- src/dev/bedrock/mod.rs | 5 +++ src/ip53_machine.rs | 2 + src/ppmem/ppmem.rs | 3 +- 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/dev/bedrock/md.rs b/src/dev/bedrock/md.rs index 009681343..4d294923e 100644 --- a/src/dev/bedrock/md.rs +++ b/src/dev/bedrock/md.rs @@ -33,11 +33,12 @@ use super::mem::{Memory, RamExtent, BANK_SPAN}; use super::Bedrock; +use crate::cpu::smp::World; use crate::traits::*; use parking_lot::Mutex; use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Weak}; +use std::sync::{Arc, OnceLock, Weak}; pub mod reg { pub const CELL: u64 = 0x0; @@ -156,6 +157,10 @@ pub(super) struct Md { /// and the board's Hub-special windows make. Written under `state`. cell: AtomicU64, unsupported: AtomicU64, + /// The node's processors. A bank configuration write moves memory in + /// every processor's cached view, which only a remap with the others + /// parked may do (`PpMemSpace`'s contract). + world: OnceLock>, } #[derive(Clone, Copy)] pub(super) enum View { @@ -182,8 +187,13 @@ impl Md { remapping: Mutex::new(()), cell: AtomicU64::new(State::default().cell), unsupported: AtomicU64::new(0), + world: OnceLock::new(), } } + /// The processors a bank configuration write parks while it remaps. + pub(super) fn set_world(&self, world: Arc) { + let _ = self.world.set(world); + } /// Where fitted RAM can answer, for the processor bus's cached view. pub(super) fn ram_extent(&self) -> RamExtent { self.memory.extent() @@ -269,8 +279,17 @@ impl Md { return self.refuse("access", addr); } // A bank configuration write remaps memory, on the CPU thread as - // HEART's MEMCFG does. + // HEART's MEMCFG does, with the other processors parked: another + // processor's cache instruction changes the pages its view holds, + // and the remap walks every view's held pages. Parked before taking + // `remapping`, which a processor waiting to park could be waiting for. let reconfigure = write.is_some() && matches!(view, View::Registers) && addr == reg::CONFIG; + match self.world.get() { + Some(world) if reconfigure => world.stop_others(|| self.access_inner(view, addr, bytes, write, true)), + _ => self.access_inner(view, addr, bytes, write, reconfigure), + } + } + fn access_inner(&self, view: View, addr: u64, bytes: u64, write: Option, reconfigure: bool) -> Option { let remapping = reconfigure.then(|| self.remapping.lock()); // Sample outside state: even an injected clock must not call out under // the lock. Only register observations need the self-test timer. @@ -1099,6 +1118,82 @@ mod tests { assert_eq!(md.dma_view().gen_ptr(above), pointer, "a bank swap keeps the counter"); } + /// A bank configuration write moves memory under every processor's + /// cached view, so the others park for it; another register write + /// leaves them running. The other processor holds and drops line + /// pages in its view while it runs, as its cache instructions do: the + /// remap walks those pages, and doing so unparked panicked a Tezro. + /// MD reads its clock inside the write, which is where the test looks. + #[test] + fn a_bank_configuration_write_parks_the_other_processors() { + use crate::cpu::smp::{on_cpu_thread, set_current_cpu, ParkSlot, World, HOST_PARK_BIT}; + use std::sync::atomic::AtomicBool; + let watched: Arc>> = Arc::new(OnceLock::new()); + let seen = Arc::new(Mutex::new(Vec::new())); + let clock: crate::dev::heart::Clock = { + let (watched, seen) = (watched.clone(), seen.clone()); + Arc::new(move || { + if let Some(slot) = watched.get().filter(|_| on_cpu_thread() == Some(0)) { + seen.lock().push(slot.is_parked()); + } + 0 + }) + }; + let md = TestMd::with_clock(&FUEL_BANKS, clock).unwrap(); + assert_eq!(md.md_bus().write64(reg::CONFIG, 0x33), BUS_OK); + let space = md.physical().ppmem_space().expect("a window"); + let _mine = space.add_cached_view(&[]).unwrap(); + let theirs = space.add_cached_view(&[]).unwrap(); + let words: Vec> = (0..2).map(|_| Arc::new(AtomicU64::new(0))).collect(); + let slots: Vec> = words.iter().map(|w| Arc::new(ParkSlot::new(Arc::as_ptr(w)))).collect(); + let world = Arc::new(World::new()); + world.set_slots(slots.clone()); + md.0.set_world(world); + let running = Arc::new(AtomicBool::new(true)); + slots[1].set_active(true); + let other = { + let (md, slot, word, running) = (md.clone(), slots[1].clone(), words[1].clone(), running.clone()); + std::thread::spawn(move || { + set_current_cpu(1); + let space = md.physical().ppmem_space().unwrap(); + let page = crate::ppmem::map::granularity() as u64; + while running.load(Ordering::Relaxed) { + if word.load(Ordering::Relaxed) & HOST_PARK_BIT != 0 { + slot.park(&running); + continue; + } + for n in 0..32 { + space.hold_line_page(theirs, 0x10_0000 + n * page); + } + for n in 0..32 { + space.release_line_page(theirs, 0x10_0000 + n * page); + } + } + slot.set_active(false); + }) + }; + let _ = watched.set(slots[1].clone()); + let regs = md.md_bus(); + std::thread::scope(|s| { + s.spawn(|| { + set_current_cpu(0); + for swap in [1u64, 0, 1, 0] { + assert_eq!(regs.write64(reg::CONFIG, 0x33 | swap << 32), BUS_OK); + assert_eq!(std::mem::take(&mut *seen.lock()), [true], "the other processor parked for the remap"); + } + // Once it runs again, other registers leave it running. + while watched.get().unwrap().is_parked() { + std::thread::yield_now(); + } + assert_eq!(regs.write64(reg::REFRESH, 0), BUS_OK); + assert_eq!(std::mem::take(&mut *seen.lock()), [false], "a refresh write moves no memory"); + }); + }); + running.store(false, Ordering::Relaxed); + other.join().unwrap(); + assert_eq!(md.physical().read64(BANK_SPAN).status, BUS_OK); + } + /// Guest density settings remap fitted RAM, and reset restores the default map. #[test] fn bank_configuration_remaps_memory_and_reset_restores_defaults() { diff --git a/src/dev/bedrock/mod.rs b/src/dev/bedrock/mod.rs index 472b78e03..1838ecdaf 100644 --- a/src/dev/bedrock/mod.rs +++ b/src/dev/bedrock/mod.rs @@ -342,6 +342,11 @@ impl Bedrock { pi.publish(&self.levels); } } + /// The node's processors, which a bank configuration write parks + /// while it moves memory under their cached views. + pub fn set_world(&self, world: Arc) { + self.md.set_world(world); + } /// The PIs' comparators and profile timers fire on `tm`. Held weakly. pub fn set_timer_manager(&self, tm: &Arc) { let _ = self.timers.set(Arc::downgrade(tm)); diff --git a/src/ip53_machine.rs b/src/ip53_machine.rs index 886962639..35d788658 100644 --- a/src/ip53_machine.rs +++ b/src/ip53_machine.rs @@ -518,6 +518,8 @@ impl TezroMachine { // processor's state through the core it is called with. let world = Arc::new(World::new()); world.set_slots(cpus.iter().map(|c| c.park_slot()).collect()); + // A bank configuration write parks the others while it moves memory. + board.bedrock.set_world(world.clone()); #[cfg(feature = "jitv2")] if cpus.len() > 1 { if cfg!(feature = "jitv2_lockstep") { diff --git a/src/ppmem/ppmem.rs b/src/ppmem/ppmem.rs index e15af6d79..260e1b9d4 100644 --- a/src/ppmem/ppmem.rs +++ b/src/ppmem/ppmem.rs @@ -736,7 +736,8 @@ struct OwnBitmap(Box); // `set_bitmap_sink2` — is reached only from `Physical::remap_banks`, which // the MC invokes synchronously inside the CPU's store to MEMCFG. That is // always a CPU thread, and with several CPUs the others are parked for it -// (`smp::World::stop_others`, wired where HEART's descriptors reach memory). +// (`smp::World::stop_others`, wired where HEART's descriptors reach memory +// and where Bedrock's MD takes a bank configuration write). // * Device threads (MC-DMA, seeq, VINO) reach RAM through `BusDevice` on the // individual `PpMemory` banks. They never touch `PpMemSpace`. // * `hold_line_page`/`release_line_page` run on a CPU thread at a CACHE From 4debd438fb7f54a416a099c95dc9f918c0b12756 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 08:20:04 -0700 Subject: [PATCH 28/33] tezro: hold the PROM's liveness and presence pollers for their peers After the reset barrier the IP35 PROM runs two handshakes that assume a node's processors left reset together and run at one speed. In the first, each processor of an interface with both slots fitted stores a status value to its own PI real-time compare register and reads its partner's, at most 200,000 times. If the reads run out it clears the partner's CPU enable and prints "peer_pled failed: Other CPU on this node died." Each processor here is a host thread, and over the seconds since the barrier a pair drifts apart: in traced four-processor boots the posts of a pair came up to 822 ms apart under jitv2 and 378 ms under the interpreter. In the five failures checked, the late thread never went more than 12 ms without retiring an instruction: the drift is a difference in rate, not one long deschedule. Compiled, the poll reads up to 4,500 times a millisecond, so its 200,000 reads last 44 to 85 ms; interpreted they lasted 320 ms. Without a hold for this handshake 18 of 40 four-processor jitv2 PROM boots lost it, at host load 70 to 400, and came up with one to three processors, took a TLB refill exception in the PROM or hung there. Every such failure in the traced boots began with that enable clear. In the second, each interface's lowest processor takes the hub lock (LB scratch 3, through its test-and-set twin), sets its interface's slot bits in II scratch 0 and releases the lock. Every processor then waits a fixed 2000 units of the PI's real-time counter, which runs on host time, and tests II scratch 0 for the other interface's bits: it waits for that interface only if its bits are already set, and otherwise carries on alone (the later polls have no timer and no count; read from PROM 6.210 and 6.211). So a publisher whose other interface is descheduled through that wait finds it absent, and the PROM loses that interface's processors; an earlier boot without this hold failed so. The board now holds the first of a pair to post until its partner posts, and a publisher that has published with the hub lock free until the other populated interface publishes. Each processor checks at its batch boundary from atomics Bedrock exposes: the posts (each processor's first store to its own compare register after its presence join on LB scratch 1), II scratch 0 and the hub lock bit. Nothing calls out of the chip. A hold is bounded by the cycles the processors it waits for have retired, two billion, so a host thread the scheduler leaves off a core cannot run it out; host time backs it only as a two-minute safety net. A liveness hold that runs out ends only itself, and the PROM then finds the partner dead as it would a dead one. A publisher's hold that runs out ends the join until the next reset. Once both interfaces have published the check is one atomic load. The holds have their own reason in the processor control, so their release ends neither a world stop nor another hold. With them, 40 of 40 four-processor jitv2 boots, 20 of 20 four-processor interpreter boots and 20 of 20 two-processor jitv2 boots counted every processor, interleaved with the runs above. These are approximations with no hardware counterpart: they change when a processor runs, never a value it reads. QEMU holds the same processors, the first poster at its post and the publisher at its release of the hub lock. The partner's wait at its counter read, which QEMU also holds, is not carried: with the reset barrier ordered in the LB, four-processor boots reached login without it. Ported from hw/misc/sgi-bedrock.c, hw/misc/sgi-bedrock-lb.c and hw/misc/sgi-bedrock-io.c in jamesbraid/qemu-sgi-origami. --- docs/tezro-ip53.md | 15 +- src/cpu/smp.rs | 3 + src/dev/bedrock/io.rs | 11 ++ src/dev/bedrock/lb.rs | 34 +++- src/dev/bedrock/mod.rs | 23 +++ src/ip35.rs | 1 + src/ip35/presence.rs | 323 +++++++++++++++++++++++++++++++++++++ src/ip35/presence/tests.rs | 261 ++++++++++++++++++++++++++++++ src/ip53_machine.rs | 34 +++- 9 files changed, 695 insertions(+), 10 deletions(-) create mode 100644 src/ip35/presence.rs create mode 100644 src/ip35/presence/tests.rs diff --git a/docs/tezro-ip53.md b/docs/tezro-ip53.md index 16ae48819..f89dc40f4 100644 --- a/docs/tezro-ip53.md +++ b/docs/tezro-ip53.md @@ -144,7 +144,20 @@ reset barrier on LB scratch 1 depends on that: every processor clears the word before the boot master stores its release there. Bedrock's LB orders those stores itself, by who stored in what order, so a late clear does not land on the release (`src/dev/bedrock/lb.rs`); hardware drops no store, so -this is a model rule. An NMI, and a write that clears a running processor's +this is a model rule. Two later handshakes need more. In the first, each +processor of an interface with both slots fitted posts a status value in +its own PI real-time compare register and reads its partner's, at most +200,000 times, before it declares the partner dead (`peer_pled failed: Other +CPU on this node died.`). In the second, the lowest processor of each +interface publishes its interface's processors in II scratch 0, waits a +fixed 2,000 units of the PI's real-time counter and then looks for the other +interface's; if they are not there yet it carries on without them. The host threads drift apart by more than either +wait allows, so the first of a pair to post is held at its batch boundary +until its partner posts, and a publisher that has published while the other +interface has not is held until that interface publishes; each hold also +ends once the processors it waits for have retired 2,000,000,000 cycles +(`src/ip35/presence.rs`). The holds change when a processor runs, never a +value it reads, and have no hardware counterpart. An NMI, and a write that clears a running processor's enable bit, are logged by the Bedrock PI and not carried out: the CPU has no NMI entry. diff --git a/src/cpu/smp.rs b/src/cpu/smp.rs index dbac65a69..90f7d25eb 100644 --- a/src/cpu/smp.rs +++ b/src/cpu/smp.rs @@ -26,6 +26,9 @@ pub const HOST_PARK_BIT: u64 = 1 << 62; pub const HOLD_HEART: u32 = 1 << 0; /// Another thread needs the machine still ([`World::stop_others`]). pub const HOLD_WORLD: u32 = 1 << 1; +/// The board holds this processor at a firmware rendezvous until the ones +/// it must follow catch up (the IP35 PROM's presence publication). +pub const HOLD_RENDEZVOUS: u32 = 1 << 2; const NOT_A_CPU: u8 = u8::MAX; diff --git a/src/dev/bedrock/io.rs b/src/dev/bedrock/io.rs index 28629b0f1..e42710bad 100644 --- a/src/dev/bedrock/io.rs +++ b/src/dev/bedrock/io.rs @@ -124,6 +124,10 @@ pub(super) struct Io { device_permission: AtomicU64, /// The large-window translation entries (0x160..0x190). itte: [AtomicU64; ITTES], + /// Scratch register 0 (0x150), which the IP35 PROM uses to publish each + /// interface's processors; an atomic so a board can read it without a + /// lock. + scratch0: AtomicU64, widgets: [OnceLock>; 16], } @@ -136,6 +140,7 @@ impl Io { widget_permission: AtomicU64::new(0), device_permission: AtomicU64::new(0), itte: Default::default(), + scratch0: AtomicU64::new(0), widgets: Default::default(), }; io.reset_permissions(); @@ -217,6 +222,7 @@ impl Io { reg::WIDGET_PERMISSION => Some(&self.widget_permission), reg::INBOUND_DEVICE_PERMISSION => Some(&self.device_permission), reg::ITTE_BASE..=reg::ITTE_LAST if a % 8 == 0 => Some(&self.itte[((a - reg::ITTE_BASE) / 8) as usize]), + reg::REGISTER_0150 => Some(&self.scratch0), _ => None, } } @@ -423,6 +429,11 @@ impl Io { *r.words.entry(reg::XIO_STATUS).or_default() |= PEER; } self.reset_permissions(); + self.scratch0.store(0, Relaxed); + } + /// Scratch register 0. One atomic load. + pub(super) fn scratch0(&self) -> u64 { + self.scratch0.load(Relaxed) } } diff --git a/src/dev/bedrock/lb.rs b/src/dev/bedrock/lb.rs index 9f8ede2dc..0b6f07904 100644 --- a/src/dev/bedrock/lb.rs +++ b/src/dev/bedrock/lb.rs @@ -185,11 +185,39 @@ pub(super) struct Lb { /// landed since the block's last reset ([`Scratch1Store::Joins`]). /// Written under the block's lock, read without it. joined: AtomicU64, + /// Scratch 3's bit 0, which the IP35 PROM takes as its hub lock through + /// the test-and-set twin at 0x8058: kept with the register under the + /// block's lock, read without it. + scratch3: AtomicU64, + /// Processors, by CPU number, that have stored to their own PI + /// real-time compare register since their presence join: the IP35 + /// PROM's pair liveness post ([`Lb::note_rt_compare`]). Cleared with + /// the join mask. + posted: AtomicU64, } impl Lb { pub(super) fn new() -> Self { - Self { regs: Mutex::new(Regs::default()), joined: AtomicU64::new(0) } + Self { regs: Mutex::new(Regs::default()), joined: AtomicU64::new(0), scratch3: AtomicU64::new(0), posted: AtomicU64::new(0) } + } + /// Processor `cpu` stored to its own real-time compare register. The + /// first such store after its presence join is the PROM's liveness post + /// to its pair partner, whatever the value: the PROM's earlier stores + /// there are progress codes. A store before the join changes nothing. + pub(super) fn note_rt_compare(&self, cpu: usize) { + if let Some(bit) = cpu_bit(Some(cpu)).filter(|bit| self.joined.load(Ordering::Acquire) & bit != 0) { + self.posted.fetch_or(bit, Ordering::Release); + } + } + /// The processors, one bit per CPU number, that have posted since their + /// presence join. One atomic load. + pub(super) fn rt_posted(&self) -> u64 { + self.posted.load(Ordering::Acquire) + } + /// Whether scratch 3's bit 0, the PROM's hub lock, is clear. One atomic + /// load. + pub(super) fn hub_lock_free(&self) -> bool { + self.scratch3.load(Ordering::Acquire) & 1 == 0 } /// Whether processor `cpu`'s presence join on scratch 1 has landed since /// the block's last reset. One atomic load. @@ -228,6 +256,8 @@ impl Lb { let survivors = [r.scratch[1], r.scratch[2], r.words.get(®::ERROR_STATUS).copied().unwrap_or(0)]; *r = Regs::default(); self.joined.store(0, Ordering::Release); + self.posted.store(0, Ordering::Release); + self.scratch3.store(0, Ordering::Release); r.scratch[1] = survivors[0]; r.scratch[2] = survivors[1]; r.words.insert(reg::ERROR_STATUS, survivors[2]); @@ -248,6 +278,7 @@ impl Lb { let n = 3 + (a - reg::ATOMIC_READ0) as usize / 8; let v = r.scratch[n]; r.scratch[n] = 1; + self.scratch3.store(r.scratch[3], Ordering::Release); v } reg::VECTOR_STATUS_ALIAS => r.words.remove(®::VECTOR_STATUS).unwrap_or(0), @@ -352,6 +383,7 @@ impl Lb { } } } + self.scratch3.store(r.scratch[3], Ordering::Release); if levels.set(Levels::LB_ERROR, r.error_level()) { Effect::Reflect } else { Effect::None } } } diff --git a/src/dev/bedrock/mod.rs b/src/dev/bedrock/mod.rs index 1838ecdaf..80f8ccf5e 100644 --- a/src/dev/bedrock/mod.rs +++ b/src/dev/bedrock/mod.rs @@ -270,6 +270,23 @@ impl Bedrock { pub fn scratch1_joined(&self, cpu: usize) -> bool { self.lb.scratch1_joined(cpu) } + /// II scratch 0 (0x150), where the IP35 PROM publishes each interface's + /// processors, bit 4 + n for slot n (A, B on PI0, C, D on PI1). One + /// atomic load. + pub fn io_scratch0(&self) -> u64 { + self.io.scratch0() + } + /// Whether LB scratch 3's bit 0, which the IP35 PROM takes as its hub + /// lock, is clear. One atomic load. + pub fn hub_lock_free(&self) -> bool { + self.lb.hub_lock_free() + } + /// The processors, one bit per CPU number, that have stored to their own + /// PI real-time compare register since their presence join on LB + /// scratch 1: the IP35 PROM's pair liveness posts. One atomic load. + pub fn rt_posted(&self) -> u64 { + self.lb.rt_posted() + } #[cfg(feature = "jitv2")] pub(crate) fn generation_base(&self) -> *mut AtomicU64 { self.md.generation_base() @@ -432,6 +449,12 @@ impl Bedrock { pi.read(a, now, &self.levels).map_or(BusRead64::err(), BusRead64::ok) } (8, Some(v)) if a < pi::SIZE && a & 7 == 0 => { + if a == pi::reg::RT_COMPARE_A || a == pi::reg::RT_COMPARE_B { + let slot = (index as u8, usize::from(a == pi::reg::RT_COMPARE_B)); + if let Some(cpu) = crate::cpu::smp::on_cpu_thread().filter(|&cpu| self.processors.slot_of(cpu) == Some(slot)) { + self.lb.note_rt_compare(cpu); + } + } if pi.write(a, v, now, &self.levels) { let tm = self.timer_manager(); pi.reschedule(tm.as_deref().map(|tm| self.host_timer(tm)).as_ref(), now); diff --git a/src/ip35.rs b/src/ip35.rs index 5867a6710..ce187590d 100644 --- a/src/ip35.rs +++ b/src/ip35.rs @@ -431,5 +431,6 @@ impl BusDevice for CachedPhysical { } pub(crate) mod node; +pub(crate) mod presence; #[cfg(test)] mod tests; diff --git a/src/ip35/presence.rs b/src/ip35/presence.rs new file mode 100644 index 000000000..ec89bd878 --- /dev/null +++ b/src/ip35/presence.rs @@ -0,0 +1,323 @@ +//! The holds on the IP35 PROM's pair liveness check and its presence +//! publisher, outside the chip. +//! +//! The PROM brings a node's processors up on the assumption that they left +//! reset on one clock edge and have run in step since. Two handshakes +//! depend on it beyond what Bedrock can order by itself. +//! +//! - Pair liveness. After its presence join on LB scratch 1, each processor +//! of an interface with both slots fitted stores a status value to its own +//! PI real-time compare register and reads its partner's until the +//! partner's value appears, at most 200,000 reads (PROM 6.210 at +//! `0x1fc69234`). If the reads run out, it clears the partner's CPU enable +//! and prints `peer_pled failed: Other CPU on this node died.` The bound +//! is the poller's own progress. On hardware the pair reach their posts +//! microseconds apart; here each processor is a host thread whose +//! progress over the seconds since the reset barrier drifts from its +//! partner's, by more than the reads last once the poll runs compiled. +//! So the first of a pair to post is held until its partner posts. +//! - Presence publisher. The lowest processor of each interface takes the +//! hub lock (LB scratch 3, through its test-and-set twin), sets its +//! interface's slot bits in II scratch 0 (bit 4 + n for slot n: A and B on +//! PI0, C and D on PI1), releases the lock, waits a fixed 2000 units of +//! the PI's real-time counter, which runs on host time, and then tests II +//! scratch 0 for the other interface's bits: it waits for that interface +//! only if its bits are already set, and otherwise goes on alone (the +//! later polls have no timer and no count; read from PROM 6.210 and +//! 6.211). So a publisher whose partner interface is descheduled for that +//! wait finds the other interface absent and the PROM loses its +//! processors. A publisher that has published, +//! with the hub lock free, while another populated interface has not, is +//! held until that interface publishes. +//! +//! A held processor ([`HOLD_RENDEZVOUS`]) stops at its batch boundary. The +//! check runs on every processor's batch boundary and reads atomics Bedrock +//! exposes ([`Bedrock::rt_posted`], [`Bedrock::io_scratch0`], +//! [`Bedrock::hub_lock_free`]); nothing calls back from the chip. A hold is +//! bounded by the cycles the processors it waits for have retired +//! ([`HOLD_CYCLES`]), not host time, so a host thread the scheduler leaves +//! off a core cannot run it out; host time backs it only as a deadlock +//! safety net ([`SAFETY_NS`]). A liveness hold that runs out ends only +//! itself, and the PROM then finds the partner dead as it would a dead one; +//! a publisher's hold that runs out ends the join until the next reset. +//! While nothing is held and the processor has no hold to start, the check +//! takes no lock; once every populated interface has published, it is one +//! atomic load. +//! +//! These are approximations with no hardware counterpart: they change when +//! a processor next runs, never a value it reads. A held processor stops at +//! its next batch boundary, not at once. Nothing holds with fewer than two +//! processors, and the liveness hold needs both slots of an interface +//! fitted. QEMU holds the same processors, the first poster at its post and +//! the publisher at its release of the hub lock, and bounds both at 2 s of +//! its virtual clock. +//! +//! Ported from hw/misc/sgi-bedrock.c in jamesbraid/qemu-sgi-origami. + +use crate::cpu::mips_core::CyclesPtr; +use crate::cpu::mips_exec::CpuDevice; +use crate::cpu::smp::{ParkSlot, HOLD_RENDEZVOUS}; +use crate::dev::bedrock::{Bedrock, Population}; +use crate::dev::heart::Clock; +use crate::devlog::LogModule; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock}; + +/// How far the processors a hold waits for may run, in their retired +/// cycles, before the hold gives up: 2 s of a 1 GHz processor, the order of +/// QEMU's 2 s bound. +pub(crate) const HOLD_CYCLES: u64 = 2_000_000_000; +/// The longest a hold lasts in board nanoseconds whatever the processors +/// do: a deadlock safety net, far past any host stall. +pub(crate) const SAFETY_NS: u64 = 120_000_000_000; + +/// One processor the hold may stop: its park slot and its retired cycles. +pub(crate) struct Cpu { + pub slot: Arc, + pub cycles: Box u64 + Send + Sync>, +} + +/// What a held processor waits for. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Kind { + /// Its pair partner's liveness post. + Partner, + /// The other interface's publication. + Publication, +} + +/// One held processor: what for, when the hold started, in board +/// nanoseconds, and how many cycles the processors it waits for had retired +/// by then. +#[derive(Clone, Copy)] +struct Held { + kind: Kind, + since: u64, + progress: u64, +} + +#[derive(Default)] +struct State { + held: [Option; 4], + /// A hold ran out: no more holds until the next reset. + abandoned: bool, +} + +pub(crate) struct PresenceHold { + processors: Population, + clock: Clock, + bedrock: Arc, + cpus: OnceLock>, + state: Mutex, + /// Every populated interface has published, or the join was abandoned, + /// and nothing is held: the check is over until the next reset. + done: AtomicBool, + /// Whether any processor is held, so a boundary with nothing to release + /// and no hold to start takes no lock. + holding: AtomicBool, + /// Each processor's pair partner: the other slot of its interface, if + /// fitted. + partners: [Option; 4], + /// Processors, one bit per CPU number, whose liveness hold ran out: not + /// held for their partner again until the next reset. + gave_up: AtomicU64, +} + +impl PresenceHold { + pub fn new(processors: Population, clock: Clock, bedrock: &Arc) -> Arc { + Arc::new(Self { + processors, + clock, + bedrock: bedrock.clone(), + cpus: OnceLock::new(), + state: Mutex::new(State::default()), + done: AtomicBool::new(processors.count() < 2), + holding: AtomicBool::new(false), + partners: std::array::from_fn(|cpu| { + let (interface, slot) = processors.slot_of(cpu)?; + (0..processors.count()).find(|&other| processors.slot_of(other) == Some((interface, 1 - slot))) + }), + gave_up: AtomicU64::new(0), + }) + } + + /// The processors, CPU 0 first, in the order the population numbers + /// them. Once. + pub fn set_cpus(&self, cpus: &[Arc]) { + self.set_processors(cpus.iter().map(|c| { + let cycles: CyclesPtr = c.cycles_ptr(); + Cpu { slot: c.park_slot(), cycles: Box::new(move || cycles.retired()) } + }).collect()); + } + + pub(crate) fn set_processors(&self, cpus: Vec) { + assert!(self.cpus.set(cpus).is_ok(), "presence hold processors set twice"); + } + + fn interface_of(&self, cpu: usize) -> Option { + self.processors.slot_of(cpu).map(|(interface, _)| interface) + } + + fn populated(&self, interface: u8) -> bool { + (0..2).any(|slot| self.processors.fitted(interface, slot)) + } + + /// Whether `interface` has published its processors in `presence`. + fn published(presence: u64, interface: u8) -> bool { + presence & (0x30 << (2 * interface)) != 0 + } + + /// The interface's publisher: its lowest fitted processor. + fn publisher(&self, interface: u8) -> Option { + (0..self.processors.count()).find(|&cpu| self.interface_of(cpu) == Some(interface)) + } + + /// The retired cycles of the processors on interfaces other than `own`. + fn progress(&self, own: u8) -> u64 { + let Some(cpus) = self.cpus.get() else { return 0 }; + cpus.iter().enumerate() + .filter(|&(cpu, _)| self.interface_of(cpu) != Some(own)) + .fold(0u64, |sum, (_, c)| sum.saturating_add((c.cycles)())) + } + + /// The retired cycles of processor `cpu`. + fn cycles(&self, cpu: usize) -> u64 { + self.cpus.get().and_then(|cpus| cpus.get(cpu)).map_or(0, |c| (c.cycles)()) + } + + /// What a hold of `cpu` started now waits for has retired so far. + fn waited_progress(&self, cpu: usize, kind: Kind) -> u64 { + match kind { + Kind::Partner => self.partners[cpu].map_or(0, |partner| self.cycles(partner)), + Kind::Publication => self.interface_of(cpu).map_or(0, |own| self.progress(own)), + } + } + + /// Whether `cpu` has made its liveness post and waits for its partner's: + /// the partner has not posted and `cpu`'s hold has not run out since the + /// last reset. + fn awaits_partner(&self, posted: u64, cpu: usize) -> bool { + let bit = |cpu: usize| 1u64 << cpu; + self.partners[cpu].is_some_and(|partner| posted & bit(partner) == 0) + && posted & bit(cpu) != 0 + && self.gave_up.load(Ordering::Relaxed) & bit(cpu) == 0 + } + + fn release(&self, s: &mut State, cpu: usize, outcome: &str) { + if s.held[cpu].take().is_some() { + dlog!(LogModule::Bedrock, "presence: CPU {cpu} hold {outcome}"); + if let Some(c) = self.cpus.get().and_then(|cpus| cpus.get(cpu)) { + c.slot.release(HOLD_RENDEZVOUS); + } + } + } + + /// CPU `cpu`'s batch boundary: end the holds that are satisfied or past + /// their bound, and hold `cpu` if it has posted and waits for its + /// partner's post, or is a publisher waiting for the other interface. + pub fn at_batch(&self, cpu: usize) { + if self.done.load(Ordering::Relaxed) { + return; + } + let presence = self.bedrock.io_scratch0(); + let posted = self.bedrock.rt_posted(); + // Nothing held and nothing for this processor to start: no lock. + if !self.holding.load(Ordering::Acquire) && !self.may_hold(presence, cpu) && !self.awaits_partner(posted, cpu) { + let all = (0..2).filter(|&i| self.populated(i)).all(|i| Self::published(presence, i)); + if all && !self.done.swap(true, Ordering::Relaxed) { + dlog!(LogModule::Bedrock, "presence: the join is over"); + } + return; + } + let now = (self.clock)(); + let mut s = self.state.lock(); + for held in 0..s.held.len() { + let Some(hold) = s.held[held] else { continue }; + let satisfied = match hold.kind { + Kind::Partner => !self.awaits_partner(posted, held), + Kind::Publication => self.interface_of(held).is_none_or(|own| Self::published(presence, 1 - own)), + }; + let ran_out = self.waited_progress(held, hold.kind).saturating_sub(hold.progress) >= HOLD_CYCLES + || now.saturating_sub(hold.since) >= SAFETY_NS; + if satisfied { + self.release(&mut s, held, "satisfied"); + } else if ran_out && hold.kind == Kind::Partner { + self.gave_up.fetch_or(1 << held, Ordering::Relaxed); + self.release(&mut s, held, "ran out; its partner is left to the PROM's check"); + } else if ran_out { + s.abandoned = true; + self.release(&mut s, held, "ran out; no more holds until a reset"); + } + } + let all = (0..2).filter(|&i| self.populated(i)).all(|i| Self::published(presence, i)); + if (all || s.abandoned) && s.held.iter().all(Option::is_none) { + if !self.done.swap(true, Ordering::Relaxed) { + dlog!(LogModule::Bedrock, "presence: the join is over"); + } + return; + } + let kind = if s.held[cpu].is_some() { + None + } else if self.awaits_partner(posted, cpu) { + Some(Kind::Partner) + } else if !s.abandoned && self.may_hold(presence, cpu) { + Some(Kind::Publication) + } else { + None + }; + let Some(kind) = kind else { + self.holding.store(s.held.iter().any(Option::is_some), Ordering::Release); + return; + }; + s.held[cpu] = Some(Held { kind, since: now, progress: self.waited_progress(cpu, kind) }); + self.holding.store(true, Ordering::Release); + match kind { + Kind::Partner => dlog!(LogModule::Bedrock, "presence: holding CPU {cpu} until its partner posts"), + Kind::Publication => dlog!(LogModule::Bedrock, "presence: holding CPU {cpu} until the other interface publishes"), + } + if let Some(c) = self.cpus.get().and_then(|cpus| cpus.get(cpu)) { + c.slot.hold(HOLD_RENDEZVOUS); + } + } + + /// Whether `cpu` is a publisher that should wait now: its interface has + /// published and another populated one has not, with the hub lock free. + fn may_hold(&self, presence: u64, cpu: usize) -> bool { + let Some(own) = self.interface_of(cpu) else { return false }; + let other = 1 - own; + self.publisher(own) == Some(cpu) + && Self::published(presence, own) + && self.populated(other) + && !Self::published(presence, other) + && self.bedrock.hub_lock_free() + } + + /// Every hold released and the join started over. + pub fn reset(&self) { + let mut s = self.state.lock(); + for cpu in 0..s.held.len() { + self.release(&mut s, cpu, "ended by a reset"); + } + *s = State::default(); + self.holding.store(false, Ordering::Release); + self.gave_up.store(0, Ordering::Relaxed); + self.done.store(self.processors.count() < 2, Ordering::Relaxed); + } + + /// The processors held now, for tests. + #[cfg(test)] + pub(crate) fn held(&self) -> Vec { + let s = self.state.lock(); + (0..s.held.len()).filter(|&cpu| s.held[cpu].is_some()).collect() + } + + /// Whether the check is over, for tests. + #[cfg(test)] + pub(crate) fn is_done(&self) -> bool { + self.done.load(Ordering::Relaxed) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip35/presence/tests.rs b/src/ip35/presence/tests.rs new file mode 100644 index 000000000..947bc2839 --- /dev/null +++ b/src/ip35/presence/tests.rs @@ -0,0 +1,261 @@ +use super::*; +use crate::cpu::smp::HOST_PARK_BIT; +use crate::dev::bedrock::{reg, BedrockConfig}; +use crate::traits::{BusDevice, Device, BUS_OK}; +use std::sync::atomic::AtomicU64; + +/// A node of `count` processors with its register window, each processor a +/// park slot and a retired-cycle counter the test drives. +struct Rig { + hold: Arc, + regs: Arc, + words: Vec>, + cycles: Vec>, + slots: Vec>, + now: Arc, +} + +impl Rig { + fn new(count: usize) -> Self { + let now = Arc::new(AtomicU64::new(0)); + let clock_now = now.clone(); + let clock: Clock = Arc::new(move || clock_now.load(Ordering::SeqCst)); + let processors = Population::fitted_cpus(count).unwrap(); + let bedrock = Bedrock::new(BedrockConfig { name: "test", clock: clock.clone(), processors, banks_mib: &[64], events: None }).unwrap(); + bedrock.start(); + let hold = PresenceHold::new(processors, clock, &bedrock); + let words: Vec<_> = (0..count).map(|_| Arc::new(AtomicU64::new(0))).collect(); + let cycles: Vec<_> = (0..count).map(|_| Arc::new(AtomicU64::new(0))).collect(); + let slots: Vec<_> = words.iter().map(|w| Arc::new(ParkSlot::new(Arc::as_ptr(w)))).collect(); + hold.set_processors(slots.iter().zip(&cycles).map(|(slot, c)| { + let c = c.clone(); + Cpu { slot: slot.clone(), cycles: Box::new(move || c.load(Ordering::SeqCst)) } + }).collect()); + Self { hold, regs: bedrock.registers(), words, cycles, slots, now } + } + /// II scratch 0, as a publisher's store leaves it. + fn publish(&self, bits: u64) { + assert_eq!(self.regs.write64(reg::IO_BASE + 0x150, bits), BUS_OK); + } + /// Take the hub lock through scratch 3's test-and-set twin, or release it. + fn hub_lock(&self, take: bool) { + if take { + assert_eq!(self.regs.read64(reg::LB_BASE + 0x8058).data, 0, "the lock was free"); + } else { + assert_eq!(self.regs.write64(reg::LB_BASE + 0x8018, 0), BUS_OK); + } + } + /// A store of `value` at `offset` in the register window by processor + /// `cpu`'s thread. + fn store_as(&self, cpu: usize, offset: u64, value: u64) { + let regs = self.regs.clone(); + let status = std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(cpu as u8); + regs.write64(offset, value) + }) + .join() + .unwrap(); + assert_eq!(status, BUS_OK); + } + /// The PROM's reset barrier on LB scratch 1: every processor clears it, + /// CPU 0 releases, and then `joins` store again, their presence join. + fn barrier(&self, joins: &[usize]) { + for cpu in 0..self.slots.len() { + self.store_as(cpu, reg::LB_BASE + 0x8008, 0); + } + self.store_as(0, reg::LB_BASE + 0x8008, 0x07f8_0000); + for &cpu in joins { + self.store_as(cpu, reg::LB_BASE + 0x8008, 0); + } + } + /// Processor `cpu` stores `value` to the real-time compare register of + /// `slot` (0 = A, 1 = B) on PI `interface`. + fn compare(&self, cpu: usize, interface: u64, slot: u64, value: u64) { + self.store_as(cpu, interface * reg::PI1_BASE + 0x108 + 8 * slot, value); + } + /// Processor `cpu` posts its liveness value in its own compare register. + fn post(&self, cpu: usize) { + let (interface, slot) = Population::fitted_cpus(self.slots.len()).unwrap().slot_of(cpu).unwrap(); + self.compare(cpu, u64::from(interface), slot as u64, 0x24); + } + fn parked(&self, cpu: usize) -> bool { + self.words[cpu].load(Ordering::SeqCst) & HOST_PARK_BIT != 0 && self.slots[cpu].holds() & HOLD_RENDEZVOUS != 0 + } +} + +/// PI0's publisher, having published with the lock free while PI1 has not, +/// is held at its batch boundary; its partner on PI0 and PI1's processors +/// are not. PI1's publication releases it at the next boundary any +/// processor reaches, and the join is over. +#[test] +fn a_publisher_waits_for_the_other_interface() { + let rig = Rig::new(4); + rig.hub_lock(true); + rig.publish(0x30); + rig.hold.at_batch(0); + assert!(rig.hold.held().is_empty(), "not while the hub lock is held"); + rig.hub_lock(false); + for cpu in [2, 1, 3] { + rig.hold.at_batch(cpu); + } + assert!(rig.hold.held().is_empty(), "only a publisher whose interface has published"); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0]); + assert!(rig.parked(0)); + rig.publish(0xf0); + rig.hold.at_batch(1); + assert!(rig.hold.held().is_empty() && !rig.parked(0)); + assert!(rig.hold.is_done()); +} + +/// The bound counts the waited-for processors' cycles: a PI1 that runs +/// HOLD_CYCLES without publishing ends the hold, and no later hold starts +/// until a reset, which starts the join over. +#[test] +fn a_hold_ends_once_the_other_interface_has_run_its_bound() { + let rig = Rig::new(2); + rig.publish(0x10); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0]); + rig.cycles[1].store(HOLD_CYCLES - 1, Ordering::SeqCst); + rig.hold.at_batch(1); + assert_eq!(rig.hold.held(), [0], "not yet"); + rig.cycles[0].store(u64::MAX, Ordering::SeqCst); + rig.hold.at_batch(1); + assert_eq!(rig.hold.held(), [0], "the held processor's own cycles do not count"); + rig.cycles[1].store(HOLD_CYCLES, Ordering::SeqCst); + rig.hold.at_batch(1); + assert!(rig.hold.held().is_empty() && !rig.parked(0)); + assert!(rig.hold.is_done(), "abandoned"); + rig.hold.reset(); + assert!(!rig.hold.is_done()); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0], "a reset starts the join over"); + rig.hold.reset(); + assert!(rig.hold.held().is_empty() && !rig.parked(0)); +} + +/// Host time backs the bound only as a safety net. +#[test] +fn the_safety_net_ends_a_hold_whatever_the_cycles() { + let rig = Rig::new(2); + rig.publish(0x10); + rig.hold.at_batch(0); + rig.now.store(SAFETY_NS - 1, Ordering::SeqCst); + rig.hold.at_batch(1); + assert_eq!(rig.hold.held(), [0]); + rig.now.store(SAFETY_NS, Ordering::SeqCst); + rig.hold.at_batch(1); + assert!(rig.hold.held().is_empty()); +} + +/// One processor has nobody to wait for. +#[test] +fn one_processor_is_never_held() { + let rig = Rig::new(1); + assert!(rig.hold.is_done()); + rig.publish(0x10); + rig.hold.at_batch(0); + assert!(rig.hold.held().is_empty()); +} + +/// A boundary with nothing held and no hold to start takes no lock: CPU 2, +/// not a publisher, passes its boundary while the state is locked, and so +/// does a publisher whose partner interface has published. +#[test] +fn a_boundary_with_nothing_to_do_takes_no_lock() { + let rig = Rig::new(4); + rig.publish(0x30); + let held = rig.hold.state.lock(); + rig.hold.at_batch(2); + rig.hold.at_batch(1); + drop(held); + rig.publish(0xf0); + let held = rig.hold.state.lock(); + rig.hold.at_batch(0); + drop(held); + assert!(rig.hold.is_done()); +} + +/// The first of a pair to post its liveness value after its join is held +/// until its partner posts: CPU 0 (PI0/A) waits for CPU 2 (PI0/B), and CPU 3 +/// (PI1/B) for CPU 1 (PI1/A). Nobody else is held, and the partner's post +/// releases the hold at the next boundary any processor reaches. +#[test] +fn the_first_of_a_pair_to_post_waits_for_its_partner() { + let rig = Rig::new(4); + rig.barrier(&[0, 1, 2, 3]); + rig.post(0); + for cpu in [1, 2, 3, 0] { + rig.hold.at_batch(cpu); + } + assert_eq!(rig.hold.held(), [0]); + assert!(rig.parked(0)); + rig.post(3); + rig.hold.at_batch(3); + assert_eq!(rig.hold.held(), [0, 3]); + rig.post(2); + rig.hold.at_batch(1); + assert_eq!(rig.hold.held(), [3], "CPU 2's post released CPU 0"); + assert!(!rig.parked(0)); + rig.post(1); + rig.hold.at_batch(2); + assert!(rig.hold.held().is_empty() && !rig.parked(3)); + for cpu in 0..4 { + rig.hold.at_batch(cpu); + } + assert!(rig.hold.held().is_empty(), "both pairs posted"); +} + +/// Only a processor's first store to its own compare register after its +/// join is a post: earlier stores are progress codes, and a store to the +/// partner's register is not the processor's. +#[test] +fn only_a_store_to_its_own_register_after_its_join_posts() { + let rig = Rig::new(4); + rig.post(0); + rig.hold.at_batch(0); + assert!(rig.hold.held().is_empty(), "a progress code before the join"); + rig.barrier(&[0, 2]); + rig.compare(0, 0, 1, 0x24); + rig.hold.at_batch(0); + assert!(rig.hold.held().is_empty(), "CPU 0 wrote CPU 2's register"); + rig.post(0); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0]); +} + +/// A partner that runs HOLD_CYCLES without posting ends the hold, and the +/// processor is not held for it again: the PROM's own check then finds the +/// partner dead. The presence join goes on. +#[test] +fn a_liveness_hold_runs_out_on_the_partners_cycles_and_ends_only_itself() { + let rig = Rig::new(4); + rig.barrier(&[0, 1, 2, 3]); + rig.post(0); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0]); + rig.cycles[1].store(u64::MAX, Ordering::SeqCst); + rig.hold.at_batch(1); + assert_eq!(rig.hold.held(), [0], "only the partner's cycles count"); + rig.cycles[2].store(HOLD_CYCLES, Ordering::SeqCst); + rig.hold.at_batch(2); + assert!(rig.hold.held().is_empty() && !rig.parked(0)); + rig.hold.at_batch(0); + assert!(rig.hold.held().is_empty(), "not held for that partner again"); + rig.publish(0x10); + rig.hold.at_batch(0); + assert_eq!(rig.hold.held(), [0], "the publisher's hold still applies"); +} + +/// Two processors sit one per interface: neither has a partner to wait for. +#[test] +fn two_processors_have_no_pair() { + let rig = Rig::new(2); + rig.barrier(&[0, 1]); + rig.post(0); + rig.post(1); + rig.hold.at_batch(0); + rig.hold.at_batch(1); + assert!(rig.hold.held().is_empty()); +} diff --git a/src/ip53_machine.rs b/src/ip53_machine.rs index 35d788658..f88131a18 100644 --- a/src/ip53_machine.rs +++ b/src/ip53_machine.rs @@ -51,8 +51,10 @@ //! is inferred from them. //! - Every fitted processor runs from reset, each on its own thread, as in //! QEMU's IP35 node; CPU enable is stored and starts or stops nothing. The -//! PROM's reset barrier is ordered by Bedrock's LB, an approximation with -//! no hardware counterpart. +//! PROM's reset barrier is ordered by Bedrock's LB; the first of a pair +//! to post its liveness is held until its partner posts, and a presence +//! publisher until the other interface publishes ([`PresenceHold`]): +//! approximations with no hardware counterpart. //! - The second PIC is a port of the IO9 PIC's PXBOW, beside the host's //! and the IO9 PIC's links: the PROM and IRIX learn which widgets exist //! only from widget 0's link status and auxiliary status, so one @@ -104,6 +106,7 @@ use crate::gfx_display::GfxDisplay; use crate::hptimer::TimerManager; use crate::ip35::{Ip35Bus, CPU_PHYS_MASK}; use crate::ip35::node::{connect_cpus, Node, NodeConfig}; +use crate::ip35::presence::PresenceHold; use crate::machine_state::GuestState; use crate::monitor::{Monitor, DEFAULT_PORT as DEFAULT_MONITOR_PORT}; use crate::net::{GatewayConfig, Link, NatControl}; @@ -530,6 +533,7 @@ impl TezroMachine { c.share_jitv2(jit.clone(), world.clone()); } } + board.presence.set_cpus(&cpus); connect_cpus("Tezro", &board.bedrock, &board.l1, &board.events, &cpus)?; board.bedrock.set_timer_manager(&timers); let rx = board.receiver.lock().take().expect("the board's events are taken once"); @@ -873,8 +877,9 @@ fn build_cpus(board: &Arc, timers: &Arc, count_hz: u64 } /// Processor `id` of `count`, of type `C`, with its timers; CPU 0 also -/// runs the board's poll. A chip with a secondary cache of another size is -/// another type, instantiated here. +/// runs the board's poll, and on a board of several processors every one +/// takes part in the PROM's holds at its batch boundary. A chip with a +/// secondary cache of another size is another type, instantiated here. fn start_cpu(board: &Arc, timers: &Arc, count_hz: u64, id: u8, count: usize) -> Arc where C: CacheCpuModel + From> + 'static, @@ -888,10 +893,17 @@ where exec.core.count_write_samples = true; exec.core.cpu_id = id; exec.core.smp = count > 1; - if id == 0 { - let poll_board = board.clone(); - exec.batch_hook = Some(Box::new(move |_| poll_board.poll())); - } + let poll_board = board.clone(); + let cpu = usize::from(id); + exec.batch_hook = match (id, count > 1) { + (0, false) => Some(Box::new(move |_| poll_board.poll())), + (0, true) => Some(Box::new(move |_| { + poll_board.poll(); + poll_board.presence.at_batch(cpu); + })), + (_, true) => Some(Box::new(move |_| poll_board.presence.at_batch(cpu))), + (_, false) => None, + }; Arc::new(MipsCpu::new(*exec)) } @@ -960,6 +972,9 @@ pub(crate) struct TezroBoard { receiver: Mutex>>, /// The slots whose processors run: every fitted one. pub processors: Population, + /// The holds on the PROM's pair liveness check and presence publisher + /// across them. + pub presence: Arc, /// The PICs in QEMU's host order, which numbers their PI level sources: /// widget 11's first when the board has it, then the IO9's at widget 15. pub pics: Vec>, @@ -1055,6 +1070,7 @@ impl TezroBoard { let pxbow = io9.enable_pxbow(HOST_WIDGET, clock.clone()).map_err(|e| format!("Tezro PXBOW: {e:?}"))?; bedrock.connect_widget(0, pxbow.widget_zero()); bedrock.connect_widget(IO9_WIDGET, pxbow.route(IO9_WIDGET)); + let presence = PresenceHold::new(running, clock.clone(), &bedrock); let heartbeat = Arc::new(AtomicU64::new(0)); let fasttick = Arc::new(AtomicU64::new(0)); let status = crate::disp::StatusSource::new(heartbeat.clone(), fasttick.clone()); @@ -1106,6 +1122,7 @@ impl TezroBoard { events, receiver: Mutex::new(Some(receiver)), processors: running, + presence, pics, ioc4, scsi, @@ -1134,6 +1151,7 @@ impl TezroBoard { /// Everything back to its power-on state. pub fn power_on(&self) { + self.presence.reset(); self.bedrock.power_on(); self.l1.power_on(); self.flash.power_on(); From 884ac99e72bae01a018c5d31b6fc71d12b31e17f Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 08:20:04 -0700 Subject: [PATCH 29/33] tezro: fit the VPro V12 at widget 12 The Tezro takes an optional VPro V12, but the board fitted no graphics, so [graphics] could only be none. Add board = "v12", on the VPro model the Octane and the Fuel use. The V12 is attached at widget 12 behind the PXBOW of the PIC that owns widget 0, and the processor reaches it through that route with widget offsets, as the Fuel's graphics slot does. Its interrupt packets reach Bedrock as the PIC's do, from widget 12, and its DMA enters Bedrock as widget 12's inbound traffic. Each PI keeps its graphics flow-control registers as written, as on the Fuel; nothing counts the processors' stores into the graphics page. On the two- and four-processor board widget 12 is QEMU's graphics slot. On the one-processor board the tower guide gives one XIO connector for a VPro board but names neither its widget nor its route, so the same placement there is inferred, and the code says so. The V12 is registered with the monitor, as the Fuel's VPro is, so the `vpro` commands resolve on a Tezro that has one. --- CHANGELOG.md | 1 + FEATURES.md | 2 +- docs/tezro-ip53.md | 15 +++++- src/config.rs | 27 ++++++----- src/ip53_machine.rs | 96 ++++++++++++++++++++++++++++++++++++--- src/ip53_machine/tests.rs | 63 +++++++++++++++++++++++++ 6 files changed, 183 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f78a3ef..cf018f902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -549,6 +549,7 @@ where they differed: ### SGI Tezro (IP53 board, IP35 software platform) - Added a headless Tezro profile with the 700 MHz R16000 (`r16000_700`, the default; `cpu = "r16000"` takes it too), 4 MiB cache, 4 GiB RAM, a PIC with PXBOW, and the IO9's IOC4, ISP12160 and BCM5701. The 800, 900 and 1000 MHz chips (`r16000_800`, `r16000_900`, `r16000_1000`) are listed and take four processors; see the [Tezro guide](docs/tezro-ip53.md). +- A Tezro can fit a VPro V12 at widget 12 with `[graphics] board = "v12"`, on the VPro model the Octane and the Fuel use. - A Tezro fits one, two or four processors, four by default, and two or four bring a second PIC at widget 11 on the PXBOW's link 11. Every fitted processor runs, each on its own host thread, so a default four-processor Tezro runs four. - The Tezro's L1 names its node board for the chip and the processor count, as inventories print it (`IP53_4CPU` 030-1868-001 at 700 MHz, 030-1956-002 at 800 MHz, `IP59_4CPU` and `IP59_2CPU` at 1 GHz, `IP53_2CPU` and `IP53_1CPU` for the smaller 700 MHz boards), so the PROM tries only the fitted processor positions and a one- or two-processor Tezro's first boot does not power-cycle the module for its empty ones. - The Tezro's IO9 bus has the TI PCI-to-PCI bridge every Tezro capture shows at device 2, with nothing behind it. diff --git a/FEATURES.md b/FEATURES.md index c026c131a..28f4e8af2 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -236,7 +236,7 @@ config only when one of these keys is set. See the | Key | Default | Values and behavior | |---|---|---| -| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). The Fuel takes `v12` or `v10` in its graphics slot, or `none`, its default. | +| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). The Fuel takes `v12` or `v10` in its graphics slot, or `none`, its default. The Tezro takes `v12` at widget 12, or `none`, its default. | | `graphics.heads` | `1` | 1 or 2; dual-head requires Newport. | | `graphics.resolution` | `guest` | guest, 1024x768, 1280x960, 1280x1024; presets require Newport. | | `graphics.threads` | `2` | Worker threads the board's fragment pipeline draws on, at least 1. VPro: the screen tiles' owners, besides its front-end and raster threads; more help on a host with cores to spare. | diff --git a/docs/tezro-ip53.md b/docs/tezro-ip53.md index f89dc40f4..6e73a86a6 100644 --- a/docs/tezro-ip53.md +++ b/docs/tezro-ip53.md @@ -72,7 +72,8 @@ carrying the IO9. The two- and four-processor board adds a second PIC at widget 11, with nothing on its buses. It sits on link 11 of the IO9 PIC's PXBOW, beside the hub's link 10 and the IO9's link 15, so the PROM and IRIX find it through that crossbar's link status. Whether it has a crossbar of -its own in front of it is not known. +its own in front of it is not known. Either board takes a V12 at widget 12 +with `[graphics] board = "v12"`. The IO9's PCI bus, head 0 of the PIC at widget 15, holds the IOC4 at device 1, a TI PCI2050 PCI-to-PCI bridge (`104c:ac28`, revision 1) at device 2, the @@ -204,7 +205,17 @@ The IRIX 6.5.30 disks tested so far were installed on an Origin 300, not a Tezro. On them the IOC4 serial ports appear as `tty3` and `tty4`, as on real single-node IP59 inventories. -Graphics is not attached. Machine snapshots are unsupported. +`[graphics] board = "v12"` fits a VPro V12 at widget 12 behind the PXBOW, +on either board, using the VPro model the Octane and the Fuel use. On the +two- and four-processor board this is QEMU's graphics slot. On the +one-processor board the tower guide gives one XIO connector for a VPro board +but names neither its widget nor its route, so widget 12 there is inferred. +Each PI keeps its graphics flow-control registers as written, as on the Fuel; +nothing counts a processor's stores into the graphics page or returns credits +for them. IRIX 6.5.30 with the VPro driver reaches the X desktop on the V12 +that way. +Without the V12 the machine has no graphics. +Machine snapshots are unsupported. ## Source basis diff --git a/src/config.rs b/src/config.rs index 5591724ef..a545cfe73 100644 --- a/src/config.rs +++ b/src/config.rs @@ -917,9 +917,9 @@ pub enum GraphicsBoard { /// Octane2's Crossbow and Bridge revisions. #[clap(name = "v10", alias = "vpro:v10", alias = "vpro_v10")] V10, - /// VPro V12: BUZZ B, 128 MB. Octane and Fuel. On the Octane it shipped - /// only in the Octane2 and needs Crossbow 1.4, so the machine takes the - /// Octane2's Crossbow and Bridge revisions. + /// VPro V12: BUZZ B, 128 MB. Octane, Fuel and Tezro. On the Octane it + /// shipped only in the Octane2 and needs Crossbow 1.4, so the machine + /// takes the Octane2's Crossbow and Bridge revisions. #[clap(name = "v12", alias = "vpro:v12", alias = "vpro_v12")] V12, /// No graphics board: the machine runs on its serial console. Octane @@ -987,7 +987,7 @@ impl GraphicsBoard { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => self == Self::None, MachineProfile::FuelIp34 => matches!(self, Self::V10 | Self::V12 | Self::None), - MachineProfile::TezroIp53 => self == Self::None, + MachineProfile::TezroIp53 => matches!(self, Self::None | Self::V12), MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -2974,8 +2974,8 @@ impl MachineConfig { } crate::ip53_machine::chip(self.cpu()).accepts_cpus(self.machine.cpus())?; self.tezro_memory()?.check_for_cpus(usize::from(self.machine.cpus())).map_err(|e| format!("banks: {e}"))?; - if board != GraphicsBoard::None { - return Err("Tezro currently supports graphics.board = \"none\" only".into()); + if !board.supports(MachineProfile::TezroIp53) { + return Err(format!("graphics.board \"{}\" is not a Tezro board (valid: v12, none)", board.as_str())); } } else if self.machine.profile == MachineProfile::OctaneIp30 { // A HEART bank is a matched DIMM pair: 64 MB to 2 GB, filled from @@ -4846,10 +4846,10 @@ mod tezro_config_tests { assert_eq!(cfg.tezro_memory().unwrap(), crate::ip53_machine::MemoryPopulation::default()); } - /// The processor is one of the Tezro's R16000 chips and graphics is - /// absent. + /// The processor is one of the Tezro's R16000 chips, with no graphics + /// board or a V12. #[test] - fn tezro_requires_an_r16000_and_no_graphics() { + fn tezro_requires_an_r16000_and_takes_no_graphics_or_a_v12() { let mut cfg = tezro(); cfg.set_cpu(&parts::R16000_700_EARLY); cfg.validate().unwrap(); @@ -4863,8 +4863,13 @@ mod tezro_config_tests { assert_eq!(cfg.graphics_board(), GraphicsBoard::None); cfg.graphics.board = Some(GraphicsBoard::Newport); assert!(cfg.validate().is_err()); - cfg.graphics.board = Some(GraphicsBoard::V12); - assert!(cfg.validate().is_err()); + cfg.graphics.board = Some(GraphicsBoard::V10); + assert!(cfg.validate().is_err(), "the V10 is not a Tezro board"); + for cpus in [1, 4] { + cfg.graphics.board = Some(GraphicsBoard::V12); + cfg.machine.cpus = Some(cpus); + cfg.validate().unwrap(); + } let mut cfg = tezro(); cfg.network.mode = NetMode::Off; cfg.validate().unwrap(); diff --git a/src/ip53_machine.rs b/src/ip53_machine.rs index f88131a18..59b304cfa 100644 --- a/src/ip53_machine.rs +++ b/src/ip53_machine.rs @@ -6,7 +6,8 @@ //! NVRAM, memory and SCSI devices; `[tezro]` selects the processor //! population, the serial console and writable flash. The node itself is //! the one the Fuel uses ([`Node`]); this board adds its widgets and its -//! inputs. Graphics is not attached. +//! inputs. With `[graphics] board = "v12"` a VPro V12 sits at widget 12 +//! behind the PXBOW; otherwise there is no graphics. //! //! The board follows the QEMU Tezro compositions: //! @@ -80,7 +81,7 @@ //! Board composition ported from hw/mips/sgi/sn-machine.c and //! hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. -use crate::config::{scsi_label, MachineConfig, TezroConsole}; +use crate::config::{scsi_label, GraphicsBoard, MachineConfig, TezroConsole}; use crate::cpu::mips_cache_v2::{CpuModel as CacheCpuModel, MipsCache}; use crate::cpu::parts::{self, Part}; use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; @@ -95,7 +96,9 @@ use crate::dev::heart::{host_clock, Clock}; use crate::dev::ioc4::{Ioc4, Ioc4Config}; use crate::dev::isp12160::Isp12160; use crate::dev::l1::{BoardIdentity, CpuContext, L1Config}; +use crate::dev::odyssey::{Odyssey, PacketSender}; use crate::dev::pic::{Function, Pic, PicPeer, Refused}; +use crate::dev::pxbow::Pxbow; use crate::dev::prom_image; use crate::dev::ps2::Ps2Controller; use crate::dev::serial_host::{null_serial_backend, open_log, tcp_serial_backend, CiSerialBackend, LogMode, SerialBackend, TeeBackend}; @@ -503,6 +506,9 @@ impl TezroMachine { memory: &memory, cpus: usize::from(cfg.machine.cpus()), chip, + v12: cfg.graphics_board() == GraphicsBoard::V12, + graphics_pool: (cfg.graphics.threads, cfg.graphics.tile), + graphics_raster: (cfg.graphics.raster, cfg.graphics.gpu_backend.as_str()), })?); let timers = Arc::new(TimerManager::new()); board.ioc4.set_timer_manager(timers.clone()); @@ -593,10 +599,11 @@ impl TezroMachine { board.eth.attach(link); } eprintln!( - "iris: Tezro IP53, {} R16000 {} MHz, {} MiB, serial console {:?}", + "iris: Tezro IP53, {} R16000 {} MHz, {} MiB, {}, serial console {:?}", cpus.len(), chip.chip.mhz, memory.total_mib(), + if board.v12.is_some() { cfg.graphics_board().label() } else { "no graphics" }, cfg.tezro.console ); Ok(Self { @@ -779,7 +786,7 @@ impl Workstation for TezroMachine { self.lifecycle = state; } fn get_display(&self) -> Option> { - None + self.board.v12.as_ref().map(|v12| v12.display()) } fn register_monitor_device(&self, device: Arc) { self.monitor.register_device(device); @@ -951,6 +958,12 @@ pub(crate) struct TezroBoardConfig<'a> { pub cpus: usize, /// The R16000 chip fitted. pub chip: &'static TezroChip, + /// A VPro V12 at widget 12. + pub v12: bool, + /// Rendering threads and tile size for the V12. + pub graphics_pool: (usize, u32), + /// Which unit draws and, for the GPU, the wgpu backend (`[graphics]`). + pub graphics_raster: (crate::config::RasterUnit, &'a str), } /// The Tezro's devices around its Bedrock node. @@ -985,6 +998,8 @@ pub(crate) struct TezroBoard { pub serial: [Arc; SERIAL_PORTS], /// The R16000 chip fitted, whose flash record the node holds. pub chip: &'static TezroChip, + /// The VPro V12, when the board has one. + pub v12: Option>, /// The status bar's activity bits and the CPU's Compare matches, which /// the graphics board shows. pub heartbeat: Arc, @@ -1000,9 +1015,18 @@ pub(crate) struct TezroBoard { impl TezroBoard { /// The board's devices that take monitor commands: the physical map, the /// L1, the SCSI controller, the keyboard and mouse (`ps2 type` and - /// friends). + /// friends) and, when one is fitted, the VPro (`vpro ...`). pub fn monitor_devices(&self) -> Vec> { - vec![self.bedrock.physical().clone(), self.l1.clone(), self.scsi.clone(), self.ioc4.ps2()] + let mut devices: Vec> = vec![ + self.bedrock.physical().clone(), + self.l1.clone(), + self.scsi.clone(), + self.ioc4.ps2(), + ]; + if let Some(v12) = &self.v12 { + devices.push(v12.clone()); + } + devices } /// Flush the disks on both channels of the storage controller. @@ -1021,11 +1045,14 @@ impl TezroBoard { memory: &MemoryPopulation::default(), cpus, chip: &CHIPS[0], + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }) } pub fn with_config(config: TezroBoardConfig<'_>) -> Result { - let TezroBoardConfig { payload, clock, flash_path, station_mac, timekeeper, memory, cpus, chip } = config; + let TezroBoardConfig { payload, clock, flash_path, station_mac, timekeeper, memory, cpus, chip, v12, graphics_pool, graphics_raster } = config; if ![1, 2, 4].contains(&cpus) { return Err(format!("{cpus} processors; a Tezro board fits 1, 2 or 4")); } @@ -1074,6 +1101,7 @@ impl TezroBoard { let heartbeat = Arc::new(AtomicU64::new(0)); let fasttick = Arc::new(AtomicU64::new(0)); let status = crate::disp::StatusSource::new(heartbeat.clone(), fasttick.clone()); + let v12 = v12.then(|| fit_v12(&bedrock, &pxbow, &status, graphics_pool, graphics_raster)).transpose()?; let pics = if cpus == 1 { vec![io9.clone()] } else { @@ -1130,6 +1158,7 @@ impl TezroBoard { timekeeper, serial, chip, + v12, heartbeat, fasttick, status, @@ -1161,9 +1190,15 @@ impl TezroBoard { for pic in &self.pics { pic.power_on(); } + if let Some(v12) = &self.v12 { + v12.power_on(); + } } pub fn start(&self) { + if let Some(v12) = &self.v12 { + v12.start(); + } self.last_clock.store((self.clock)(), Ordering::Release); self.running.store(true, Ordering::Release); self.bedrock.start(); @@ -1184,6 +1219,9 @@ impl TezroBoard { pub fn stop(&self) { self.poll(); + if let Some(v12) = &self.v12 { + v12.stop(); + } self.running.store(false, Ordering::Release); self.scsi.stop(); self.eth.stop(); @@ -1199,6 +1237,50 @@ impl Drop for TezroBoard { } } +/// Where the V12 sits: widget 12 of the crossbar behind the PIC that owns +/// widget 0. On the two- and four-processor board this is QEMU's graphics +/// slot. On the one-processor board the tower guide gives one XIO connector +/// for a VPro board but names neither its widget nor its route; widget 12 +/// behind the same PXBOW is inferred, not established. +const V12_WIDGET: u8 = 12; + +/// Fit the V12 behind the PXBOW: its interrupt packets and DMA reach +/// Bedrock IO as the PIC's do. Nothing counts the processors' stores into +/// its graphics page or returns credits for them. +fn fit_v12( + bedrock: &Arc, + pxbow: &Arc, + status: &Arc, + graphics_pool: (usize, u32), + graphics_raster: (crate::config::RasterUnit, &str), +) -> Result, String> { + let io = Arc::downgrade(bedrock); + let packets: PacketSender = Arc::new(move |widget, address, vector, set| { + if widget != HOST_WIDGET { + dlog!(LogModule::Bedrock, "Tezro: V12 interrupt to widget {widget} refused; only Bedrock IO is reachable"); + return false; + } + let Some(io) = io.upgrade() else { return false }; + let payload = u64::from(vector) | if set { 1 << 8 } else { 0 }; + io.inbound_write(V12_WIDGET, Some(INBOUND_DEVICE), address, 8, payload) == BUS_OK + }); + let dma = bedrock.widget_dma(V12_WIDGET); + let v12 = Odyssey::new_widget_relative( + V12_WIDGET, + GraphicsBoard::V12.as_str(), + packets, + dma, + status.clone(), + graphics_pool, + graphics_raster, + )?; + pxbow + .attach_endpoint(V12_WIDGET, v12.clone() as Arc) + .map_err(|e| format!("Tezro V12: {e:?}"))?; + bedrock.connect_widget(V12_WIDGET, pxbow.route(V12_WIDGET)); + Ok(v12) +} + /// The far end of a PIC's link: Bedrock IO. Interrupt packets and DMA arrive /// there as inbound packets from the PIC's widget, device 0, as QEMU's /// Bedrock IO takes them by default. DMA aimed at another widget is refused: diff --git a/src/ip53_machine/tests.rs b/src/ip53_machine/tests.rs index 7aa4edef3..a2cb64864 100644 --- a/src/ip53_machine/tests.rs +++ b/src/ip53_machine/tests.rs @@ -161,6 +161,9 @@ fn board_with_chip(cpus: usize, chip: &'static TezroChip) -> TezroBoard { memory: &MemoryPopulation::default(), cpus, chip, + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }) .unwrap() } @@ -335,6 +338,9 @@ fn board_with_clock(cpus: usize) -> (TezroBoard, Arc) { memory: &MemoryPopulation::default(), cpus, chip: &CHIPS[0], + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }) .unwrap(); (board, now) @@ -484,6 +490,9 @@ fn the_board_refuses_a_population_it_cannot_fit() { memory: &MemoryPopulation::default(), cpus: 1, chip: one_ghz, + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }); assert!(refused.err().is_some_and(|e| e.contains("fitted with 2, 4 processors only"))); } @@ -500,6 +509,9 @@ fn board_with(chip: &Part) -> TezroBoard { memory: &MemoryPopulation::one_gib(), cpus: usize::from(self::chip(chip).cpus[0]), chip: self::chip(chip), + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }) .unwrap() } @@ -656,6 +668,57 @@ fn the_processor_reads_and_writes_ioc4_registers_in_its_own_byte_order() { assert_eq!(io.pio_read(reg(0x314), 4).data, 0x3f8, "port 0 TX producer"); } +// VPro host-interface registers the tests below touch, as byte offsets in +// the board's widget space. The board's own constants are private to it. +const ODSY_IDENTITY: u64 = 0x00; + +fn board_with_v12(cpus: usize) -> TezroBoard { + TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::default(), + cpus, + chip: &CHIPS[0], + v12: true, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), + }) + .unwrap() +} + +/// The V12 answers as widget 12 behind the PXBOW on either board, and +/// widget 12 stays empty without one. (On the one-processor board the +/// placement is inferred; see V12_WIDGET.) +#[test] +fn the_v12_answers_at_widget_12_on_both_boards() { + for cpus in [1, 4] { + let board = board_with_v12(cpus); + let id = pio_read(&board.bedrock, widget(12, ODSY_IDENTITY), 4); + let direct = board.v12.as_ref().unwrap().read32(ODSY_IDENTITY); + assert_eq!((id.status, id.data), (BUS_OK, u64::from(direct.data)), "{cpus} CPUs"); + assert_eq!(board.pics[board.pics.len() - 1].pxbow().unwrap().endpoint_widgets() & (1 << 12), 1 << 12); + } + assert_eq!(pio_read(&TezroBoard::new(1).unwrap().bedrock, widget(12, ODSY_IDENTITY), 4).status, BUS_ERR); +} + +/// The monitor's `vpro` commands resolve on a Tezro that has a V12, and are +/// unknown on one that has none. +#[test] +fn the_monitor_resolves_vpro_commands_on_a_tezro_with_a_v12() { + let resolves = |board: &TezroBoard, command: &str| { + board.monitor_devices().iter().any(|d| d.register_commands().iter().any(|(c, _)| c == command)) + }; + let with = board_with_v12(4); + assert!(resolves(&with, "vpro"), "a Tezro with a V12 answers `vpro`"); + assert!(resolves(&with, "ps2"), "the other board commands stay"); + let without = TezroBoard::new(1).unwrap(); + assert!(!resolves(&without, "vpro"), "a Tezro with no V12 has no `vpro`"); + assert!(resolves(&without, "ps2")); +} + /// The cached view the processors' caches sit on swaps RAM above 4 GiB, so /// SC stays atomic there too. #[test] From 6e8650763cbcc5f17a237ec2dfa78eec9d939f2f Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 16:29:02 -0700 Subject: [PATCH 30/33] identity: add the IP35 system serial and station address rules An IP35 machine's guest prints one host id, through sysinfo -s and lmhostid, and today every emulated Fuel and Tezro prints the same one. Two of them cannot hold node-locked licences side by side, and nothing marks the id as an emulator's. Add the rules that mint an id from a small emu_id, with iris as kind 2. A Tezro's IRIX asks the L1 for the system serial and folds the low nibble of each of its first ten characters into the id, so its serial is LEMU, the kind, the emu_id as four digits and 0: emu_id 42 reads LEMU200420 and folds to d5200420. A Fuel's IRIX takes the id from the last four bytes of the station address, so its default address is 08:00:69:E followed by the emu_id, with the low nibble left for a node or port number. Nothing uses the rules yet. --- src/identity.rs | 64 +++++++++++++++++++++++++++++++++++++ src/identity/tests.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 138 insertions(+) create mode 100644 src/identity.rs create mode 100644 src/identity/tests.rs diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 000000000..4380caec7 --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,64 @@ +//! The system id and serial numbers of an emulated IP35 machine. +//! +//! A machine has one observable, the 32-bit system id that IRIX reports +//! through `sysinfo -s` and FLEXlm's `lmhostid`. `[machine] emu_id`, 0 to +//! 4095, mints it in the emulator layout, a pure function of the kind, the +//! `emu_id` and a small sub-index (a node or port), with no hashing. The kind +//! is a digit that marks an id as minted by an emulator, and iris's is 2: +//! +//! - The system serial every L1 of a Tezro answers with is `LEMU`, the kind +//! digit, the `emu_id` as four digits and `0`. +//! - The default station address is `08:00:69:E:> 4>: +//! `. The sub-index takes the low nibble of the +//! last byte, so an address derived from the default by adding a node or +//! port number up to 15 never reaches the id. +//! +//! IRIX builds the id from a text serial by taking the low nibble of each of +//! its first ten characters and keeping the low 32 bits, or from the last +//! four bytes of the station address. +//! +//! These are synthetic values, not any machine's factory identity. +//! +//! Ported from include/hw/mips/sgi/sgi-board-identity.h and +//! hw/mips/sgi/board-mac.c in jamesbraid/qemu-sgi-origami. + +/// The kind of every machine iris builds. +pub const KIND: u32 = 2; + +/// The `emu_id` is 0 (the default) or 1 to this. +pub const EMU_ID_MAX: u32 = 4095; + +/// Nodes or ports a default station address can number, 0 to 15. +pub const SUB_INDEX_COUNT: u32 = 16; + +/// `LEMU`, the kind digit, four `emu_id` digits and `0`: the system serial an +/// L1 answers with. It names the system, not a brick, so every L1 of a +/// machine gives the same text. +pub fn system_serial(kind: u32, emu_id: u32) -> String { + assert!((1..=9).contains(&kind) && emu_id <= EMU_ID_MAX); + format!("LEMU{kind}{emu_id:04}0") +} + +/// The default station address of node or port `sub`: `08:00:69:E`, the +/// `emu_id`'s high 8 bits, then its low 4 bits over `sub`. `None` when `sub` +/// is 16 or more. +pub fn default_mac(kind: u32, emu_id: u32, sub: u32) -> Option<[u8; 6]> { + assert!((1..=15).contains(&kind) && emu_id <= EMU_ID_MAX); + (sub < SUB_INDEX_COUNT).then(|| [0x08, 0x00, 0x69, 0xe0 | kind as u8, (emu_id >> 4) as u8, ((emu_id & 0xf) << 4 | sub) as u8]) +} + +/// The id IRIX computes from a text serial: the low nibble of each of the +/// first ten characters shifted in, the low 32 bits kept. A shorter text folds +/// in fewer nibbles; an empty one gives 0. +pub fn hostid_from_text(text: &str) -> u32 { + let id = text.bytes().take(10).fold(0u64, |id, c| id << 4 | u64::from(c & 0xf)); + id as u32 +} + +/// The id IRIX takes from a station address: its last four bytes. +pub fn hostid_from_mac(mac: &[u8; 6]) -> u32 { + u32::from_be_bytes([mac[2], mac[3], mac[4], mac[5]]) +} + +#[cfg(test)] +mod tests; diff --git a/src/identity/tests.rs b/src/identity/tests.rs new file mode 100644 index 000000000..9ce902e2a --- /dev/null +++ b/src/identity/tests.rs @@ -0,0 +1,73 @@ +//! The vectors are literals worked out by hand from the layouts in the module +//! header, so a change to the layout shows up here. Most use kind 1, which no +//! iris machine has, so that they differ from iris's own where the kind shows. + +use super::*; + +const MAC: [u8; 6] = [0x08, 0x00, 0x69, 0xe1, 0x02, 0xa0]; + +#[test] +fn iris_is_kind_two() { + assert_eq!(KIND, 2); +} + +/// IRIX folds only the first ten characters; the first two never show. +#[test] +fn the_fold_keeps_the_low_nibbles_of_the_first_ten_characters() { + assert_eq!(hostid_from_text("LEMU1042"), 0xc5d5_1042); + assert_eq!(hostid_from_text("EMU1-0042"), 0xd51d_0042); + assert_eq!(hostid_from_text("EMU1L0000042"), 0x51c0_0000); + assert_eq!(hostid_from_text("M2001347"), 0xd200_1347); + assert_eq!(hostid_from_text("L3962017"), 0xc396_2017); + assert_eq!(hostid_from_text("L1000302"), 0xc100_0302); + assert_eq!(hostid_from_text(""), 0); +} + +#[test] +fn the_default_station_address_carries_the_kind_and_the_emu_id() { + assert_eq!(default_mac(1, 42, 0).unwrap(), MAC); + assert_eq!(hostid_from_mac(&MAC), 0x69e1_02a0); + // Kind 2 changes only the nibble after E. + assert_eq!(hostid_from_mac(&default_mac(2, 42, 0).unwrap()), 0x69e2_02a0); + assert_eq!(hostid_from_mac(&default_mac(1, 0, 0).unwrap()), 0x69e1_0000); + assert_eq!(default_mac(KIND, 0, 0).unwrap(), [0x08, 0x00, 0x69, 0xe2, 0x00, 0x00]); + assert_eq!(hostid_from_mac(&default_mac(KIND, 7, 0).unwrap()), 0x69e2_0070); +} + +/// The sub-index fills the low nibble of the last byte and never reaches the +/// kind or the emu-id, at both ends of the range. +#[test] +fn the_sub_index_never_reaches_the_emu_id() { + for emu_id in [0, 1, 42, 255, 4094, 4095] { + let base = default_mac(1, emu_id, 0).unwrap(); + assert_eq!(base[5] & 0xf, 0); + for sub in 0..SUB_INDEX_COUNT { + let mac = default_mac(1, emu_id, sub).unwrap(); + assert_eq!(mac[..5], base[..5]); + assert_eq!(mac[5] >> 4, base[5] >> 4); + assert_eq!(u32::from(mac[5] & 0xf), sub); + assert_eq!(mac[3], 0xe1); + assert_eq!(u32::from(mac[4]) << 4 | u32::from(mac[5] >> 4), emu_id); + } + assert_eq!(default_mac(1, emu_id, 16), None); + } +} + +#[test] +fn the_system_serial_ends_in_a_zero_and_spans_the_emu_id_range() { + assert_eq!(system_serial(1, 42), "LEMU100420"); + assert_eq!(hostid_from_text("LEMU100420"), 0xd510_0420); + assert_eq!(system_serial(1, 0), "LEMU100000"); + assert_eq!(system_serial(KIND, 42), "LEMU200420"); + assert_eq!(hostid_from_text(&system_serial(KIND, 42)), 0xd520_0420); + assert_eq!(system_serial(KIND, EMU_ID_MAX), "LEMU240950"); + assert_eq!(hostid_from_text(&system_serial(KIND, EMU_ID_MAX)), 0xd524_0950); + // Ten characters, so all of the kind and the emu_id reach the id. + assert_eq!(system_serial(KIND, 1).len(), 10); +} + +#[test] +#[should_panic] +fn the_system_serial_refuses_an_emu_id_past_the_range() { + system_serial(KIND, EMU_ID_MAX + 1); +} diff --git a/src/lib.rs b/src/lib.rs index 5772763c7..bfb0c1d91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,6 +164,7 @@ pub mod build_features { pub mod validate_config; pub mod config; +pub mod identity; pub mod traits; #[macro_use] pub mod devlog; From 52fe1734542437d41c2258a00b75f345891c9cab Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 16:29:51 -0700 Subject: [PATCH 31/33] l1: let a machine give the system serial its L1 answers with The L1 answers the system serial request with the station address in hex. That is what the Fuel's L1 does, and the Fuel's IRIX takes its host id from the address. The Tezro's IRIX instead folds the low nibble of each of the first ten characters of that text into its host id, so a Tezro whose system serial is fixed to the address cannot be given an id of its own without also changing the address. Add a system serial to the L1's configuration. When it is set, request 0x0008 answers with it; unset, it is the address text as before. It replaces nothing else the L1 holds: the brick serial and the board serials in the EEPROM records stay put, because the PROM ties its NVRAM to the node board's serial and starts it afresh when that changes. --- src/dev/l1.rs | 7 +++++++ src/dev/l1/model.rs | 26 +++++++++++++++----------- src/dev/l1/tests.rs | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/dev/l1.rs b/src/dev/l1.rs index d370f96e8..126987f51 100644 --- a/src/dev/l1.rs +++ b/src/dev/l1.rs @@ -148,6 +148,13 @@ impl L1 { { return Err("L1 brick serial must fit a 37-byte text field".into()); } + if config + .system_serial + .as_ref() + .is_some_and(|s| s.len() > 37 || s.as_bytes().contains(&0)) + { + return Err("L1 system serial must fit a 37-byte text field".into()); + } let protocol_rx = Arc::new(Mutex::new(VecDeque::new())); let state = Arc::new(Mutex::new(State::new(config, protocol_rx.clone()))); diff --git a/src/dev/l1/model.rs b/src/dev/l1/model.rs index 2c24fe175..47d8a8a26 100644 --- a/src/dev/l1/model.rs +++ b/src/dev/l1/model.rs @@ -49,6 +49,12 @@ pub struct L1Config { pub revision: Option<[u32; 3]>, pub mac: [u8; 6], pub brick_serial: Option, + /// The system serial request 0x0008 answers with. `None` answers with + /// the station address in hex, as the Fuel's L1 does. It names the + /// system, so every L1 of a machine is given the same text; the board + /// serials in the records stay as they are, because the PROM ties its + /// NVRAM to the node board's serial. + pub system_serial: Option, pub auto_power: bool, /// First integer of the port-speed reply (request 0xa). The PROM uses it /// to select a display branch; it is not a link speed. Zero or one. @@ -106,6 +112,7 @@ impl Default for L1Config { revision: Some(DEFAULT_REVISION), mac: [8, 0, 0x69, 0x12, 0x34, 0x56], brick_serial: Some(DEFAULT_BRICK_SERIAL.into()), + system_serial: None, auto_power: true, port_speed_first: 0, router: false, @@ -512,18 +519,15 @@ impl State { self.eeprom(channel, &a); return; } - // The system serial is the station address as twelve uppercase - // hexadecimal digits in address order, no separators or OUI - // added: the text the stock PROM converts straight back into the - // Ethernet address. + // Without a configured system serial it is the station address + // as twelve uppercase hexadecimal digits in address order, no + // separators or OUI added: the text the stock PROM converts + // straight back into the Ethernet address. 8 => { - let mac = self - .config - .mac - .iter() - .map(|b| format!("{b:02X}")) - .collect::(); - text_field(&mut fields, mac.as_bytes()); + let serial = self.config.system_serial.clone().unwrap_or_else(|| { + self.config.mac.iter().map(|b| format!("{b:02X}")).collect() + }); + text_field(&mut fields, serial.as_bytes()); field_count = 1; } // Not partitioned. diff --git a/src/dev/l1/tests.rs b/src/dev/l1/tests.rs index d224b0063..aca533fe8 100644 --- a/src/dev/l1/tests.rs +++ b/src/dev/l1/tests.rs @@ -199,6 +199,26 @@ fn the_l1_system_serial_is_the_station_address_in_hex() { assert_eq!(request(&l1, 8, &[], 0), text_reply("00A0C91F02BE")); } +/// A configured system serial replaces the address text in request 0x0008 +/// and nothing else: the brick serial and the board serial in the node +/// board's record, which the PROM ties its NVRAM to, stay the same. +#[test] +fn a_system_serial_changes_only_the_system_serial_reply() { + let plain = L1::new(Default::default()).unwrap(); + let set = L1::new(L1Config { system_serial: Some("LEMU200420".into()), ..Default::default() }).unwrap(); + for l1 in [&plain, &set] { + l1.power_on(); + } + assert_eq!(request(&set, 8, &[], 0), text_reply("LEMU200420")); + assert_eq!(request(&plain, 8, &[], 0), text_reply("080069123456")); + assert_eq!(request(&set, 4, &[], 0), request(&plain, 4, &[], 0)); + assert_eq!(set.state.lock().records, plain.state.lock().records); + assert_eq!( + L1::new(L1Config { system_serial: Some("x".repeat(38)), ..Default::default() }).err().as_deref(), + Some("L1 system serial must fit a 37-byte text field") + ); +} + /// The L1 parser recovers after malformed frames. #[test] fn the_l1_resynchronizes_after_bad_frames() { From a790a9c07938d2f6e85eeb805eac774d68dc4cda Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 16:32:22 -0700 Subject: [PATCH 32/33] tezro: take the L1 system serial from [machine] emu_id Every emulated Tezro prints the same host id, so two of them cannot hold node-locked licences side by side. A Tezro's IRIX asks the L1 for the system serial and folds the low nibble of each of its first ten characters into sysinfo -s and lmhostid, and the L1 answers with the station address in hex, so the only way to move the id was to move the address, and the id that came out was the fold of hex digits rather than anything that reads as an emulator's. Add [machine] emu_id, 0 to 4095, and give the Tezro's L1 the system serial LEMU20: emu_id 42 answers LEMU200420 and IRIX prints d5200420. iris prints the id and the serial at start-up. A nonzero emu_id also gives the station address 08:00:69:e2:, so two Tezros on one network do not share an address; [network] mac still wins, and the address is never the id. emu_id 0 keeps today's address. The board serial in the node board's L1 record stays EMUN00 whatever the emu_id, because the PROM ties its NVRAM to that serial and starts it afresh when it changes; an emu_id change keeps the saved settings. Other machines refuse a nonzero emu_id rather than ignore it, since it would set nothing there. --- CHANGELOG.md | 1 + FEATURES.md | 1 + docs/tezro-ip53.md | 12 +++++ src/config.rs | 92 +++++++++++++++++++++++++++++++++++++-- src/ip53_machine.rs | 14 ++++-- src/ip53_machine/tests.rs | 45 +++++++++++++++++++ src/workstation.rs | 29 ++++++++++++ tezro.toml.example | 3 ++ 8 files changed, 191 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf018f902..fa9a98211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -556,6 +556,7 @@ where they differed: - A Tezro refuses a `banks` list whose two banks of a pair differ in size: Bedrock keeps one size code for each pair. - The Tezro's BCM5701 reports PCI revision 0x15 and has the chip's capability list with vital product data reads, so IRIX's driver no longer waits out its VPD poll at every attach. - The Tezro's PIC fails a configuration read of an empty slot as a master timeout, so the PROM no longer records two phantom devices per empty slot. +- `[machine] emu_id` (0 to 4095) gives a Tezro its own host id: the L1 answers the system serial request with `LEMU20`, IRIX's `sysinfo -s` and `lmhostid` print its fold (`d5200420` for emu_id 42), and iris prints both at start-up. The board serial stays `EMUN00`, so the PROM keeps its NVRAM across an emu_id change. ### `IRIS_GUI_GL` works in iris-gui diff --git a/FEATURES.md b/FEATURES.md index 28f4e8af2..aebce73fb 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -179,6 +179,7 @@ the first `[section]`, because TOML tables continue until the next header. | `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)), origin200_ip29 (configured by `[origin200]`, see [the Origin 200 guide](docs/origin200-ip29.md)), origin2000_ip27 (configured by `[origin2000]`, see [the Origin 2000 guide](docs/origin2000-ip27.md)), onyx2_ip27 (also configured by `[origin2000]`, see [the Onyx2 guide](docs/onyx2-ip27.md)), fuel_ip34 (configured by `[fuel]`, see [the Fuel guide](docs/fuel-ip34.md)), tezro_ip53 (configured by `[tezro]`, see [the Tezro guide](docs/tezro-ip53.md)). | | `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000; Origin 200, Origin 2000 and Onyx2: r10000 (`r10000_180`), r12000 (`r12000_270`); Fuel: r14000 (`r14000_500`); Tezro: r16000 (a bare `r16000` is `r16000_700`; `r16000_800`, `r16000_900` and `r16000_1000` are the other parts it ships with). The first listed is the default. | | `machine.cpus` | `1`; `4` on the Tezro | Processors fitted. 2 on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). 1, 2 or 4 on the Tezro, each run on its own host thread; one selects the one-processor board with one PIC, two or four the board with two PICs. | +| `machine.emu_id` | `0` | 0 to 4095; only the Tezro takes it, and other machines refuse a nonzero one. Mints the machine's system id (`sysinfo -s`, `lmhostid`) in iris's layout: the L1 system serial `LEMU20`, whose fold is the id, and, when nonzero, the station address `08:00:69:e2:`. 0 is the generic id. See [the Tezro guide](docs/tezro-ip53.md). | ### [origin2000] diff --git a/docs/tezro-ip53.md b/docs/tezro-ip53.md index 6e73a86a6..c598d7c3e 100644 --- a/docs/tezro-ip53.md +++ b/docs/tezro-ip53.md @@ -201,6 +201,18 @@ device answers; what silicon does there is not defined. A configuration read of an empty PCI slot fails as a master timeout; the error enable gating and the treatment of device 0 there are inferred. +The L1 answers the system serial request with `LEMU`, iris's kind digit 2, +`[machine] emu_id` as four digits and `0` (`LEMU200420` for emu_id 42, +`LEMU200000` by default), and IRIX folds the low nibble of each of its first +ten characters into the host id `sysinfo -s` and `lmhostid` print +(`d5200420`). At start-up iris prints that id and the serial. A nonzero +emu_id also gives the station address `08:00:69:e2:` and the emu_id, unless +`[network] mac` names one; the address is never the id. The board serial in +the node board's L1 record stays `EMUN00`, so the PROM keeps its NVRAM when +the emu_id changes. The serial is a synthetic value, not a factory identity; +a real unit's is a letter and seven digits, and whether real L1 or L2 +firmware takes other text is untested. + The IRIX 6.5.30 disks tested so far were installed on an Origin 300, not a Tezro. On them the IOC4 serial ports appear as `tty3` and `tty4`, as on real single-node IP59 inventories. diff --git a/src/config.rs b/src/config.rs index a545cfe73..4fa0c0876 100644 --- a/src/config.rs +++ b/src/config.rs @@ -454,7 +454,8 @@ pub struct NetworkSection { pub nfs_pcap_ip: Option, /// Ethernet station address for the built-in SEEQ controller (`ec0`), e.g. /// "08:00:69:12:34:56" (SGI's registered OUI). Defaults to - /// `08:00:69:12:34:56` when unset. Real hardware has no way to leave this + /// `08:00:69:12:34:56` when unset, or on a machine that takes + /// `[machine] emu_id` to the address that gives. Real hardware has no way to leave this /// unset — every SGI ships with a MAC burned into NVRAM (Indy) or a serial /// EEPROM (Indigo2) — so iris backdoor-injects it into the emulated /// NVRAM/EEPROM before boot rather than requiring a guest-side `setenv`. @@ -1222,6 +1223,16 @@ pub struct MachineSection { /// Tezro. #[serde(default, skip_serializing_if = "Option::is_none")] pub cpus: Option, + /// Mints the machine's system id (what `sysinfo -s` and `lmhostid` + /// print) in iris's own layout: 0 to 4095, 0 the default and a plainly + /// generic id. Machines with different ids can hold node-locked licenses + /// side by side. Only the Tezro takes it. See [`crate::identity`]. + #[serde(default, skip_serializing_if = "is_zero_u32")] + pub emu_id: u32, +} + +fn is_zero_u32(v: &u32) -> bool { + *v == 0 } impl MachineSection { @@ -1238,7 +1249,7 @@ impl MachineSection { impl Default for MachineSection { fn default() -> Self { - Self { profile: MachineProfile::default(), cpu: None, cache: CacheModel::default(), cpus: None } + Self { profile: MachineProfile::default(), cpu: None, cache: CacheModel::default(), cpus: None, emu_id: 0 } } } @@ -3040,6 +3051,13 @@ impl MachineConfig { if let Some(ref mac) = self.network.mac { parse_mac(mac).map_err(|e| format!("network.mac: {}", e))?; } + let emu_id = self.machine.emu_id; + if emu_id > crate::identity::EMU_ID_MAX { + return Err(format!("machine identity: emu_id must be 0 to {}, not {emu_id}", crate::identity::EMU_ID_MAX)); + } + if emu_id != 0 && self.machine.profile != MachineProfile::TezroIp53 { + return Err(format!("machine identity: emu_id = {emu_id} is not taken by this machine; only the Tezro takes it")); + } for (id, dev) in &self.scsi { // A CD-ROM may legitimately start with an empty tray (no path, no // discs) and have media loaded at runtime; any discs list is valid @@ -3135,6 +3153,25 @@ impl MachineConfig { } } + /// The station address when `[network] mac` is unset: the address + /// `[machine] emu_id` gives on a machine that takes it, else + /// [`DEFAULT_MAC`], which is also what emu_id 0 keeps. + fn default_station_mac(&self) -> [u8; 6] { + use crate::identity::{default_mac, KIND}; + match self.machine.profile { + MachineProfile::TezroIp53 if self.machine.emu_id != 0 => { + default_mac(KIND, self.machine.emu_id, 0).expect("node 0 always fits the default address") + } + _ => DEFAULT_MAC, + } + } + + /// The system serial a Tezro's L1 answers with, the text its IRIX folds + /// into the host id: `LEMU`, iris's kind and `[machine] emu_id`. + pub fn tezro_system_serial(&self) -> String { + crate::identity::system_serial(crate::identity::KIND, self.machine.emu_id) + } + /// Extract network-related settings into a `NetworkConfig`. /// Parses `nat_subnet` from CIDR — safe to unwrap because `validate()` already accepted it. pub fn network(&self) -> NetworkConfig { @@ -3145,7 +3182,7 @@ impl MachineConfig { }); let mac = self.network.mac.as_deref() .map(|s| parse_mac(s).expect("network.mac: validate() should have caught this")) - .unwrap_or(DEFAULT_MAC); + .unwrap_or_else(|| self.default_station_mac()); NetworkConfig { nfs: self.nfs.clone(), port_forward: self.port_forward.clone(), @@ -4657,6 +4694,55 @@ mod fuel_input_tests { /// The Fuel's processor is fixed, so `machine.cpu` may only name it (or the /// family, as the key did before the R14000 had a name); the default /// `r4400` is refused rather than silently ignored. + fn identity_cfg(profile: &str, extra: &str) -> MachineConfig { + toml::from_str(&format!("[machine]\nprofile = \"{profile}\"\n{extra}")).expect("parse") + } + + /// The Tezro's id is the fold of the system serial its L1 answers with, + /// LEMU, the kind, the emu_id and 0; the station address follows the + /// emu_id unless `[network] mac` names one, and is never the id. + #[test] + fn the_tezro_takes_its_id_from_the_l1_system_serial() { + use crate::identity::hostid_from_text; + let cfg = identity_cfg("tezro_ip53", ""); + cfg.validate().unwrap(); + assert_eq!(cfg.tezro_system_serial(), "LEMU200000"); + assert_eq!(hostid_from_text(&cfg.tezro_system_serial()), 0xd520_0000); + assert_eq!(cfg.network().mac, DEFAULT_MAC); + let cfg = identity_cfg("tezro_ip53", "emu_id = 42\n"); + cfg.validate().unwrap(); + assert_eq!(cfg.tezro_system_serial(), "LEMU200420"); + assert_eq!(hostid_from_text(&cfg.tezro_system_serial()), 0xd520_0420); + assert_eq!(cfg.network().mac, [0x08, 0x00, 0x69, 0xe2, 0x02, 0xa0]); + let mut cfg = identity_cfg("tezro_ip53", "emu_id = 42\n"); + cfg.network.mac = Some("08:00:69:aa:bb:cc".into()); + assert_eq!(hostid_from_text(&cfg.tezro_system_serial()), 0xd520_0420); + assert_eq!(cfg.network().mac, [0x08, 0x00, 0x69, 0xaa, 0xbb, 0xcc]); + identity_cfg("tezro_ip53", "emu_id = 4095\n").validate().unwrap(); + let e = identity_cfg("tezro_ip53", "emu_id = 4096\n").validate().unwrap_err(); + assert!(e.contains("emu_id must be 0 to 4095, not 4096"), "{e}"); + } + + /// A machine that does not take emu_id refuses one rather than ignoring + /// it, and keeps the station address it had. + #[test] + fn emu_id_is_refused_where_it_sets_nothing() { + let e = identity_cfg("octane_ip30", "emu_id = 7\n").validate().unwrap_err(); + assert!(e.contains("emu_id = 7 is not taken by this machine"), "{e}"); + let cfg = identity_cfg("octane_ip30", "emu_id = 0\n"); + cfg.validate().unwrap(); + assert_eq!(cfg.network().mac, DEFAULT_MAC); + } + + #[test] + fn emu_id_round_trips_and_stays_out_of_a_default_file() { + let cfg = identity_cfg("tezro_ip53", "emu_id = 7\n"); + let back: MachineConfig = toml::from_str(&toml::to_string_pretty(&cfg).unwrap()).unwrap(); + assert_eq!(back.machine.emu_id, 7); + let plain = toml::to_string_pretty(&MachineConfig::default()).unwrap(); + assert!(!plain.contains("emu_id"), "{plain}"); + } + /// The L1 identity keys parse, validate and round trip; unset they are /// left out of a written config. #[test] diff --git a/src/ip53_machine.rs b/src/ip53_machine.rs index 59b304cfa..c183b7f40 100644 --- a/src/ip53_machine.rs +++ b/src/ip53_machine.rs @@ -28,8 +28,10 @@ //! - Bedrock board word 0x2001, the Tezro firmware record (the chip's clock, //! an authored configuration block), and an L1 that reports //! brick type 0x39, revision 1.48.1, the first port-speed integer 1, the -//! router `brick` reply and, in the node board's record, the CPU -//! configuration the flash block holds. +//! router `brick` reply, the system serial `[machine] emu_id` gives +//! (IRIX's host id is its fold) and, in the node board's record, the CPU +//! configuration the flash block holds. The board serial in that record +//! stays `EMUN00` whatever the emu_id: the PROM ties its NVRAM to it. //! //! # What is approximated here //! @@ -502,6 +504,7 @@ impl TezroMachine { clock: host_clock(), flash_path: cfg.tezro.flash.as_deref().map(Path::new), station_mac: cfg.network().mac, + system_serial: Some(cfg.tezro_system_serial()), timekeeper: clock, memory: &memory, cpus: usize::from(cfg.machine.cpus()), @@ -951,6 +954,9 @@ pub(crate) struct TezroBoardConfig<'a> { pub clock: Clock, pub flash_path: Option<&'a Path>, pub station_mac: [u8; 6], + /// The system serial every L1 answers with, the text IRIX folds into + /// its host id. `None` answers with the station address in hex. + pub system_serial: Option, /// The DS1744-compatible clock and NVRAM in the IOC4's BAR. pub timekeeper: Arc, pub memory: &'a MemoryPopulation, @@ -1041,6 +1047,7 @@ impl TezroBoard { clock: Arc::new(|| 0), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968))?), memory: &MemoryPopulation::default(), cpus, @@ -1052,7 +1059,7 @@ impl TezroBoard { } pub fn with_config(config: TezroBoardConfig<'_>) -> Result { - let TezroBoardConfig { payload, clock, flash_path, station_mac, timekeeper, memory, cpus, chip, v12, graphics_pool, graphics_raster } = config; + let TezroBoardConfig { payload, clock, flash_path, station_mac, system_serial, timekeeper, memory, cpus, chip, v12, graphics_pool, graphics_raster } = config; if ![1, 2, 4].contains(&cpus) { return Err(format!("{cpus} processors; a Tezro board fits 1, 2 or 4")); } @@ -1072,6 +1079,7 @@ impl TezroBoard { type_code: 0x39, revision: Some([1, 48, 1]), mac: station_mac, + system_serial, port_speed_first: 1, router: true, cpu_context: Some(cpu_context(&chip.firmware)), diff --git a/src/ip53_machine/tests.rs b/src/ip53_machine/tests.rs index a2cb64864..f89842764 100644 --- a/src/ip53_machine/tests.rs +++ b/src/ip53_machine/tests.rs @@ -150,6 +150,46 @@ fn the_node_board_record_agrees_with_the_flash_configuration() { } } +/// The L1 answers the system serial request with the text the emu_id gives, +/// and nothing else moves with it: the node board's record keeps its EMUN00 +/// serial, which the PROM ties its NVRAM to, so a machine given a new emu_id +/// keeps the settings it saved. +#[test] +fn the_system_serial_moves_and_the_board_serial_stays() { + let board = |emu_id: u32| { + let mut cfg = crate::config::MachineConfig::default(); + cfg.machine.profile = crate::config::MachineProfile::TezroIp53; + cfg.machine.emu_id = emu_id; + TezroBoard::with_config(TezroBoardConfig { + payload: &[], + clock: Arc::new(|| 0), + flash_path: None, + station_mac: cfg.network().mac, + system_serial: Some(cfg.tezro_system_serial()), + timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), + memory: &MemoryPopulation::default(), + cpus: 1, + chip: &CHIPS[0], + v12: false, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), + }) + .unwrap() + }; + let (first, second) = (board(42), board(43)); + assert_eq!(first.l1.config().system_serial.as_deref(), Some("LEMU200420")); + assert_eq!(second.l1.config().system_serial.as_deref(), Some("LEMU200430")); + assert_eq!(first.l1.station_mac(), [0x08, 0x00, 0x69, 0xe2, 0x02, 0xa0]); + for component in 0..2 { + for area in [1, 2, 4] { + assert_eq!(first.l1.record(component, area), second.l1.record(component, area), "component {component}, area {area}"); + } + } + let area = first.l1.record(0, 2).expect("component 0's board area"); + assert_eq!(area_fields(&area)[2], b"EMUN00"); + assert_eq!(first.l1.config().brick_serial, second.l1.config().brick_serial); +} + /// A board of `cpus` processors of `chip`, with the defaults otherwise. fn board_with_chip(cpus: usize, chip: &'static TezroChip) -> TezroBoard { TezroBoard::with_config(TezroBoardConfig { @@ -157,6 +197,7 @@ fn board_with_chip(cpus: usize, chip: &'static TezroChip) -> TezroBoard { clock: Arc::new(|| 0), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), memory: &MemoryPopulation::default(), cpus, @@ -334,6 +375,7 @@ fn board_with_clock(cpus: usize) -> (TezroBoard, Arc) { clock: Arc::new(move || clock.load(Ordering::SeqCst)), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), memory: &MemoryPopulation::default(), cpus, @@ -486,6 +528,7 @@ fn the_board_refuses_a_population_it_cannot_fit() { clock: Arc::new(|| 0), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), memory: &MemoryPopulation::default(), cpus: 1, @@ -505,6 +548,7 @@ fn board_with(chip: &Part) -> TezroBoard { clock: Arc::new(|| 0), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), memory: &MemoryPopulation::one_gib(), cpus: usize::from(self::chip(chip).cpus[0]), @@ -678,6 +722,7 @@ fn board_with_v12(cpus: usize) -> TezroBoard { clock: Arc::new(|| 0), flash_path: None, station_mac: L1Config::default().mac, + system_serial: None, timekeeper: Arc::new(Ds1744::new(YearBase::From(1968)).unwrap()), memory: &MemoryPopulation::default(), cpus, diff --git a/src/workstation.rs b/src/workstation.rs index 63cc1472e..e000bcdbc 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -39,6 +39,9 @@ use crate::traits::{Device, MachineEvent}; /// a thread with a large one, as `main.rs` and iris-gui do. pub fn build(cfg: MachineConfig) -> Result, String> { crate::machine::set_emulator_profile(cfg.machine.profile); + if let Some(line) = host_id_line(&cfg) { + eprintln!("iris: {line}"); + } match cfg.machine.profile { crate::config::MachineProfile::FuelIp34 => Ok(Box::new(crate::ip34_machine::FuelMachine::new(&cfg)?)), crate::config::MachineProfile::TezroIp53 => Ok(Box::new(crate::ip53_machine::TezroMachine::new(&cfg)?)), @@ -53,6 +56,32 @@ pub fn build(cfg: MachineConfig) -> Result, String> { } } +/// The line a machine that takes `[machine] emu_id` prints at start-up: the +/// host id its guest will print, and where it comes from. `None` for other +/// machines, and for a configuration the machine itself will refuse. +fn host_id_line(cfg: &MachineConfig) -> Option { + use crate::identity::hostid_from_text; + match cfg.machine.profile { + crate::config::MachineProfile::TezroIp53 if cfg.validate().is_ok() => { + let serial = cfg.tezro_system_serial(); + Some(format!("host id {:08x} (system serial {serial})", hostid_from_text(&serial))) + } + _ => None, + } +} + +#[cfg(test)] +#[test] +fn a_tezro_says_which_host_id_its_guest_will_print() { + let cfg = |text: &str| -> MachineConfig { toml::from_str(text).unwrap() }; + let tezro = cfg("[machine]\nprofile = \"tezro_ip53\"\nemu_id = 42\n"); + assert_eq!(host_id_line(&tezro).as_deref(), Some("host id d5200420 (system serial LEMU200420)")); + let tezro = cfg("[machine]\nprofile = \"tezro_ip53\"\n"); + assert_eq!(host_id_line(&tezro).as_deref(), Some("host id d5200000 (system serial LEMU200000)")); + assert_eq!(host_id_line(&cfg("[machine]\nprofile = \"tezro_ip53\"\nemu_id = 4096\n")), None); + assert_eq!(host_id_line(&cfg("[machine]\nprofile = \"indy_ip24\"\n")), None); +} + /// An emulated machine as the front ends see it. See the module header. pub trait Workstation: Send { // ---- lifecycle ---- diff --git a/tezro.toml.example b/tezro.toml.example index e3829b156..6a0dadba7 100644 --- a/tezro.toml.example +++ b/tezro.toml.example @@ -22,6 +22,9 @@ profile = "tezro_ip53" cpu = "r16000_700" # Processors fitted: 1, 2 or 4 (the default). Each runs on its own host thread. cpus = 4 +# 0 to 4095: the system serial the L1 answers with, LEMU20, whose fold +# is IRIX's host id (emu_id 42 prints d5200420). 0, the default, is generic. +# emu_id = 42 [graphics] board = "none" From 61b65652e63cc19760b0d36d65946d05e88f9f9d Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 10 Nov 2026 16:33:32 -0700 Subject: [PATCH 33/33] fuel: take the station address from [machine] emu_id Every emulated Fuel prints the same host id, 69123456. The Fuel's L1 answers the system serial request with the station address in hex, the PROM turns that text back into the address, and IRIX prints the address's last four bytes, so the id follows the address and nothing else. Setting [network] mac by hand moves it, but leaves each user to invent an address that will not collide with another emulated machine. Let a Fuel take [machine] emu_id too: a nonzero one gives the station address 08:00:69:e2:, the same address the Tezro takes, so emu_id 7 prints 69e20070. [network] mac still wins, and a Fuel without an emu_id keeps 08:00:69:12:34:56 and its id. iris prints the id and the address at start-up, as it does for the Tezro. --- CHANGELOG.md | 3 +++ FEATURES.md | 2 +- docs/fuel-ip34.md | 9 +++++++-- fuel.toml.example | 4 ++++ src/config.rs | 30 ++++++++++++++++++++++++++---- src/workstation.rs | 16 ++++++++++++++-- 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9a98211..c7baec36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,9 @@ hashes are given where a change is easiest to understand by reading the commit. - **The Fuel takes a VPro board**: `[graphics] board = "v12"` or `"v10"` fits the Octane's VPro model at crossbar widget 13. IRIX 6.5.30 with the VPro driver brings the X login screen up on it. +- `[machine] emu_id` (0 to 4095) gives a Fuel its own host id: the station + address becomes `08:00:69:e2:` and IRIX prints its last four bytes + (`69e20070` for emu_id 7). Unset, the Fuel keeps `08:00:69:12:34:56`. - **jitv2 compiles the reset PROM from the PROM.** The PROM runs from the reset window uncached; once memory covers that window the cached view returns RAM there, and the compile pool used to compile RAM's bytes in place of the diff --git a/FEATURES.md b/FEATURES.md index aebce73fb..728d40a26 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -179,7 +179,7 @@ the first `[section]`, because TOML tables continue until the next header. | `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)), origin200_ip29 (configured by `[origin200]`, see [the Origin 200 guide](docs/origin200-ip29.md)), origin2000_ip27 (configured by `[origin2000]`, see [the Origin 2000 guide](docs/origin2000-ip27.md)), onyx2_ip27 (also configured by `[origin2000]`, see [the Onyx2 guide](docs/onyx2-ip27.md)), fuel_ip34 (configured by `[fuel]`, see [the Fuel guide](docs/fuel-ip34.md)), tezro_ip53 (configured by `[tezro]`, see [the Tezro guide](docs/tezro-ip53.md)). | | `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000; Origin 200, Origin 2000 and Onyx2: r10000 (`r10000_180`), r12000 (`r12000_270`); Fuel: r14000 (`r14000_500`); Tezro: r16000 (a bare `r16000` is `r16000_700`; `r16000_800`, `r16000_900` and `r16000_1000` are the other parts it ships with). The first listed is the default. | | `machine.cpus` | `1`; `4` on the Tezro | Processors fitted. 2 on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). 1, 2 or 4 on the Tezro, each run on its own host thread; one selects the one-processor board with one PIC, two or four the board with two PICs. | -| `machine.emu_id` | `0` | 0 to 4095; only the Tezro takes it, and other machines refuse a nonzero one. Mints the machine's system id (`sysinfo -s`, `lmhostid`) in iris's layout: the L1 system serial `LEMU20`, whose fold is the id, and, when nonzero, the station address `08:00:69:e2:`. 0 is the generic id. See [the Tezro guide](docs/tezro-ip53.md). | +| `machine.emu_id` | `0` | 0 to 4095; only the Fuel and Tezro take it, and other machines refuse a nonzero one. Mints the machine's system id (`sysinfo -s`, `lmhostid`) in iris's layout. When nonzero it gives the station address `08:00:69:e2:`, which is the Fuel's id. The Tezro's L1 system serial is `LEMU20`, whose fold is the Tezro's id. 0 is the generic id. See [the Fuel guide](docs/fuel-ip34.md) and [the Tezro guide](docs/tezro-ip53.md). | ### [origin2000] diff --git a/docs/fuel-ip34.md b/docs/fuel-ip34.md index 3d204fae7..182b01967 100644 --- a/docs/fuel-ip34.md +++ b/docs/fuel-ip34.md @@ -35,8 +35,13 @@ mode` defaults to `nat`; `off` disconnects the link. The L1 reports firmware revision 1.48.1, the one a real Fuel's L1 prints, and the made-up brick serial `EMUN00`; `[fuel] l1_revision` and `brick_serial` replace them. Its system serial is the machine's Ethernet -address (`[network] mac`) as twelve uppercase hexadecimal digits, the -text the PROM converts back into that address. IRIX's `numastatd` reads +address as twelve uppercase hexadecimal digits, the text the PROM +converts back into that address, and IRIX's `sysinfo -s` and `lmhostid` +print the address's last four bytes. The address is `[network] mac`, or +for a nonzero `[machine] emu_id` (0 to 4095) `08:00:69:e2:` and the +emu_id (emu_id 7: `08:00:69:e2:00:70`, host id `69e20070`), or +`08:00:69:12:34:56`; iris prints the id and the address at start-up. +IRIX's `numastatd` reads the revision at startup and logs `SNDRV_GET_ELSC_VERSION: I/O error` without one. The L1 answers the requests the PROM and IRIX send and refuses others as unknown. A power cycle the PROM asks for restarts the diff --git a/fuel.toml.example b/fuel.toml.example index b10e31456..602c17fe3 100644 --- a/fuel.toml.example +++ b/fuel.toml.example @@ -19,6 +19,10 @@ profile = "fuel_ip34" # The Fuel's one processor, the R14000 at 500 MHz with a 2 MiB secondary # cache (`r14000_500`); other values are refused. Unset takes the same. cpu = "r14000" +# 0 to 4095: the station address 08:00:69:e2:, whose last four bytes +# are IRIX's host id (emu_id 7 prints 69e20070). 0, the default, keeps +# 08:00:69:12:34:56. +# emu_id = 7 [graphics] # "v12" or "v10" fits a VPro in the graphics slot; IRIX needs a kernel with diff --git a/src/config.rs b/src/config.rs index 4fa0c0876..355b4e1f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1226,7 +1226,8 @@ pub struct MachineSection { /// Mints the machine's system id (what `sysinfo -s` and `lmhostid` /// print) in iris's own layout: 0 to 4095, 0 the default and a plainly /// generic id. Machines with different ids can hold node-locked licenses - /// side by side. Only the Tezro takes it. See [`crate::identity`]. + /// side by side. Only the Fuel and Tezro take it. See + /// [`crate::identity`]. #[serde(default, skip_serializing_if = "is_zero_u32")] pub emu_id: u32, } @@ -3055,8 +3056,8 @@ impl MachineConfig { if emu_id > crate::identity::EMU_ID_MAX { return Err(format!("machine identity: emu_id must be 0 to {}, not {emu_id}", crate::identity::EMU_ID_MAX)); } - if emu_id != 0 && self.machine.profile != MachineProfile::TezroIp53 { - return Err(format!("machine identity: emu_id = {emu_id} is not taken by this machine; only the Tezro takes it")); + if emu_id != 0 && !matches!(self.machine.profile, MachineProfile::FuelIp34 | MachineProfile::TezroIp53) { + return Err(format!("machine identity: emu_id = {emu_id} is not taken by this machine; only the Fuel and Tezro take it")); } for (id, dev) in &self.scsi { // A CD-ROM may legitimately start with an empty tray (no path, no @@ -3159,7 +3160,7 @@ impl MachineConfig { fn default_station_mac(&self) -> [u8; 6] { use crate::identity::{default_mac, KIND}; match self.machine.profile { - MachineProfile::TezroIp53 if self.machine.emu_id != 0 => { + MachineProfile::FuelIp34 | MachineProfile::TezroIp53 if self.machine.emu_id != 0 => { default_mac(KIND, self.machine.emu_id, 0).expect("node 0 always fits the default address") } _ => DEFAULT_MAC, @@ -4723,6 +4724,27 @@ mod fuel_input_tests { assert!(e.contains("emu_id must be 0 to 4095, not 4096"), "{e}"); } + /// The Fuel's L1 answers the system serial request with the station + /// address in hex and its IRIX takes the id from the address's last four + /// bytes, so emu_id moves the address. Unset, the Fuel keeps the address + /// it always had. + #[test] + fn the_fuel_takes_its_id_from_the_station_address() { + use crate::identity::hostid_from_mac; + let base = "banks = [256, 256]\n"; + let cfg: MachineConfig = toml::from_str(&format!("{base}[machine]\nprofile = \"fuel_ip34\"\n")).unwrap(); + cfg.validate().unwrap(); + assert_eq!(cfg.network().mac, DEFAULT_MAC); + assert_eq!(hostid_from_mac(&cfg.network().mac), 0x6912_3456); + let cfg: MachineConfig = toml::from_str(&format!("{base}[machine]\nprofile = \"fuel_ip34\"\nemu_id = 7\n")).unwrap(); + cfg.validate().unwrap(); + assert_eq!(cfg.network().mac, [0x08, 0x00, 0x69, 0xe2, 0x00, 0x70]); + assert_eq!(hostid_from_mac(&cfg.network().mac), 0x69e2_0070); + let mut cfg = cfg; + cfg.network.mac = Some("08:00:69:aa:bb:cc".into()); + assert_eq!(cfg.network().mac, [0x08, 0x00, 0x69, 0xaa, 0xbb, 0xcc]); + } + /// A machine that does not take emu_id refuses one rather than ignoring /// it, and keeps the station address it had. #[test] diff --git a/src/workstation.rs b/src/workstation.rs index e000bcdbc..5b503e3ce 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -60,25 +60,37 @@ pub fn build(cfg: MachineConfig) -> Result, String> { /// host id its guest will print, and where it comes from. `None` for other /// machines, and for a configuration the machine itself will refuse. fn host_id_line(cfg: &MachineConfig) -> Option { - use crate::identity::hostid_from_text; + use crate::identity::{hostid_from_mac, hostid_from_text}; match cfg.machine.profile { crate::config::MachineProfile::TezroIp53 if cfg.validate().is_ok() => { let serial = cfg.tezro_system_serial(); Some(format!("host id {:08x} (system serial {serial})", hostid_from_text(&serial))) } + crate::config::MachineProfile::FuelIp34 if cfg.validate().is_ok() => { + let m = cfg.network().mac; + Some(format!( + "host id {:08x} (station address {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x})", + hostid_from_mac(&m), + m[0], m[1], m[2], m[3], m[4], m[5] + )) + } _ => None, } } #[cfg(test)] #[test] -fn a_tezro_says_which_host_id_its_guest_will_print() { +fn an_ip35_machine_says_which_host_id_its_guest_will_print() { let cfg = |text: &str| -> MachineConfig { toml::from_str(text).unwrap() }; let tezro = cfg("[machine]\nprofile = \"tezro_ip53\"\nemu_id = 42\n"); assert_eq!(host_id_line(&tezro).as_deref(), Some("host id d5200420 (system serial LEMU200420)")); let tezro = cfg("[machine]\nprofile = \"tezro_ip53\"\n"); assert_eq!(host_id_line(&tezro).as_deref(), Some("host id d5200000 (system serial LEMU200000)")); assert_eq!(host_id_line(&cfg("[machine]\nprofile = \"tezro_ip53\"\nemu_id = 4096\n")), None); + let fuel = cfg("banks = [256, 256]\n[machine]\nprofile = \"fuel_ip34\"\nemu_id = 7\n"); + assert_eq!(host_id_line(&fuel).as_deref(), Some("host id 69e20070 (station address 08:00:69:e2:00:70)")); + let fuel = cfg("banks = [256, 256]\n[machine]\nprofile = \"fuel_ip34\"\n"); + assert_eq!(host_id_line(&fuel).as_deref(), Some("host id 69123456 (station address 08:00:69:12:34:56)")); assert_eq!(host_id_line(&cfg("[machine]\nprofile = \"indy_ip24\"\n")), None); }