From 0af4a493376cffb5ae7398b5ec086f1c78c90d99 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:03:24 +0000 Subject: [PATCH 01/38] pci: let functions choose header write policies PCI functions need different writable and write-one-to-clear header fields. Describe those fields in one construction policy while keeping the current header defaults for existing functions. Forward narrow Bridge configuration writes with their byte enables so write-one-to-clear bits are handled correctly when a function supplies them. Existing Octane functions do not use that status policy, so their behavior stays unchanged. --- CHANGELOG.md | 7 ++ src/dev/bridge.rs | 2 +- src/pci.rs | 269 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 265 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae3724ad..435dc3d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -394,6 +394,13 @@ hashes are given where a change is easiest to understand by reading the commit. - Memory past 4 GB is as fast as the rest: such a machine gets a 16 GB direct window, and the JIT compiles code anywhere in it. - A store-conditional on memory outside the window is now atomic with two CPUs. +### PCI configuration + +- A PCI function can describe its own writable header fields and + write-one-to-clear status bits. Byte and halfword configuration writes + through a Bridge carry their byte enables, so such a function keeps the + status events a narrow write did not select. No Octane function has + write-one-to-clear status bits, so the Octane behaves as before. ### VNC, and screen and pointer commands diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index c96818932..7c890939b 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -1057,7 +1057,7 @@ impl Bridge { let i = (reg >> 2) as usize; let shift = 8 * (reg & 3); let mask = (u64::MAX >> (64 - 8 * size)) << shift; - cfg.write(i, ((cfg.read(i) as u64 & !mask) | ((val << shift) & mask)) as u32); + cfg.write_masked(i, (val << shift) as u32, mask as u32); true } diff --git a/src/pci.rs b/src/pci.rs index f81627099..679e23fe8 100644 --- a/src/pci.rs +++ b/src/pci.rs @@ -46,6 +46,50 @@ pub enum Space { Mem, } +/// Initial value and writable bits for a function-specific configuration dword. +#[derive(Clone, Copy, Debug)] +pub struct PciRegisterPolicy { + /// Dword index. Header, BAR, subsystem identity, and interrupt dwords are managed separately. + pub reg_dword: usize, + /// Value restored by [`PciConfig::reset`]. + pub initial: u32, + /// Bits that configuration writes may change. + pub writable_mask: u32, +} + +/// Function-specific write behavior for a type 0 PCI configuration header. +/// +/// Register policies are copied into the header during construction; the +/// borrowed slice is not retained. +#[derive(Clone, Copy, Debug)] +pub struct PciConfigPolicy<'a> { + /// Fixed Status bits that read as one. + pub status: u16, + /// Device-latched Status bits that writes acknowledge with one. + pub status_w1c_mask: u16, + /// Writable Command bits. + pub command_mask: u16, + /// Writable bits in the latency timer byte. + pub latency_mask: u8, + /// Writable bits in the cache-line-size byte. + pub cache_line_mask: u8, + /// Initial values and write masks for function-specific dwords. + pub registers: &'a [PciRegisterPolicy], +} + +impl<'a> Default for PciConfigPolicy<'a> { + fn default() -> Self { + Self { + status: 0x0200, + status_w1c_mask: 0, + command_mask: 0xffff, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[], + } + } +} + /// One function's type 0 configuration header. /// /// Enough of it to be discovered and configured: identity, class, command @@ -61,6 +105,12 @@ pub struct PciConfig { /// Dwords `16..caps_end` hold a capability list, which firmware cannot /// overwrite. 0 when the function has none. caps_end: usize, + status: u16, + status_w1c_mask: u16, + command_mask: u16, + latency_mask: u8, + cache_line_mask: u8, + writable_masks: [u32; 64], } impl PciConfig { @@ -68,6 +118,27 @@ impl PciConfig { /// three bytes of register 0x08. `pin` is 0 for none, or [`PCI_INTA`] /// and on; the line register is left for firmware to assign. pub fn new(vendor: u16, device: u16, class: u32, revision: u8, bars: [Bar; 6], pin: u8) -> Self { + Self::new_with_policy( + vendor, + device, + class, + revision, + bars, + pin, + PciConfigPolicy::default(), + ) + } + + /// Construct a type 0 header with function-specific register behavior. + pub fn new_with_policy( + vendor: u16, + device: u16, + class: u32, + revision: u8, + bars: [Bar; 6], + pin: u8, + policy: PciConfigPolicy<'_>, + ) -> Self { let mut r = [0u32; 64]; r[0] = ((device as u32) << 16) | vendor as u32; r[2] = (class << 8) | revision as u32; @@ -77,7 +148,26 @@ impl PciConfig { r[4 + i] = 1; } } - Self { regs: Mutex::new(r), bars, power_on: r, caps_end: 0 } + let mut writable_masks = [u32::MAX; 64]; + for register in policy.registers { + assert!( + matches!(register.reg_dword, 10 | 12..=14 | 16..=63), + "pci: register policy requires a function-specific dword in 0..64"); + r[register.reg_dword] = register.initial; + writable_masks[register.reg_dword] = register.writable_mask; + } + Self { + regs: Mutex::new(r), + bars, + power_on: r, + status: policy.status, + status_w1c_mask: policy.status_w1c_mask, + command_mask: policy.command_mask, + latency_mask: policy.latency_mask, + cache_line_mask: policy.cache_line_mask, + writable_masks, + caps_end: 0, + } } /// Give the function a capability list: `caps` are the raw dwords of the @@ -113,24 +203,40 @@ impl PciConfig { pub fn read(&self, reg: usize) -> u32 { let r = self.regs.lock().unwrap(); match reg { - // Status: a medium-speed device with no errors to report. // A function with a capability list also sets status bit 4. - 1 => (r[1] & 0xffff) | 0x0200_0000 | if r[13] != 0 { 0x0010_0000 } else { 0 }, + 1 => (r[1] & (0xffff | ((self.status_w1c_mask as u32) << 16))) + | ((self.status as u32) << 16) + | if r[13] != 0 { 0x0010_0000 } else { 0 }, _ => r[reg], } } pub fn write(&self, reg: usize, val: u32) { + self.write_masked(reg, val, u32::MAX); + } + + /// Write selected bits of dword `reg`. Both `val` and `mask` use the + /// register's bit positions. Unselected Status events are not acknowledged. + pub fn write_masked(&self, reg: usize, val: u32, mask: u32) { + if mask == 0 { + return; + } let mut r = self.regs.lock().unwrap(); match reg { - // Identity and class are read-only. + // Identity, class and header type are read-only. 0 | 2 => {} - // Cache line size and latency timer are stored; the header type - // and BIST bytes above them are not. - 3 => r[3] = (r[3] & 0xffff_0000) | (val & 0xffff), - // Command; status bits are write-one-to-clear and none are set. - 1 => r[1] = val & 0xffff, + 3 => { + let mask = mask & (((self.latency_mask as u32) << 8) | self.cache_line_mask as u32); + r[3] = (r[3] & !mask) | (val & mask); + } + // Command writes replace selected bits; Status events clear on ones. + 1 => { + let clear = val & mask & ((self.status_w1c_mask as u32) << 16); + r[1] = (r[1] & !(mask & 0xffff) & !clear) + | (val & mask & self.command_mask as u32); + } 4..=9 => { + let val = (r[reg] & !mask) | (val & mask); let bar = self.bars[reg - 4]; r[reg] = match bar { Bar::Unused => 0, @@ -144,11 +250,27 @@ impl PciConfig { // The capability list is read-only. 16.. if reg < self.caps_end => {} // Interrupt pin is read-only; the line is firmware's scratch. - 15 => r[15] = (r[15] & 0xff00) | (val & !0xff00), - _ => r[reg] = val, + 15 => { + let mask = mask & !0xff00; + r[15] = (r[15] & !mask) | (val & mask); + } + _ => { + let mask = mask & self.writable_masks[reg]; + r[reg] = (r[reg] & !mask) | (val & mask); + } } } + /// Latch selected device Status events without replacing earlier events. + pub fn set_status_bits(&self, bits: u16) { + self.regs.lock().unwrap()[1] |= ((bits & self.status_w1c_mask) as u32) << 16; + } + + /// Set the board-assigned Subsystem ID, which survives function reset. + pub fn set_subsystem_identity(&self, value: u32) { + self.regs.lock().unwrap()[11] = value; + } + /// The base address firmware programmed into BAR `i`, type bits masked. pub fn bar_base(&self, i: usize) -> u32 { let v = self.regs.lock().unwrap()[4 + i]; @@ -436,6 +558,14 @@ mod tests { [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA) } + fn scsi_like_with_policy(policy: PciConfigPolicy<'_>) -> PciConfig { + PciConfig::new_with_policy( + 0x9004, 0x8078, 0x0100_00, 1, + [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA, + policy, + ) + } + /// Firmware learns how much space a device wants by writing all ones to /// a BAR and reading back which address bits stuck. #[test] @@ -502,7 +632,7 @@ mod tests { #[test] fn reset_keeps_the_subsystem_identity() { let c = scsi_like(); - c.write(11, 0xc304_10a9); + c.set_subsystem_identity(0xc304_10a9); c.write(4, 0x0000_1000); c.reset(); assert_eq!(c.read(11), 0xc304_10a9); @@ -529,6 +659,121 @@ mod tests { assert_eq!(c.interrupt_pin(), PCI_INTA); } + #[test] + fn header_policies_mask_writes_and_restore_reset_values() { + for (status, command, latency, cache) in [(0x0200, 0xffff, 0, 0), (0x02b0, 0x0156, 0xf0, 0x0f)] { + let c = scsi_like_with_policy(PciConfigPolicy { + status, + status_w1c_mask: 0, + command_mask: command, + latency_mask: latency, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0020, + writable_mask: 0xff00, + }], + }); + c.write(1, u32::MAX); + c.write(3, u32::MAX); + c.write(18, u32::MAX); + assert_eq!(c.read(1), (u32::from(status) << 16) | u32::from(command)); + assert_eq!(c.read(3), (u32::from(latency) << 8) | u32::from(cache)); + assert_eq!(c.read(18), 0xaabb_ff20); + c.reset(); + assert_eq!(c.read(1), u32::from(status) << 16); + assert_eq!(c.read(3), 0); + assert_eq!(c.read(18), 0xaabb_0020); + } + } + + #[test] + fn masked_writes_preserve_unselected_fields_and_status_events() { + let c = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: 0x8040, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0000, + writable_mask: 0xffff, + }], + }); + c.set_status_bits(0x8040); + c.write(1, 0x0157); + c.write(3, 0x1234); + c.write(5, 0x1234_5000); + c.write(4, 0x1201); + for (reg, value, mask, expected) in [ + (1, 0xffff_0002, 0xff, 0x8240_0102), + (1, 0xffff_0000, 0x8000_0000, 0x0240_0102), + (1, 0x0040_0000, 0x00ff_0000, 0x0200_0102), + (1, u32::MAX, 0, 0x0200_0102), + (3, 0xffff_00ab, 0xff, 0x12ab), + (5, 0xabcd_ffff, 0xffff_0000, 0xabcd_5000), + (4, 0x3400, 0xff00, 0x3401), + (15, u32::MAX, 0xff00, 0x0100), + (18, u32::MAX, 0xff, 0xaabb_00ff), + ] { + c.write_masked(reg, value, mask); + assert_eq!(c.read(reg), expected, "register {reg}"); + } + c.set_status_bits(u16::MAX); + c.reset(); + assert_eq!(c.read(1), 0x0200_0000); + let default = scsi_like(); + default.set_status_bits(u16::MAX); + assert_eq!(default.read(1), 0x0200_0000); + } + + #[test] + fn snapshots_restore_registers_with_destination_write_policies() { + use crate::traits::Saveable; + let src = scsi_like_with_policy(PciConfigPolicy { + status: 0x02b0, + status_w1c_mask: 0xc000, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0, + writable_mask: 0x100, + }], + }); + src.set_status_bits(0x8000); + src.write(3, 0x1234); + src.write(18, 0x100); + let saved = src.save_state(); + for (events, command, cache) in [(0, 7, 0x0f), (0xc000, 0x0157, 0xff)] { + let dst = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: events, + command_mask: command, + latency_mask: 0, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0x20, + writable_mask: 0x200, + }], + }); + dst.load_state(&saved).unwrap(); + assert_eq!(dst.save_state(), saved); + assert_eq!(dst.read(1), 0x0200_0000 | ((0x8000 & u32::from(events)) << 16)); + dst.write_masked(1, u32::MAX, u32::MAX); + dst.write(3, u32::MAX); + dst.write(18, u32::MAX); + assert_eq!(dst.read(1), 0x0200_0000 | u32::from(command)); + assert_eq!(dst.read(3), (0x1234 & !u32::from(cache)) | u32::from(cache)); + assert_eq!(dst.read(18), 0x300); + dst.reset(); + assert_eq!(dst.read(18), 0x20); + } + } + struct Nothing; impl BusDevice for Nothing {} From f51642e78934c3454094246e356725cf272aa20b Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 02/38] dp83843: add the Fuel Ethernet PHY Fuel fits a DP83843 at MII management address 1, where the Octane has an ICS1890. The IOC3 Ethernet controller needs the same packet and link machinery with either PHY, so keep that machinery shared: move the Octane's ICS1890 register model behind a PHY interface, and supply the DP83843's register and negotiation behavior through it. The MAC keeps its existing packet path. The interface carries the snapshot hooks too: the ICS1890 keeps the register save and load it had in the IOC3 model, and the DP83843 saves its registers and negotiation timers the same way, so the shared Ethernet block saves whichever PHY it holds. --- src/dev/dp83843.rs | 573 ++++++++++++++++++++++++++++++++++++++ src/dev/ics1890.rs | 423 ++++++++++++++++++++++++++++ src/dev/ioc3_eth.rs | 473 ++++--------------------------- src/dev/ioc3_eth/tests.rs | 36 +++ src/dev/mod.rs | 2 + 5 files changed, 1092 insertions(+), 415 deletions(-) create mode 100644 src/dev/dp83843.rs create mode 100644 src/dev/ics1890.rs diff --git a/src/dev/dp83843.rs b/src/dev/dp83843.rs new file mode 100644 index 000000000..8d3bb40c5 --- /dev/null +++ b/src/dev/dp83843.rs @@ -0,0 +1,573 @@ +//! The DP83843 MII model exposes the control, status and auto-negotiation +//! registers used by the Fuel IOC3 Ethernet port. A guest starts negotiation +//! through MII; the backend link level determines the simulated partner result. +//! +//! # What is approximated here +//! +//! Link settling and partner responses are software models. BMSR reports the +//! current link level rather than the hardware's latched-low status. +//! +//! Ported from hw/net/dp83840.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +use crate::snapshot::{field, field_bool, field_u32, field_u64, field_u8, toml_u16, StateTable}; +pub mod reg { + pub const BASIC_CONTROL: u8 = 0x0; + pub const BASIC_STATUS: u8 = 0x1; + pub const PHY_ID1: u8 = 0x2; + pub const PHY_ID2: u8 = 0x3; + pub const AUTO_NEG_ADVERTISE: u8 = 0x4; + pub const AUTO_NEG_PARTNER: u8 = 0x5; + pub const AUTO_NEG_EXPANSION: u8 = 0x6; + pub const NEXT_PAGE_TRANSMIT: u8 = 0x7; + pub const PHY_STATUS: u8 = 0x10; + pub const INTERRUPT_CONTROL: u8 = 0x11; + pub const INTERRUPT_STATUS: u8 = 0x12; + pub const DISCONNECT_COUNTER: u8 = 0x13; + pub const FALSE_CARRIER_COUNTER: u8 = 0x14; + pub const RECEIVE_ERROR_COUNTER: u8 = 0x15; + pub const PCS_CONFIG: u8 = 0x16; + pub const LOOPBACK: u8 = 0x17; + pub const TEN_BASE_T_STATUS: u8 = 0x18; + pub const PHY_CONTROL: u8 = 0x19; +} +const SETTLE: Duration = Duration::from_secs(2); +const BREAK: Duration = Duration::from_millis(1500); + +/// Management state for one transceiver. No board GPIO reset is connected. +pub struct Dp83843 { + configured_addr: u8, + regs: [u16; 32], + backend_up: bool, + done: bool, + forced: bool, + resolved: u8, + armed: bool, + deadline: Option, + complete: Option, + pending: bool, + restarting: bool, + pub unimplemented_accesses: u32, +} +impl Dp83843 { + pub fn new(addr: u8) -> Self { + let mut p = Self { + configured_addr: addr & 31, + regs: [0; 32], + backend_up: true, + done: false, + forced: false, + resolved: 0, + armed: false, + deadline: None, + complete: None, + pending: false, + restarting: false, + unimplemented_accesses: 0, + }; + p.reset(); + p + } + pub fn reset(&mut self) { + self.regs = [0; 32]; + self.regs[reg::BASIC_CONTROL as usize] = 0x3100 | if self.configured_addr == 0 { 0x400 } else { 0 }; + self.regs[reg::AUTO_NEG_ADVERTISE as usize] = 0x1e1; + self.regs[reg::NEXT_PAGE_TRANSMIT as usize] = 0x2001; + self.regs[reg::PCS_CONFIG as usize] = 0x20; + self.regs[reg::PHY_CONTROL as usize] = u16::from(self.configured_addr); + self.done = false; + self.forced = false; + self.resolved = 0; + self.armed = false; + self.deadline = None; + self.complete = None; + self.pending = false; + self.restarting = false; + } + pub fn address(&self) -> u8 { + (self.regs[reg::PHY_CONTROL as usize] & 31) as u8 + } + pub fn backend_up(&self) -> bool { + self.backend_up + } + pub fn deadline(&self) -> Option { + self.deadline + } + fn autoneg(&self) -> bool { + self.regs[reg::BASIC_CONTROL as usize] & 0x1000 != 0 + } + fn supported(&self) -> bool { + self.regs[reg::PHY_CONTROL as usize] & 0x220 == 0 + && self.regs[reg::TEN_BASE_T_STATUS as usize] & 0x1a20 == 0 + && self.regs[reg::LOOPBACK as usize] & 0x3f80 == 0 + } + fn link(&self) -> bool { + self.backend_up + && self.supported() + && self.regs[reg::BASIC_CONTROL as usize] & 0x800 == 0 + && if self.autoneg() { + self.done && self.resolved != 0 + } else { + self.forced + } + } + pub fn data_path(&self) -> bool { + self.supported() && self.regs[reg::BASIC_CONTROL as usize] & 0xc00 == 0 + } + pub fn wire_enabled(&self) -> bool { + self.data_path() && self.link() + } + fn loopback_selected(&self) -> bool { + self.regs[reg::BASIC_CONTROL as usize] & 0x4000 != 0 || self.regs[reg::LOOPBACK as usize] & 0x30 != 0 + } + pub fn in_loopback(&self) -> bool { + self.data_path() && self.loopback_selected() + } + fn status(&self) -> u16 { + let mut v = if self.autoneg() { 0x200 } else { 0 }; + if self.loopback_selected() { + v |= 8; + } + if !self.link() { + return v; + } + v |= 1; + let (speed, full) = if self.autoneg() { + v |= 0x20; + (self.resolved <= 2, self.resolved == 1 || self.resolved == 3) + } else { + (self.regs[reg::BASIC_CONTROL as usize] & 0x2000 != 0, self.regs[reg::BASIC_CONTROL as usize] & 0x100 != 0) + }; + if !speed { + v |= 2; + } + if full { + v |= 4; + } + if self.regs[reg::BASIC_CONTROL as usize] & 0x400 == 0 { + v |= 0x800; + } + v + } + fn event(&mut self, before: u16) { + let after = self.status(); + if self.regs[reg::INTERRUPT_CONTROL as usize] & 2 != 0 && ((before ^ after) & 1 != 0 || !before & after & 0x20 != 0) { + self.pending = true; + } + } + fn resolve(&mut self) { + self.resolved = 0; + if self.autoneg() && self.supported() { + for (i, bit) in [0x100, 0x80, 0x40, 0x20].iter().enumerate() { + if self.regs[reg::AUTO_NEG_ADVERTISE as usize] & bit != 0 { + self.resolved = i as u8 + 1; + break; + } + } + } + if self.resolved == 0 { + crate::dlog!(crate::devlog::LogModule::Ioc, "dp83843: no common supported advertised mode"); + } + } + pub fn sync(&mut self, now: Instant) { + if self.deadline.is_some_and(|d| now >= d) { + let before = self.status(); + if let Some(end) = self.complete.take() { + self.regs[reg::BASIC_CONTROL as usize] &= !0x200; + self.deadline = Some(end); + if now < end { + return; + } + } + self.deadline = None; + self.restarting = false; + self.done = true; + self.resolve(); + self.event(before); + } + } + pub fn set_backend_link(&mut self, up: bool, now: Instant) { + self.sync(now); + let before = self.status(); + self.backend_up = up; + self.event(before); + } + fn unimplemented(&mut self, r: u8) { + self.unimplemented_accesses += 1; + crate::dlog!(crate::devlog::LogModule::Ioc, "dp83843: unsupported register or control at {r:#x}"); + } + pub fn read(&mut self, r: u8, now: Instant) -> u16 { + self.sync(now); + let v = match r { + reg::BASIC_CONTROL => self.regs[reg::BASIC_CONTROL as usize], + reg::BASIC_STATUS => { + if !self.armed && !self.done && self.autoneg() { + self.armed = true; + self.deadline = Some(now + SETTLE); + } + let status = self.status(); + 0x7849 | if status & 1 != 0 { 4 } else { 0 } | (status & 0x20) + } + reg::PHY_ID1 => 0x2000, + reg::PHY_ID2 => 0x5c10, + reg::AUTO_NEG_ADVERTISE | reg::NEXT_PAGE_TRANSMIT => self.regs[r as usize], + reg::AUTO_NEG_PARTNER => { + if self.done { + 0x1e1 + } else { + 0 + } + } + reg::AUTO_NEG_EXPANSION => 4 | u16::from(self.done), + reg::PHY_STATUS => { + let v = self.status() | if self.pending { 0x100 } else { 0 }; + self.pending = false; + v + } + reg::INTERRUPT_CONTROL => self.regs[reg::INTERRUPT_CONTROL as usize], + reg::INTERRUPT_STATUS => { + let v = if self.pending { 0x8000 } else { 0 }; + self.pending = false; + v + } + reg::DISCONNECT_COUNTER..=reg::RECEIVE_ERROR_COUNTER => { + let v = self.regs[r as usize]; + self.regs[r as usize] = 0; + v + } + reg::PCS_CONFIG => { + let v = self.regs[reg::PCS_CONFIG as usize]; + if self.autoneg() { + v & !0x4000 + } else if self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + v | 0x4000 + } else { + v + } + } + reg::LOOPBACK => { + self.regs[reg::LOOPBACK as usize] + | if self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + 0x1000 + } else { + 0 + } + } + reg::TEN_BASE_T_STATUS => { + self.regs[reg::TEN_BASE_T_STATUS as usize] + | if self.status() & 4 != 0 || self.regs[reg::PHY_CONTROL as usize] & 0x200 != 0 { + 0x80 + } else { + 0 + } + } + reg::PHY_CONTROL => self.regs[reg::PHY_CONTROL as usize], + _ => { + self.unimplemented(r); + 0 + } + }; + if self.regs[reg::INTERRUPT_CONTROL as usize] & 1 != 0 { + self.pending = true; + } + v + } + pub fn write(&mut self, r: u8, val: u16, now: Instant) { + self.sync(now); + let before = self.status(); + match r { + reg::BASIC_CONTROL => { + let val = val & 0xff80; + if val & 0x8000 != 0 { + self.reset(); + return; + } + if val & 0x80 != 0 { + self.unimplemented(r); + } + self.regs[reg::LOOPBACK as usize] &= !0x380; + if val & 0x4000 != 0 { + self.regs[reg::LOOPBACK as usize] |= 0x30; + } + if val & 0x400 == 0 && val & 0x1200 == 0x1200 { + self.done = false; + self.resolved = 0; + self.forced = false; + self.armed = true; + self.restarting = true; + self.deadline = Some(now + BREAK); + self.complete = Some(now + BREAK + SETTLE); + self.regs[reg::BASIC_CONTROL as usize] = val; + } else if val & 0x400 == 0 && (self.restarting || self.forced && !self.autoneg()) { + if val & 0x1000 == 0 { + self.restarting = false; + self.complete = None; + self.deadline = None; + self.forced = val & 0x800 == 0; + } + self.regs[reg::BASIC_CONTROL as usize] = val & !0x200; + } else { + self.restarting = false; + self.complete = None; + self.deadline = None; + self.forced = false; + self.done = val & 0x400 == 0; + self.armed = self.done; + self.regs[reg::BASIC_CONTROL as usize] = val & !0x8200; + } + if !self.autoneg() { + self.resolved = 0; + self.done = false; + self.forced = self.supported() && self.regs[reg::BASIC_CONTROL as usize] & 0xc00 == 0; + } else if self.done && self.resolved == 0 { + self.resolve(); + } + } + reg::BASIC_STATUS..=reg::PHY_ID2 | reg::AUTO_NEG_PARTNER | reg::AUTO_NEG_EXPANSION | reg::PHY_STATUS | reg::INTERRUPT_STATUS => {} + reg::AUTO_NEG_ADVERTISE => self.regs[reg::AUTO_NEG_ADVERTISE as usize] = val & 0xa5ff, + reg::NEXT_PAGE_TRANSMIT => self.regs[reg::NEXT_PAGE_TRANSMIT as usize] = (val & 0xb7ff) | (self.regs[reg::NEXT_PAGE_TRANSMIT as usize] & 0x800), + reg::INTERRUPT_CONTROL => self.regs[reg::INTERRUPT_CONTROL as usize] = val & 3, + reg::DISCONNECT_COUNTER..=reg::RECEIVE_ERROR_COUNTER => self.regs[r as usize] = val, + reg::PCS_CONFIG => { + if val & !0x20 != 0 { + self.unimplemented(r); + } + let mask = if self.autoneg() || self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + 0xbfef + } else { + 0xffef + }; + self.regs[reg::PCS_CONFIG as usize] = (self.regs[reg::PCS_CONFIG as usize] & !mask) | (val & mask); + } + reg::LOOPBACK => { + if val & 0x4000 != 0 { + self.unimplemented(r); + } + self.regs[reg::LOOPBACK as usize] = val & 0x7fb0; + } + reg::TEN_BASE_T_STATUS => { + if val & 0x55c != 0 { + self.unimplemented(r); + } + self.regs[reg::TEN_BASE_T_STATUS as usize] = val & 0x1ffc; + } + reg::PHY_CONTROL => { + if val & !31 != 0 { + self.unimplemented(r); + } + self.regs[reg::PHY_CONTROL as usize] = val & 0x3bff; + } + _ => self.unimplemented(r), + } + if !self.supported() { + self.resolved = 0; + self.forced = false; + self.unimplemented(r); + } + self.event(before); + if self.regs[reg::INTERRUPT_CONTROL as usize] & 1 != 0 { + self.pending = true; + } + } +} +// The registers and negotiation progress for a snapshot. The two instants +// are saved as the time they had left. +impl Dp83843 { + fn instant_left(at: Option, now: Instant) -> u64 { + // Nanoseconds left plus one; zero for none. + at.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1) + } + + fn instant_from(left: u64, now: Instant) -> Option { + (left != 0).then(|| now + Duration::from_nanos(left - 1)) + } + + pub(crate) fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u8("configured_addr", self.configured_addr) + .u16s("regs", &self.regs) + .bool("backend_up", self.backend_up) + .bool("done", self.done) + .bool("forced", self.forced) + .u8("resolved", self.resolved) + .bool("armed", self.armed) + .u64("deadline", Self::instant_left(self.deadline, now)) + .u64("complete", Self::instant_left(self.complete, now)) + .bool("pending", self.pending) + .bool("restarting", self.restarting) + .u32("unimplemented_accesses", self.unimplemented_accesses) + .build() + } + + pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { + let items = field(v, "regs")?.as_array().ok_or("regs: not a list")?; + if items.len() != 32 { + return Err(format!("regs: {} entries, expected 32", items.len())); + } + let mut regs = [0u16; 32]; + for (r, item) in regs.iter_mut().zip(items) { + *r = toml_u16(item).ok_or("regs: an entry is out of range")?; + } + *self = Self { + configured_addr: field_u8(v, "configured_addr")?, + regs, + backend_up: field_bool(v, "backend_up")?, + done: field_bool(v, "done")?, + forced: field_bool(v, "forced")?, + resolved: field_u8(v, "resolved")?, + armed: field_bool(v, "armed")?, + deadline: Self::instant_from(field_u64(v, "deadline")?, now), + complete: Self::instant_from(field_u64(v, "complete")?, now), + pending: field_bool(v, "pending")?, + restarting: field_bool(v, "restarting")?, + unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, + }; + Ok(()) + } +} + +impl crate::dev::ioc3_eth::MiiPhy for Dp83843 { + fn address(&self) -> u8 { Dp83843::address(self) } + fn deadline(&self) -> Option { Dp83843::deadline(self) } + fn wire_enabled(&self) -> bool { Dp83843::wire_enabled(self) } + fn in_loopback(&self) -> bool { Dp83843::in_loopback(self) } + fn sync(&mut self, now: Instant) { Dp83843::sync(self, now) } + fn read(&mut self, register: u8, now: Instant) -> u16 { Dp83843::read(self, register, now) } + fn write(&mut self, register: u8, value: u16, now: Instant) { Dp83843::write(self, register, value, now) } + fn set_backend_link(&mut self, up: bool, now: Instant) { Dp83843::set_backend_link(self, up, now) } + fn power_on(&mut self) { Dp83843::reset(self) } + fn save_state(&self, now: Instant) -> toml::Value { Dp83843::save_state(self, now) } + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { Dp83843::load_state(self, v, now) } +} + +#[cfg(test)] +mod tests { + // Cases ported from tests/qtest/sgi-ioc3-eth-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + + /// The guest observes link and negotiation changes through MII status registers. + #[test] + fn the_phy_restarts_negotiation_and_resets_the_link() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + assert_eq!(p.read(reg::BASIC_STATUS, now) & 0x24, 0); + assert!(!p.wire_enabled()); + assert_eq!(p.read(reg::BASIC_STATUS, now + Duration::from_secs(2)) & 0x24, 0x24); + p.write(reg::BASIC_CONTROL, 0x3300, now + Duration::from_secs(3)); + for (millis, restart, link) in [(4499, true, false), (4500, false, false), (6500, false, true)] { + let at = now + Duration::from_millis(millis); + assert_eq!(p.read(reg::BASIC_CONTROL, at) & 0x200 != 0, restart); + assert_eq!(p.read(reg::BASIC_STATUS, at) & 4 != 0, link); + assert_eq!(p.wire_enabled(), link); + } + let at = now + Duration::from_secs(7); + p.set_backend_link(false, at); + p.write(reg::PHY_CONTROL, 7, at); + p.write(reg::BASIC_CONTROL, 0x8000, at); + assert_eq!(p.address(), 1); + assert_eq!(p.read(reg::BASIC_STATUS, at) & 4, 0); + assert_eq!(p.read(reg::BASIC_STATUS, at + Duration::from_secs(2)) & 4, 0); + p.set_backend_link(true, at + Duration::from_secs(2)); + assert!(p.wire_enabled()); + } + + /// A PROM probing the PHY reads the next-page register's reset value and + /// writes it back; the read-only toggle bit must survive every write. + #[test] + fn the_next_page_register_keeps_its_toggle_bit_and_resets_to_0x2001() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0x2001); + for bit in 0..16 { + let value = 1u16 << bit; + p.write(reg::NEXT_PAGE_TRANSMIT, value, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), value & 0xb7ff, "bit {bit}"); + } + p.write(reg::NEXT_PAGE_TRANSMIT, 0xffff, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0xb7ff); + p.write(reg::BASIC_CONTROL, 0x8000, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0x2001); + } + + /// The PROM's PHY probe expects zero loopback and masked 10BASE-T status + /// registers, plus the standard BMSR and ANER capability bits, both at + /// power-on and after a BMCR reset. + #[test] + fn the_phy_reads_the_probe_values_the_prom_expects_before_and_after_reset() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + for round in 0..2 { + assert_eq!(p.read(reg::BASIC_STATUS, now) & 0x7849, 0x7849, "round {round}"); + assert_eq!(p.read(reg::AUTO_NEG_EXPANSION, now) & 4, 4); + assert_eq!(p.read(reg::LOOPBACK, now), 0); + assert_eq!(p.read(reg::TEN_BASE_T_STATUS, now) & 0xc77f, 0); + p.write(reg::BASIC_CONTROL, 0x2100, now); + p.write(reg::LOOPBACK, 0x20, now); + p.write(reg::BASIC_CONTROL, 0x8000, now); + } + assert_eq!(p.unimplemented_accesses, 0); + } + + /// Setting BMCR loopback makes the PHY report loopback in the LBR bits the + /// MAC consults, and leaving it must drop the stale bypass bits. + #[test] + fn bmcr_loopback_selects_the_loopback_register_and_clears_the_bypass_bits() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + p.write(reg::LOOPBACK, 0x0180, now); + p.write(reg::BASIC_CONTROL, 0x7300, now); + assert_eq!(p.read(reg::LOOPBACK, now) & 0x03b0, 0x0030); + assert!(p.in_loopback()); + let lbr = p.read(reg::LOOPBACK, now); + p.write(reg::LOOPBACK, lbr | 0x0180, now); + p.write(reg::BASIC_CONTROL, 0x3100, now); + assert_eq!(p.read(reg::LOOPBACK, now) & 0x0380, 0); + p.write(reg::BASIC_CONTROL, 0x8000, now); + assert_eq!(p.read(reg::LOOPBACK, now), 0); + assert!(!p.in_loopback()); + } + + /// Drivers poll the error counters and expect each read to return the count + /// since the previous read, not a running total. + #[test] + fn the_error_counters_clear_when_read() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + for r in [reg::DISCONNECT_COUNTER, reg::FALSE_CARRIER_COUNTER, reg::RECEIVE_ERROR_COUNTER] { + p.write(r, 0xbeef, now); + assert_eq!(p.read(r, now), 0xbeef); + assert_eq!(p.read(r, now), 0); + } + } + + /// A guest that restarts negotiation twice in a row must see the break + /// interval from the second write, not a shortened one from the first. + #[test] + fn a_second_restart_request_rearms_the_break_interval() { + let now = Instant::now(); + let ms = |m: u64| now + Duration::from_millis(m); + let mut p = Dp83843::new(1); + p.write(reg::BASIC_CONTROL, 0x3200, ms(0)); + p.write(reg::BASIC_CONTROL, 0x3200, ms(750)); + assert_ne!(p.read(reg::BASIC_CONTROL, ms(2249)) & 0x200, 0); + assert_eq!(p.read(reg::BASIC_CONTROL, ms(2250)) & 0x200, 0); + assert_eq!(p.read(reg::BASIC_STATUS, ms(4249)) & 0x20, 0); + assert_eq!(p.read(reg::BASIC_STATUS, ms(4250)) & 0x20, 0x20); + } + + /// A snapshot keeps the registers, the negotiation in progress and its + /// time left. + #[test] + fn a_snapshot_round_trips_a_negotiation_in_progress() { + let now = Instant::now(); + let mut src = Dp83843::new(1); + src.write(reg::BASIC_CONTROL, 0x3300, now); + let v = src.save_state(now); + let mut dst = Dp83843::new(1); + dst.load_state(&v, now).unwrap(); + assert_eq!(v, dst.save_state(now)); + assert_eq!(src.deadline(), dst.deadline()); + assert!(dst.load_state(&toml::Value::Table(Default::default()), now).is_err()); + } +} diff --git a/src/dev/ics1890.rs b/src/dev/ics1890.rs new file mode 100644 index 000000000..04cc6706a --- /dev/null +++ b/src/dev/ics1890.rs @@ -0,0 +1,423 @@ +//! The ICS1890 transceiver fitted to Octane's IOC3. +//! +//! This module is shared by the Octane IOC3 Ethernet model. It was moved here +//! from `ioc3_eth.rs`; its management-register behavior follows the existing +//! Octane behavior and Integrated Circuit Systems, "ICS1890 10Base-T/100Base-TX +//! Integrated PHYceiver", document ICS1890RevG, 21 October 1997. +//! +//! Ported from hw/net/ics1890.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +use crate::dev::ioc3_eth::MiiPhy; +use crate::snapshot::{field_bool, field_u16, field_u32, field_u64, field_u8, StateTable}; + +/// Ordinary and command-override writable masks, by register. +fn phy_masks(r: usize) -> (u16, u16) { + match r { + 0 => (0xff80, 0), + 1 => (0, 0x7f80), + 2 | 3 => (0, 0xffff), + 4 => (0x21e0, 0x1c1f), + 6 => (0, 0xffe0), + 0x10 => (0x802d, 0), + 0x12 => (0x002f, 0), + 0x13 => (0x0011, 0), + _ => (0, 0), + } +} + +/// The ICS1890 as the Octane carries it: address 1, node mode, software +/// priority, revision 3. +pub struct Ics1890 { + addr: u8, + bmcr: u16, + bmsr_caps: u16, + id1: u16, + id2: u16, + anar: u16, + partner: u16, + exp: u16, + ext_ctl: u16, + tenbt: u16, + ext2: u16, + override_armed: bool, + page_received: bool, + link_low_latch: bool, + last_link: bool, + backend_up: bool, + nego_done: bool, + deadline: Option, + delay: Duration, + /// Accesses to registers the part does not implement. + pub unimplemented_accesses: u32, +} + +impl Ics1890 { + pub fn new(addr: u8, delay: Duration) -> Self { + let mut p = Self { + addr, + bmcr: 0, + bmsr_caps: 0, + id1: 0, + id2: 0, + anar: 0, + partner: 0, + exp: 0, + ext_ctl: 0, + tenbt: 0, + ext2: 0, + override_armed: false, + page_received: false, + link_low_latch: false, + last_link: false, + backend_up: true, + nego_done: false, + deadline: None, + delay, + unimplemented_accesses: 0, + }; + p.reset(Instant::now()); + p + } + + pub fn address(&self) -> u8 { + self.addr + } + + /// Hardware and software reset: defaults, no override, no latches, and a + /// fresh negotiation if enabled. + pub fn reset(&mut self, now: Instant) { + self.bmcr = 0x3000; + self.bmsr_caps = 0x7800; + self.id1 = 0x0015; + self.id2 = 0xf423; + self.anar = 0x01e1; + self.partner = 0; + self.exp = 0; + self.ext_ctl = 0x0008; + self.tenbt = 0; + self.ext2 = 0x0001; + self.override_armed = false; + self.page_received = false; + self.link_low_latch = false; + self.nego_done = false; + self.deadline = None; + self.last_link = false; + self.start_negotiation(now); + } + + fn autoneg(&self) -> bool { + self.bmcr & 0x1000 != 0 + } + fn power_down(&self) -> bool { + self.bmcr & 0x0800 != 0 + } + fn isolated(&self) -> bool { + self.bmcr & 0x0400 != 0 + } + + /// The live link: down in power-down, down without a backend, and with + /// negotiation enabled up only once it has completed. + fn link(&self) -> bool { + !self.power_down() && self.backend_up && (!self.autoneg() || self.nego_done) + } + + fn start_negotiation(&mut self, now: Instant) { + self.nego_done = false; + self.partner = 0; + self.page_received = false; + self.deadline = None; + if self.autoneg() && self.backend_up && !self.power_down() { + // The synthetic peer advertises 0x01e1: IEEE 802.3, no fault. + let common = self.anar & 0x01e0; + if self.anar & 0x1f == 1 && common != 0 && self.anar & 0x2000 == 0 { + self.deadline = Some(now + self.delay); + } + } + } + + /// When a negotiation in progress completes, if one is. + pub fn deadline(&self) -> Option { + self.deadline + } + + /// Advance the negotiation deadline and note link transitions. Reads never + /// start or postpone negotiation; this only observes the clock. + pub fn sync(&mut self, now: Instant) { + if let Some(d) = self.deadline { + if now >= d && !self.power_down() && self.backend_up { + self.deadline = None; + self.nego_done = true; + self.partner = 0x01e1; + self.page_received = true; + } + } + let link = self.link(); + if self.last_link && !link { + self.link_low_latch = true; + } + self.last_link = link; + } + + /// The packet backend's cable state. + pub fn set_backend_link(&mut self, up: bool, now: Instant) { + self.sync(now); + if self.backend_up == up { + return; + } + self.backend_up = up; + if up { + // Recovery starts a fresh negotiation. + self.start_negotiation(now); + } else { + self.deadline = None; + self.nego_done = false; + } + self.sync(now); + } + + /// BMCR loopback turns frames round inside the part. Isolate and + /// power-down take precedence. + pub fn in_loopback(&self) -> bool { + self.bmcr & 0x4000 != 0 && !self.power_down() && !self.isolated() + } + + /// Frames may cross the MII at all. + pub fn data_path(&self) -> bool { + !self.power_down() && !self.isolated() + } + + /// Frames may go to or come from the packet backend. + pub fn wire_enabled(&self) -> bool { + self.data_path() && self.link() + } + + fn resolved(&self) -> (bool, bool) { + // (100 Mb/s, full duplex) + if self.autoneg() { + let common = self.anar & self.partner & 0x01e0; + if common & 0x100 != 0 { + (true, true) + } else if common & 0x80 != 0 { + (true, false) + } else if common & 0x40 != 0 { + (false, true) + } else { + (false, false) + } + } else { + (self.bmcr & 0x2000 != 0, self.bmcr & 0x0100 != 0) + } + } + + pub fn read(&mut self, r: u8, now: Instant) -> u16 { + self.sync(now); + match r { + 0 => self.bmcr, + 1 => { + let mut v = 0x0009 | self.bmsr_caps; + if self.autoneg() && self.nego_done { + v |= 0x0020; + } + if self.link() && !self.link_low_latch { + v |= 0x0004; + } + self.link_low_latch = false; + v + } + 2 => self.id1, + 3 => self.id2, + 4 => self.anar, + 5 => self.partner, + 6 => { + let v = (self.exp & 0xffe0) + | if self.nego_done { 1 } else { 0 } + | if self.page_received { 2 } else { 0 }; + self.page_received = false; + v + } + 0x10 => { + // Bit 4 is a fixed reserved read choice (stock firmware + // expects it set); bit 15 reads the override state. + (self.ext_ctl & 0x002d) + | (u16::from(self.addr & 0x1f) << 6) + | 0x0010 + | if self.override_armed { 0x8000 } else { 0 } + } + 0x11 => { + let link = self.link(); + let (speed, full) = self.resolved(); + let mut v = 0; + if link { + v |= 1; + if speed { + v |= 0x8000; + } + if full { + v |= 0x4000; + } + } + if self.nego_done { + v |= 0x0010 | (7 << 11); + } + v + } + 0x12 => (self.tenbt & 0x002f) | 0x0010, + // Node mode strap in bit 14, software bit 0. + 0x13 => (self.ext2 & 0x0011) | 0x4000, + _ => { + self.unimplemented_accesses += 1; + 0 + } + } + } + + pub fn write(&mut self, r: u8, val: u16, now: Instant) { + self.sync(now); + let r = usize::from(r); + let armed = self.override_armed; + match r { + 0 => { + if val & 0x8000 != 0 { + self.reset(now); + self.sync(now); + return; + } + let old = self.bmcr; + // Reset and restart are self-clearing. + self.bmcr = val & 0x7d80; + let restart = val & 0x0200 != 0; + let pd_now = self.power_down(); + if pd_now && old & 0x0800 == 0 { + // Entering power-down clears the latching status. + self.link_low_latch = false; + self.page_received = false; + self.deadline = None; + } else if !pd_now && old & 0x0800 != 0 { + // Release: enabled negotiation starts afresh. + self.start_negotiation(now); + } else if self.autoneg() && (restart || old & 0x1000 == 0) { + self.start_negotiation(now); + } else if !self.autoneg() { + self.nego_done = false; + self.deadline = None; + } + if armed { + self.override_armed = false; + } + } + 1 | 2 | 3 | 4 | 6 | 0x10 | 0x12 | 0x13 => { + let (ord, ovr) = phy_masks(r); + let mask = ord | if armed { ovr } else { 0 }; + let cell = match r { + 1 => &mut self.bmsr_caps, + 2 => &mut self.id1, + 3 => &mut self.id2, + 4 => &mut self.anar, + 6 => &mut self.exp, + 0x10 => &mut self.ext_ctl, + 0x12 => &mut self.tenbt, + _ => &mut self.ext2, + }; + let before = *cell; + *cell = (before & !mask) | (val & mask); + // The status caps live in bits 14..7 only. + if r == 1 { + self.bmsr_caps &= 0x7f80; + } + if r == 4 && self.anar != before && self.autoneg() { + self.start_negotiation(now); + } + if armed { + self.override_armed = false; + } else if r == 0x10 && val & 0x8000 != 0 { + self.override_armed = true; + } + } + _ => { + // Partner ability, Quick Poll and unimplemented registers + // ignore writes. The part is addressed, so a write still + // consumes an armed override. + if r != 5 && r != 0x11 { + self.unimplemented_accesses += 1; + } + if armed { + self.override_armed = false; + } + } + } + self.sync(now); + } +} + +// The transceiver's registers and auto-negotiation progress for a +// snapshot. Its one deadline is saved as the time it had left. +impl Ics1890 { + pub(crate) fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u8("addr", self.addr) + .u16("bmcr", self.bmcr) + .u16("bmsr_caps", self.bmsr_caps) + .u16("id1", self.id1) + .u16("id2", self.id2) + .u16("anar", self.anar) + .u16("partner", self.partner) + .u16("exp", self.exp) + .u16("ext_ctl", self.ext_ctl) + .u16("tenbt", self.tenbt) + .u16("ext2", self.ext2) + .bool("override_armed", self.override_armed) + .bool("page_received", self.page_received) + .bool("link_low_latch", self.link_low_latch) + .bool("last_link", self.last_link) + .bool("backend_up", self.backend_up) + .bool("nego_done", self.nego_done) + // Nanoseconds left plus one; zero for none. + .u64("deadline", self.deadline.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1)) + .u64("delay", self.delay.as_nanos() as u64) + .u32("unimplemented_accesses", self.unimplemented_accesses) + .build() + } + + pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { + let deadline = field_u64(v, "deadline")?; + *self = Self { + addr: field_u8(v, "addr")?, + bmcr: field_u16(v, "bmcr")?, + bmsr_caps: field_u16(v, "bmsr_caps")?, + id1: field_u16(v, "id1")?, + id2: field_u16(v, "id2")?, + anar: field_u16(v, "anar")?, + partner: field_u16(v, "partner")?, + exp: field_u16(v, "exp")?, + ext_ctl: field_u16(v, "ext_ctl")?, + tenbt: field_u16(v, "tenbt")?, + ext2: field_u16(v, "ext2")?, + override_armed: field_bool(v, "override_armed")?, + page_received: field_bool(v, "page_received")?, + link_low_latch: field_bool(v, "link_low_latch")?, + last_link: field_bool(v, "last_link")?, + backend_up: field_bool(v, "backend_up")?, + nego_done: field_bool(v, "nego_done")?, + deadline: (deadline != 0).then(|| now + Duration::from_nanos(deadline - 1)), + delay: Duration::from_nanos(field_u64(v, "delay")?), + unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, + }; + Ok(()) + } +} + +impl MiiPhy for Ics1890 { + fn address(&self) -> u8 { Ics1890::address(self) } + fn deadline(&self) -> Option { Ics1890::deadline(self) } + fn wire_enabled(&self) -> bool { Ics1890::wire_enabled(self) } + fn in_loopback(&self) -> bool { Ics1890::in_loopback(self) } + fn sync(&mut self, now: Instant) { Ics1890::sync(self, now) } + fn read(&mut self, register: u8, now: Instant) -> u16 { Ics1890::read(self, register, now) } + fn write(&mut self, register: u8, value: u16, now: Instant) { Ics1890::write(self, register, value, now) } + fn set_backend_link(&mut self, up: bool, now: Instant) { Ics1890::set_backend_link(self, up, now) } + fn power_on(&mut self) { Ics1890::reset(self, Instant::now()) } + fn save_state(&self, now: Instant) -> toml::Value { Ics1890::save_state(self, now) } + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { Ics1890::load_state(self, v, now) } +} diff --git a/src/dev/ioc3_eth.rs b/src/dev/ioc3_eth.rs index cbe3c0c88..4b9e9fa63 100644 --- a/src/dev/ioc3_eth.rs +++ b/src/dev/ioc3_eth.rs @@ -1,5 +1,5 @@ -//! The Ethernet MAC inside IOC3, with the ICS1890 transceiver the Octane -//! carries on its management bus. +//! The Ethernet MAC inside IOC3, with the Octane ICS1890 or Fuel DP83843 +//! transceiver on its management bus. //! //! IOC3 forwards the register offsets 0xf0..=0x150 of its register BAR here //! through [`Ioc3EthPort`]. The MAC is SGI's own design with no vendor @@ -17,7 +17,7 @@ //! Ring entries and descriptor words are big-endian bytes at bus addresses, //! frame bytes are in wire order; the Bridge applies any programmed swap. //! -//! Frames go through iris's network engine like the O2's MEC does: see +//! Frames use iris's shared network link. The machine attaches it through //! [`Ioc3Eth::attach`]. //! //! Approximations (the QEMU model this was ported from has the same ones, @@ -38,8 +38,8 @@ //! sends whenever the PHY's data path is open (not powered down or //! isolated), link or not. //! -//! Ported from hw/net/sgi-ioc3-eth.c and hw/net/ics1890.c in -//! jamesbraid/qemu-sgi-origami. +//! Ported from hw/net/sgi-ioc3-eth.c in jamesbraid/qemu-sgi-origami. The +//! Octane PHY model lives in `ics1890.rs`. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, OnceLock, Weak}; @@ -48,8 +48,9 @@ use parking_lot::Mutex; use std::time::{Duration, Instant}; use crate::dev::ioc3_port::Ioc3EthPort; +use crate::dev::ics1890::Ics1890; use crate::pci::{PciDma, PciDmaStatus}; -use crate::snapshot::{field, field_bool, field_u16, field_u32, field_u32s, field_u64, field_u8, StateTable}; +use crate::snapshot::{field, field_bool, field_u16, field_u32, field_u32s, field_u64, StateTable}; use crate::traits::{Device, Resettable, Saveable}; #[cfg(test)] @@ -163,6 +164,23 @@ pub const PHY_ADDR: u8 = 1; /// How long the synthetic link partner takes to complete negotiation. pub const NEGOTIATION_DELAY: Duration = Duration::from_millis(100); +/// The management and packet-path behavior that varies between IOC3 PHYs. +pub(crate) trait MiiPhy: Send { + fn address(&self) -> u8; + fn deadline(&self) -> Option; + fn wire_enabled(&self) -> bool; + fn in_loopback(&self) -> bool; + fn sync(&mut self, now: Instant); + fn read(&mut self, register: u8, now: Instant) -> u16; + fn write(&mut self, register: u8, value: u16, now: Instant); + fn set_backend_link(&mut self, up: bool, now: Instant); + fn power_on(&mut self); + /// The transceiver's registers and negotiation progress for a snapshot; + /// its deadline is saved as the time it had left. + fn save_state(&self, now: Instant) -> toml::Value; + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String>; +} + fn ridx(off: u32) -> usize { ((off - MAC_CONTROL) / 4) as usize } @@ -171,345 +189,6 @@ fn ridx(off: u32) -> usize { // ICS1890 transceiver // --------------------------------------------------------------------------- -/// Ordinary and command-override writable masks, by register. -fn phy_masks(r: usize) -> (u16, u16) { - match r { - 0 => (0xff80, 0), - 1 => (0, 0x7f80), - 2 | 3 => (0, 0xffff), - 4 => (0x21e0, 0x1c1f), - 6 => (0, 0xffe0), - 0x10 => (0x802d, 0), - 0x12 => (0x002f, 0), - 0x13 => (0x0011, 0), - _ => (0, 0), - } -} - -/// The ICS1890 as the Octane carries it: address 1, node mode, software -/// priority, revision 3. -pub struct Ics1890 { - addr: u8, - bmcr: u16, - bmsr_caps: u16, - id1: u16, - id2: u16, - anar: u16, - partner: u16, - exp: u16, - ext_ctl: u16, - tenbt: u16, - ext2: u16, - override_armed: bool, - page_received: bool, - link_low_latch: bool, - last_link: bool, - backend_up: bool, - nego_done: bool, - deadline: Option, - delay: Duration, - /// Accesses to registers the part does not implement. - pub unimplemented_accesses: u32, -} - -impl Ics1890 { - pub fn new(addr: u8, delay: Duration) -> Self { - let mut p = Self { - addr, - bmcr: 0, - bmsr_caps: 0, - id1: 0, - id2: 0, - anar: 0, - partner: 0, - exp: 0, - ext_ctl: 0, - tenbt: 0, - ext2: 0, - override_armed: false, - page_received: false, - link_low_latch: false, - last_link: false, - backend_up: true, - nego_done: false, - deadline: None, - delay, - unimplemented_accesses: 0, - }; - p.reset(Instant::now()); - p - } - - pub fn address(&self) -> u8 { - self.addr - } - - /// Hardware and software reset: defaults, no override, no latches, and a - /// fresh negotiation if enabled. - pub fn reset(&mut self, now: Instant) { - self.bmcr = 0x3000; - self.bmsr_caps = 0x7800; - self.id1 = 0x0015; - self.id2 = 0xf423; - self.anar = 0x01e1; - self.partner = 0; - self.exp = 0; - self.ext_ctl = 0x0008; - self.tenbt = 0; - self.ext2 = 0x0001; - self.override_armed = false; - self.page_received = false; - self.link_low_latch = false; - self.nego_done = false; - self.deadline = None; - self.last_link = false; - self.start_negotiation(now); - } - - fn autoneg(&self) -> bool { - self.bmcr & 0x1000 != 0 - } - fn power_down(&self) -> bool { - self.bmcr & 0x0800 != 0 - } - fn isolated(&self) -> bool { - self.bmcr & 0x0400 != 0 - } - - /// The live link: down in power-down, down without a backend, and with - /// negotiation enabled up only once it has completed. - fn link(&self) -> bool { - !self.power_down() && self.backend_up && (!self.autoneg() || self.nego_done) - } - - fn start_negotiation(&mut self, now: Instant) { - self.nego_done = false; - self.partner = 0; - self.page_received = false; - self.deadline = None; - if self.autoneg() && self.backend_up && !self.power_down() { - // The synthetic peer advertises 0x01e1: IEEE 802.3, no fault. - let common = self.anar & 0x01e0; - if self.anar & 0x1f == 1 && common != 0 && self.anar & 0x2000 == 0 { - self.deadline = Some(now + self.delay); - } - } - } - - /// When a negotiation in progress completes, if one is. - pub fn deadline(&self) -> Option { - self.deadline - } - - /// Advance the negotiation deadline and note link transitions. Reads never - /// start or postpone negotiation; this only observes the clock. - pub fn sync(&mut self, now: Instant) { - if let Some(d) = self.deadline { - if now >= d && !self.power_down() && self.backend_up { - self.deadline = None; - self.nego_done = true; - self.partner = 0x01e1; - self.page_received = true; - } - } - let link = self.link(); - if self.last_link && !link { - self.link_low_latch = true; - } - self.last_link = link; - } - - /// The packet backend's cable state. - pub fn set_backend_link(&mut self, up: bool, now: Instant) { - self.sync(now); - if self.backend_up == up { - return; - } - self.backend_up = up; - if up { - // Recovery starts a fresh negotiation. - self.start_negotiation(now); - } else { - self.deadline = None; - self.nego_done = false; - } - self.sync(now); - } - - /// BMCR loopback turns frames round inside the part. Isolate and - /// power-down take precedence. - pub fn in_loopback(&self) -> bool { - self.bmcr & 0x4000 != 0 && !self.power_down() && !self.isolated() - } - - /// Frames may cross the MII at all. - pub fn data_path(&self) -> bool { - !self.power_down() && !self.isolated() - } - - /// Frames may go to or come from the packet backend. - pub fn wire_enabled(&self) -> bool { - self.data_path() && self.link() - } - - fn resolved(&self) -> (bool, bool) { - // (100 Mb/s, full duplex) - if self.autoneg() { - let common = self.anar & self.partner & 0x01e0; - if common & 0x100 != 0 { - (true, true) - } else if common & 0x80 != 0 { - (true, false) - } else if common & 0x40 != 0 { - (false, true) - } else { - (false, false) - } - } else { - (self.bmcr & 0x2000 != 0, self.bmcr & 0x0100 != 0) - } - } - - pub fn read(&mut self, r: u8, now: Instant) -> u16 { - self.sync(now); - match r { - 0 => self.bmcr, - 1 => { - let mut v = 0x0009 | self.bmsr_caps; - if self.autoneg() && self.nego_done { - v |= 0x0020; - } - if self.link() && !self.link_low_latch { - v |= 0x0004; - } - self.link_low_latch = false; - v - } - 2 => self.id1, - 3 => self.id2, - 4 => self.anar, - 5 => self.partner, - 6 => { - let v = (self.exp & 0xffe0) - | if self.nego_done { 1 } else { 0 } - | if self.page_received { 2 } else { 0 }; - self.page_received = false; - v - } - 0x10 => { - // Bit 4 is a fixed reserved read choice (stock firmware - // expects it set); bit 15 reads the override state. - (self.ext_ctl & 0x002d) - | (u16::from(self.addr & 0x1f) << 6) - | 0x0010 - | if self.override_armed { 0x8000 } else { 0 } - } - 0x11 => { - let link = self.link(); - let (speed, full) = self.resolved(); - let mut v = 0; - if link { - v |= 1; - if speed { - v |= 0x8000; - } - if full { - v |= 0x4000; - } - } - if self.nego_done { - v |= 0x0010 | (7 << 11); - } - v - } - 0x12 => (self.tenbt & 0x002f) | 0x0010, - // Node mode strap in bit 14, software bit 0. - 0x13 => (self.ext2 & 0x0011) | 0x4000, - _ => { - self.unimplemented_accesses += 1; - 0 - } - } - } - - pub fn write(&mut self, r: u8, val: u16, now: Instant) { - self.sync(now); - let r = usize::from(r); - let armed = self.override_armed; - match r { - 0 => { - if val & 0x8000 != 0 { - self.reset(now); - self.sync(now); - return; - } - let old = self.bmcr; - // Reset and restart are self-clearing. - self.bmcr = val & 0x7d80; - let restart = val & 0x0200 != 0; - let pd_now = self.power_down(); - if pd_now && old & 0x0800 == 0 { - // Entering power-down clears the latching status. - self.link_low_latch = false; - self.page_received = false; - self.deadline = None; - } else if !pd_now && old & 0x0800 != 0 { - // Release: enabled negotiation starts afresh. - self.start_negotiation(now); - } else if self.autoneg() && (restart || old & 0x1000 == 0) { - self.start_negotiation(now); - } else if !self.autoneg() { - self.nego_done = false; - self.deadline = None; - } - if armed { - self.override_armed = false; - } - } - 1 | 2 | 3 | 4 | 6 | 0x10 | 0x12 | 0x13 => { - let (ord, ovr) = phy_masks(r); - let mask = ord | if armed { ovr } else { 0 }; - let cell = match r { - 1 => &mut self.bmsr_caps, - 2 => &mut self.id1, - 3 => &mut self.id2, - 4 => &mut self.anar, - 6 => &mut self.exp, - 0x10 => &mut self.ext_ctl, - 0x12 => &mut self.tenbt, - _ => &mut self.ext2, - }; - let before = *cell; - *cell = (before & !mask) | (val & mask); - // The status caps live in bits 14..7 only. - if r == 1 { - self.bmsr_caps &= 0x7f80; - } - if r == 4 && self.anar != before && self.autoneg() { - self.start_negotiation(now); - } - if armed { - self.override_armed = false; - } else if r == 0x10 && val & 0x8000 != 0 { - self.override_armed = true; - } - } - _ => { - // Partner ability, Quick Poll and unimplemented registers - // ignore writes. The part is addressed, so a write still - // consumes an armed override. - if r != 5 && r != 0x11 { - self.unimplemented_accesses += 1; - } - if armed { - self.override_armed = false; - } - } - } - self.sync(now); - } -} - // --------------------------------------------------------------------------- // Checksums and the multicast hash // --------------------------------------------------------------------------- @@ -616,7 +295,7 @@ struct State { mii_data_w: u16, /// Apart from its index: an index-only write must not disarm. rx_timer_armed: bool, - phy: Ics1890, + phy: Box, irq_asserted: bool, /// Level changes not yet delivered to the callback, in order. irq_events: Vec, @@ -626,14 +305,14 @@ struct State { } impl State { - fn new(delay: Duration) -> Self { + fn new(phy: Box) -> Self { Self { regs: [0; NREGS], int_deferred: 0, mii_data: MII_ABSENT, mii_data_w: 0, rx_timer_armed: false, - phy: Ics1890::new(PHY_ADDR, delay), + phy, irq_asserted: false, irq_events: Vec::new(), frame: vec![0; FRAME_MAX], @@ -642,6 +321,20 @@ impl State { } } + fn power_on(&mut self) { + self.phy.power_on(); + self.regs = [0; NREGS]; + self.int_deferred = 0; + self.mii_data = MII_ABSENT; + self.mii_data_w = 0; + self.rx_timer_armed = false; + self.irq_asserted = false; + self.irq_events.clear(); + self.frame.fill(0); + self.tx_frames = 0; + self.rx_frames = 0; + } + fn get(&self, off: u32) -> u32 { self.regs[ridx(off)] } @@ -1025,6 +718,7 @@ type IrqFn = Box; /// through the Bridge and HEART, which never call back into this device. pub struct Ioc3Eth { mac: [u8; 6], + thread_prefix: &'static str, st: Mutex, dma: Mutex>>, irq: Mutex>, @@ -1062,9 +756,19 @@ impl Ioc3Eth { /// As [`Ioc3Eth::new`], with the PHY's synthetic negotiation delay set. pub fn with_negotiation_delay(mac: [u8; 6], delay: Duration) -> Arc { + Self::with_phy(mac, Box::new(Ics1890::new(PHY_ADDR, delay)), "ip30-eth") + } + + /// Fuel uses the DP83843 at management address 1. + pub fn new_fuel(mac: [u8; 6]) -> Arc { + Self::with_phy(mac, Box::new(crate::dev::dp83843::Dp83843::new(PHY_ADDR)), "ioc3-eth") + } + + fn with_phy(mac: [u8; 6], phy: Box, thread_prefix: &'static str) -> Arc { Arc::new_cyclic(|me| Self { mac, - st: Mutex::new(State::new(delay)), + thread_prefix, + st: Mutex::new(State::new(phy)), dma: Mutex::new(None), irq: Mutex::new(None), irq_order: Mutex::new(()), @@ -1101,13 +805,7 @@ impl Ioc3Eth { /// with the state saying low. pub fn reset(&self) { let _order = self.irq_order.lock(); - { - let mut st = self.st.lock(); - let delay = st.phy.delay; - let backend_up = st.phy.backend_up; - *st = State::new(delay); - st.phy.set_backend_link(backend_up, Instant::now()); - } + self.st.lock().power_on(); if let Some(cb) = self.irq.lock().as_ref() { cb(false); } @@ -1175,8 +873,9 @@ impl Ioc3Eth { fn spawn_nat(&self) { let slot = self.backend.clone(); let Some(mut engine) = slot.lock().take() else { return }; + let thread_name = format!("{}-net", self.thread_prefix); let t = std::thread::Builder::new() - .name("ip30-eth-net".into()) + .name(thread_name) .spawn(move || { engine.run(); *slot.lock() = Some(engine); @@ -1281,8 +980,9 @@ impl Device for Ioc3Eth { } 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 thread_name = format!("{}-rx", self.thread_prefix); let rx = std::thread::Builder::new() - .name("ip30-eth-rx".into()) + .name(thread_name) .spawn(move || Self::rx_loop(me, running, wake)) .expect("ioc3 eth rx thread"); self.threads.lock().push(rx); @@ -1377,63 +1077,6 @@ impl Ioc3EthPort for Ioc3Eth { } } -// The transceiver's registers and auto-negotiation progress for a -// snapshot. Its one deadline is saved as the time it had left. -impl Ics1890 { - pub(crate) fn save_state(&self, now: Instant) -> toml::Value { - StateTable::new() - .u8("addr", self.addr) - .u16("bmcr", self.bmcr) - .u16("bmsr_caps", self.bmsr_caps) - .u16("id1", self.id1) - .u16("id2", self.id2) - .u16("anar", self.anar) - .u16("partner", self.partner) - .u16("exp", self.exp) - .u16("ext_ctl", self.ext_ctl) - .u16("tenbt", self.tenbt) - .u16("ext2", self.ext2) - .bool("override_armed", self.override_armed) - .bool("page_received", self.page_received) - .bool("link_low_latch", self.link_low_latch) - .bool("last_link", self.last_link) - .bool("backend_up", self.backend_up) - .bool("nego_done", self.nego_done) - // Nanoseconds left plus one; zero for none. - .u64("deadline", self.deadline.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1)) - .u64("delay", self.delay.as_nanos() as u64) - .u32("unimplemented_accesses", self.unimplemented_accesses) - .build() - } - - pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { - let deadline = field_u64(v, "deadline")?; - *self = Self { - addr: field_u8(v, "addr")?, - bmcr: field_u16(v, "bmcr")?, - bmsr_caps: field_u16(v, "bmsr_caps")?, - id1: field_u16(v, "id1")?, - id2: field_u16(v, "id2")?, - anar: field_u16(v, "anar")?, - partner: field_u16(v, "partner")?, - exp: field_u16(v, "exp")?, - ext_ctl: field_u16(v, "ext_ctl")?, - tenbt: field_u16(v, "tenbt")?, - ext2: field_u16(v, "ext2")?, - override_armed: field_bool(v, "override_armed")?, - page_received: field_bool(v, "page_received")?, - link_low_latch: field_bool(v, "link_low_latch")?, - last_link: field_bool(v, "last_link")?, - backend_up: field_bool(v, "backend_up")?, - nego_done: field_bool(v, "nego_done")?, - deadline: (deadline != 0).then(|| now + Duration::from_nanos(deadline - 1)), - delay: Duration::from_nanos(field_u64(v, "delay")?), - unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, - }; - Ok(()) - } -} - // The Ethernet block's state for a snapshot: the MAC's registers, the // MII port and the transceiver. Frames in flight between the receive // thread and the host network are not state: with the threads stopped diff --git a/src/dev/ioc3_eth/tests.rs b/src/dev/ioc3_eth/tests.rs index 12186d75a..ec5a9d167 100644 --- a/src/dev/ioc3_eth/tests.rs +++ b/src/dev/ioc3_eth/tests.rs @@ -816,6 +816,42 @@ fn the_last_reference_released_on_another_thread_stops_the_mac() { assert!(!running.load(Ordering::SeqCst), "the last release stops it"); } +#[test] +fn fuel_phy_mdio_decoder_and_machine_reset() { + let eth = Ioc3Eth::new_fuel(STATION); + assert_eq!(eth.mac(), STATION); + let read = |phy: u32, r: u32| { + eth.write32(MII_CONTROL, MII_READ | (phy << 5) | r); + eth.read32(MII_DATA_R) + }; + assert_eq!(read(1, 2), 0x2000); + assert_eq!(read(1, 3), 0x5c10); + assert_eq!(read(0, 2), 0xffff); + eth.write32(MII_DATA_W, 7); + eth.write32(MII_CONTROL, (1 << 5) | 0x19); + assert_eq!(read(1, 2), 0xffff); + assert_eq!(read(7, 2), 0x2000); + // A wrong-address reset has no effect. + eth.write32(MII_DATA_W, 0x8000); + eth.write32(MII_CONTROL, 1 << 5); + assert_eq!(read(7, 2), 0x2000); + eth.reset(); + assert_eq!(read(1, 3), 0x5c10); + assert_eq!(read(7, 2), 0xffff); + // The MAC soft reset leaves PHY programming intact. + eth.write32(MII_DATA_W, 0x21); + eth.write32(MII_CONTROL, (1 << 5) | 4); + eth.write32(MAC_CONTROL, MCR_RESET); + assert_eq!(read(1, 4), 0x21); + // Forced mode exposes the configured backend cable without waiting. + eth.write32(MII_DATA_W, 0x2100); + eth.write32(MII_CONTROL, 1 << 5); + for up in [false, true] { + eth.set_backend_link(up); + assert_eq!(read(1, 1) & 4 != 0, up); + } +} + /// A guest that does nothing but ACK: the whole host side of a lock-step /// TFTP read (transmit descriptor, NAT thread, built-in server, receive /// thread, receive ring), with the guest's own software left out. Every diff --git a/src/dev/mod.rs b/src/dev/mod.rs index fdd54e4b1..631caab2a 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,3 +60,5 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod dp83843; +pub mod ics1890; From 93a42eb5fb3b9874b573961b304dfb833016a648 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 03/38] timekeeper: add the M48T35 clock and NVRAM Fuel stores its clock and nonvolatile configuration in an M48T35 on the IOC3 byte bus. Keep the register overlay and battery-backed bytes together so latching and persistence share one owner. --- src/dev/mod.rs | 1 + src/dev/timekeeper.rs | 701 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 702 insertions(+) create mode 100644 src/dev/timekeeper.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 631caab2a..2f5a843f7 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -62,3 +62,4 @@ pub mod wd33c93a; pub mod z85c30; pub mod dp83843; pub mod ics1890; +pub mod timekeeper; diff --git a/src/dev/timekeeper.rs b/src/dev/timekeeper.rs new file mode 100644 index 000000000..95a2557a5 --- /dev/null +++ b/src/dev/timekeeper.rs @@ -0,0 +1,701 @@ +//! The Fuel M48T35 Timekeeper exposes 32 KiB of byte-wide NVRAM and calendar +//! registers at the end of that window. The guest reads the clock independently +//! of processor byte order, and software can stage clock changes through the +//! control register before committing them. +//! +//! # What is approximated here +//! +//! Host time and a persisted sidecar stand in for the battery-backed clock. +//! The model does not reproduce the physical oscillator or battery behavior. +//! +//! Ported from hw/rtc/m48t35.c in jamesbraid/qemu-sgi-origami. + +use crate::traits::{BusDevice, BusRead8, Device, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +pub const SIZE: usize = 0x8000; +pub mod reg { + pub const CONTROL: usize = 0x7ff8; + pub const SECONDS: usize = 0x7ff9; + pub const MINUTES: usize = 0x7ffa; + pub const HOURS: usize = 0x7ffb; + pub const CLOCK_WRITE: usize = 0x7ffc; + pub const DAY: usize = 0x7ffd; + pub const MONTH: usize = 0x7ffe; + pub const YEAR: usize = 0x7fff; +} +pub const CONTROL: usize = reg::CONTROL; +const CENTURY: i64 = 36525 * 86400; +const MAGIC: &[u8; 8] = b"M48TREF1"; +fn bcd(v: i64) -> u8 { + ((v / 10) * 16 + v % 10) as u8 +} +fn unbcd(v: u8) -> i64 { + i64::from((v >> 4) * 10 + (v & 15)) +} +fn month_days(y: i64, m: i64) -> i64 { + match m { + 2 => 28 + i64::from(y % 4 == 0), + 4 | 6 | 9 | 11 => 30, + _ => 31, + } +} +#[derive(Clone, Copy)] +struct Calendar { + year: i64, + month: i64, + day: i64, + hour: i64, + minute: i64, + second: i64, +} +impl Calendar { + fn seconds(self) -> i64 { + let year = (self.year + (self.month - 1).div_euclid(12)).rem_euclid(100); + let month = (self.month - 1).rem_euclid(12) + 1; + let mut days = year * 365 + (year + 3) / 4 + self.day - 1; + for m in 1..month { + days += month_days(year, m); + } + (days * 86400 + self.hour * 3600 + self.minute * 60 + self.second).rem_euclid(CENTURY) + } + fn from_seconds(t: i64) -> Self { + let t = t.rem_euclid(CENTURY); + let mut days = t / 86400; + let mut year = 0; + while days >= 365 + i64::from(year % 4 == 0) { + days -= 365 + i64::from(year % 4 == 0); + year += 1; + } + let mut month = 1; + while days >= month_days(year, month) { + days -= month_days(year, month); + month += 1; + } + Self { + year, + month, + day: days + 1, + hour: t / 3600 % 24, + minute: t / 60 % 60, + second: t % 60, + } + } +} +fn saved_calendar(image: &[u8]) -> Option { + if image.len() != SIZE { + return None; + } + let values = [ + image[0x7ff9] & 0x7f, + image[0x7ffa], + image[0x7ffb], + image[0x7ffd], + image[0x7ffe], + image[0x7fff], + ]; + let limits = [(0, 59), (0, 59), (0, 23), (1, 31), (1, 12), (0, 99)]; + let valid = values + .iter() + .zip(limits) + .all(|(&v, (lo, hi))| v & 15 <= 9 && v >> 4 <= 9 && (lo..=hi).contains(&unbcd(v))); + if !valid { + return None; + } + Some(Calendar { + second: unbcd(values[0]), + minute: unbcd(values[1]), + hour: unbcd(values[2]), + day: unbcd(values[3]), + month: unbcd(values[4]), + year: unbcd(values[5]), + }) +} +struct State { + cells: Vec, + offset: i64, + stopped: Option, + frozen: Option, + staged: Option<(Calendar, bool)>, + file: Option, + #[cfg(test)] + extra: i64, +} +impl State { + fn running(&self, host: i64) -> i64 { + self.stopped + .unwrap_or_else(|| (host + self.offset).rem_euclid(CENTURY)) + } + fn calendar(&self, host: i64) -> Calendar { + Calendar::from_seconds(self.frozen.unwrap_or_else(|| self.running(host))) + } +} + +/// Fuel's 1968-based clock. Ordinary storage starts blank; the guest owns it. +/// The lower candidate clock addresses are independent RAM, with no aliases. +pub struct Timekeeper { + initial_epoch: i64, + st: Mutex, + clock: Arc i64 + Send + Sync>, +} +impl Timekeeper { + pub fn fuel() -> Result { + let host = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_secs() as i64; + // Gregorian host date, used only to initialize the part's own calendar. + let mut days = host / 86400; + let mut year = 1970; + loop { + let n = 365 + i64::from(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); + if days < n { + break; + } + days -= n; + year += 1; + } + let mut month = 1; + while days >= month_days(year, month) { + days -= month_days(year, month); + month += 1; + } + let t = Self::fuel_at( + year, + month, + days + 1, + host / 3600 % 24, + host / 60 % 60, + host % 60, + )?; + Ok(t) + } + /// Open authoritative saved clock state before applying new-store year limits. + pub fn fuel_with_nvram_file(path: &Path) -> Result { + Self::backed(path, Self::fuel()) + } + fn backed(path: &Path, initial: Result) -> Result { + let saved = match std::fs::read(path) { + Ok(v) => v, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(), + Err(e) => return Err(e.to_string()), + }; + let clock = if saved_calendar(&saved).is_some() { + // This seed is replaced by the authoritative image before exposure. + Self::fuel_at(1968, 1, 1, 0, 0, 0)? + } else { + initial? + }; + clock.set_nvram_file(path)?; + Ok(clock) + } + /// A deterministic initial date; subsequent seconds follow monotonic host time. + pub fn fuel_at( + year: i64, + month: i64, + day: i64, + hour: i64, + minute: i64, + second: i64, + ) -> Result { + if !(1968..=2067).contains(&year) { + return Err("Timekeeper: initial year is outside 1968-2067".into()); + } + if !(1..=12).contains(&month) + || !(1..=month_days(year, month)).contains(&day) + || !(0..24).contains(&hour) + || !(0..60).contains(&minute) + || !(0..60).contains(&second) + { + return Err("Timekeeper: invalid initial calendar".into()); + } + let epoch = Calendar { + year: year - 1968, + month, + day, + hour, + minute, + second, + } + .seconds(); + let at = Instant::now(); + Ok(Self { + initial_epoch: epoch, + st: Mutex::new(State { + cells: vec![0; SIZE], + offset: epoch, + stopped: None, + frozen: None, + staged: None, + file: None, + #[cfg(test)] + extra: 0, + }), + clock: Arc::new(move || at.elapsed().as_secs() as i64), + }) + } + fn host(&self, st: &State) -> i64 { + #[cfg(not(test))] + let _ = st; + (self.clock)() + { + #[cfg(test)] + { + st.extra + } + #[cfg(not(test))] + { + 0 + } + } + } + #[cfg(test)] + fn advance_for_test(&self, seconds: i64) { + self.st.lock().extra += seconds; + } + + fn image(st: &State, host: i64, saved_at: i64) -> (Vec, [u8; 16]) { + let c = Calendar::from_seconds(st.running(host)); + let mut image = st.cells.clone(); + image[CONTROL + 1] = bcd(c.second) | if st.stopped.is_some() { 0x80 } else { 0 }; + image[CONTROL + 2] = bcd(c.minute); + image[CONTROL + 3] = bcd(c.hour); + image[CONTROL + 5] = bcd(c.day); + image[CONTROL + 6] = bcd(c.month); + image[CONTROL + 7] = bcd(c.year); + let mut reference = [0; 16]; + reference[..8].copy_from_slice(MAGIC); + reference[8..].copy_from_slice(&saved_at.to_le_bytes()); + (image, reference) + } + /// Export the exact raw image and companion clock reference, without changing RAM. + pub fn persistence_image_at(&self, saved_at: i64) -> (Vec, [u8; 16]) { + let st = self.st.lock(); + Self::image(&st, self.host(&st), saved_at) + } + /// Restore validated storage. Invalid/unwritten clock cells keep the initial clock. + /// A missing/invalid reference adds no elapsed time, matching the model's policy. + pub fn restore_image_at( + &self, + image: &[u8], + reference: Option<&[u8]>, + now: i64, + ) -> Result<(), String> { + if image.len() != SIZE { + return Err(format!( + "Timekeeper: backing image has {} bytes, expected {SIZE}", + image.len() + )); + } + if reference.is_some_and(|r| r.len() < 16) { + return Err("Timekeeper: clock reference is shorter than 16 bytes".into()); + } + let mut st = self.st.lock(); + st.cells.copy_from_slice(image); + st.frozen = None; + st.staged = None; + st.stopped = None; + if let Some(c) = saved_calendar(image) { + let stopped = image[0x7ff9] & 0x80 != 0; + let elapsed = reference.filter(|r| &r[..8] == MAGIC).map_or(0, |r| { + now.saturating_sub(i64::from_le_bytes(r[8..16].try_into().unwrap())) + .max(0) + }); + let epoch = (c.seconds() + + if stopped { + 0 + } else { + elapsed.rem_euclid(CENTURY) + }) + .rem_euclid(CENTURY); + st.offset = epoch - self.host(&st); + if stopped { + st.stopped = Some(epoch); + } + } else { + st.offset = self.initial_epoch; + crate::dlog!(crate::devlog::LogModule::Rtc, "Timekeeper: unwritten or invalid clock cells; following initial clock"); + } + Ok(()) + } + /// Bind a raw store and its `.clock` companion. Validate both before creating either. + 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) => Some(v), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + if image.as_ref().is_some_and(|v| v.len() != SIZE) { + return Err("Timekeeper: backing image must contain exactly 32768 bytes".into()); + } + let reference = match std::fs::read(&clock_path) { + Ok(v) => Some(v), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + if reference.as_ref().is_some_and(|v| v.len() < 16) { + return Err("Timekeeper: clock reference is shorter than 16 bytes".into()); + } + if let Some(image) = image.as_ref() { + self.restore_image_at(image, reference.as_deref(), unix_now())?; + } + use std::io::Write; + if image.is_none() { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .and_then(|mut f| f.write_all(&vec![0; SIZE])) + .map_err(|e| e.to_string())?; + } + if reference.is_none() { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(clock_path) + .and_then(|mut f| f.write_all(&[0; 16])) + .map_err(|e| e.to_string())?; + } + self.st.lock().file = Some(path.to_path_buf()); + Ok(()) + } + pub fn flush_nvram(&self) -> Result<(), String> { + let st = self.st.lock(); + if let Some(path) = &st.file { + let (image, reference) = Self::image(&st, self.host(&st), unix_now()); + std::fs::write(path, image).map_err(|e| e.to_string())?; + use std::io::Write; + std::fs::OpenOptions::new() + .write(true) + .open(format!("{}.clock", path.display())) + .and_then(|mut f| f.write_all(&reference)) + .map_err(|e| e.to_string())?; + } + Ok(()) + } +} +fn unix_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |d| d.as_secs() as i64) +} +impl BusDevice for Timekeeper { + fn read8(&self, addr: u64) -> BusRead8 { + let Ok(a) = usize::try_from(addr) else { + return BusRead8::err(); + }; + if a >= SIZE { + return BusRead8::err(); + } + let st = self.st.lock(); + let c = st.calendar(self.host(&st)); + let data = match a { + reg::SECONDS => bcd(c.second) | if st.stopped.is_some() { 0x80 } else { 0 }, + reg::MINUTES => bcd(c.minute), + reg::HOURS => bcd(c.hour), + reg::DAY => bcd(c.day), + reg::MONTH => bcd(c.month), + reg::YEAR => bcd(c.year), + _ => st.cells[a], + }; + BusRead8 { + status: BUS_OK, + data, + } + } + fn write8(&self, addr: u64, val: u8) -> u32 { + let Ok(a) = usize::try_from(addr) else { + return BUS_ERR; + }; + if a >= SIZE { + return BUS_ERR; + } + let mut st = self.st.lock(); + let host = self.host(&st); + let old_control = st.cells[CONTROL]; + let transfer = a == CONTROL && st.cells[CONTROL] & 0x80 != 0 && val & 0x80 == 0; + if a == CONTROL { + let old = st.cells[a]; + if val & 0x40 != 0 && old & 0x40 == 0 { + st.frozen = Some(st.running(host)); + } else if val & 0x40 == 0 { + st.frozen = None; + } + if val & 0x80 != 0 && old & 0x80 == 0 { + st.staged = Some((st.calendar(host), st.stopped.is_some())); + } else if val & 0x80 == 0 && old & 0x80 != 0 { + if let Some((c, stop)) = st.staged.take() { + let t = c.seconds(); + st.offset = t - host; + st.stopped = if stop { Some(t) } else { None }; + } + } + st.cells[a] = val; + } else if a > CONTROL { + if a == reg::CLOCK_WRITE && st.staged.is_some() { + st.cells[a] = val; + } else if let Some((c, stop)) = &mut st.staged { + match a { + reg::SECONDS => { + c.second = unbcd(val & 0x7f); + *stop = val & 0x80 != 0; + } + reg::MINUTES => c.minute = unbcd(val), + reg::HOURS => c.hour = unbcd(val), + reg::DAY => c.day = unbcd(val), + reg::MONTH => c.month = unbcd(val), + reg::YEAR => c.year = unbcd(val), + _ => {} + } + } else { + crate::dlog!(crate::devlog::LogModule::Rtc, "Timekeeper: clock write at {a:#x} without write enable"); + } + } else { + st.cells[a] = val; + } + if !transfer + && (a < CONTROL + || a == CONTROL && (old_control ^ val) & !0xc0 != 0 + || a == reg::CLOCK_WRITE && st.staged.is_some()) + { + // Ordinary storage writes must not save or replace the clock cells. + 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(&[st.cells[a]]) + }); + if let Err(e) = result { + log::error!("Timekeeper: backing write failed: {e}"); + } + } + } + drop(st); + if transfer { + if let Err(e) = self.flush_nvram() { + log::error!("Timekeeper: clock save failed: {e}"); + } + } + BUS_OK + } +} +impl Resettable for Timekeeper { + fn power_on(&self) { + let mut st = self.st.lock(); + st.frozen = None; + st.staged = None; + st.cells[CONTROL] &= !0xc0; + } +} +impl Device for Timekeeper { + fn start(&self) {} + fn step(&self, _cycles: u64) {} + fn is_running(&self) -> bool { + false + } + fn get_clock(&self) -> u64 { + 0 + } + fn stop(&self) { + if let Err(e) = self.flush_nvram() { + log::error!("Timekeeper: saving failed: {e}"); + } + } +} +impl Drop for Timekeeper { + fn drop(&mut self) { + Device::stop(self); + } +} +#[cfg(test)] +mod tests { + // Cases ported from tests/qtest/sn1-fuel-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + use crate::traits::{BusDevice, Resettable}; + + /// The guest can freeze, stop, stage, and resume the M48T35 calendar. + #[test] + fn clock_control_freezes_stops_and_resets_the_calendar() { + let t = Timekeeper::fuel_at(2067, 12, 31, 23, 59, 59).unwrap(); + t.write8(0x7f8, 0x42); + t.write8(CONTROL as u64, 0x40); + t.advance_for_test(2); + assert_eq!(t.read8(reg::YEAR as u64).data, 0x99); + t.power_on(); + assert_eq!(t.read8(CONTROL as u64).data, 0); + assert_eq!(t.read8(reg::YEAR as u64).data, 0); + assert_eq!(t.read8(reg::MONTH as u64).data, 1); + assert_eq!(t.read8(reg::DAY as u64).data, 1); + assert_eq!(t.read8(0x7f8).data, 0x42); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x89); + t.write8(CONTROL as u64, 0); + t.advance_for_test(60); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x89); + } + + /// A restored guest clock advances by host elapsed time unless its stop bit was saved. + #[test] + fn restored_clock_accounts_for_elapsed_time_unless_stopped() { + for stopped in [false, true] { + let t = Timekeeper::fuel_at(2024, 2, 28, 23, 59, 59).unwrap(); + t.write8(0x7ff0, 0xa5); + if stopped { + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x85); + t.write8(CONTROL as u64, 0); + } + let (image, reference) = t.persistence_image_at(100); + let restored = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + restored.restore_image_at(&image, Some(&reference), 102).unwrap(); + assert_eq!(restored.read8(reg::DAY as u64).data, if stopped { 0x28 } else { 0x29 }); + assert_eq!(restored.read8(reg::SECONDS as u64).data, if stopped { 0x85 } else { 1 }); + assert_eq!(restored.read8(0x7ff0).data, 0xa5); + assert!(restored.restore_image_at(&image[..2048], None, 102).is_err()); + } + } + + /// NVRAM persists across emulator restarts, and malformed backing images are rejected. + #[test] + fn backed_nvram_preserves_saved_state_and_rejects_invalid_images() { + let dir = std::env::current_exe().unwrap().parent().unwrap() + .join(format!("timekeeper-test-{}", std::process::id())); + std::fs::create_dir(&dir).unwrap(); + let path = dir.join("nvram"); + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.set_nvram_file(&path).unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), vec![0; SIZE]); + t.write8(0x7f8, 0x42); + assert_eq!(std::fs::read(&path).unwrap()[0x7f8], 0x42); + t.flush_nvram().unwrap(); + drop(t); + // Saved clock state is authoritative even if the host date is out of range. + let restored = Timekeeper::backed(&path, Timekeeper::fuel_at(2068, 1, 1, 0, 0, 0)).unwrap(); + restored.power_on(); + assert_eq!(restored.read8(0x7f8).data, 0x42); + assert_eq!(restored.read8(reg::YEAR as u64).data, 0x58); + drop(restored); + std::fs::remove_file(dir.join("nvram.clock")).unwrap(); + std::fs::write(&path, [0xa5; 2048]).unwrap(); + assert!(Timekeeper::backed(&path, Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0)).is_err()); + assert_eq!(std::fs::read(&path).unwrap(), vec![0xa5; 2048]); + assert!(!dir.join("nvram.clock").exists()); + std::fs::remove_file(&path).unwrap(); + assert!(Timekeeper::backed(&path, Timekeeper::fuel_at(2068, 1, 1, 0, 0, 0)).is_err()); + assert!(!path.exists()); + std::fs::remove_dir(dir).unwrap(); + } + + const WEEKDAY: u64 = reg::CLOCK_WRITE as u64; + + fn clock_cells(t: &Timekeeper) -> [u8; 7] { + std::array::from_fn(|i| t.read8(reg::SECONDS as u64 + i as u64).data) + } + + fn set_clock(t: &Timekeeper, cells: [u8; 7]) { + t.write8(CONTROL as u64, 0x80); + for (i, v) in cells.into_iter().enumerate() { + t.write8(reg::SECONDS as u64 + i as u64, v); + } + t.write8(CONTROL as u64, 0); + } + + // Cells are seconds, minutes, hours, weekday, day, month, year, all BCD. + + /// The PROM and kernel derive file timestamps from this calendar, so + /// 2024 must have a 29 February and 2026 must not. + #[test] + fn the_calendar_rolls_over_february_in_leap_years_only() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x28, 0x02, 0x56]); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x29, 0x02, 0x56]); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x28, 0x02, 0x58]); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x01, 0x03, 0x58]); + } + + /// The part counts a two-digit year from 1968, so the calendar wraps after + /// 2067; the guest must see year 00 again, not garbage. + #[test] + fn the_year_wraps_from_2067_to_1968() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x31, 0x12, 0x99]); + assert_eq!(t.read8(reg::YEAR as u64).data, 0x99); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x01, 0x01, 0]); + } + + /// A guest reading the clock one byte at a time needs the read bit to + /// freeze every counter at once, or a rollover tears the timestamp. + #[test] + fn the_read_bit_freezes_all_counters_until_it_is_cleared() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + let end_of_century = [0x59, 0x59, 0x23, 0x05, 0x31, 0x12, 0x99]; + set_clock(&t, end_of_century); + t.write8(CONTROL as u64, 0x40); + assert_eq!(t.read8(CONTROL as u64).data, 0x40); + t.advance_for_test(2); + assert_eq!(clock_cells(&t), end_of_century); + t.write8(CONTROL as u64, 0); + assert_eq!(clock_cells(&t), [0x01, 0, 0, 0x05, 0x01, 0x01, 0]); + } + + /// Writes in the write-enable window are staged: the guest sets six + /// counters and the weekday, and nothing but the weekday is visible until + /// the window closes. + #[test] + fn staged_clock_writes_apply_only_when_the_write_bit_clears() { + let t = Timekeeper::fuel_at(2026, 1, 2, 3, 4, 5).unwrap(); + let before = clock_cells(&t); + t.write8(CONTROL as u64, 0x80); + for (i, v) in [0x59u8, 0x58, 0x17, 0x03, 0x15, 0x06, 0x30].into_iter().enumerate() { + t.write8(reg::SECONDS as u64 + i as u64, v); + } + let during = clock_cells(&t); + for i in (0..7).filter(|&i| i != 3) { + assert_eq!(during[i], before[i], "cell {i}"); + } + assert_eq!(t.read8(WEEKDAY).data, 0x03); + t.write8(CONTROL as u64, 0); + assert_eq!(clock_cells(&t), [0x59, 0x58, 0x17, 0x03, 0x15, 0x06, 0x30]); + } + + /// The stop bit is how a guest parks the oscillator while it sets the time; + /// the clock must hold until the guest clears it, then continue from there. + #[test] + fn the_stop_bit_holds_the_seconds_until_it_is_cleared() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x82); + t.write8(CONTROL as u64, 0); + t.advance_for_test(3); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x82); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x02); + t.write8(CONTROL as u64, 0); + t.advance_for_test(1); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x03); + } + + /// The 32 KiB window ends at the year register; an access past it must + /// fault, and ordinary storage must keep its contents across clock writes. + #[test] + fn accesses_past_the_window_fault_and_ordinary_storage_is_untouched() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.write8(0, 0x5a); + t.write8(0x7ff7, 0xa5); + assert_eq!(t.write8(SIZE as u64, 0x69), BUS_ERR); + assert_ne!(t.read8(SIZE as u64).status, BUS_OK); + set_clock(&t, [0, 0, 0, 0x05, 0x01, 0x03, 0x58]); + assert_eq!(t.read8(0).data, 0x5a); + assert_eq!(t.read8(0x7ff7).data, 0xa5); + } +} From 508e80ef1269b3a7a7e632a2a279d4b53dc80768 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 04/38] ioc3: add the Fuel peripheral population Fuel attaches an M48T35 and has different PCI header policy from the Octane. Carry those choices in the population configuration and wire the timekeeper once so byte-bus accesses need no mutable attachment slot. The Fuel's onboard IOC3 also carries the PS/2 keyboard and mouse controller: OpenBSD configures a keyboard controller through the onboard IOC3 (pci/ioc.c, dev/iockbc.c), and the controller core is the one the Octane already has. Turn the controller on in the Fuel's population, as the Octane's is, and test it by sending a host key through the IOC3 registers: it latches the keyboard interrupt and arrives in the K_RX register, and a mouse command answers from its own port, both before and after a chip reset. --- src/dev/ioc3.rs | 97 ++++++++++++++++++++++++++++++++++++------- src/dev/ioc3_tests.rs | 79 +++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 14 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index f82bbbde5..a17803a9b 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -1,10 +1,11 @@ -//! SGI IOC3 base I/O controller, as the Octane (IP30) fits it. +//! SGI IOC3 base I/O controller, with Octane and Fuel board populations. //! //! One PCI function with one 1 MiB memory BAR. Behind it: two 16550 serial //! ports with a ring-DMA engine each, a PS/2 keyboard and mouse controller, //! a parallel port, a MicroLAN (one-wire) master, the GPIO pins, a DS1685 -//! clock on the byte bus, the Ethernet buffer RAM and, in the register range -//! at 0xf0, the Ethernet MAC (which lives in `ioc3_eth.rs` behind +//! clock on the Octane byte bus or Fuel's flat system Timekeeper, the +//! Ethernet buffer RAM and, in the register range at 0xf0, +//! the Ethernet MAC (which lives in `ioc3_eth.rs` behind //! [`Ioc3EthPort`]). //! //! Behaviour follows the public Linux and OpenBSD drivers and measured guest @@ -60,7 +61,7 @@ use crate::dev::w1::sgi::{self, BoardRecord}; use crate::dev::ns16550::Ns16550; use crate::dev::serial_host::SerialBackend; use crate::irq::{InterruptController, IrqLine}; -use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PciDmaStatus, PCI_INTA}; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciDmaStatus, PCI_INTA}; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; use crate::traits::{BusDevice, Device, MachineEvent, Resettable, Saveable}; use crate::snapshot::{ @@ -72,6 +73,8 @@ pub const DEVICE_ID: u16 = 0x0003; pub const REVISION: u8 = 0x01; /// PCI subsystem id of the Octane's system board. pub const SUBSYS_IP30_SYSBOARD: u16 = 0xc304; +/// Fuel system-board population. +pub const SUBSYS_IP34_SYSBOARD: u16 = 0xc30a; pub const BAR_SIZE: u32 = 1 << 20; /// The configuration dwords the chip decodes; the rest of the 16 repeat them. const CONFIG_DECODED_DWORDS: usize = 8; @@ -113,6 +116,7 @@ pub mod reg { pub const PAR_ECP_BASE: u32 = 0x20150; pub const SSRAM: u32 = 0x40000; pub const SSRAM_SIZE: u32 = 0x40000; + pub const BYTEBUS_DEV0: u32 = 0x80000; pub const BYTEBUS_DEV1: u32 = 0xa0000; pub const BYTEBUS_DEV2: u32 = 0xc0000; pub const BYTEBUS_SIZE: u32 = 0x20000; @@ -734,6 +738,10 @@ fn int_out_valid(v: u32) -> bool { #[derive(Clone, Debug)] pub struct Ioc3Config { pub subsystem_id: u16, + /// Function-specific PCI header write behavior. + pub pci_policy: PciConfigPolicy<'static>, + /// Reset the PCI header along with the chip on power-on. + pub reset_pci_on_power_on: bool, /// The board's station address, which the one-wire part holds. pub mac: [u8; 6], pub serial: [bool; 2], @@ -763,6 +771,30 @@ impl BoardRecordText { } impl Ioc3Config { + /// Fuel's IOC3 fits the keyboard and mouse controller but no indexed Dallas clock. + pub fn fuel(mac: [u8; 6]) -> Self { + Self { + subsystem_id: SUBSYS_IP34_SYSBOARD, + pci_policy: PciConfigPolicy { + status: 0x0280, + status_w1c_mask: 0, + command_mask: 0xffff, + latency_mask: 0xff, + cache_line_mask: 0, + ..Default::default() + }, + reset_pci_on_power_on: true, + mac, + serial: [true, true], + parallel: true, + keyboard_mouse: true, + rtc: false, + gpio_input_mask: 0, + gpio_input_values: 0, + records: Vec::new(), + } + } + /// The Octane's IOC3 (`octane2` selects the Octane2's board records). /// The fictitious barcodes and ROM serials are the /// marked-synthetic ones (barcode `EMU...`). @@ -774,6 +806,8 @@ impl Ioc3Config { }; Self { subsystem_id: SUBSYS_IP30_SYSBOARD, + pci_policy: PciConfigPolicy::default(), + reset_pci_on_power_on: false, mac, serial: [true, true], parallel: true, @@ -2382,6 +2416,7 @@ impl Shared { /// The IOC3 PCI function. pub struct Ioc3 { sh: Arc, + timekeeper: OnceLock>, /// The serial receive threads and the NVRAM writer, joined in `stop`. threads: Mutex>>, } @@ -2389,22 +2424,26 @@ pub struct Ioc3 { impl Ioc3 { pub fn new(cfg: Ioc3Config) -> Ioc3 { let subsystem = cfg.subsystem_id; + let pci_policy = cfg.pci_policy; let sh = Arc::new_cyclic(|weak: &Weak| { let line = IrqLine::new(Arc::new(KbcSink(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 pci = PciConfig::new( - VENDOR_ID, - DEVICE_ID, - 0xff_0000, - REVISION, - [Bar::Mem(BAR_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], - PCI_INTA, + let bars = [ + Bar::Mem(BAR_SIZE), + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + ]; + let pci = PciConfig::new_with_policy( + VENDOR_ID, DEVICE_ID, 0xff_0000, REVISION, bars, PCI_INTA, pci_policy, ); // The subsystem identity selects what a guest instantiates. - pci.write(11, ((subsystem as u32) << 16) | VENDOR_ID as u32); + pci.set_subsystem_identity(((subsystem as u32) << 16) | VENDOR_ID as u32); Shared { pci, st: Mutex::new(St::new(cfg)), @@ -2429,7 +2468,7 @@ impl Ioc3 { nvram_ready: Condvar::new(), } }); - Ioc3 { sh, threads: Mutex::new(Vec::new()) } + Ioc3 { sh, timekeeper: OnceLock::new(), threads: Mutex::new(Vec::new()) } } /// The Octane's (or Octane2's) onboard IOC3: both serial ports, PS/2, @@ -2438,7 +2477,13 @@ impl Ioc3 { Ioc3::new(Ioc3Config::octane(mac, octane2)) } - /// The timer manager the deadlines are armed on. Call before `start`. + /// Attach Fuel's independently owned flat select-0 system clock. + pub fn attach_timekeeper(&self, clock: Arc) { + assert!(!self.sh.st.lock().cfg.rtc, "indexed Dallas and flat Timekeeper populations conflict"); + assert!(self.timekeeper.set(clock).is_ok(), "IOC3 timekeeper attached more than once"); + } + + /// The timer manager for device deadlines. Set it before start. pub fn set_timer_manager(&self, tm: Arc) { let _ = self.sh.timers.set(tm); } @@ -2545,6 +2590,9 @@ impl Ioc3 { /// Ethernet block is a separate device here and keeps its state: the /// machine resets it on its own. pub fn reset(&self) { + if self.sh.st.lock().cfg.reset_pci_on_power_on { + self.sh.pci.reset(); + } self.sh.with_st(false, |st, _| st.reset()); } @@ -2703,6 +2751,16 @@ impl PciBusDevice for Ioc3 { if width == 4 && a <= reg::HEADER_LAST && a & 3 == 0 { return self.header_dword((a / 4) as usize); } + if (reg::BYTEBUS_DEV0..reg::BYTEBUS_DEV0 + crate::dev::timekeeper::SIZE as u32).contains(&a) { + if let Some(clock) = self.timekeeper.get() { + if width != 1 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC3 Timekeeper {width}-byte read refused"); + return 0; + } + return u32::from(clock.read8(u64::from(a - reg::BYTEBUS_DEV0)).data); + } + } + if (reg::ENET_FIRST..=reg::ENET_LAST).contains(&a) { // Only 32-bit accesses reach the Ethernet block. if width != 4 { @@ -2719,6 +2777,17 @@ impl PciBusDevice for Ioc3 { return; } let a = off ^ (4 - width.min(4) as u32); + if (reg::BYTEBUS_DEV0..reg::BYTEBUS_DEV0 + crate::dev::timekeeper::SIZE as u32).contains(&a) { + if let Some(clock) = self.timekeeper.get() { + if width != 1 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC3 Timekeeper {width}-byte write refused"); + return; + } + clock.write8(u64::from(a - reg::BYTEBUS_DEV0), val as u8); + return; + } + } + if (reg::ENET_FIRST..=reg::ENET_LAST).contains(&a) { if width == 4 && a & 3 == 0 { if let Some(e) = self.sh.eth() { diff --git a/src/dev/ioc3_tests.rs b/src/dev/ioc3_tests.rs index ec7f1bfe5..825907170 100644 --- a/src/dev/ioc3_tests.rs +++ b/src/dev/ioc3_tests.rs @@ -1636,3 +1636,82 @@ fn a_short_buffer_ram_is_refused() { chip.insert("ssram".into(), toml::Value::Array(Vec::new())); assert!(c.load_state(&v).unwrap_err().contains("ssram")); } + +/// The Octane keeps the IOC3's PCI header across a chip reset; only the +/// Fuel population resets it along with the chip. +#[test] +fn an_octane_chip_reset_keeps_the_pci_header() { + let r = Rig::new(); + let cfg = r.c.config(); + cfg.write(1, 0xffff_0146); + cfg.write(3, 0xffff_ffff); + r.c.reset(); + assert_eq!(cfg.read(1), 0x0200_0146); + assert_eq!(cfg.read(3), 0xffff); +} + +/// Fuel's IOC3 has its own header write policy and resets the header with +/// the chip, reaches the flat Timekeeper on byte-bus device 0, leaves the +/// Dallas clock ports unfitted, and has no PHY reset on GPIO 5. +#[test] +fn the_fuel_population_keeps_its_clock_and_header_policy_across_reset() { + let r = Rig::with(Ioc3::new(Ioc3Config::fuel(MAC))); + let clock = Arc::new(crate::dev::timekeeper::Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap()); + r.c.attach_timekeeper(clock.clone()); + let cfg = r.c.config(); + cfg.write(1, 0xffff_0146); + cfg.write(3, 0xffff_ffff); + assert_eq!(cfg.read(1), 0x0280_0146); + assert_eq!(cfg.read(3), 0x0000_ff00); + let base = reg::BYTEBUS_DEV0; + r.w8(base + 0x7f8, 0xa5); + r.c.reset(); + clock.power_on(); + assert_eq!(r.r8(base + 0x7fff), 0x58); + assert_eq!(r.r8(base + 0x7f8), 0xa5); + assert_eq!(cfg.read(1), 0x0280_0000); + assert_eq!(cfg.read(3), 0); + // Fuel selects the flat clock, leaving indexed Dallas ports unfitted. + r.w8(reg::BYTEBUS_DEV1, 0x40); + r.w8(reg::BYTEBUS_DEV2, 0x55); + assert_eq!(r.r8(reg::BYTEBUS_DEV2), 0); + r.c.attach_eth(crate::dev::ioc3_eth::Ioc3Eth::new_fuel(MAC)); + use crate::dev::ioc3_eth::{bits::MII_READ, reg::{MII_CONTROL, MII_DATA_R, MII_DATA_W}}; + r.w32(MII_DATA_W, 7); + r.w32(MII_CONTROL, (1 << 5) | 0x19); + r.w32(reg::PIN_CONTROL_SET, 1 << 5); + r.w32(reg::PIN_REG_FIRST + 5 * 4, 0); + r.w32(reg::PIN_DATA, 1 << 5); + r.w32(MII_CONTROL, MII_READ | (7 << 5) | 3); + assert_eq!(r.r32(MII_DATA_R), 0x5c10, "Fuel GPIO5 has no PHY reset connection"); +} + +/// Fuel's IOC3 fits the keyboard and mouse controller, as the Octane's does +/// (OpenBSD configures it through the onboard IOC3's keyboard controller). A +/// key pushed from the host arrives in K_RX through the IOC3 registers and +/// latches the keyboard interrupt, and a mouse report arrives in M_RX; both +/// survive a chip reset. +#[test] +fn the_fuel_ioc3_delivers_keyboard_and_mouse_traffic_across_reset() { + let r = Rig::with(Ioc3::new(Ioc3Config::fuel(MAC))); + for _ in 0..2 { + r.c.reset(); + r.c.ps2().power_on(); + r.w32(K_TX, 0xff); + let v = r.r32(K_RX); + assert_ne!(v & VALID[2], 0, "the keyboard answers a reset"); + assert_eq!((v >> 16) & 0xff, 0xfa, "with an ACK"); + r.w32(M_TX, 0xf2); + let v = r.r32(M_RX); + assert_ne!(v & (1 << 27), 0, "the mouse answers from its own port"); + // Enable scanning, then inject a key from the host. + r.w32(K_TX, 0xf4); + while r.r32(K_RX) & VALID[2] != 0 {} + r.w32(reg::INT_STATUS, int::KEYBOARD); + r.c.ps2().push_kb(winit::keyboard::KeyCode::KeyA, true); + assert_ne!(r.r32(reg::INT_STATUS) & int::KEYBOARD, 0, "a key latches the interrupt"); + let v = r.r32(K_RX); + assert_ne!(v & VALID[2], 0, "and arrives in K_RX"); + assert_eq!(v & (1 << 27), 0, "from the keyboard"); + } +} From 78c1223c6499b81bd89cc14f6b02da34a30a84df Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 05/38] sflash: add the AMD command set Fuel boot flash uses AMD unlock, program and erase commands. Keep that state machine separate from array storage and bus decode so its command handling can serve each fitted flash array. The Intel and AMD command sets need the same rules for seeding and writing flash images, so share the file storage while each command engine keeps its own command state. --- HACKING.md | 3 + debug.md | 3 +- src/dev/sflash.rs | 39 ++--- src/dev/sflash/amd.rs | 313 ++++++++++++++++++++++++++++++++++++++ src/dev/sflash/storage.rs | 62 ++++++++ src/devlog.rs | 8 +- 6 files changed, 400 insertions(+), 28 deletions(-) create mode 100644 src/dev/sflash/amd.rs create mode 100644 src/dev/sflash/storage.rs diff --git a/HACKING.md b/HACKING.md index 0ad56cbf7..587b95981 100644 --- a/HACKING.md +++ b/HACKING.md @@ -98,6 +98,9 @@ way: `scsi::attach_from_config` walks the `[scsi.N]` tables for every machine, where the key is a slot (controller × 16 + id, see `config::scsi_slot`), and the machine supplies only a route from controller number to bus. +Flash command engines share `FlashBacking` file storage while retaining +their own command state. + ## 2. Data path and endianness **Word-transparent architecture.** Host `u32`/`u64` values are bit diff --git a/debug.md b/debug.md index 7b68b25e4..74ffeef8c 100644 --- a/debug.md +++ b/debug.md @@ -593,9 +593,10 @@ Per-module logging; all of it needs a developer build to produce output. | `log file ` | Send a module's log to a file | Modules: `net hpc3 seeq hal2 mc rex3 mips ioc scsi pdma vino dcb vc2 cmap -xmap bt445 scc ps2 rtc eeprom l1i l1d l2c`. Mask categories: `pdma` — +xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock`. Mask categories: `pdma` — `hal enet scsi`; `mips` — `insn tlb mem fpu cp0`; `l1i`/`l1d`/`l2c` — `hit miss op`; every module also takes `on`/`all`, `off`/`none` or a hex mask. +`bedrock` covers the Fuel's devices on its Bedrock hub. `IRIS_DEBUG_LOG` or `[debug] debug_log` set the same thing at startup. ### Performance and locks diff --git a/src/dev/sflash.rs b/src/dev/sflash.rs index 6d0e75ea6..5d35bfe91 100644 --- a/src/dev/sflash.rs +++ b/src/dev/sflash.rs @@ -30,8 +30,7 @@ //! [`Bridge::attach_flash`]: crate::dev::bridge::Bridge::attach_flash use std::cell::Cell; -use std::fs::{File, OpenOptions}; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io; use std::path::Path; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -40,6 +39,10 @@ use parking_lot::Mutex; use crate::snapshot::{field_u8, StateTable}; use crate::traits::{BusDevice, BusRead16, Saveable, BUS_ERR, BUS_OK}; +pub(crate) mod storage; +pub(crate) mod amd; +use self::storage::FlashBacking; + /// The Octane's part: 1 MiB. pub const FLASH_SIZE: usize = 1 << 20; /// Erase block size. @@ -78,7 +81,7 @@ struct State { data: Vec, mode: Mode, status: u8, - file: Option, + backing: Option, } pub struct CfiFlash { @@ -114,7 +117,7 @@ impl CfiFlash { assert!(image.len() <= size, "image must fit the part"); let mut data = vec![0xff; size]; data[..image.len()].copy_from_slice(image); - Self { st: Mutex::new(State { data, mode: Mode::Array, status: SR_READY, file: None }), gen: AtomicU64::new(0) } + Self { st: Mutex::new(State { data, mode: Mode::Array, status: SR_READY, backing: None }), gen: AtomicU64::new(0) } } /// A reset: back to read-array mode with the status clear. The array @@ -132,23 +135,10 @@ impl CfiFlash { /// untouched, since it is probably some other file. pub fn with_file(image: &[u8], size: usize, path: impl AsRef) -> io::Result { let flash = Self::new(image, size); - let path = path.as_ref(); - let mut file = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(path)?; - { - let mut st = flash.st.lock(); - let len = file.metadata()?.len(); - if len == size as u64 { - file.read_exact(&mut st.data)?; - } else if len == 0 { - file.write_all(&st.data)?; - } else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("flash file {} is {len} bytes, expected {size} (or empty); not overwriting it", path.display()), - )); - } - st.file = Some(file); - } + let mut st = flash.st.lock(); + let backing = FlashBacking::open(path, &mut st.data)?; + st.backing = Some(backing); + drop(st); Ok(flash) } @@ -168,10 +158,9 @@ impl CfiFlash { } impl State { - fn sync(&self, off: usize, len: usize) { - if let Some(mut f) = self.file.as_ref() { - // `&File` seeks and writes without needing `&mut self`. - let res = f.seek(SeekFrom::Start(off as u64)).and_then(|_| f.write_all(&self.data[off..off + len])); + fn sync(&mut self, off: usize, len: usize) { + if let Some(backing) = self.backing.as_mut() { + let res = backing.write_range(off, &self.data[off..off + len]); if let Err(e) = res { // The guest sees a completed cycle; the file is only the backing store. log::warn!("flash: backing file write at {off:#x} failed: {e}"); diff --git a/src/dev/sflash/amd.rs b/src/dev/sflash/amd.rs new file mode 100644 index 000000000..41c4cdff9 --- /dev/null +++ b/src/dev/sflash/amd.rs @@ -0,0 +1,313 @@ +//! The AMD flash command set: unlock, word program, sector erase and CFI +//! query state, over storage supplied by the caller. +//! +//! Array bytes and bus translation belong to the enclosing device. Fuel uses +//! this 8 MiB command engine for each boot-flash array. The CFI data and timing +//! are compatibility values; they do not identify the fitted flash chip. +//! Buffered writes, protection, bypass and erase suspend are absent. +//! +//! Ported from MAME's intelfsh.cpp AMD command path, revision +//! 2ee9b1e30299b122b131cab0bd7912abc2b095ba (BSD-3-Clause). + +// Copyright (c) Aaron Giles + +pub const SECTOR_SIZE: usize = 64 * 1024; +const SECTORS: usize = 128; +pub const PART_SIZE: usize = SECTOR_SIZE * SECTORS; +pub const ERASE_SETUP_NS: u64 = 50_000; +pub const ERASE_NS: u64 = 512_000_000; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mode { + Array, + Unlock1, + Unlock2, + Program, + Erase1, + Erase2, + Erase3, + Query, + Identify, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Effect { + None, + Program { offset: usize, value: u16 }, +} + +pub struct AmdCore { + mode: Mode, + selected: [bool; SECTORS], + /// Remaining time includes the sector-selection interval. + remaining_ns: u64, + setup_ns: u64, + toggle: bool, + failed: bool, +} + +impl Default for AmdCore { + fn default() -> Self { + Self { + mode: Mode::Array, + selected: [false; SECTORS], + remaining_ns: 0, + setup_ns: 0, + toggle: false, + failed: false, + } + } +} + +impl AmdCore { + pub fn reset(&mut self) { + *self = Self::default(); + } + pub fn busy(&self) -> bool { + self.remaining_ns != 0 || self.failed + } + pub fn selecting(&self) -> bool { + self.setup_ns != 0 + } + pub fn array_reads(&self) -> bool { + !self.busy() && self.mode != Mode::Query && self.mode != Mode::Identify + } + /// None selects ordinary backing bytes. Polling never advances time. + pub fn read16(&mut self, offset: usize) -> Option { + if self.busy() { + let selected = self.selected[offset / SECTOR_SIZE]; + self.toggle = !self.toggle; + return Some( + (if self.toggle { + 0x40 | if selected { 4 } else { 0 } + } else { + 0 + }) | if self.setup_ns == 0 { 8 } else { 0 } + | if self.failed { 0x20 } else { 0 }, + ); + } + match self.mode { + Mode::Query => Some(query(offset / 2) as u16), + // IDs remain unspecified until a part/profile is selected. + Mode::Identify => Some(0), + _ => None, + } + } + pub fn write16(&mut self, offset: usize, value: u16) -> Result { + if offset & 1 != 0 || offset >= PART_SIZE { + return Err(()); + } + let cmd = value as u8; + let word = offset / 2; + if self.busy() { + if (self.failed || self.setup_ns != 0) && cmd == 0xf0 { + self.reset(); + } else if self.setup_ns != 0 && cmd == 0x30 { + self.selected[offset / SECTOR_SIZE] = true; + self.setup_ns = ERASE_SETUP_NS; + self.remaining_ns = ERASE_SETUP_NS + ERASE_NS; + } else if self.setup_ns != 0 { + self.reset(); + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: sector selection cancelled by command {cmd:#x} at word offset {word:#x}" + ); + return Err(()); + } else if cmd != 0xf0 { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: unsupported command {cmd:#x} while erasing at word offset {word:#x}" + ); + return Err(()); + } + return Ok(Effect::None); + } + // The pending program data cycle consumes every bit, including F0. + if self.mode == Mode::Program { + self.mode = Mode::Array; + return Ok(Effect::Program { offset, value }); + } + if cmd == 0xf0 { + self.reset(); + return Ok(Effect::None); + } + let next = match (self.mode, word, cmd) { + (Mode::Array, 0x55, 0x98) => Mode::Query, + (Mode::Array, 0x555, 0xaa) => Mode::Unlock1, + (Mode::Unlock1, 0x2aa, 0x55) => Mode::Unlock2, + (Mode::Unlock2, 0x555, 0xa0) => Mode::Program, + (Mode::Unlock2, 0x555, 0x80) => Mode::Erase1, + (Mode::Unlock2, 0x555, 0x90) => Mode::Identify, + (Mode::Erase1, 0x555, 0xaa) => Mode::Erase2, + (Mode::Erase2, 0x2aa, 0x55) => Mode::Erase3, + (Mode::Erase3, _, 0x30) => { + self.selected[offset / SECTOR_SIZE] = true; + self.setup_ns = ERASE_SETUP_NS; + self.remaining_ns = ERASE_SETUP_NS + ERASE_NS; + Mode::Array + } + _ => { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: unsupported cycle {value:#06x} at byte offset {offset:#x} in {:?}", + self.mode + ); + self.mode = Mode::Array; + return Err(()); + } + }; + self.mode = next; + Ok(Effect::None) + } + /// The owner persists and applies selected sectors before calling finish. + pub fn advance_ns(&mut self, ns: u64) -> bool { + if self.remaining_ns == 0 || self.failed { + return false; + } + self.setup_ns = self.setup_ns.saturating_sub(ns); + self.remaining_ns = self.remaining_ns.saturating_sub(ns); + self.remaining_ns == 0 + } + pub fn selected_sectors(&self) -> impl Iterator + '_ { + self.selected + .iter() + .enumerate() + .filter_map(|(i, selected)| selected.then_some(i)) + } + pub fn finish_erase(&mut self, success: bool) { + if success { + self.reset(); + } else { + self.failed = true; + } + } +} + +// Generic model response; voltage/timing do not identify Fuel's fitted part. +// Field positions/units: AMD publication 21444 rev E+2, pp14-15. +// Region encoding also follows Renode AMDCFIFlash.cs (MIT), revision +// 5448105bc7875aa6c5305761d97a371dc640dd7f. No Renode mutation code is used. +fn query(index: usize) -> u8 { + const PROFILE: [u8; 33] = [ + b'Q', b'R', b'Y', 2, 0, 0, 0, 0, 0, 0, 0, 0x27, 0x36, 0, 0, 3, 0, 9, 0, 4, 0, 4, 0, 23, 1, + 0, 0, 0, 1, 0x7f, 0, 0, 1, + ]; + index + .checked_sub(0x10) + .and_then(|i| PROFILE.get(i)) + .copied() + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + // Cases ported from upstream QEMU's tests/qtest/pflash-cfi02-test.c. + use super::*; + + // Word addresses from the command sequences, as byte offsets on the 16-bit bus. + const UNLOCK0: usize = 0x555 * 2; + const UNLOCK1: usize = 0x2aa * 2; + const CFI: usize = 0x55 * 2; + + fn cmd(f: &mut AmdCore, offset: usize, value: u16) -> Effect { + f.write16(offset, value).unwrap() + } + + fn unlock(f: &mut AmdCore) { + cmd(f, UNLOCK0, 0xaa); + cmd(f, UNLOCK1, 0x55); + } + + fn start_sector_erase(f: &mut AmdCore, offset: usize) { + unlock(f); + cmd(f, UNLOCK0, 0x80); + unlock(f); + cmd(f, offset, 0x30); + } + + /// The guest's erase loop polls DQ3, DQ6 and DQ7; get the phases wrong and + /// firmware either times out or reads the sector before it is blank. + #[test] + fn a_sector_erase_reports_toggle_status_until_it_completes() { + let mut f = AmdCore::default(); + let sector = 5 * SECTOR_SIZE; + start_sector_erase(&mut f, sector); + assert!(f.busy() && f.selecting()); + assert!(!f.array_reads()); + // DQ3 clear: still inside the window where more sectors may be added. + let first = f.read16(sector).unwrap(); + assert_eq!(first & 0x08, 0); + assert_eq!(first & 0x80, 0); + f.advance_ns(ERASE_SETUP_NS); + assert!(!f.selecting()); + let a = f.read16(sector).unwrap(); + let b = f.read16(sector).unwrap(); + assert_eq!(a & 0x08, 0x08); + assert_eq!(a & 0x80, 0); + assert_eq!(a & 0x40, !b & 0x40, "DQ6 toggles on successive reads"); + assert!(f.advance_ns(ERASE_NS)); + assert_eq!(f.selected_sectors().collect::>(), [5]); + f.finish_erase(true); + assert!(f.array_reads()); + assert_eq!(f.read16(sector), None); + } + + /// Firmware erases several sectors with one command; each further 0x30 + /// inside the selection window must join the set, not start a second erase. + #[test] + fn extra_sectors_join_a_pending_erase_inside_the_selection_window() { + let mut f = AmdCore::default(); + start_sector_erase(&mut f, 0); + cmd(&mut f, 3 * SECTOR_SIZE, 0x30); + assert_eq!(f.selected_sectors().collect::>(), [0, 3]); + // The window restarts on each selection. + f.advance_ns(ERASE_SETUP_NS - 1); + assert!(f.selecting()); + // A non-erase command in the window cancels the whole operation. + assert!(f.write16(0, 0x55).is_err()); + assert!(!f.busy()); + assert_eq!(f.selected_sectors().count(), 0); + } + + /// Reset (F0) is how firmware backs out of a half-finished command or + /// leaves CFI mode, so it must always return the part to array reads. + #[test] + fn a_reset_command_abandons_a_partial_unlock_and_leaves_cfi_mode() { + let mut f = AmdCore::default(); + cmd(&mut f, UNLOCK0, 0xaa); + cmd(&mut f, 0, 0xf0); + // The unlock is forgotten: a program setup now is not a valid cycle. + assert!(f.write16(UNLOCK1, 0x55).is_err()); + + cmd(&mut f, CFI, 0x98); + assert!(!f.array_reads()); + cmd(&mut f, 0, 0xf0); + assert!(f.array_reads()); + assert_eq!(f.read16(0x10 * 2), None); + } + + /// Firmware that probes the part reads the CFI "QRY" signature and the + /// device-size exponent to learn how much flash it has. + #[test] + fn the_cfi_query_returns_the_qry_signature_and_the_size_exponent() { + let mut f = AmdCore::default(); + cmd(&mut f, CFI, 0x98); + let q = |f: &mut AmdCore, word: usize| f.read16(word * 2).unwrap(); + assert_eq!([q(&mut f, 0x10), q(&mut f, 0x11), q(&mut f, 0x12)], [b'Q' as u16, b'R' as u16, b'Y' as u16]); + assert_eq!(1usize << q(&mut f, 0x27), PART_SIZE); + } + + /// Word program is the unlock sequence, A0, then one data cycle; the data + /// cycle goes to the owner as an effect and is not decoded as a command. + #[test] + fn a_program_sequence_hands_the_data_word_to_the_owner() { + let mut f = AmdCore::default(); + unlock(&mut f); + cmd(&mut f, UNLOCK0, 0xa0); + // 0xf0 as data must not be taken for a reset. + assert_eq!(cmd(&mut f, 0x1234 * 2, 0xa5f0), Effect::Program { offset: 0x1234 * 2, value: 0xa5f0 }); + assert!(f.array_reads()); + // Odd byte offsets are not word cycles. + assert!(f.write16(UNLOCK0 + 1, 0xaa).is_err()); + } +} diff --git a/src/dev/sflash/storage.rs b/src/dev/sflash/storage.rs new file mode 100644 index 000000000..815e1aa6d --- /dev/null +++ b/src/dev/sflash/storage.rs @@ -0,0 +1,62 @@ +//! Exact-size file backing shared by flash command-set models. + +use std::fs::{File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +pub(crate) struct FlashBacking { + file: File, + size: usize, +} + +impl FlashBacking { + #[cfg(test)] + pub(crate) fn from_file(file: File, size: usize) -> Self { + Self { file, size } + } + + /// Open a flash store, loading its bytes or seeding a new empty file. + /// A non-empty file must match the supplied array size and is left intact + /// when its size is refused. + pub(crate) fn open(path: impl AsRef, data: &mut [u8]) -> io::Result { + let path = path.as_ref(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path)?; + let len = file.metadata()?.len(); + if len == data.len() as u64 { + file.read_exact(data)?; + } else if len == 0 { + file.write_all(data)?; + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "flash file {} is {len} bytes, expected {} (or empty); not overwriting it", + path.display(), + data.len() + ), + )); + } + Ok(Self { + file, + size: data.len(), + }) + } + + /// Write one byte range through to the backing file. + pub(crate) fn write_range(&mut self, offset: usize, bytes: &[u8]) -> io::Result<()> { + if offset > self.size || bytes.len() > self.size - offset { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "flash backing range is outside the array", + )); + } + self.file.seek(SeekFrom::Start(offset as u64))?; + self.file.write_all(bytes)?; + self.file.flush() + } +} diff --git a/src/devlog.rs b/src/devlog.rs index d082e7ba1..b9ee4f6fa 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -55,6 +55,7 @@ pub enum LogModule { /// The Octane's VPro (Odyssey) graphics board: guest accesses the /// model does not support. Odyssey = 28, + Bedrock = 29, } // Mask bits shared by L1i / L1d / L2c @@ -63,7 +64,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 29; + pub const COUNT: usize = 30; pub fn from_str(s: &str) -> Option { match s { @@ -96,6 +97,7 @@ impl LogModule { "gr2" => Some(Self::Gr2), "rad1" => Some(Self::Rad1), "odyssey" => Some(Self::Odyssey), + "bedrock" => Some(Self::Bedrock), _ => None, } } @@ -131,6 +133,7 @@ impl LogModule { Self::Gr2 => "gr2", Self::Rad1 => "rad1", Self::Odyssey => "odyssey", + Self::Bedrock => "bedrock", } } @@ -141,7 +144,7 @@ impl LogModule { Self::Vino, Self::Dcb, Self::Vc2, Self::Cmap, Self::Xmap, Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, - Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, + Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, ] } @@ -283,6 +286,7 @@ impl DevLog { ModuleLog::new(), // Gr2 ModuleLog::new(), // Rad1 ModuleLog::new(), // Odyssey + ModuleLog::new(), // Bedrock ], } } From e84a35ef4041d851b5f8f42966d299a46c33f872 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:03:25 +0000 Subject: [PATCH 06/38] cpu: send uncached byte and halfword stores with the whole register Fuel PROM 6.210's memory test fails when uncached SB and SH reach the bus carrying only their low bits. Its L1 console shows: mtest_bank_main failed: Walking Address RSLT mtest_bank_mai FAIL diag_rc = 43 Walking Address In both memory banks the expected halfwords 0xa55a, 0x5aa5, 0x9999 and 0x6666 read back from the ECC check-bit view as 0x0000. The PROM stores each check word with SH from a register whose low 16 bits are zero; the word sits higher in the register, at a position that depends on the tested address. With the whole register visible to the ECC view and the same PROM configuration it reports ALL DIAGS PASSED and enables all 512 MiB. Add BusDevice::write_narrow_from, whose default stores the architectural byte or halfword exactly as write8/write16 do, and call it for every uncached SB and SH. Ordinary devices therefore still see the low bits of the register, and only a device that models a path observing the other lanes overrides it; Bedrock's ECC view does, in its own commit. A partial-word write drives all of SysAD[63:0] in its data cycle, but the R10000 User's Manual (v2.0, p. 104) defines only the lanes the address and size select and says nothing of the others. Debug writes keep the ordinary-width path, and the JIT callouts pass the same source register. The call replaces the one to write8 or write16 and adds only a size branch. --- HACKING.md | 4 ++ src/cpu/mips_exec.rs | 23 +++++-- src/cpu/mips_exec_test.rs | 140 ++++++++++++++++++++++++++++++++++++++ src/traits.rs | 17 +++++ 4 files changed, 177 insertions(+), 7 deletions(-) diff --git a/HACKING.md b/HACKING.md index 587b95981..52d123c0d 100644 --- a/HACKING.md +++ b/HACKING.md @@ -378,6 +378,10 @@ consumer thread never takes `rec`. monomorphised over it, so every CPU is in every binary with no per-model branch on the hot path. `Machine::new` picks one from `[machine] cpu`. +- Every uncached SB/SH goes to the bus through `BusDevice::write_narrow_from` + with the whole source register: the byte or halfword is the low bits, the + other lanes are undefined by hardware, and only Bedrock's ECC view reads + them. - With `opcodefusion` (implied by `lightning`), the interpreter executes common pairs (branch + NOP, LUI + ORI/ADDIU, address + load/store) as one dispatch. diff --git a/src/cpu/mips_exec.rs b/src/cpu/mips_exec.rs index 459a28fb0..782883bf7 100644 --- a/src/cpu/mips_exec.rs +++ b/src/cpu/mips_exec.rs @@ -1571,20 +1571,21 @@ unsafe extern "C" fn jit_read64(ctx: *mut core::ffi::c_void #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write8(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *exec_from_core::(ctx) }; - let val = val as u8; // mask to the real width ourselves — see write8_fn's doc comment on why the FFI parameter is u64 + // The whole register, as the interpreter's SB passes it: `write_data` + // narrows it where it stores, and an uncached store hands it to the bus + // (`BusDevice::write_narrow_from`). #[cfg(feature = "jitv2_lockstep")] - { return exec.lockstep_jit_write::<1>(va, val as u64); } + { return exec.lockstep_jit_write::<1>(va, val); } #[cfg(not(feature = "jitv2_lockstep"))] - exec.write_data::<1>(va, val as u64) + exec.write_data::<1>(va, val) } #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write16(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *exec_from_core::(ctx) }; - let val = val as u16; // mask to the real width ourselves — see write16_fn's doc comment #[cfg(feature = "jitv2_lockstep")] - { return exec.lockstep_jit_write::<2>(va, val as u64); } + { return exec.lockstep_jit_write::<2>(va, val); } #[cfg(not(feature = "jitv2_lockstep"))] - exec.write_data::<2>(va, val as u64) + exec.write_data::<2>(va, val) } #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write32(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { @@ -5457,7 +5458,15 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", if !DEBUG && mips_log(MIPS_LOG_MEM) { dlog_dev!(LogModule::Mips, "Uncached Write{}: PC={:016x} VA={:016x} PA={:016x} Val={:016x}", SIZE*8, self.core.pc, virt_addr, phys_addr, val); } - let ws = if SIZE == 1 { + let ws = if !DEBUG && SIZE <= 2 { + // The bus sees the whole source register: Fuel PROM 6.210's + // Walking Address test reads its check words back from + // Bedrock's ECC view, and the word sits elsewhere in the + // register than the addressed lanes. Ordinary devices take + // the architectural low bits. Debug writes carry just the + // value they name, so they keep the ordinary-width path. + self.sysad.write_narrow_from(phys_addr, SIZE, val) + } else if SIZE == 1 { self.sysad.write8(phys_addr, val as u8) } else if SIZE == 2 { self.sysad.write16(phys_addr, val as u16) diff --git a/src/cpu/mips_exec_test.rs b/src/cpu/mips_exec_test.rs index 644fd21ca..4127eff99 100644 --- a/src/cpu/mips_exec_test.rs +++ b/src/cpu/mips_exec_test.rs @@ -13,12 +13,18 @@ mod tests { // Mock Memory Interface pub struct MockMemory { pub data: Mutex>, + /// `write8`/`write16` calls as (width, address, value). + pub narrow_writes: Mutex>, + /// `write_narrow_from` calls as (width, address, source register). + pub narrow_from_writes: Mutex>, } impl MockMemory { pub fn new() -> Self { Self { data: Mutex::new(HashMap::new()), + narrow_writes: Mutex::new(Vec::new()), + narrow_from_writes: Mutex::new(Vec::new()), } } @@ -67,6 +73,7 @@ mod tests { } fn write8(&self, addr: u64, val: u8) -> u32 { + self.narrow_writes.lock().unwrap().push((1, addr, val as u64)); self.set_byte(addr as u64, val); BUS_OK } @@ -82,6 +89,7 @@ mod tests { fn write16(&self, addr: u64, val: u16) -> u32 { let aligned_addr = (addr & !1) as u64; + self.narrow_writes.lock().unwrap().push((2, aligned_addr, val as u64)); let bytes = val.to_be_bytes(); for i in 0..2 { self.set_byte(aligned_addr + i as u64, bytes[i]); @@ -111,6 +119,12 @@ mod tests { BUS_OK } + fn write_narrow_from(&self, addr: u64, size: usize, gpr: u64) -> u32 { + self.narrow_from_writes.lock().unwrap().push((size, addr, gpr)); + // Same architectural bytes as the default. + if size == 1 { self.write8(addr, gpr as u8) } else { self.write16(addr, gpr as u16) } + } + // This whole file predates jitv2 and no test here calls // `install_jit_hooks` — every `create_executor*` helper below // explicitly disables both the real dispatch gate @@ -575,6 +589,132 @@ mod tests { assert_eq!(mem.get_byte(0x100B), 0xEF); } + /// Fuel PROM memory diagnostics need the whole source register on the ECC + /// view, so every CPU hands an uncached SB or SH to + /// `BusDevice::write_narrow_from` with it. The default of that call stores + /// the architectural byte or halfword, which is all an ordinary device + /// sees, and the addressed lanes hold it either way. + #[test] + fn uncached_sb_sh_carry_the_whole_source_register() { + fn check>>() { + let mem = Arc::new(MockMemory::new()); + let bus: Arc = mem.clone(); + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip22_bus::memory_map(false))); + let mut exec = MipsExecutor::::new( + bus, + PassthroughTlb::default(), + &cfg, + ); + disable_jitv2(&mut exec); + + let base = 0x2000u64; + let source = 0xFEDC_BA98_7654_3210u64; + let original = 0xAABB_CCDD_EEFF_0011u64; + exec.core.write_gpr(2, base); + exec.core.write_gpr(1, source); + + for (size, op, offsets) in [(1usize, OP_SB, vec![0u64, 1, 2, 3, 4, 5, 6, 7]), + (2usize, OP_SH, vec![0u64, 2, 4, 6])] { + for offset in offsets { + mem.set_double(base, original); + mem.narrow_writes.lock().unwrap().clear(); + mem.narrow_from_writes.lock().unwrap().clear(); + let instr = make_i(op, 2, 1, offset as u16); + assert_eq!(exec.exec(instr), EXEC_COMPLETE); + + let addr = base + offset; + assert_eq!( + *mem.narrow_from_writes.lock().unwrap(), + vec![(size, addr, source)], + "SB/SH source register at byte offset {offset}", + ); + + let first = if size == 1 { source as u8 } else { (source as u16 >> 8) as u8 }; + let last = if size == 1 { first } else { source as u8 }; + assert_eq!(mem.get_byte(addr), first, "stored leading byte at offset {offset}"); + if size == 2 { + assert_eq!(mem.get_byte(addr + 1), last, "stored trailing byte at offset {offset}"); + } + let original_bytes = original.to_be_bytes(); + for lane in 0..8u64 { + if lane < offset || lane >= offset + size as u64 { + assert_eq!(mem.get_byte(base + lane), original_bytes[lane as usize], + "inactive byte lane {lane} at subword offset {offset}"); + } + } + } + } + + // A zero source register still takes the same path and clears + // only the addressed lanes. + exec.core.write_gpr(0, u64::MAX); + mem.set_double(base, original); + mem.narrow_from_writes.lock().unwrap().clear(); + exec.core.write_gpr(2, base + 7); + assert_eq!(exec.exec(make_i(OP_SB, 2, 0, 0)), EXEC_COMPLETE); + assert_eq!(mem.get_byte(base + 7), 0); + assert_eq!(mem.narrow_from_writes.lock().unwrap().len(), 1); + + mem.set_double(base, original); + exec.core.write_gpr(2, base + 6); + assert_eq!(exec.exec(make_i(OP_SH, 2, 0, 0)), EXEC_COMPLETE); + assert_eq!(mem.get_byte(base + 6), 0); + assert_eq!(mem.get_byte(base + 7), 0); + assert_eq!(mem.narrow_from_writes.lock().unwrap().len(), 2); + + mem.narrow_writes.lock().unwrap().clear(); + mem.narrow_from_writes.lock().unwrap().clear(); + assert_eq!(exec.debug_write(base + 1, source, 1, u64::MAX), EXEC_COMPLETE); + assert_eq!(*mem.narrow_writes.lock().unwrap(), vec![(1, base + 1, source as u8 as u64)]); + assert!(mem.narrow_from_writes.lock().unwrap().is_empty(), "debug writes keep the ordinary-width path"); + } + + check::(); + check::(); + check::(); + check::(); + } + + /// JIT store callouts carry the same source register as the interpreter. + #[test] + #[cfg(feature = "jitv2")] + fn jit_subword_hooks_carry_the_source_register() { + fn check>>() { + let mem = Arc::new(MockMemory::new()); + let bus: Arc = mem.clone(); + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip22_bus::memory_map(false))); + let mut exec = MipsExecutor::::new( + bus, + PassthroughTlb::default(), + &cfg, + ); + disable_jitv2(&mut exec); + exec.install_jit_hooks(); + + let base = 0x2000u64; + let source = 0xFEDC_BA98_7654_3210u64; + let write8 = exec.core.write8_fn; + let write16 = exec.core.write16_fn; + let core = &mut exec.core as *mut crate::cpu::mips_core::MipsCore; + // JIT callouts receive the core pointer biased by the shared + // ABI constant; core_from_arg removes this before recovery. + let ctx = core.cast::() + .wrapping_add(crate::cpu::jitv2::codegen::CALLOUT_CORE_BIAS as usize) + .cast(); + unsafe { + write8(ctx, base, source); + write16(ctx, base + 2, source); + } + assert_eq!( + *mem.narrow_from_writes.lock().unwrap(), + vec![(1, base, source), (2, base + 2, source)], + ); + } + + check::(); + check::(); + } + #[test] fn test_load_store_pc_advances() { // Regression test: every load/store dispatch-target handler must diff --git a/src/traits.rs b/src/traits.rs index 604cb65c2..fad24c66a 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -223,6 +223,23 @@ pub trait BusDevice: Send + Sync { BUS_OK } + /// An uncached SB (`size` 1) or SH (`size` 2) to `addr`, with the whole + /// source register `gpr` for a bus that can see the lanes the store does + /// not select. The CPU calls this instead of `write8`/`write16` for an + /// uncached SB or SH. + /// + /// The architectural byte or halfword is the low bits of `gpr`, which is + /// all the default delivers. A partial-word write drives all of + /// SysAD[63:0] in its data cycle, but the R10000 User's Manual (v2.0, + /// p. 104, "SysAD[63:0] Data Cycle Encoding") defines only the lanes the + /// address and size select; the other lanes are not specified. So only a + /// device that models a path observing them (Bedrock's memory ECC view) + /// overrides this, and a bus that forwards accesses must forward this + /// call too. + fn write_narrow_from(&self, addr: u64, size: usize, gpr: u64) -> u32 { + if size == 1 { self.write8(addr, gpr as u8) } else { self.write16(addr, gpr as u16) } + } + /// JIT v2 (clanker/jitv2/jit-v2-design.md §2.4): pointer to the generation counter /// for the 4KB physical page containing `addr`, or null if this device doesn't /// back JIT-compilable memory (MMIO, etc). RAM devices return one counter per From bfbb10b8ef039bc5ecac0b65b02fa2e7318fc85d Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 13:26:59 -0700 Subject: [PATCH 07/38] prom_image: add the PROM image loader Load a boot PROM image from a file, whether it is a version-six single-segment container or a raw payload, and reject a container that is malformed instead of retrying it as raw bytes. The machines that boot a stock PROM file read it through this, bounded by the size of the flash they fill. The file is the one the Fuel uses; carrying it here lets a second machine use the loader before the Fuel lands. --- src/dev/mod.rs | 1 + src/dev/prom_image.rs | 210 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 src/dev/prom_image.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 2f5a843f7..dd945f955 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -63,3 +63,4 @@ pub mod z85c30; pub mod dp83843; pub mod ics1890; pub mod timekeeper; +pub mod prom_image; diff --git a/src/dev/prom_image.rs b/src/dev/prom_image.rs new file mode 100644 index 000000000..1a50f89b9 --- /dev/null +++ b/src/dev/prom_image.rs @@ -0,0 +1,210 @@ +//! SGI PROM image loading: version-six single-segment containers and raw +//! payloads. A recognized but malformed container is rejected rather than +//! retried as a raw image. +//! +//! Ported from hw/mips/sgi/prom-image.c in jamesbraid/qemu-sgi-origami. + +use std::io::{self, Read}; + +fn invalid(message: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} +fn word(data: &[u8], off: usize) -> u64 { + u64::from_be_bytes(data[off..off + 8].try_into().unwrap()) +} +fn byte_sum(data: &[u8]) -> u64 { + data.iter().map(|b| *b as u64).sum() +} + +/// Read a PROM file and decode its payload, retaining the path in errors. +pub fn load(path: &std::path::Path, payload_limit: usize) -> io::Result> { + let with_path = |error: io::Error| { + io::Error::new(error.kind(), format!("PROM image {}: {error}", path.display())) + }; + let file = std::fs::File::open(path).map_err(with_path)?; + // Bound the encoded file by zlib's compression bound, the container + // header, gzip framing and the accepted zero tail. + let size = payload_limit as u64; + let file_limit = size + .saturating_add(size >> 12) + .saturating_add(size >> 14) + .saturating_add(size >> 25) + .saturating_add(13 + 4096 + 12 + 7); + if file.metadata().map_err(with_path)?.len() > file_limit { + return Err(with_path(invalid( + "encoded PROM image exceeds flash capacity", + ))); + } + let mut bytes = Vec::new(); + file.take(file_limit.saturating_add(1)) + .read_to_end(&mut bytes) + .map_err(with_path)?; + if bytes.len() as u64 > file_limit { + return Err(with_path(invalid( + "encoded PROM image exceeds flash capacity", + ))); + } + decode(&bytes, payload_limit).map_err(with_path) +} + +/// Accept raw payload bytes or the supported stored/gzip container subset. +/// Gzip output is bounded before decoding, and exactly one member is required. +pub fn decode(file: &[u8], payload_limit: usize) -> io::Result> { + if file.len() < 0x48 || word(file, 0x40) != 0x4a464b535743534d { + if file.len() > payload_limit { + return Err(invalid("PROM image exceeds flash capacity")); + } + return Ok(file.to_vec()); + } + if file.len() < 4096 { + return Err(invalid("truncated SGI PROM container header")); + } + if word(file, 0x48) != 6 { + return Err(invalid("unsupported SGI PROM container version")); + } + if word(file, 0x50) != file.len() as u64 { + return Err(invalid("SGI PROM declared end does not match file size")); + } + if u32::from_be_bytes(file[0x7c..0x80].try_into().unwrap()) != 1 { + return Err(invalid("unsupported SGI PROM segment count")); + } + if word(file, 0x98) != 4096 { + return Err(invalid("unsupported SGI PROM payload offset")); + } + let flags = word(file, 0x90); + if flags != 0x10 && flags != 0x13 { + return Err(invalid("unsupported SGI PROM flags")); + } + let length = word(file, 0xb0); + if length == 0 || length > payload_limit as u64 { + return Err(invalid( + "SGI PROM decoded length is zero or exceeds capacity", + )); + } + let payload = if flags == 0x10 { + if word(file, 0xb8) != 0 || word(file, 0xc8) != 0 || word(file, 0xd0) != length { + return Err(invalid("unsupported SGI PROM uncompressed metadata")); + } + if length != (file.len() - 4096) as u64 { + return Err(invalid("invalid SGI PROM stored length")); + } + file[4096..].to_vec() + } else { + let stored = word(file, 0xb8); + if stored == 0 || stored > (file.len() - 4096) as u64 { + return Err(invalid("invalid SGI PROM stored length")); + } + let end = 4096 + stored as usize; + if file.len() - end > 7 || file[end..].iter().any(|b| *b != 0) { + return Err(invalid("unsupported SGI PROM trailing bytes")); + } + if byte_sum(&file[4096..end]) != word(file, 0xc8) { + return Err(invalid("SGI PROM stored checksum mismatch")); + } + let mut decoder = flate2::bufread::GzDecoder::new(&file[4096..end]); + let mut decoded = Vec::new(); + (&mut decoder).take(length + 1).read_to_end(&mut decoded)?; + if decoded.len() as u64 != length || !decoder.get_ref().is_empty() { + return Err(invalid("invalid SGI PROM gzip segment or decoded length")); + } + decoded + }; + if byte_sum(&payload) != word(file, 0xc0) { + return Err(invalid("SGI PROM decoded checksum mismatch")); + } + Ok(payload) +} + +#[cfg(test)] +mod tests { + // Cases follow tests/unit/test-sgi-prom-image.c in + // jamesbraid/qemu-sgi-origami: wrapped, gzip, raw, malformed and files. + use super::*; + use std::io::Write; + fn put(v: &mut [u8], off: usize, n: u64) { + v[off..off + 8].copy_from_slice(&n.to_be_bytes()); + } + fn container(payload: &[u8], gzip: bool) -> Vec { + let stored = if gzip { + let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + enc.write_all(payload).unwrap(); + enc.finish().unwrap() + } else { + payload.to_vec() + }; + let mut v = vec![0; 4096]; + put(&mut v, 0x40, 0x4a464b535743534d); + put(&mut v, 0x48, 6); + put(&mut v, 0x50, (4096 + stored.len()) as u64); + v[0x7c..0x80].copy_from_slice(&1u32.to_be_bytes()); + put(&mut v, 0x90, if gzip { 0x13 } else { 0x10 }); + put(&mut v, 0x98, 4096); + put(&mut v, 0xb0, payload.len() as u64); + put(&mut v, 0xc0, byte_sum(payload)); + if gzip { + put(&mut v, 0xb8, stored.len() as u64); + put(&mut v, 0xc8, byte_sum(&stored)); + } else { + put(&mut v, 0xd0, payload.len() as u64); + } + v.extend(stored); + v + } + /// Equivalent PROM payloads must boot from each supported image format. + #[test] + fn raw_stored_and_gzip_images_load_with_the_same_capacity_limit() { + let path = std::env::temp_dir().join(format!("iris-prom-load-{}", std::process::id())); + for file in [vec![9, 8, 7, 6], container(&[9, 8, 7, 6], false), container(&[9, 8, 7, 6], true)] { + std::fs::write(&path, file).unwrap(); + assert_eq!(load(&path, 4).unwrap(), [9, 8, 7, 6]); + assert!(load(&path, 3).is_err(), "decoded payload exceeds capacity"); + } + std::fs::write(&path, []).unwrap(); + assert!(load(&path, 4).unwrap().is_empty()); + std::fs::remove_file(path).unwrap(); + } + + /// An invalid PROM must identify its input file instead of executing a + /// malformed container. + #[test] + fn invalid_or_unavailable_images_report_their_path() { + let path = std::env::temp_dir().join(format!("iris-prom-invalid-{}", std::process::id())); + let error = load(&path, 4).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert!(error.to_string().contains(&path.display().to_string())); + let reject = |file: &[u8]| { + std::fs::write(&path, file).unwrap(); + let error = load(&path, 4).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert!(error.to_string().contains(&path.display().to_string())); + }; + reject(&vec![0; 5000]); // encoded file bound + let original = container(&[4, 3, 2, 1], false); + reject(&original[..100]); // recognized but truncated header + for (off, n) in [(0x48, 7), (0x50, 4096), (0x90, 0x14), (0x98, 4095), (0xb0, 0), (0xd0, 3)] { + let mut file = original.clone(); + put(&mut file, off, n); + reject(&file); + } + for gzip in [false, true] { + let mut file = container(&[4, 3, 2, 1], gzip); + file[0xc7] ^= 1; + reject(&file); + } + let gzip = container(&[4, 3, 2, 1], true); + for tail in [7, 8] { + let mut file = gzip.clone(); + file.extend(vec![0; tail]); + let size = file.len(); + put(&mut file, 0x50, size as u64); + if tail == 7 { + std::fs::write(&path, &file).unwrap(); + assert_eq!(load(&path, 4).unwrap(), [4, 3, 2, 1]); + } else { + reject(&file); + } + } + std::fs::remove_file(path).unwrap(); + } + +} From f73b7b5523f31cead2eab015d396d424a3aaa272 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 15:06:52 -0700 Subject: [PATCH 08/38] ip35: add the bounded MMIO windows Map a device into a processor view as an exact-bounds window: a read or write outside the device's own size is refused even where the 4 KiB page the decode table rounds to would otherwise answer it. SparseWindowMap keeps a short sorted list of such windows for a view that has a handful of apertures, so a view needs no full decode table and no entry in the process-wide fault-handler registry. This is the part of the Fuel's processor bus the SN0 node board's uncached views share. The Fuel's copy has the rest of the bus beside it. --- src/ip35.rs | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 2 files changed, 181 insertions(+) create mode 100644 src/ip35.rs diff --git a/src/ip35.rs b/src/ip35.rs new file mode 100644 index 000000000..432d8f497 --- /dev/null +++ b/src/ip35.rs @@ -0,0 +1,179 @@ +//! The IP35 processor bus: windows over small MMIO apertures. +//! +//! Maps replace whole 4 KiB pages as Physical does, while each endpoint still +//! rejects padding and enforces its cell permission. +//! +//! Ported from hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. + +use crate::physical::Physical; +use crate::traits::*; +#[cfg(feature = "jitv2")] +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +const PA_MASK: u64 = (1u64 << 40) - 1; + +struct Unclaimed; +impl BusDevice for Unclaimed {} + +/// An exact-bounds chip-relative window, optionally restricted by a cell permission. +/// Physical rounds maps to pages, so the adapter also refuses their padding. +pub(crate) struct Window { + base: u64, + size: u64, + device: Arc, + permission: Option bool + Send + Sync>>, +} +impl Window { + pub(crate) fn new(base: u64, size: u64, device: Arc) -> Self { + Self { base, size, device, permission: None } + } + pub(crate) fn permitted(mut self, permission: Arc bool + Send + Sync>) -> Self { + self.permission = Some(permission); + self + } + pub(crate) fn contains(&self, a: u64, n: u64) -> bool { self.local(a, n).is_some() } + fn local(&self, a: u64, n: u64) -> Option { + let local = a.checked_sub(self.base)?; + (local.checked_add(n)? <= self.size + && self.permission.as_ref().is_none_or(|allowed| allowed())) + .then_some(local) + } + fn write_masked_bytes(&self, a: u64, val: u64, mask: u64) -> u32 { + for i in 0..8usize { + let shift = (7 - i) * 8; + if (mask >> shift) & 0xff == 0 { + continue; + } + let Some(local) = self.local(a + i as u64, 1) else { + return BUS_ERR; + }; + let status = self.device.write8(local, (val >> shift) as u8); + if status != BUS_OK { + return status; + } + } + BUS_OK + } +} +macro_rules! window_access { + ($r:ident,$w:ident,$t:ty,$out:ident,$n:expr) => { + fn $r(&self, a: u64) -> $out { + self.local(a, $n).map(|at| self.device.$r(at)).unwrap_or_else($out::err) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.local(a, $n).map(|at| self.device.$w(at, v)).unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for Window { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + self.local(address, 1).map(|local| self.device.gen_ptr(local)).unwrap_or(std::ptr::null()) + } + fn write64_masked(&self, a: u64, val: u64, mask: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + if mask == 0 { + return BUS_OK; + } + if let Some(local) = self.local(a, 8) { + self.device.write64_masked(local, val, mask) + } else { + self.write_masked_bytes(a, val, mask) + } + } + fn write_narrow_from(&self, a: u64, size: usize, gpr: u64) -> u32 { + self.local(a, size as u64) + .map(|local| self.device.write_narrow_from(local, size, gpr)) + .unwrap_or(BUS_ERR) + } + window_access!(read8, write8, u8, BusRead8, 1); + window_access!(read16, write16, u16, BusRead16, 2); + window_access!(read32, write32, u32, BusRead32, 4); + window_access!(read64, write64, u64, BusRead64, 8); +} +pub(crate) fn map_device( + view: &Physical, + base: u64, + size: u64, + device: Arc, + name: &str, +) { + assert!(size > 0 && base.checked_add(size).is_some_and(|end| end <= PA_MASK + 1)); + view.map(base, base + size, Arc::new(Window::new(base, size, device)), name); +} + +struct DecodeRange { + start: u64, + end: u64, + window: Arc, +} + +/// Small MMIO maps have no RAM, direct pointers or host fault-handler entries. +/// Last map wins over whole pages, including padding rejected by its Window. +#[derive(Default)] +pub(crate) struct SparseWindowMap { + ranges: Vec, +} +impl SparseWindowMap { + pub(crate) fn map(&mut self, base: u64, size: u64, device: Arc) { + const PAGE: u64 = 1 << crate::ppmem::devmap::SLOT_SHIFT; + let end = base.checked_add(size).filter(|&end| size != 0 && end <= PA_MASK + 1) + .expect("IP35 window exceeds physical address space"); + let start = base & !(PAGE - 1); + let end = end.next_multiple_of(PAGE); + let window = Arc::new(Window::new(base, size, device)); + let mut ranges = Vec::with_capacity(self.ranges.len() + 2); + for old in self.ranges.drain(..) { + if old.end <= start || old.start >= end { + ranges.push(old); + } else { + if old.start < start { + ranges.push(DecodeRange { start: old.start, end: start, window: old.window.clone() }); + } + if old.end > end { + ranges.push(DecodeRange { start: end, end: old.end, window: old.window }); + } + } + } + ranges.push(DecodeRange { start, end, window }); + ranges.sort_unstable_by_key(|range| range.start); + self.ranges = ranges; + } + fn target(&self, address: u64) -> Option<&Window> { + let i = self.ranges.partition_point(|range| range.start <= address).checked_sub(1)?; + let range = &self.ranges[i]; + (address < range.end).then_some(range.window.as_ref()) + } +} +macro_rules! sparse_access { + ($read:ident,$write:ident,$ty:ty,$result:ident) => { + fn $read(&self, address: u64) -> $result { + self.target(address).map(|window| window.$read(address)).unwrap_or_else($result::err) + } + fn $write(&self, address: u64, value: $ty) -> u32 { + self.target(address).map(|window| window.$write(address, value)).unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for SparseWindowMap { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + self.target(address).map(|window| window.gen_ptr(address)).unwrap_or(std::ptr::null()) + } + fn write64_masked(&self, address: u64, value: u64, mask: u64) -> u32 { + self.target(address).map(|window| window.write64_masked(address, value, mask)) + .unwrap_or_else(|| Unclaimed.write64_masked(address, value, mask)) + } + fn write_narrow_from(&self, address: u64, size: usize, gpr: u64) -> u32 { + self.target(address).map(|window| window.write_narrow_from(address, size, gpr)) + .unwrap_or(BUS_ERR) + } + sparse_access!(read8, write8, u8, BusRead8); + sparse_access!(read16, write16, u16, BusRead16); + sparse_access!(read32, write32, u32, BusRead32); + sparse_access!(read64, write64, u64, BusRead64); +} + diff --git a/src/lib.rs b/src/lib.rs index be0a64737..9268a6b71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,8 @@ pub mod ip32_machine; /// The SGI Octane (IP30): its bus and machine -- see `docs/octane-ip30-address-map.md`. pub mod ip30; pub mod ip30_machine; +/// Fuel has an IP34 board and uses the IP35 software platform and processor views. +pub mod ip35; pub mod machine_state; pub mod platform; pub mod physical; From 513ccf8d67d4b16f17e0810ddcbe7beb1520162c Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 15:07:52 -0700 Subject: [PATCH 09/38] odyssey: add the processor interface's graphics registers A Hub or Bedrock processor interface counts graphics credits for the stores the processor sends to a graphics head, and exposes the counters as a small register block: a page, a credit count, a bias and an interrupt count and compare for each of two slots. Add that block as PiGfx, with the register decode and the starvation rule, as pure state the interface embeds. This is the register block of the Fuel's pi_gfx.rs without the credit-link trait and the store-mark helpers, which only the Fuel's graphics board uses and which land with it. The file is therefore not byte-identical to the Fuel's. --- src/dev/odyssey/mod.rs | 2 + src/dev/odyssey/pi_gfx.rs | 154 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/dev/odyssey/pi_gfx.rs diff --git a/src/dev/odyssey/mod.rs b/src/dev/odyssey/mod.rs index b3dab555f..e5cad6423 100644 --- a/src/dev/odyssey/mod.rs +++ b/src/dev/odyssey/mod.rs @@ -83,6 +83,7 @@ mod interface; mod clip; mod logic; mod mmio; +mod pi_gfx; mod pixel_dma; mod pipe; mod pixel_write; @@ -115,6 +116,7 @@ use crate::traits::{ BUS_OK, }; +pub use pi_gfx::{PiGfx, PI_GFX_REGS}; use board::Board; use consts::ODSY_APERTURE; use display::Display; diff --git a/src/dev/odyssey/pi_gfx.rs b/src/dev/odyssey/pi_gfx.rs new file mode 100644 index 000000000..8e303e8bd --- /dev/null +++ b/src/dev/odyssey/pi_gfx.rs @@ -0,0 +1,154 @@ +//! Graphics flow control in the Hub and Bedrock processor interfaces. The +//! Octane has no processor-interface link (its board's `pi` stays `None`), +//! so on that machine the credit calls are no-ops. `PiGfx` is the register +//! block a node's processor interface embeds; its functions are pure state. +//! The credit-link trait and the store-mark helpers the Fuel's graphics board +//! uses are not carried here. +//! +//! Ported from hw/mips/sgi/pi-gfx.c in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; + +pub(super) const PI_GFX_SLOTS: usize = 2; +pub const PI_GFX_REGS: u32 = 10; + +/// The five registers of one slot, in address order. +pub(super) const PI_GFX_PAGE: u32 = 0; +pub(super) const PI_GFX_CREDIT: u32 = 1; +pub(super) const PI_GFX_BIAS: u32 = 2; +pub(super) const PI_GFX_INT_COUNT: u32 = 3; +pub(super) const PI_GFX_SLOT_REGS: u32 = 5; + +/// Page: bit 40 enables it, bits 39:17 name it; the rest is reserved. +pub(super) const PI_GFX_PAGE_ENABLE: u64 = 1 << 40; +pub(super) const PI_GFX_PAGE_ADDRESS: u64 = 0xfffffe0000; +pub(super) const PI_GFX_PAGE_MASK: u64 = PI_GFX_PAGE_ENABLE | PI_GFX_PAGE_ADDRESS; +pub(super) const PI_GFX_CREDIT_MASK: u32 = 0xfff; +pub(super) const PI_GFX_BIAS_MASK: u32 = 0xfff; +pub(super) const PI_GFX_INT_MASK: u32 = 0x3ffffff; + +#[derive(Clone, Copy, Default)] +pub struct PiGfx { + pub page: [u64; PI_GFX_SLOTS], + pub credit: [u32; PI_GFX_SLOTS], + pub bias: [u32; PI_GFX_SLOTS], + pub int_count: [u32; PI_GFX_SLOTS], + pub int_compare: [u32; PI_GFX_SLOTS], + /// The slot's graphics interrupt has been raised for the counter falling + /// below the bias, and not yet re-armed by the counter rising above it. + pub starved: [bool; PI_GFX_SLOTS], + /// Host-only: the zero-length stall has been logged for this slot. + pub stall_logged: [bool; PI_GFX_SLOTS], + /// Bedrock: a counted store left the counter below the bias, and the + /// slot's next counted store waits until the processor has taken the + /// graphics interrupt or the counter has risen above the bias. + pub stopped: [bool; PI_GFX_SLOTS], + /// Bedrock: the interrupt counter runs (the credit counter is at zero). + pub int_running: [bool; PI_GFX_SLOTS], + /// The clock tick at which int_count held its value. + pub int_epoch: [u64; PI_GFX_SLOTS], +} + +impl PiGfx { + /// The counter above the bias re-arms the slot's graphics interrupt. + fn rearm(&mut self, slot: usize) { + if self.credit[slot] > self.bias[slot] { + self.starved[slot] = false; + } + } + + /// Register n of the block, 0 to PI_GFX_REGS - 1, counted in + /// doublewords from PI_GFX_BASE. + pub fn read(&self, n: u32) -> u64 { + let slot = (n / PI_GFX_SLOT_REGS) as usize; + + assert!(n < PI_GFX_REGS); + match n % PI_GFX_SLOT_REGS { + PI_GFX_PAGE => self.page[slot], + PI_GFX_CREDIT => self.credit[slot] as u64, + PI_GFX_BIAS => self.bias[slot] as u64, + PI_GFX_INT_COUNT => self.int_count[slot] as u64, + _ => self.int_compare[slot] as u64, + } + } + + /// A write keeps the field's bits; a write to the credit counter replaces + /// the count. + pub fn write(&mut self, n: u32, value: u64) { + let slot = (n / PI_GFX_SLOT_REGS) as usize; + + assert!(n < PI_GFX_REGS); + match n % PI_GFX_SLOT_REGS { + PI_GFX_PAGE => self.page[slot] = value & PI_GFX_PAGE_MASK, + PI_GFX_CREDIT => { + self.credit[slot] = (value as u32) & PI_GFX_CREDIT_MASK; + self.rearm(slot); + } + PI_GFX_BIAS => self.bias[slot] = (value as u32) & PI_GFX_BIAS_MASK, + PI_GFX_INT_COUNT => self.int_count[slot] = (value as u32) & PI_GFX_INT_MASK, + _ => self.int_compare[slot] = (value as u32) & PI_GFX_INT_MASK, + } + } + + /// Whether the slot's page is enabled and covers physical address `addr`. + pub fn covers(&self, slot: usize, addr: u64) -> bool { + assert!(slot < PI_GFX_SLOTS); + let page = self.page[slot]; + (page & PI_GFX_PAGE_ENABLE) != 0 + && (addr & PI_GFX_PAGE_ADDRESS) == (page & PI_GFX_PAGE_ADDRESS) + } + + /// An uncached I/O store by the processor in `slot` reached physical + /// address bits 39:0 `addr`. Charges one credit when the slot's page is + /// enabled and covers it, and returns whether it did. `who` names the + /// device in log messages. + pub fn count_store(&mut self, slot: usize, addr: u64, who: &str) -> bool { + if !self.covers(slot, addr) { + return false; + } + // Whether the twelve-bit counter wraps, saturates or goes negative is + // unknown. It wraps, and says so. + if self.credit[slot] == 0 { + crate::dlog!(LogModule::Odyssey, "{}: graphics credit counter {} wrapped below zero", who, (b'A' + slot as u8) as char); + } + self.credit[slot] = self.credit[slot].wrapping_sub(1) & PI_GFX_CREDIT_MASK; + true + } + + /// After a counted store: whether the slot's graphics interrupt must be + /// raised now. True once when the counter has fallen below the bias; the + /// counter rising above the bias again re-arms it. The stall that + /// accompanies it on the chip is modelled as zero length. + pub fn take_starved(&mut self, slot: usize, who: &str) -> bool { + assert!(slot < PI_GFX_SLOTS); + if self.starved[slot] || self.credit[slot] >= self.bias[slot] { + return false; + } + self.starved[slot] = true; + if !self.stall_logged[slot] { + self.stall_logged[slot] = true; + crate::dlog!( + LogModule::Odyssey, + "{}: graphics credit counter {} fell below its bias; interrupt raised at once, the write stall and the interrupt counter are not modelled", + who, + (b'A' + slot as u8) as char + ); + } + true + } + +} + +impl PiGfx { + /// `credits` credits returned to `slot`. + pub fn add_credits(&mut self, slot: usize, credits: u32, who: &str) { + assert!(slot < PI_GFX_SLOTS); + let sum = self.credit[slot] as u64 + credits as u64; + if sum > PI_GFX_CREDIT_MASK as u64 { + crate::dlog!(LogModule::Odyssey, "{}: graphics credit counter {} wrapped above {:#x}", who, (b'A' + slot as u8) as char, PI_GFX_CREDIT_MASK); + } + self.credit[slot] = (sum & PI_GFX_CREDIT_MASK as u64) as u32; + self.rearm(slot); + } +} + From 8ce82b52588343ca82b41d42f525a0ff2faf6e78 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:41:17 -0700 Subject: [PATCH 10/38] bridge: take the host chip's addressing from the machine The Bridge hard-coded HEART's widget number and interrupt-packet offset to decide where its interrupts and DMA go. Interrupt delivery already goes through InterruptController and memory through a BusDevice, so no new trait is needed; what is missing is how the host chip is addressed. Another host (the Hub on the Origin boards) differs only in those values: its widget number, taking a packet at any address, and 128 vectors instead of 64. Add XtalkHost (widget number, packet address match, vector count) and have the machine pass it to Bridge::new. The Octane passes HEART_HOST, so the comparisons that were against constants are against a field of the same value, and nothing about the Octane's behaviour moves. IOC3 needs no change: it reaches the host through the Bridge's PCI pins and DMA. Tests build a Bridge with other hosts and check that packets follow them. Measured with iris-bench run --quick --cpu r10000, ship profile, A B B A x4 per side. Host instructions per guest instruction (perf stat instructions:u / icount, median of 4): base branch interpreter 551.8 550.9 (-0.15%) jitv2 could not be measured: the host load average was about 100 and the compile threads swamp the count. The bench kernels do not reach the Bridge, so this shows the build is not disturbed, not the packet path itself; that gained one mask and compare per interrupt packet and none per DMA byte. The numbers predate the final packet-address mask. --- src/dev/bridge.rs | 57 +++++++++++++++++++---------------------- src/dev/bridge_tests.rs | 48 ++++++++++++++++++++++++++++++++-- src/ip30.rs | 5 ++++ src/ip30/tests.rs | 4 +-- src/ip30_machine.rs | 2 +- src/lib.rs | 1 + src/xtalk.rs | 33 ++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 src/xtalk.rs diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index 7c890939b..fc7cf3c2f 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -57,10 +57,9 @@ use crate::irq::{InterruptController, IrqLine}; use crate::pci::{bar_runs, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; use crate::snapshot::{field, field_u32, field_u32s, field_u64, field_u64_vec, field_u8, field_u8s, StateTable}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Saveable, BUS_ERR, BUS_OK}; +use crate::xtalk::XtalkHost; -/// The widgets this chip addresses by number on the Crossbow: HEART takes -/// its interrupt packets and memory transfers, widget 15 is its own port. -const WIDGET_HEART: u8 = 0x8; +/// The widget number of this chip's own port on the Crossbow. const WIDGET_BRIDGE: u8 = 0xf; /// A widget's 16 MB small window. const WIDGET_SMALL_SIZE: u32 = 0x0100_0000; @@ -149,8 +148,6 @@ fn window_slot(off: u32) -> Option { } const SLOT_DATA: u32 = 4; -/// Where in HEART's widget space an interrupt packet lands. -const HEART_INT_PACKET: u64 = 0x80; const REGS_DEFINED: u32 = 0x300; const REG_SLOTS: usize = (REGS_DEFINED / 8) as usize; /// The one reserved data half this chip answers specially. @@ -375,7 +372,7 @@ struct FlashPart { size: u32, } -/// The packets a locked section decided to send to HEART: (vector, set). +/// The packets a locked section decided to send to the host: (vector, set). type Packets = Vec<(u8, bool)>; /// The onboard Bridge. Hand it out as the `BusDevice` of the widget's small @@ -384,13 +381,16 @@ type Packets = Vec<(u8, bool)>; pub struct Bridge { st: Mutex, /// Held across "decide a packet, send it", never with `st`. - /// Nothing but HEART's `set_line` is called under it. + /// Nothing but the host's `set_line` is called under it. irq_order: Mutex<()>, /// Memory as a bus master reaches it: physical addresses, fitted SDRAM /// only, a bus error for anything else. mem: Arc, - /// HEART: a line is an interrupt bit, the vector a packet names. + /// The host chip's interrupt inputs: a line is the vector a packet names. heart: Arc, + /// How the host chip is addressed: it takes this chip's interrupt + /// packets and memory transfers. + host: XtalkHost, slots: RwLock<[Option>; DEVICES]>, flash: RwLock<[Option; 2]>, flash_strap: AtomicBool, @@ -399,7 +399,7 @@ pub struct Bridge { /// The one-wire net behind the NIC register; none fitted reads as an /// empty bus. microlan: Mutex>, - /// Whether the Crossbow has a live link to a widget; default only HEART. + /// Whether the Crossbow has a live link to a widget; default only the host chip. link_alive: RwLock bool + Send + Sync>>, /// Where the runs of the device windows a BAR decodes whole go (the /// machine maps them over this widget's window), and the table entries @@ -487,10 +487,10 @@ impl Placement { } impl Bridge { - /// A Bridge that masters memory through `mem` and interrupts HEART - /// through `heart` (line = vector). Registers start at their power-up - /// values. - pub fn new(mem: Arc, heart: Arc) -> Arc { + /// A Bridge that masters memory through `mem` and interrupts the host + /// chip, addressed as `host`, through `heart` (line = vector). Registers + /// start at their power-up values. + pub fn new(mem: Arc, host: XtalkHost, heart: Arc) -> Arc { let mut st = State { reg: [0; REG_SLOTS], ate: [0; ATE_ENTRIES], @@ -509,12 +509,13 @@ impl Bridge { irq_order: Mutex::new(()), mem, heart, + host, slots: RwLock::new(Default::default()), flash: RwLock::new(Default::default()), flash_strap: AtomicBool::new(false), revision: AtomicU32::new(REVISION), microlan: Mutex::new(None), - link_alive: RwLock::new(Box::new(|w| w == WIDGET_HEART)), + link_alive: RwLock::new(Box::new(move |w| w == host.widget)), bar_sink: RwLock::new(None), bar_entries: Mutex::new(Default::default()), }) @@ -529,7 +530,7 @@ impl Bridge { /// Run `f` on the state, then send the packets it decided on. The /// order lock spans both, so packets for one pin leave in the order - /// they were decided; HEART is called with the state lock dropped. + /// they were decided; the host is called with the state lock dropped. fn irq_locked(&self, f: impl FnOnce(&mut State, &mut Packets) -> R) -> R { let _order = self.irq_order.lock(); let mut packets = Packets::new(); @@ -649,7 +650,7 @@ impl Bridge { /// Tell the Bridge which widgets the Crossbow has a live link to. A DMA /// request to any other widget is dropped by the Crossbow, which the - /// Bridge's request timer reports. The default is HEART alone. + /// Bridge's request timer reports. The default is the host chip alone. pub fn set_link_alive(&self, alive: Box bool + Send + Sync>) { *self.link_alive.write() = alive; } @@ -768,15 +769,15 @@ impl Bridge { if level && !was && enabled { self.send_interrupt(st, p, pin, true); } else if !level && was && enabled && st.r(reg::INT_MODE) >> pin & 1 != 0 { - // A falling edge clears HEART's bit only when the mode bit asks; + // A falling edge clears the host's bit only when the mode bit asks; // otherwise the guest clears it in its own handler. self.send_interrupt(st, p, pin, false); } } /// A pin's interrupt goes to the destination pair's widget with the - /// vector from that pin's host register. HEART is the only widget that - /// takes it; a pair naming any other has nowhere to go. The packet is + /// vector from that pin's host register. The host chip is the only + /// widget that takes it; a pair naming any other has nowhere to go. The packet is /// queued for `irq_locked` to send once the state lock is dropped. fn send_interrupt(&self, st: &State, p: &mut Packets, pin: usize, set: bool) { let host = st.r(reg::int_host(pin as u32)); @@ -790,25 +791,19 @@ impl Bridge { // destination address. let field = ((host & INT_HOST_ADDRESS) >> 8) as u64; let address = (dest.address & !(0x3ffu64 << 38)) | field << 38; - if !Self::heart_accepts(&dest, address, (host & INT_HOST_FIELD) as u8) { - log::debug!("bridge: interrupt packet to widget {} address {address:#x} is not HEART's; dropped", dest.widget); + if !self.host.accepts(dest.widget, address, (host & INT_HOST_FIELD) as u8) { + log::debug!("bridge: interrupt packet to widget {} address {address:#x} is not the host's (widget {}); dropped", dest.widget, self.host.widget); return; } p.push(((host & INT_HOST_FIELD) as u8, set)); } - /// HEART takes an interrupt packet only at its interrupt-set offset - /// (0x80), naming one of its 64 bits. - fn heart_accepts(dest: &IntDest, address: u64, vector: u8) -> bool { - dest.widget == WIDGET_HEART && address == HEART_INT_PACKET && vector < 64 - } - /// Chip errors go to the host-error bit at the destination pair. fn raise_host_error(&self, st: &State, p: &mut Packets) { let number = (st.r(reg::HOST_ERR_FIELD) & INT_HOST_FIELD) as u8; let dest = int_dest(st.r(reg::INT_DEST_UPPER), st.r(reg::INT_DEST_LOWER)); // An unrouted error is one nobody asked for. - if !Self::heart_accepts(&dest, dest.address, number) { + if !self.host.accepts(dest.widget, dest.address, number) { log::debug!("bridge: error interrupt with no usable destination; dropped"); return; } @@ -1292,15 +1287,15 @@ impl Bridge { st.dma_device = slot as u32; self.translate(&st, slot, a) }; - // Only HEART is memory. The Crossbow drops a request to a widget + // Only the host chip is memory. The Crossbow drops a request to a widget // it has no live link to (1 to 7 are illegal, 15 is this Bridge's // own port), and the Bridge's request timer reports a dropped - // read. A refused translation, or a read that reaches HEART and + // read. A refused translation, or a read that reaches the host and // finds no memory, is a silent master abort. let mut dropped = false; let good = match target { None => false, - Some((_, _, w)) if w != WIDGET_HEART => { + Some((_, _, w)) if w != self.host.widget => { // Approximation: QEMU routes peer DMA to a fitted widget; // no supported Octane guest is known to need it. dropped = w == WIDGET_BRIDGE || !(self.link_alive.read())(w); diff --git a/src/dev/bridge_tests.rs b/src/dev/bridge_tests.rs index fc13bd69f..8bce72583 100644 --- a/src/dev/bridge_tests.rs +++ b/src/dev/bridge_tests.rs @@ -7,6 +7,7 @@ //! Ported from tests/qtest/sgi-bridge-bus-test.c in jamesbraid/qemu-sgi-origami. use super::*; +use crate::ip30::{HEART_HOST, WIDGET_HEART}; use crate::irq::IrqLine; use crate::pci::{Bar, PciConfig, PCI_COMMAND_IO, PCI_COMMAND_MEMORY, PCI_INTA}; @@ -168,9 +169,13 @@ struct Rig { impl Rig { fn new() -> Self { + Self::with_host(HEART_HOST) + } + + fn with_host(host: XtalkHost) -> Self { let sink = Arc::new(Sink::default()); let mem = Arc::new(Mem::new()); - Rig { bridge: Bridge::new(mem.clone(), sink.clone()), sink, mem } + Rig { bridge: Bridge::new(mem.clone(), host, sink.clone()), sink, mem } } /// Write register `off` (a slot base such as `reg::CONTROL`). @@ -920,6 +925,45 @@ fn interrupt_packets_must_land_on_hearts_interrupt_offset() { assert!(r.sink.take().is_empty()); } +/// The host chip's widget number, packet offset and vector count are the +/// machine's to state: a packet follows the host it was built with. +#[test] +fn interrupt_packets_follow_the_host_the_bridge_was_built_with() { + let host = XtalkHost { widget: 0xa, int_packet: 0x40, int_packet_mask: !0, vectors: 16 }; + let r = Rig::with_host(host); + let c = r.card(2, PCI_INTA); + r.set(reg::INT_ENABLE, 1 << 2); + r.set(reg::int_host(2), 5); + // HEART's address is nothing to this host. + r.set(reg::INT_DEST_UPPER, (WIDGET_HEART as u32) << 16); + r.set(reg::INT_DEST_LOWER, 0x80); + c.raise(true); + c.raise(false); + assert!(r.sink.take().is_empty()); + r.set(reg::INT_DEST_UPPER, 0xa << 16); + r.set(reg::INT_DEST_LOWER, 0x40); + c.raise(true); + c.raise(false); + assert_eq!(r.sink.take(), [(5, true)]); + r.set(reg::int_host(2), 16); + c.raise(true); + assert!(r.sink.take().is_empty(), "only 16 vectors exist"); +} + +/// A host with an empty packet mask takes a packet at any address, and its +/// vector count can pass 64. +#[test] +fn a_host_with_no_packet_address_takes_any_address_and_over_64_vectors() { + let r = Rig::with_host(XtalkHost { widget: 0xa, int_packet: 0, int_packet_mask: 0, vectors: 128 }); + let c = r.card(2, PCI_INTA); + r.set(reg::INT_ENABLE, 1 << 2); + r.set(reg::INT_DEST_UPPER, 0xa << 16); + r.set(reg::INT_DEST_LOWER, 0x1230); + r.set(reg::int_host(2), 100); + c.raise(true); + assert_eq!(r.sink.take(), [(100, true)]); +} + // ---- flash ---- fn image() -> Vec { @@ -1091,7 +1135,7 @@ fn a_packet_reaches_heart_outside_the_state_lock() { } } let heart = Arc::new(Prying(Mutex::new(None), Mutex::new(Vec::new()))); - let bridge = Bridge::new(Arc::new(Mem::new()), heart.clone()); + let bridge = Bridge::new(Arc::new(Mem::new()), HEART_HOST, heart.clone()); *heart.0.lock() = Some(bridge.clone()); let r = Rig { bridge, sink: Arc::new(Sink::default()), mem: Arc::new(Mem::new()) }; r.route(4, 3); diff --git a/src/ip30.rs b/src/ip30.rs index 369e9d766..4bc57da89 100644 --- a/src/ip30.rs +++ b/src/ip30.rs @@ -40,6 +40,11 @@ pub const WIDGET_XBOW: u8 = 0x0; pub const WIDGET_HEART: u8 = 0x8; pub const WIDGET_BRIDGE: u8 = 0xf; +/// HEART as the widgets address it: widget 8, taking an interrupt packet at +/// offset 0x80 that names one of its 64 interrupt bits. +pub const HEART_HOST: crate::xtalk::XtalkHost = + crate::xtalk::XtalkHost { widget: WIDGET_HEART, int_packet: 0x80, int_packet_mask: !0, vectors: 64 }; + /// Widget `w`'s small window: 16 MiB at `0x1000_0000 + (w << 24)`. pub const fn widget_small_base(w: u8) -> u64 { 0x1000_0000 | (w as u64) << 24 diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 4f9c89e86..884289588 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -63,7 +63,7 @@ fn board_with(prom: &[u8], banks_mb: [u32; 4]) -> Board { let heart = Arc::new(Heart::new(HeartConfig::octane(banks_mb.map(|mb| (mb as u64) << 20)), crate::dev::heart::host_clock())); let xbow = Arc::new(Xbow::new(XBOW_REVISION, 1 << WIDGET_HEART | 1 << WIDGET_BRIDGE)); xbow.connect_heart(heart.clone()); - let bridge = Bridge::new(ram.dma_view(), heart.clone()); + let bridge = Bridge::new(ram.dma_view(), HEART_HOST, heart.clone()); bridge.attach_flash(Arc::new(RawFlash::new(prom, FLASH_SIZE)), FLASH_SIZE as u32); let phys = Ip30Bus::new(ram); phys.map(PIU_BASE, PIU_BASE + PIU_SIZE, heart.clone(), "heart piu"); @@ -462,7 +462,7 @@ impl Octane { ram.set_memcfg([0x8001_0000, 0, 0, 0]); let mem = ram.dma_view(); let heart = Arc::new(Heart::new(HeartConfig::octane([64 << 20, 0, 0, 0]), Arc::new(|| 0))); - let bridge = Bridge::new(mem.clone(), heart.clone()); + let bridge = Bridge::new(mem.clone(), HEART_HOST, heart.clone()); let eth = Ioc3Eth::with_negotiation_delay(MAC, std::time::Duration::ZERO); let (link, sent, inject) = crate::net::Link::loopback(); diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 679f23b8d..25bf44af0 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -219,7 +219,7 @@ impl OctaneMachine { // The onboard Bridge: memory and interrupts through HEART, the boot // flash on its first chip select, the system board's record on its // MicroLAN. - let bridge = Bridge::new(mem.clone(), heart.clone()); + let bridge = Bridge::new(mem.clone(), crate::ip30::HEART_HOST, heart.clone()); bridge.set_revision(if octane2 { BRIDGE_REVISION_OCTANE2 } else { BRIDGE_REVISION }); // The flash holds the PROM's own records as well as its code; with // `[octane] flash` they last from run to run. diff --git a/src/lib.rs b/src/lib.rs index 9268a6b71..02f2c4645 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,6 +202,7 @@ pub mod scsi_monitor; pub mod irq; pub mod host_audio; pub mod pci; +pub mod xtalk; pub mod ui; pub mod cpu; pub mod dev; diff --git a/src/xtalk.rs b/src/xtalk.rs new file mode 100644 index 000000000..33a2dd45c --- /dev/null +++ b/src/xtalk.rs @@ -0,0 +1,33 @@ +//! The chip that sits at the top of a widget bus and takes what the widgets +//! send upstream: interrupt packets and memory transfers. +//! +//! Its interrupt inputs are an [`InterruptController`](crate::irq::InterruptController) +//! like any other. What the widgets also need to know is how it is +//! addressed: the widget number their requests name, where in that +//! widget's space an interrupt packet is accepted, and how many +//! vectors a packet can name. A machine states these once when it builds the +//! Bridge and the Crossbow, and they compare against them where they used to +//! compare against constants. + +/// How a widget bus's host chip is addressed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct XtalkHost { + /// The widget number requests for the host name. + pub widget: u8, + /// Where in the host's widget space an interrupt packet is taken: the + /// address bits in `int_packet_mask` must equal those of `int_packet`. + /// A host that takes a packet at any address has a zero mask. + pub int_packet: u64, + pub int_packet_mask: u64, + /// Vectors a packet can name: 0..vectors. + pub vectors: u16, +} + +impl XtalkHost { + /// Whether an interrupt packet to `widget` at `address` naming `vector` + /// is one the host takes. + #[inline] + pub fn accepts(&self, widget: u8, address: u64, vector: u8) -> bool { + widget == self.widget && address & self.int_packet_mask == self.int_packet && (vector as u16) < self.vectors + } +} From 903aa0b208c5849e78c48de6c3a700dc43d66e0b Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 09:36:42 -0700 Subject: [PATCH 11/38] bridge: send to the widget the guest programs The Bridge had one host fixed at build time and dropped any transfer or interrupt packet whose widget was not that host. On a machine with two host chips behind one Crossbow the guest programs its own node's widget in the direct map, the translation entries and the interrupt destination, and with the other node as the host both the transfer and its completion interrupt were refused. Bridge::new now takes an Upstream: the host chips it can reach, indexed by widget number, each with its addressing, memory view and interrupt inputs. The widget the guest programmed selects the entry, so on the single-host path the transfer swaps one compare against a constant for one indexed load, with no lock and no dyn call beyond the memory access it already made. A widget with no entry is logged and dropped, as before. A machine with one host (HEART, a Hub with no Crossbow) builds Upstream::single, so the Octane behaves as it did. A host behind a Crossbow carries that port's link (Xbow::upstream): the Bridge asks it once per transfer chunk and once per interrupt packet, never per bus word, and a dropped read is timed out as the Crossbow's drop is. That check is one branch per 4 KB chunk on the single-host path (no link to ask) and a dyn call per chunk behind a Crossbow. No machine uses Xbow::upstream yet; the Octane and the Origin 200 build Upstream::single. Tests put two hosts behind a Crossbow and move DMA, translation-entry transfers, device and chip-error interrupts between them by reprogramming the target widget, and check that a host with its link down gets nothing and times out a read. Measured on the transfer path with a bridge test loop (4 KB DMA write plus 4 KB read through the direct map, ship profile, every function aligned to 64 bytes on both sides, A B B A x4, host load about 100), ns per 4 KB transfer, median of the per-run minimums: before 7119 after 6930 (-2.7%, inside run-to-run noise of about +-5%) An Octane IRIX guest booted to the login prompt on the new build, ran hinv and df against its SCSI disk, and halted cleanly. The Origin 200 shares this transfer path (Upstream::single), so the same figure applies; no Origin 200 machine was run. --- src/dev/bridge.rs | 111 +++++++++++++++++++++------------------- src/dev/bridge_tests.rs | 100 +++++++++++++++++++++++++++++++++++- src/dev/xbow.rs | 14 +++++ src/ip30/tests.rs | 5 +- src/ip30_machine.rs | 3 +- src/xtalk.rs | 83 ++++++++++++++++++++++++++++++ 6 files changed, 259 insertions(+), 57 deletions(-) diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index fc7cf3c2f..46ed7c255 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -57,7 +57,7 @@ use crate::irq::{InterruptController, IrqLine}; use crate::pci::{bar_runs, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; use crate::snapshot::{field, field_u32, field_u32s, field_u64, field_u64_vec, field_u8, field_u8s, StateTable}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Saveable, BUS_ERR, BUS_OK}; -use crate::xtalk::XtalkHost; +use crate::xtalk::Upstream; /// The widget number of this chip's own port on the Crossbow. const WIDGET_BRIDGE: u8 = 0xf; @@ -372,8 +372,8 @@ struct FlashPart { size: u32, } -/// The packets a locked section decided to send to the host: (vector, set). -type Packets = Vec<(u8, bool)>; +/// The packets a locked section decided to send: (host widget, vector, set). +type Packets = Vec<(u8, u8, bool)>; /// The onboard Bridge. Hand it out as the `BusDevice` of the widget's small /// window: `Arc` coerces to `Arc`. As an @@ -383,14 +383,9 @@ pub struct Bridge { /// Held across "decide a packet, send it", never with `st`. /// Nothing but the host's `set_line` is called under it. irq_order: Mutex<()>, - /// Memory as a bus master reaches it: physical addresses, fitted SDRAM - /// only, a bus error for anything else. - mem: Arc, - /// The host chip's interrupt inputs: a line is the vector a packet names. - heart: Arc, - /// How the host chip is addressed: it takes this chip's interrupt - /// packets and memory transfers. - host: XtalkHost, + /// The host chips this Bridge's transfers and interrupt packets can go + /// to, by the widget number the guest programs. + up: Upstream, slots: RwLock<[Option>; DEVICES]>, flash: RwLock<[Option; 2]>, flash_strap: AtomicBool, @@ -399,7 +394,7 @@ pub struct Bridge { /// The one-wire net behind the NIC register; none fitted reads as an /// empty bus. microlan: Mutex>, - /// Whether the Crossbow has a live link to a widget; default only the host chip. + /// Whether the Crossbow has a live link to a widget; default the hosts. link_alive: RwLock bool + Send + Sync>>, /// Where the runs of the device windows a BAR decodes whole go (the /// machine maps them over this widget's window), and the table entries @@ -487,10 +482,10 @@ impl Placement { } impl Bridge { - /// A Bridge that masters memory through `mem` and interrupts the host - /// chip, addressed as `host`, through `heart` (line = vector). Registers - /// start at their power-up values. - pub fn new(mem: Arc, host: XtalkHost, heart: Arc) -> Arc { + /// A Bridge that masters memory and interrupts the host chips of `up`, + /// each addressed as it states and chosen by the widget number the guest + /// programs. Registers start at their power-up values. + pub fn new(up: Upstream) -> Arc { let mut st = State { reg: [0; REG_SLOTS], ate: [0; ATE_ENTRIES], @@ -504,18 +499,17 @@ impl Bridge { dma_device: 0, }; st.reset_core(false, REVISION); + let hosts = (0..16u8).filter(|&w| up.get(w).is_some()).fold(0u16, |m, w| m | 1 << w); Arc::new(Self { st: Mutex::new(st), irq_order: Mutex::new(()), - mem, - heart, - host, + up, slots: RwLock::new(Default::default()), flash: RwLock::new(Default::default()), flash_strap: AtomicBool::new(false), revision: AtomicU32::new(REVISION), microlan: Mutex::new(None), - link_alive: RwLock::new(Box::new(move |w| w == host.widget)), + link_alive: RwLock::new(Box::new(move |w| hosts.checked_shr(w as u32).is_some_and(|m| m & 1 != 0))), bar_sink: RwLock::new(None), bar_entries: Mutex::new(Default::default()), }) @@ -535,8 +529,11 @@ impl Bridge { let _order = self.irq_order.lock(); let mut packets = Packets::new(); let ret = f(&mut self.st.lock(), &mut packets); - for (vector, set) in packets { - self.heart.set_line(vector as u32, set); + for (widget, vector, set) in packets { + match self.up.get(widget) { + Some(h) if h.link_up() => h.irq.set_line(vector as u32, set), + _ => log::debug!("bridge: interrupt packet to widget {widget} dropped; no live link to a host"), + } } ret } @@ -791,11 +788,12 @@ impl Bridge { // destination address. let field = ((host & INT_HOST_ADDRESS) >> 8) as u64; let address = (dest.address & !(0x3ffu64 << 38)) | field << 38; - if !self.host.accepts(dest.widget, address, (host & INT_HOST_FIELD) as u8) { - log::debug!("bridge: interrupt packet to widget {} address {address:#x} is not the host's (widget {}); dropped", dest.widget, self.host.widget); - return; + let vector = (host & INT_HOST_FIELD) as u8; + match self.up.get(dest.widget) { + Some(h) if h.host.accepts(dest.widget, address, vector) => p.push((dest.widget, vector, set)), + Some(_) => log::debug!("bridge: interrupt packet to widget {} address {address:#x} vector {vector} is not one its host takes; dropped", dest.widget), + None => log::debug!("bridge: interrupt packet to widget {}, which is not a host; dropped", dest.widget), } - p.push(((host & INT_HOST_FIELD) as u8, set)); } /// Chip errors go to the host-error bit at the destination pair. @@ -803,11 +801,10 @@ impl Bridge { let number = (st.r(reg::HOST_ERR_FIELD) & INT_HOST_FIELD) as u8; let dest = int_dest(st.r(reg::INT_DEST_UPPER), st.r(reg::INT_DEST_LOWER)); // An unrouted error is one nobody asked for. - if !self.host.accepts(dest.widget, dest.address, number) { - log::debug!("bridge: error interrupt with no usable destination; dropped"); - return; + match self.up.get(dest.widget) { + Some(h) if h.host.accepts(dest.widget, dest.address, number) => p.push((dest.widget, number, true)), + _ => log::debug!("bridge: error interrupt with no usable destination; dropped"), } - p.push((number, true)); } /// The first enabled error in the PCI group owns its capture registers @@ -1295,15 +1292,25 @@ impl Bridge { let mut dropped = false; let good = match target { None => false, - Some((_, _, w)) if w != self.host.widget => { - // Approximation: QEMU routes peer DMA to a fitted widget; - // no supported Octane guest is known to need it. - dropped = w == WIDGET_BRIDGE || !(self.link_alive.read())(w); - false - } - Some((xio, p, _)) => match &mut buf { - DmaBuf::Read(b) => self.placed_read(xio, p, &mut b[done..done + step]), - DmaBuf::Write(b) => self.placed_write(xio, p, &b[done..done + step]), + Some((xio, p, w)) => match self.up.get(w) { + Some(h) if !h.link_up() => { + // The Crossbow drops it; a dropped read is timed out. + log::debug!("bridge: transfer to widget {w}, whose link is down; dropped"); + dropped = true; + false + } + Some(h) => match &mut buf { + DmaBuf::Read(b) => self.placed_read(&*h.mem, xio, p, &mut b[done..done + step]), + DmaBuf::Write(b) => self.placed_write(&*h.mem, xio, p, &b[done..done + step]), + }, + None => { + // Approximation: a transfer to a widget that is not a + // host is dropped, not routed to a peer; no supported + // guest is known to need peer DMA. + log::debug!("bridge: transfer to widget {w}, which is not a host; dropped"); + dropped = w == WIDGET_BRIDGE || !(self.link_alive.read())(w); + false + } }, }; if !good { @@ -1322,47 +1329,47 @@ impl Bridge { /// Memory bytes at `addr`, as every DMA master moves them /// (`physical::read_bytes`); a failed read leaves the buffer zero. - fn mem_read(&self, addr: u64, out: &mut [u8]) -> bool { - let ok = crate::physical::read_bytes(&*self.mem, addr, out) == crate::traits::BUS_OK; + fn mem_read(&self, mem: &dyn BusDevice, addr: u64, out: &mut [u8]) -> bool { + let ok = crate::physical::read_bytes(mem, addr, out) == crate::traits::BUS_OK; if !ok { out.fill(0); } ok } - fn mem_write(&self, addr: u64, data: &[u8]) -> bool { - crate::physical::write_bytes(&*self.mem, addr, data) == crate::traits::BUS_OK + fn mem_write(&self, mem: &dyn BusDevice, addr: u64, data: &[u8]) -> bool { + crate::physical::write_bytes(mem, addr, data) == crate::traits::BUS_OK } /// Read `out.len()` bytes at PCI-order position `xio`, undoing the /// placement: the PCI byte at address p is memory byte p ^ mask. - fn placed_read(&self, xio: u64, placement: Placement, out: &mut [u8]) -> bool { + fn placed_read(&self, mem: &dyn BusDevice, xio: u64, placement: Placement, out: &mut [u8]) -> bool { let mask = placement.mask(); if mask == 0 { - return self.mem_read(xio, out); + return self.mem_read(mem, xio, out); } let unit = mask as usize + 1; if xio & mask == 0 && out.len() % unit == 0 { // Whole units: the bytes come straight into place and each unit // is reversed where it lies. - let ok = self.mem_read(xio, out); + let ok = self.mem_read(mem, xio, out); out.chunks_exact_mut(unit).for_each(<[u8]>::reverse); return ok; } let lo = xio & !mask; let hi = (xio + out.len() as u64 + mask) & !mask; let mut span = vec![0u8; (hi - lo) as usize]; - let ok = self.mem_read(lo, &mut span); + let ok = self.mem_read(mem, lo, &mut span); for (i, b) in out.iter_mut().enumerate() { *b = if ok { span[(((xio + i as u64) ^ mask) - lo) as usize] } else { 0 }; } ok } - fn placed_write(&self, xio: u64, placement: Placement, data: &[u8]) -> bool { + fn placed_write(&self, mem: &dyn BusDevice, xio: u64, placement: Placement, data: &[u8]) -> bool { let mask = placement.mask(); if mask == 0 { - return self.mem_write(xio, data); + return self.mem_write(mem, xio, data); } let unit = mask as usize + 1; if xio & mask == 0 && data.len() % unit == 0 { @@ -1377,7 +1384,7 @@ impl Bridge { }; swapped.copy_from_slice(data); swapped.chunks_exact_mut(unit).for_each(<[u8]>::reverse); - return self.mem_write(xio, swapped); + return self.mem_write(mem, xio, swapped); } let lo = xio & !mask; let hi = (xio + data.len() as u64 + mask) & !mask; @@ -1386,14 +1393,14 @@ impl Bridge { span[(((xio + i as u64) ^ mask) - lo) as usize] = b; } if xio == lo && xio + data.len() as u64 == hi { - return self.mem_write(lo, &span); + return self.mem_write(mem, lo, &span); } // Partial units: write only the bytes covered, so a neighbour // another master is changing is not overwritten with a stale copy. let mut ok = true; for i in 0..data.len() as u64 { let p = (xio + i) ^ mask; - ok &= self.mem_write(p, &span[(p - lo) as usize..(p - lo) as usize + 1]); + ok &= self.mem_write(mem, p, &span[(p - lo) as usize..(p - lo) as usize + 1]); } ok } diff --git a/src/dev/bridge_tests.rs b/src/dev/bridge_tests.rs index 8bce72583..8bc2e7a21 100644 --- a/src/dev/bridge_tests.rs +++ b/src/dev/bridge_tests.rs @@ -10,6 +10,7 @@ use super::*; use crate::ip30::{HEART_HOST, WIDGET_HEART}; use crate::irq::IrqLine; use crate::pci::{Bar, PciConfig, PCI_COMMAND_IO, PCI_COMMAND_MEMORY, PCI_INTA}; +use crate::xtalk::{Upstream, XtalkHost}; /// HEART stand-in: the interrupt packets it received, in order. A line is /// the ISR bit, so `set_line(vector, set)` is a packet. @@ -175,7 +176,7 @@ impl Rig { fn with_host(host: XtalkHost) -> Self { let sink = Arc::new(Sink::default()); let mem = Arc::new(Mem::new()); - Rig { bridge: Bridge::new(mem.clone(), host, sink.clone()), sink, mem } + Rig { bridge: Bridge::new(Upstream::single(host, mem.clone(), sink.clone())), sink, mem } } /// Write register `off` (a slot base such as `reg::CONTROL`). @@ -964,6 +965,101 @@ fn a_host_with_no_packet_address_takes_any_address_and_over_64_vectors() { assert_eq!(r.sink.take(), [(100, true)]); } +/// Behind a Crossbow with two hosts, the widget the guest programs picks the +/// host: the direct map moves DMA and the interrupt destination moves +/// packets, and a host whose link is down hears neither. +#[test] +fn the_programmed_widget_picks_the_host_behind_a_crossbow() { + use crate::dev::xbow::Xbow; + use crate::xtalk::HostPort; + let host_b = XtalkHost { widget: 10, int_packet: 0, int_packet_mask: 0, vectors: 128 }; + let build = |links: u16| { + let xbow = Arc::new(Xbow::new(5, links)); + let (mem_a, sink_a) = (Arc::new(Mem::new()), Arc::new(Sink::default())); + let (mem_b, sink_b) = (Arc::new(Mem::new()), Arc::new(Sink::default())); + let up = xbow.upstream(vec![ + HostPort::new(HEART_HOST, mem_a.clone(), sink_a.clone()), + HostPort::new(host_b, mem_b.clone(), sink_b.clone()), + ]); + let r = Rig { bridge: Bridge::new(up), sink: sink_a.clone(), mem: mem_a }; + (r, mem_b, sink_b) + }; + let (r, mem_b, sink_b) = build(1 << 8 | 1 << 10 | 1 << 15); + let c = r.card(2, PCI_INTA); + let dma_to = |widget: u32| r.set(reg::DIRECT_MAP, widget << DIRECT_WIDGET_SHIFT); + + dma_to(8); + assert!(c.dma().dma_write(0x8000_1000, &[1, 2, 3, 4]).is_ok()); + assert_eq!(r.mem.peek(0x1000, 4), [4, 3, 2, 1]); + assert_eq!(mem_b.peek(0x1000, 4), [0; 4]); + dma_to(10); + assert!(c.dma().dma_write(0x8000_2000, &[5, 6, 7, 8]).is_ok()); + assert_eq!(mem_b.peek(0x2000, 4), [8, 7, 6, 5]); + assert_eq!(r.mem.peek(0x2000, 4), [0; 4]); + let mut back = [0u8; 4]; + assert!(c.dma().dma_read(0x8000_2000, &mut back).is_ok()); + assert_eq!(back, [5, 6, 7, 8]); + // A widget with no host is dropped, as an unreachable one always was. + dma_to(9); + assert_eq!(c.dma().dma_write(0x8000_3000, &[1]), PciDmaStatus::MasterAbort); + + r.set(reg::INT_ENABLE, 1 << 2); + r.set(reg::int_host(2), 5); + r.set(reg::INT_DEST_UPPER, 8 << 16); + r.set(reg::INT_DEST_LOWER, 0x80); + c.raise(true); + c.raise(false); + assert_eq!(r.sink.take(), [(5, true)]); + assert!(sink_b.take().is_empty()); + r.set(reg::INT_DEST_UPPER, 10 << 16); + r.set(reg::INT_DEST_LOWER, 0); + c.raise(true); + c.raise(false); + assert_eq!(sink_b.take(), [(5, true)]); + assert!(r.sink.take().is_empty()); + + // A translation entry names its own widget. + r.set(reg::device(2), r.get(reg::device(2)) | DEV_SWAP_TRANSLATED); + let entry = |w: u32, a: u64| ATE_VALID | ((w as u64) << ATE_TARGET_WIDGET_SHIFT) | a; + r.bridge.write64(u64::from(ATE_BASE), entry(10, 0x5000)); + r.bridge.write64(u64::from(ATE_BASE + 8), entry(8, 0x6000)); + assert!(c.dma().dma_write(0x4000_0000, &[1, 2, 3, 4]).is_ok()); + assert!(c.dma().dma_write(0x4000_1000, &[5, 6, 7, 8]).is_ok()); + assert_eq!(mem_b.peek(0x5000, 4), [1, 2, 3, 4]); + assert_eq!(r.mem.peek(0x5000, 4), [0; 4]); + assert_eq!(r.mem.peek(0x6000, 4), [5, 6, 7, 8]); + assert_eq!(mem_b.peek(0x6000, 4), [0; 4]); + + // The chip's own error interrupt goes to the destination widget too. + r.set(reg::INT_ENABLE, INT_PCI_MASTER_TIMEOUT); + r.set(reg::INT_DEST_UPPER, 10 << 16); + assert!(!r.bridge.read32(u64::from(Rig::cfg(5, 0))).is_ok()); + assert_eq!(sink_b.take().len(), 1); + assert!(r.sink.take().is_empty()); + r.set(reg::INT_RESET, INT_RESET_PCI_GRP | INT_RESET_MULTI_ERR); + r.set(reg::INT_DEST_UPPER, 8 << 16); + r.set(reg::INT_DEST_LOWER, 0x80); + assert!(!r.bridge.read32(u64::from(Rig::cfg(5, 0))).is_ok()); + assert_eq!(r.sink.take().len(), 1); + assert!(sink_b.take().is_empty()); + + // With no live link to host B, neither its DMA nor its interrupt arrives, + // and a read the Crossbow drops is timed out. + let (r, mem_b, sink_b) = build(1 << 8 | 1 << 15); + let c = r.card(2, PCI_INTA); + r.set(reg::DIRECT_MAP, 10 << DIRECT_WIDGET_SHIFT); + assert_eq!(c.dma().dma_write(0x8000_1000, &[1]), PciDmaStatus::MasterAbort); + assert_eq!(mem_b.peek(0x1000, 1), [0]); + r.set(reg::INT_ENABLE, INT_XREAD_REQ_TIMEOUT); + assert_eq!(c.dma().dma_read(0x8000_1000, &mut [0u8; 4]), PciDmaStatus::MasterAbort); + assert_ne!(r.get(reg::INT_STATUS) & INT_XREAD_REQ_TIMEOUT, 0); + r.set(reg::INT_ENABLE, 1 << 2); + r.set(reg::int_host(2), 5); + r.set(reg::INT_DEST_UPPER, 10 << 16); + c.raise(true); + assert!(sink_b.take().is_empty()); +} + // ---- flash ---- fn image() -> Vec { @@ -1135,7 +1231,7 @@ fn a_packet_reaches_heart_outside_the_state_lock() { } } let heart = Arc::new(Prying(Mutex::new(None), Mutex::new(Vec::new()))); - let bridge = Bridge::new(Arc::new(Mem::new()), HEART_HOST, heart.clone()); + let bridge = Bridge::new(Upstream::single(HEART_HOST, Arc::new(Mem::new()), heart.clone())); *heart.0.lock() = Some(bridge.clone()); let r = Rig { bridge, sink: Arc::new(Sink::default()), mem: Arc::new(Mem::new()) }; r.route(4, 3); diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index b14269431..d5686eafe 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -32,6 +32,7 @@ use parking_lot::Mutex; 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}; +use crate::xtalk::{HostPort, Upstream}; /// A widget ID register: JTAG IDCODE layout, bit 0 set. pub const fn widget_id(rev: u32, part: u32, mfgr: u32) -> u32 { @@ -599,6 +600,19 @@ impl crate::traits::Resettable for Xbow { /// address, of which the window's 16 MB is decoded. const WINDOW_MASK: u64 = REG_SPAN as u64 - 1; +impl Xbow { + /// What a Bridge behind this Crossbow sends to: each of `hosts` sits on + /// its widget's port, and a request to it is dropped while that link is + /// down. + pub fn upstream(self: &Arc, hosts: Vec) -> Upstream { + hosts.into_iter().fold(Upstream::new(), |up, h| { + let (xbow, widget) = (self.clone(), h.host.widget); + assert!(link_bit(widget).is_some(), "widget {widget} is not a Crossbow port"); + up.with(h.with_link(Arc::new(move || xbow.carries(widget)))) + }) + } +} + impl BusDevice for Xbow { fn read8(&self, addr: u64) -> BusRead8 { self.read((addr & WINDOW_MASK) as u32, 1).map_or(BusRead8::err(), |v| BusRead8::ok(v as u8)) diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 884289588..437e10e08 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -36,6 +36,7 @@ use crate::dev::ioc3_eth::{bits as ebit, reg as ereg, Ioc3Eth}; use crate::dev::isp1040::{mbox, reg as ireg, Isp1040, CTRL_INT_ENABLE, CTRL_RISC_INT_ENABLE, ENTRY_SIZE, ENTRY_TYPE_REQUEST, ENTRY_TYPE_STATUS}; use crate::dev::mgras::hq4::ImpactXio; use crate::dev::xbow::Xbow; +use crate::xtalk::Upstream; use crate::traits::{BusDevice, Device, Resettable, BUS_ERR, BUS_OK}; const FLASH_SIZE: usize = 1 << 20; @@ -63,7 +64,7 @@ fn board_with(prom: &[u8], banks_mb: [u32; 4]) -> Board { let heart = Arc::new(Heart::new(HeartConfig::octane(banks_mb.map(|mb| (mb as u64) << 20)), crate::dev::heart::host_clock())); let xbow = Arc::new(Xbow::new(XBOW_REVISION, 1 << WIDGET_HEART | 1 << WIDGET_BRIDGE)); xbow.connect_heart(heart.clone()); - let bridge = Bridge::new(ram.dma_view(), HEART_HOST, heart.clone()); + let bridge = Bridge::new(Upstream::single(HEART_HOST, ram.dma_view(), heart.clone())); bridge.attach_flash(Arc::new(RawFlash::new(prom, FLASH_SIZE)), FLASH_SIZE as u32); let phys = Ip30Bus::new(ram); phys.map(PIU_BASE, PIU_BASE + PIU_SIZE, heart.clone(), "heart piu"); @@ -462,7 +463,7 @@ impl Octane { ram.set_memcfg([0x8001_0000, 0, 0, 0]); let mem = ram.dma_view(); let heart = Arc::new(Heart::new(HeartConfig::octane([64 << 20, 0, 0, 0]), Arc::new(|| 0))); - let bridge = Bridge::new(mem.clone(), HEART_HOST, heart.clone()); + let bridge = Bridge::new(Upstream::single(HEART_HOST, mem.clone(), heart.clone())); let eth = Ioc3Eth::with_negotiation_delay(MAC, std::time::Duration::ZERO); let (link, sent, inject) = crate::net::Link::loopback(); diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 25bf44af0..41d768ec8 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -43,6 +43,7 @@ use crate::chunk_store::{get_chunks_as_words, put_words_as_chunks, ChunkStore}; use crate::snapshot::{field, ChunksManifest, Manifest, Snapshot, StateTable, SCHEMA_VERSION}; use crate::traits::{BusDevice, Device, MachineEvent, Resettable, Saveable}; use crate::workstation::{MachineRef, MachineSlot, Workstation}; +use crate::xtalk::Upstream; /// The PROM image tried when `prom` is left at the shared default, named /// as IRIX ships it in /usr/cpu/firmware. The Octane has no embedded PROM. @@ -219,7 +220,7 @@ impl OctaneMachine { // The onboard Bridge: memory and interrupts through HEART, the boot // flash on its first chip select, the system board's record on its // MicroLAN. - let bridge = Bridge::new(mem.clone(), crate::ip30::HEART_HOST, heart.clone()); + let bridge = Bridge::new(Upstream::single(crate::ip30::HEART_HOST, mem.clone(), heart.clone())); bridge.set_revision(if octane2 { BRIDGE_REVISION_OCTANE2 } else { BRIDGE_REVISION }); // The flash holds the PROM's own records as well as its code; with // `[octane] flash` they last from run to run. diff --git a/src/xtalk.rs b/src/xtalk.rs index 33a2dd45c..84c73d5b5 100644 --- a/src/xtalk.rs +++ b/src/xtalk.rs @@ -9,6 +9,11 @@ //! Bridge and the Crossbow, and they compare against them where they used to //! compare against constants. +use std::sync::Arc; + +use crate::irq::InterruptController; +use crate::traits::BusDevice; + /// How a widget bus's host chip is addressed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct XtalkHost { @@ -31,3 +36,81 @@ impl XtalkHost { widget == self.widget && address & self.int_packet_mask == self.int_packet && (vector as u16) < self.vectors } } + +/// A host chip as the widgets upstream of it reach it: how it is addressed, +/// the memory a transfer to it lands in (physical addresses, a bus error for +/// anything not fitted) and the interrupt inputs a packet to it sets (line = +/// vector). +pub struct HostPort { + pub host: XtalkHost, + pub mem: Arc, + pub irq: Arc, + /// Whether the link to the host is up, for a host behind a Crossbow. + /// None is a host with no link to lose. + link: Option bool + Send + Sync>>, +} + +impl HostPort { + pub fn new(host: XtalkHost, mem: Arc, irq: Arc) -> Self { + Self { host, mem, irq, link: None } + } + + /// The same host behind a link that is up while `up` says so. The Bridge + /// asks once per transfer chunk and once per interrupt packet, never per + /// bus word. + pub fn with_link(mut self, up: Arc bool + Send + Sync>) -> Self { + self.link = Some(up); + self + } + + /// Whether a request to the host gets through. + #[inline] + pub fn link_up(&self) -> bool { + self.link.as_ref().is_none_or(|up| up()) + } +} + +/// The hosts a widget can send to, by widget number. A machine with one host +/// (HEART, a Hub with no Crossbow) has one entry. A widget behind a Crossbow +/// has one per host chip on its ports, each with that port's link +/// (`Xbow::upstream`), so a request to a host with no live link is dropped +/// where the Crossbow would drop it. +/// +/// The table is fixed when the machine is built and indexed by the widget +/// number the guest programmed, so a transfer finds its host with one load +/// and no lock. +pub struct Upstream { + hosts: [Option; 16], +} + +impl Upstream { + pub fn new() -> Self { + Self { hosts: [const { None }; 16] } + } + + /// An upstream with the one host `port`. + pub fn single(host: XtalkHost, mem: Arc, irq: Arc) -> Self { + Self::new().with(HostPort::new(host, mem, irq)) + } + + /// Add a host at its widget number. + pub fn with(mut self, port: HostPort) -> Self { + let w = port.host.widget as usize; + assert!(w < 16, "widget {w} is not a widget number"); + assert!(self.hosts[w].is_none(), "two hosts at widget {w}"); + self.hosts[w] = Some(port); + self + } + + /// The host at `widget`, if there is one. + #[inline] + pub fn get(&self, widget: u8) -> Option<&HostPort> { + self.hosts.get(widget as usize)?.as_ref() + } +} + +impl Default for Upstream { + fn default() -> Self { + Self::new() + } +} From 05fb85d75255ef0d32384c9b90d4fa235530d5b8 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:49:11 -0700 Subject: [PATCH 12/38] xbow: take the host chip's addressing from the machine The Crossbow sent widget 0's interrupt packets to "widget 8" by constant. Reuse XtalkHost, which the Bridge now takes the same way, and replace connect_heart with connect_host(host, irq): the packet goes to the host's interrupt inputs when the destination register names the host's widget and that link is up. The delivery path is unchanged; the compare is against a field of the OnceLock already read there. The Octane passes HEART_HOST, so its behaviour does not change. A test names a different host widget and checks that a packet addressed to widget 8 is dropped and one addressed to the host arrives. Measured with iris-bench run --quick --cpu r10000, ship profile, A B B A x4 per side. Host instructions per guest instruction (perf stat instructions:u / icount, median of 4), base being the Bridge change underneath: base branch interpreter 551.8 550.9 (-0.15%) interpreter, functions aligned to 64 bytes 549.0 550.2 (+0.23%) jitv2 could not be measured: the host load average was about 100 and the compile threads swamp the count. The bench kernels do not reach the Crossbow; the packet path gains one field compare per widget 0 interrupt. --- src/dev/xbow.rs | 57 +++++++++++++++++++++++++++++++-------------- src/ip30/tests.rs | 4 ++-- src/ip30_machine.rs | 2 +- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index d5686eafe..0f1f04672 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -6,7 +6,8 @@ //! translates no address: the machine routes a processor access by widget //! number and asks [`Xbow::carries`] whether the link is up. Packets from //! widget 0 itself are its interrupts, which go to the target its interrupt -//! destination registers name; on the Octane that is HEART. +//! destination registers name; on the Octane that is HEART, which the machine +//! gives as an [`XtalkHost`]. //! //! Which links are present and alive are atomics, so the routing decision //! (`carries`) never takes a lock; they change only under the register @@ -32,16 +33,13 @@ use parking_lot::Mutex; 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}; -use crate::xtalk::{HostPort, Upstream}; +use crate::xtalk::{HostPort, Upstream, XtalkHost}; /// A widget ID register: JTAG IDCODE layout, bit 0 set. pub const fn widget_id(rev: u32, part: u32, mfgr: u32) -> u32 { (rev << 28) | (part << 12) | (mfgr << 1) | 1 } -/// The widget HEART answers as, where widget 0's interrupt packets go. -const WIDGET_HEART: u8 = 8; - const FIRST_WIDGET: u8 = 8; const LINKS: usize = 8; @@ -210,8 +208,9 @@ pub struct Xbow { /// widget 0's one interrupt arrive in the order they were decided. /// Never held with `regs`, and nothing but HEART is called under it. irq_order: Mutex<()>, - /// Where widget 0's interrupt packets go when they target HEART. - heart: OnceLock>, + /// The host chip widget 0's interrupt packets go to when they target + /// it: how it is addressed, and its interrupt inputs. + host: OnceLock<(XtalkHost, Arc)>, /// Told when a link comes up or goes down, with the widget and its new /// state, outside the register lock. on_link_change: OnceLock>, @@ -238,7 +237,7 @@ impl Xbow { present: AtomicU8::new(present), alive: AtomicU8::new(present), irq_order: Mutex::new(()), - heart: OnceLock::new(), + host: OnceLock::new(), on_link_change: OnceLock::new(), on_link_reset: OnceLock::new(), unsupported: Mutex::new(Vec::new()), @@ -246,10 +245,11 @@ impl Xbow { } } - /// Where widget 0's interrupt packets go: HEART, whose line is the - /// vector the packet names. Once, before the machine runs. - pub fn connect_heart(&self, heart: Arc) { - let _ = self.heart.set(heart); + /// 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. + pub fn connect_host(&self, host: XtalkHost, irq: Arc) { + let _ = self.host.set((host, irq)); } /// What to do when a link comes up or goes down (`widget`, `alive`): @@ -280,7 +280,7 @@ impl Xbow { let before = self.alive.load(Ordering::Acquire); let mut out = Out::default(); let ret = f(&mut self.regs.lock(), &mut out); - if let Some(h) = self.heart.get() { + if let Some((_, h)) = self.host.get() { for (vector, set) in out.packets { h.set_line(vector as u32, set); } @@ -363,7 +363,7 @@ impl Xbow { /// Recompute the request bits and, when the summary changes, queue widget /// 0's interrupt packet: one assertion outstanding at a time, the /// deassertion once every request is acknowledged. A packet to anything - /// but HEART goes nowhere on this machine. + /// but the host chip goes nowhere on this machine. fn update_interrupt(&self, r: &mut Regs, out: &mut Out) { for link in 0..LINKS { if Self::link_requesting(r, link) { @@ -381,8 +381,9 @@ impl Xbow { } let target = ((r.int_upper >> 16) & 0xf) as u8; let vector = (r.int_upper >> 24) as u8; - if target != WIDGET_HEART || !self.carries(target) || self.heart.get().is_none() { - return; + match self.host.get() { + Some((host, _)) if target == host.widget && self.carries(target) => {} + _ => return, } out.packets.push((vector, pending)); r.interrupt_asserted = pending; @@ -711,6 +712,7 @@ impl Saveable for Xbow { #[cfg(test)] mod tests { use super::*; + use crate::ip30::HEART_HOST; use std::sync::atomic::AtomicU64; /// HEART's ISR as the Crossbow sees it: one bit per vector. @@ -774,7 +776,7 @@ mod tests { fn a_link_reset_restores_a_live_link() { let x = octane(); let isr = Arc::new(Isr::default()); - x.connect_heart(isr.clone()); + x.connect_host(HEART_HOST, isr.clone()); // Interrupt to HEART, vector 0x2a, alive events enabled on link 15. assert!(x.write(INT_UPPER + 4, 4, (0x2a << 24) | (8 << 16))); let ctl = LINK_BASE + LINK_STRIDE * 7 + LINK_CONTROL + 4; @@ -790,6 +792,27 @@ mod tests { assert!(!x.carries(11)); } + /// Widget 0's interrupt goes to the host the machine named, not to a + /// fixed widget number. + #[test] + fn interrupts_follow_the_host_the_machine_names() { + let x = octane(); + let isr = Arc::new(Isr::default()); + x.connect_host(XtalkHost { widget: 15, ..HEART_HOST }, isr.clone()); + let ctl = LINK_BASE + LINK_STRIDE * 7 + LINK_CONTROL + 4; + let reset = LINK_BASE + LINK_STRIDE * 7 + LINK_RESET + 4; + // Addressed to widget 8, which is not the host here. + assert!(x.write(INT_UPPER + 4, 4, (0x2a << 24) | (8 << 16))); + assert!(x.write(ctl, 4, LINK_ALIVE_ENABLE as u64)); + assert!(x.write(reset, 4, 0)); + assert_eq!(isr.0.load(Ordering::SeqCst), 0); + // Addressed to widget 15, which is. + x.read(LINK_BASE + LINK_STRIDE * 7 + LINK_STATUS_CLEAR + 4, 4).unwrap(); + assert!(x.write(INT_UPPER + 4, 4, (0x2b << 24) | (15 << 16))); + assert!(x.write(reset, 4, 0)); + assert_eq!(isr.0.load(Ordering::SeqCst), 1 << 0x2b); + } + #[test] fn reset_restores_startup_links() { let x = octane(); diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 437e10e08..9b4ee769e 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -63,7 +63,7 @@ fn board_with(prom: &[u8], banks_mb: [u32; 4]) -> Board { let ram = Arc::new(HeartRam::new(banks_mb)); let heart = Arc::new(Heart::new(HeartConfig::octane(banks_mb.map(|mb| (mb as u64) << 20)), crate::dev::heart::host_clock())); let xbow = Arc::new(Xbow::new(XBOW_REVISION, 1 << WIDGET_HEART | 1 << WIDGET_BRIDGE)); - xbow.connect_heart(heart.clone()); + xbow.connect_host(HEART_HOST, heart.clone()); let bridge = Bridge::new(Upstream::single(HEART_HOST, ram.dma_view(), heart.clone())); bridge.attach_flash(Arc::new(RawFlash::new(prom, FLASH_SIZE)), FLASH_SIZE as u32); let phys = Ip30Bus::new(ram); @@ -403,7 +403,7 @@ fn the_graphics_port_reaches_impact_with_its_link_up() { let id = widget_small_base(WIDGET_GFX) + 4; for (links, alive) in [(0x8100u16, false), (0x8100 | 1 << WIDGET_GFX, true)] { let xbow = Arc::new(Xbow::new(XBOW_REVISION, links)); - xbow.connect_heart(heart.clone()); + xbow.connect_host(HEART_HOST, heart.clone()); let p = Arc::new(Ip30Bus::new(ram.clone())); p.attach_widget(WIDGET_GFX, gfx.clone(), false); p.init(); diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 41d768ec8..686c1b5c2 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -215,7 +215,7 @@ impl OctaneMachine { let gfx_link = if board.is_some() || has_vpro { 1 << WIDGET_GFX } else { 0 }; let xbow = Arc::new(Xbow::new(xbow_revision, (1 << WIDGET_HEART) | (1 << WIDGET_BRIDGE) | gfx_link)); - xbow.connect_heart(heart.clone()); + xbow.connect_host(crate::ip30::HEART_HOST, heart.clone()); // The onboard Bridge: memory and interrupts through HEART, the boot // flash on its first chip select, the system board's record on its From 8cbf30fa631b61e600e94e1afe1edc934592b95b Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:58:10 -0700 Subject: [PATCH 13/38] devlog: add a hub module The SN0 node controller's unsupported-access messages need a module of their own to switch on from the monitor. Bedrock's would mislabel them, so add one. --- src/devlog.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/devlog.rs b/src/devlog.rs index b9ee4f6fa..7c481b3c5 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -56,6 +56,8 @@ pub enum LogModule { /// model does not support. Odyssey = 28, Bedrock = 29, + /// The SN0 node controller: guest accesses the model does not support. + Hub = 30, } // Mask bits shared by L1i / L1d / L2c @@ -64,7 +66,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 30; + pub const COUNT: usize = 31; pub fn from_str(s: &str) -> Option { match s { @@ -98,6 +100,7 @@ impl LogModule { "rad1" => Some(Self::Rad1), "odyssey" => Some(Self::Odyssey), "bedrock" => Some(Self::Bedrock), + "hub" => Some(Self::Hub), _ => None, } } @@ -134,6 +137,7 @@ impl LogModule { Self::Rad1 => "rad1", Self::Odyssey => "odyssey", Self::Bedrock => "bedrock", + Self::Hub => "hub", } } @@ -145,6 +149,7 @@ impl LogModule { Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, + Self::Hub, ] } @@ -287,6 +292,7 @@ impl DevLog { ModuleLog::new(), // Rad1 ModuleLog::new(), // Odyssey ModuleLog::new(), // Bedrock + ModuleLog::new(), // Hub ], } } From eea5306326de0a1e32bbcbbd3797226e244188ac Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:59:19 -0700 Subject: [PATCH 14/38] hub: add the processor interface The Hub is the node controller of the Origin 200, Origin 2000 and Onyx2. Its processor interface carries what the PROM and the kernels poll first: the two pending-interrupt banks and their masks, the 1.25 MHz real-time counter with one comparator per processor, processor presence, enable, NMI and soft reset, the cached-alias size, the graphics flow-control registers and the error record of a failed processor read. The polled registers (pending banks, counter origin, comparator pending and enable) are atomics, so a waiting processor takes no lock. The interrupt lines are levels recomputed on every change and published under a dispatch lock. The graphics registers reuse the Odyssey PiGfx block and count no stores. hub.rs holds the hooks a machine implements and an optional observer for the stores a processor rendezvous would watch. Reset values, the effect of a cleared enable and the other approximations are listed in the module header. The cases follow the register contract of the model this is ported from. --- src/dev/hub.rs | 80 +++++ src/dev/hub_pi.rs | 688 ++++++++++++++++++++++++++++++++++++++++ src/dev/hub_pi/tests.rs | 496 +++++++++++++++++++++++++++++ src/dev/mod.rs | 2 + 4 files changed, 1266 insertions(+) create mode 100644 src/dev/hub.rs create mode 100644 src/dev/hub_pi.rs create mode 100644 src/dev/hub_pi/tests.rs diff --git a/src/dev/hub.rs b/src/dev/hub.rs new file mode 100644 index 000000000..e2691e9be --- /dev/null +++ b/src/dev/hub.rs @@ -0,0 +1,80 @@ +//! The Hub, the node controller of the SN0 machines (Origin 200, Origin 2000, +//! Onyx2): one per node, joining the node's processors, memory and directory, +//! one XIO port and one CrayLink port. The register window is split by +//! section into `hub_pi`, `hub_md`, `hub_io` and `hub_ni`, as the Bedrock +//! files are. +//! +//! This module holds what the sections share: the machine-facing hooks and +//! the observer of the stores a processor rendezvous depends on. +//! +//! # What is approximated here +//! +//! The model is a functional profile for the stock IP27 PROM and the guests +//! that boot on these machines. It is not a revision-matched description of the +//! chip, and no Hub specification is public: almost every register behaviour +//! is a software expectation or an approximation. The sections name theirs. +//! +//! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. + +/// Length of the register window: 8 MiB. +pub const WINDOW_SIZE: u64 = 8 << 20; + +/// Pending interrupts are two banks of 64 bits. +pub const PENDING_BANKS: usize = 2; + +/// A store the stock PROM's processor rendezvous depends on, reported after +/// it has taken effect. These are the registers the processors of a node, and +/// the nodes of a machine, use as mailboxes: a mechanism that holds a +/// processor until a partner has run needs to see them, and the Hub changes +/// no register value on its account. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Store { + /// PI_RT_COMPARE of `slice`, as stored (52 bits). + RtCompare { slice: usize, value: u64 }, + /// PI_CPU_ENABLE of `slice`, whether or not the node populates it. + CpuEnable { slice: usize, enabled: bool }, + /// PI_INT_PEND_MOD, as written; `changed` says whether it flipped a + /// pending bit. Interrupts from a Bridge or an engine arrive here too. + IntPendMod { value: u64, changed: bool }, + /// IIO_SCRATCH_REG0 or 1, as stored (36 bits). + IoScratch { index: usize, value: u64 }, + /// NI_SCRATCH_REG0 or 1, as stored. `vector` is set when a vector PIO + /// from another node wrote it rather than a load or store on the bus. + NiScratch { index: usize, value: u64, vector: bool }, + /// NI_PORT_RESET, as written. + PortReset { value: u64 }, +} + +/// An optional observer of those stores, connected by the board. Nothing in +/// the Hub waits for it: it is called outside every section lock, on the +/// thread that did the store, and when none is connected a store costs one +/// atomic load. +pub trait HubObserver: Send + Sync { + fn stored(&self, store: Store); +} + +/// What the Hub asks of the machine around it. Called outside every section +/// lock, on the thread that wrote the register. All defaults do nothing, so a +/// test or a partial machine implements only what it wires. +pub trait HubHooks: Send + Sync { + /// PI_CPU_ENABLE of `slice` was stored, or cleared by a reset. The stock + /// PROM stores zero to its partner's enable after a failed liveness + /// handshake and never stores one; what the store does to the processor is + /// unknown. A machine that halts the processor on a cleared bit and + /// releases it on a set one is making that choice. Slices the node does not + /// populate are not reported. + fn cpu_enable(&self, _slice: usize, _enabled: bool) {} + /// A store to PI_NMI of `slice`: the processor takes a non-maskable + /// interrupt. + fn nmi(&self, _slice: usize) {} + /// A store to PI_SOFTRESET reached the enabled processor `slice`. + fn soft_reset(&self, _slice: usize) {} + /// PI_CALIAS_SIZE was written: the cached-alias window of every processor + /// view now spans `bytes` (0 disables it). + fn cached_alias_changed(&self, _bytes: u64) {} + /// The node number this Hub answers to changed, by a store to + /// NI_STATUS_REV_ID or a reset. + fn node_number_changed(&self, _number: u16) {} + /// NI_PORT_RESET bit 0: restart the whole machine. + fn machine_restart(&self) {} +} diff --git a/src/dev/hub_pi.rs b/src/dev/hub_pi.rs new file mode 100644 index 000000000..1f3d593a3 --- /dev/null +++ b/src/dev/hub_pi.rs @@ -0,0 +1,688 @@ +//! The Hub's processor interface: the node's pending-interrupt banks and +//! masks, the 1.25 MHz real-time counter and one comparator per processor, +//! processor presence, enable, NMI and soft reset, the cached-alias size, the +//! graphics flow-control registers, and the error record of a failed +//! processor read. +//! +//! Offsets are from the base of the Hub window. Pending banks, masks, the +//! counter origin and the comparator pending and enable bits are atomics, so +//! the registers a waiting processor polls take no lock. The remaining +//! register state sits behind one mutex that is never held across a hook or an +//! interrupt-line call. +//! +//! # What is approximated here +//! +//! - The reset values are the model's, except those Linux carries. The chip's +//! power-on values are unknown. +//! - The counter derives from the clock the machine supplies. Linux arms a +//! comparator by reading the counter and re-reading it to check the deadline +//! has not passed, so the counter must move between two reads a few +//! instructions apart: the clock must be fast enough to do so. +//! - A comparator written at or behind the counter does not match until the +//! counter wraps round to it. A match fires once; only a write re-arms it. +//! - PI_RT_LOCAL_CTRL stores bits 16 and 7:0; nothing written there changes the +//! counter's rate. +//! - The graphics flow-control registers keep the values written and count no +//! stores: no credit is charged, no stall and no graphics interrupt follow, +//! and the interrupt counter and compare are stored only. The field widths +//! follow the later chip's header and are inferred for this one. +//! - A failed processor read records only the first doubleword of the error +//! record (valid, overrun and the address); the type, request, supplemental +//! and command fields stay zero. +//! - The profiling and Hub error interrupts, cross-calls and the +//! processor-bus checking registers are not modelled. +//! - PI_CPU_NUM names the reader by the CPU thread's index. A store to it is +//! logged and changes nothing. +//! +//! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. + +use crate::cpu::smp::on_cpu_thread; +use crate::dev::heart::Clock; +use crate::dev::hub::{HubHooks, HubObserver, Store, PENDING_BANKS}; +use crate::dev::odyssey::{PiGfx, PI_GFX_REGS}; +use crate::devlog::LogModule; +use crate::hptimer::{TimerId, TimerManager, TimerReturn}; +use crate::irq::InterruptController; +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_OK}; +use parking_lot::{Mutex, ReentrantMutex}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; +use std::time::Duration; + +pub mod reg { + pub const REGION_PRESENT: u64 = 0x18; + pub const CPU_NUM: u64 = 0x20; + pub const CALIAS_SIZE: u64 = 0x28; + pub const CPU_PRESENT_A: u64 = 0x40; + pub const CPU_PRESENT_B: u64 = 0x48; + pub const CPU_ENABLE_A: u64 = 0x50; + pub const CPU_ENABLE_B: u64 = 0x58; + pub const NMI_A: u64 = 0x70; + pub const NMI_B: u64 = 0x78; + pub const SOFTRESET: u64 = 0x80; + pub const INT_PEND_MOD: u64 = 0x90; + pub const INT_PEND0: u64 = 0x98; + pub const INT_PEND1: u64 = 0xa0; + pub const INT_MASK0_A: u64 = 0xa8; + pub const INT_MASK1_A: u64 = 0xb0; + pub const INT_MASK0_B: u64 = 0xb8; + pub const INT_MASK1_B: u64 = 0xc0; + pub const RT_COMPARE_A: u64 = 0x108; + pub const RT_COMPARE_B: u64 = 0x110; + pub const RT_PEND_A: u64 = 0x120; + pub const RT_PEND_B: u64 = 0x128; + pub const RT_EN_A: u64 = 0x140; + pub const RT_EN_B: u64 = 0x148; + pub const RT_LOCAL_CTRL: u64 = 0x160; + pub const GFX_BASE: u64 = 0x300; + pub const GFX_END: u64 = 0x350; + pub const ERR_INT_PEND: u64 = 0x400; + pub const ERR_INT_MASK_A: u64 = 0x408; + pub const ERR_INT_MASK_B: u64 = 0x410; + pub const ERR_STACK_ADDR_A: u64 = 0x418; + pub const ERR_STACK_ADDR_B: u64 = 0x420; + pub const ERR_STACK_SIZE: u64 = 0x428; + pub const ERR_STATUS0_A: u64 = 0x430; + pub const ERR_STATUS0_A_RCLR: u64 = 0x438; + pub const ERR_STATUS1_A: u64 = 0x440; + pub const ERR_STATUS1_A_RCLR: u64 = 0x448; + pub const ERR_STATUS0_B: u64 = 0x450; + pub const ERR_STATUS0_B_RCLR: u64 = 0x458; + pub const ERR_STATUS1_B: u64 = 0x460; + pub const ERR_STATUS1_B_RCLR: u64 = 0x468; + pub const TIMEDOUT_A: u64 = 0x480; + pub const TIMEDOUT_B: u64 = 0x488; + pub const HARDRESET_BIT: u64 = 0x2_0068; + pub const RT_COUNT: u64 = 0x3_0100; +} + +/// First offset that is not the processor interface's. +pub const END: u64 = 0x20_0000; + +/// Interrupt-modify: bits 6:0 name the interrupt, bit 8 set means set. +pub const MODIFY_NUMBER: u64 = 0x7f; +pub const MODIFY_SET: u64 = 0x100; + +/// The counter ticks at 1.25 MHz, one tick per 800 ns. +const TICK_NS: u64 = 800; +const RT_MASK: u64 = (1 << 52) - 1; +const RT_LOCAL_CTRL_MASK: u64 = 0x1_00ff; +const RT_LOCAL_CTRL_RESET: u64 = 0x1_0000; +const CALIAS_MASK: u64 = 0xf; +const CALIAS_RESET: u64 = 15; +const ERR_MASK: u64 = 0x01ff_ffff; +const ERR_STACK_ADDR_MASK: u64 = 0xffff_fff8; +const ERR_STACK_SIZE_MASK: u64 = 0xf; +const STATUS0_VALID: u64 = 1 << 63; +const STATUS0_OVERRUN: u64 = 1 << 62; +const STATUS0_ADDR_SHIFT: u32 = 25; +const STATUS0_ADDR_MASK: u64 = 0x3fff_ffff_fe00_0000; +const PHYSICAL_BITS: u32 = 40; +/// Interrupt lines per processor: bank 0, bank 1, the comparator. +pub const LINES_PER_CPU: usize = 3; + +/// Bytes the cached alias spans for a PI_CALIAS_SIZE code: zero disables it, +/// one is 4 KiB and each further code doubles it, to 64 MiB at 15. +pub fn calias_bytes(code: u64) -> u64 { + if code == 0 { 0 } else { (4 << 10) << (code - 1) } +} + +fn unimp(what: &str, off: u64, value: Option) { + match value { + Some(v) => crate::dlog!(LogModule::Hub, "Hub PI: write of {v:#x} to unimplemented {what} {off:#x}"), + None => crate::dlog!(LogModule::Hub, "Hub PI: read of unimplemented {what} {off:#x}"), + } +} + +struct State { + region_present: u64, + calias: u64, + cpu_enable: [bool; 2], + rt_compare: [u64; 2], + /// Clock reading at which each comparator matches, `None` once it has + /// fired or while no processor is fitted. + due: [Option; 2], + rt_local_ctrl: u64, + err_mask: [u64; 2], + err_stack_addr: [u64; 2], + err_stack_size: u64, + err_status0: [u64; 2], + gfx: PiGfx, + running: bool, +} +impl State { + fn new() -> Self { + Self { + region_present: !0, + calias: CALIAS_RESET, + cpu_enable: [false; 2], + rt_compare: [0; 2], + due: [None; 2], + rt_local_ctrl: RT_LOCAL_CTRL_RESET, + err_mask: [0; 2], + err_stack_addr: [0; 2], + err_stack_size: 0, + err_status0: [0; 2], + gfx: PiGfx::default(), + running: false, + } + } +} + +pub struct HubPi { + cpus: u8, + cpu_base: usize, + clock: Clock, + /// Pending banks. Only `modify` changes them while the machine runs; + /// other processors poll them as mailboxes, so a bank is published with + /// release order and read with acquire. + pend: [AtomicU64; PENDING_BANKS], + mask: [AtomicU64; 4], + /// The clock reading at which the counter read zero, modulo 2^64. + rt_origin: AtomicU64, + /// A match pends whether or not enabled; enable only gates the line. + rt_pending: [AtomicBool; 2], + rt_enable: [AtomicBool; 2], + state: Mutex, + hooks: OnceLock>, + observer: OnceLock>, + irq: Mutex>>, + /// Serialises line publication, so two writers cannot publish a stale + /// level after a fresh one. Reentrant: a controller may read a register. + dispatch: ReentrantMutex<()>, + driven: Mutex<[Option; 2 * LINES_PER_CPU]>, + /// Held weakly: the manager's callbacks reach the processor, whose bus + /// reaches this Hub, so a strong handle here would keep all three alive. + timers: OnceLock>, + timer: Mutex>, + weak: Weak, +} + +impl HubPi { + /// `cpus` is how many processor sockets the node populates (1 or 2) and + /// `cpu_base` the global index of its slice A, which PI_CPU_NUM subtracts + /// from the reading processor's. + pub fn new(cpus: u8, cpu_base: usize, clock: Clock) -> Arc { + assert!((1..=2).contains(&cpus)); + let now = clock(); + Arc::new_cyclic(|weak| Self { + cpus, + cpu_base, + clock, + pend: Default::default(), + mask: Default::default(), + rt_origin: AtomicU64::new(now), + rt_pending: Default::default(), + rt_enable: Default::default(), + state: Mutex::new(State::new()), + hooks: OnceLock::new(), + observer: OnceLock::new(), + irq: Mutex::new(None), + dispatch: ReentrantMutex::new(()), + driven: Mutex::new([None; 2 * LINES_PER_CPU]), + timers: OnceLock::new(), + timer: Mutex::new(None), + weak: weak.clone(), + }) + } + + pub fn set_hooks(&self, hooks: Arc) { + let _ = self.hooks.set(hooks); + } + + /// Report the stores a processor rendezvous depends on (see [`Store`]). + pub fn set_observer(&self, observer: Arc) { + let _ = self.observer.set(observer); + } + + fn observe(&self, store: Store) { + if let Some(observer) = self.observer.get() { + observer.stored(store); + } + } + + /// The machine owns the manager; the interface only keeps a weak handle to + /// arm the comparator timer. + pub fn set_timer_manager(&self, tm: Arc) { + let _ = self.timers.set(Arc::downgrade(&tm)); + self.schedule(); + } + + /// Lines 0..2 are processor A's hardware interrupts 0 to 2 (bank 0, + /// bank 1, the comparator) and lines 3..5 are processor B's. All are + /// levels, recomputed on every change. + pub fn set_interrupt_controller(&self, irq: Arc) { + let _dispatch = self.dispatch.lock(); + *self.irq.lock() = Some(irq); + *self.driven.lock() = [None; 2 * LINES_PER_CPU]; + self.drive(); + } + + pub fn cpus(&self) -> u8 { + self.cpus + } + + /// Whether the node's PI_CPU_ENABLE bit of `slice` is set. + pub fn cpu_enabled(&self, slice: usize) -> bool { + slice < 2 && self.state.lock().cpu_enable[slice] + } + + /// The cached-alias window size the register currently names. + pub fn calias_bytes(&self) -> u64 { + calias_bytes(self.state.lock().calias) + } + + /// The two pending banks, as a processor reads them. + pub fn pending(&self, bank: usize) -> u64 { + self.pend[bank].load(Ordering::Acquire) + } + + fn hooks(&self) -> Option<&Arc> { + self.hooks.get() + } + + fn count_at(&self, now: u64) -> u64 { + (now.wrapping_sub(self.rt_origin.load(Ordering::Relaxed)) / TICK_NS) & RT_MASK + } + + /// Where the counter has got to, from the clock rather than stored state: + /// firmware measures elapsed time by polling this register. + pub fn rt_count(&self) -> u64 { + self.count_at((self.clock)()) + } + + /// The clock reading at which the counter next equals `compare`. The + /// counter reaches the compare value, never merely passes it: a value + /// behind the counter waits a whole revolution, and one equal to it too. + fn deadline(&self, compare: u64, now: u64) -> u64 { + let count = self.count_at(now); + let ticks = (compare.wrapping_sub(count).wrapping_sub(1) & RT_MASK) + 1; + let into_tick = now.wrapping_sub(self.rt_origin.load(Ordering::Relaxed)) % TICK_NS; + now.saturating_add(ticks * TICK_NS).saturating_sub(into_tick) + } + + fn arm(&self, s: &mut State, slot: usize, now: u64) { + // A socket with no processor never arms a timer. + s.due[slot] = (slot < usize::from(self.cpus)).then(|| self.deadline(s.rt_compare[slot], now)); + } + + /// Pend every comparator whose deadline has passed. + fn sync(&self, now: u64) { + let mut s = self.state.lock(); + for slot in 0..2 { + if s.due[slot].is_some_and(|due| now >= due) { + s.due[slot] = None; + self.rt_pending[slot].store(true, Ordering::Release); + } + } + } + + /// Bring the comparators up to the clock and publish the lines. Tests and + /// the machine's poll call it; the timer does the same on expiry. + pub fn poll(&self) { + self.sync((self.clock)()); + self.drive(); + } + + fn lines(&self) -> [bool; 2 * LINES_PER_CPU] { + let mut out = [false; 2 * LINES_PER_CPU]; + for slot in 0..usize::from(self.cpus) { + let (m0, m1) = (self.mask[slot * 2].load(Ordering::Acquire), self.mask[slot * 2 + 1].load(Ordering::Acquire)); + out[slot * LINES_PER_CPU] = self.pend[0].load(Ordering::Acquire) & m0 != 0; + out[slot * LINES_PER_CPU + 1] = self.pend[1].load(Ordering::Acquire) & m1 != 0; + out[slot * LINES_PER_CPU + 2] = + self.rt_pending[slot].load(Ordering::Acquire) && self.rt_enable[slot].load(Ordering::Acquire); + } + out + } + + /// Publish each line that changed. The sample is taken under the dispatch + /// lock, so the last publication is the freshest level. + fn drive(&self) { + let _dispatch = self.dispatch.lock(); + let Some(irq) = self.irq.lock().clone() else { return }; + let levels = self.lines(); + for (line, &level) in levels.iter().enumerate() { + let changed = { + let mut driven = self.driven.lock(); + let changed = driven[line] != Some(level); + driven[line] = Some(level); + changed + }; + if changed { + irq.set_line(line as u32, level); + } + } + } + + fn schedule(&self) { + let Some(tm) = self.timers.get().and_then(Weak::upgrade) else { return }; + let mut timer = self.timer.lock(); + if let Some(id) = timer.take() { + tm.remove(id); + } + let s = self.state.lock(); + if !s.running { + return; + } + let Some(next) = s.due.iter().flatten().min().copied() else { return }; + drop(s); + let delay = next.saturating_sub((self.clock)()).clamp(1, 1_000_000_000); + *timer = Some(tm.add_one_shot(Duration::from_nanos(delay), self.weak.clone(), |weak| { + if let Some(pi) = weak.upgrade() { + pi.poll(); + pi.schedule(); + } + TimerReturn::Delete + })); + } + + /// Set or clear one pending interrupt, as a write to PI_INT_PEND_MOD + /// does. Device interrupts arrive here from the Bridge as XIO writes. + pub fn int_modify(&self, value: u64) { + let number = value & MODIFY_NUMBER; + let (bank, bit) = ((number / 64) as usize, 1u64 << (number % 64)); + let before = if value & MODIFY_SET != 0 { + self.pend[bank].fetch_or(bit, Ordering::AcqRel) + } else { + self.pend[bank].fetch_and(!bit, Ordering::AcqRel) + }; + self.drive(); + self.observe(Store::IntPendMod { value, changed: (before & bit != 0) != (value & MODIFY_SET != 0) }); + } + + /// A processor read failed: record it in that slice's error status 0. + /// The first failure is kept and a later one only sets overrun. A + /// debugger's access or a device's DMA does not come here. + pub fn read_error(&self, slice: usize, physical: u64) { + if slice >= usize::from(self.cpus) { + return; + } + let address = physical & ((1 << PHYSICAL_BITS) - 1); + let mut s = self.state.lock(); + if s.err_status0[slice] & STATUS0_VALID != 0 { + s.err_status0[slice] |= STATUS0_OVERRUN; + } else { + s.err_status0[slice] = STATUS0_VALID | (((address >> 3) << STATUS0_ADDR_SHIFT) & STATUS0_ADDR_MASK); + } + } + + /// Which of this node's slices the executing thread is, or `None` for a + /// device or monitor thread, or a processor of another node. A debugger's + /// access comes from such a thread. + pub fn reader_slice(&self) -> Option { + on_cpu_thread()?.checked_sub(self.cpu_base).filter(|&i| i < usize::from(self.cpus)) + } + + fn slice(&self) -> u64 { + self.reader_slice().unwrap_or(0) as u64 + } + + /// The value of a register that stores one, without side effects: what a + /// 32-bit write merges its half with. `None` for a register that stores + /// nothing a read-modify-write could return. + pub fn peek(&self, off: u64) -> Option { + let s = self.state.lock(); + Some(match off { + reg::REGION_PRESENT => s.region_present, + reg::CALIAS_SIZE => s.calias, + reg::CPU_ENABLE_A | reg::CPU_ENABLE_B => u64::from(s.cpu_enable[((off - reg::CPU_ENABLE_A) / 8) as usize]), + reg::INT_MASK0_A..=reg::INT_MASK1_B if off % 8 == 0 => { + self.mask[((off - reg::INT_MASK0_A) / 8) as usize].load(Ordering::Acquire) + } + reg::RT_COMPARE_A | reg::RT_COMPARE_B => s.rt_compare[((off - reg::RT_COMPARE_A) / 8) as usize], + reg::RT_PEND_A | reg::RT_PEND_B => { + u64::from(self.rt_pending[((off - reg::RT_PEND_A) / 8) as usize].load(Ordering::Acquire)) + } + reg::RT_EN_A | reg::RT_EN_B => { + u64::from(self.rt_enable[((off - reg::RT_EN_A) / 8) as usize].load(Ordering::Acquire)) + } + reg::RT_LOCAL_CTRL => s.rt_local_ctrl, + reg::GFX_BASE..reg::GFX_END if off % 8 == 0 => s.gfx.read(((off - reg::GFX_BASE) / 8) as u32), + reg::ERR_INT_MASK_A | reg::ERR_INT_MASK_B => s.err_mask[((off - reg::ERR_INT_MASK_A) / 8) as usize], + reg::ERR_STACK_ADDR_A | reg::ERR_STACK_ADDR_B => { + s.err_stack_addr[((off - reg::ERR_STACK_ADDR_A) / 8) as usize] + } + reg::ERR_STACK_SIZE => s.err_stack_size, + reg::ERR_STATUS0_A => s.err_status0[0], + reg::ERR_STATUS0_B => s.err_status0[1], + reg::RT_COUNT => self.rt_count(), + _ => return None, + }) + } + + /// Read the register at `off`. `clear` is false for a 32-bit half, which + /// returns a read-to-clear register's half without clearing it: only a + /// full-width read clears, an approximation that keeps a latch until + /// software has read all of it. + pub fn read(&self, off: u64, clear: bool) -> u64 { + match off { + reg::CPU_NUM => self.slice(), + reg::CPU_PRESENT_A => u64::from(self.cpus >= 1), + reg::CPU_PRESENT_B => u64::from(self.cpus >= 2), + reg::INT_PEND_MOD => { + crate::dlog!(LogModule::Hub, "Hub PI: read of the write-only interrupt-modify register"); + 0 + } + // The registers a waiting processor polls take no lock. + reg::INT_PEND0 => self.pend[0].load(Ordering::Acquire), + reg::INT_PEND1 => self.pend[1].load(Ordering::Acquire), + reg::RT_COUNT => self.rt_count(), + reg::RT_PEND_A | reg::RT_PEND_B => { + self.sync((self.clock)()); + self.peek(off).unwrap_or(0) + } + reg::ERR_INT_PEND | reg::ERR_STATUS1_A | reg::ERR_STATUS1_A_RCLR | reg::ERR_STATUS1_B | reg::ERR_STATUS1_B_RCLR + | reg::TIMEDOUT_A | reg::TIMEDOUT_B => 0, + reg::ERR_STATUS0_A_RCLR | reg::ERR_STATUS0_B_RCLR => { + let slice = usize::from(off == reg::ERR_STATUS0_B_RCLR); + let mut s = self.state.lock(); + let value = s.err_status0[slice]; + if clear { + s.err_status0[slice] = 0; + } + value + } + _ => match self.peek(off) { + Some(v) => v, + None => { + unimp("register", off, None); + 0 + } + }, + } + } + + pub fn write(&self, off: u64, value: u64) { + self.write_sized(off, value, 8); + } + + /// `size` is the guest's access width: only a full-width zero store ends + /// a processor's error record through a clearing alias. + pub fn write_sized(&self, off: u64, value: u64, size: u32) { + let now = || (self.clock)(); + let hooks = self.hooks(); + match off { + reg::REGION_PRESENT => self.state.lock().region_present = value, + reg::CPU_NUM => { + crate::dlog!(LogModule::Hub, "Hub PI: write of {value:#x} to the processor-number register (a logic-analyzer convention, not modelled)"); + } + reg::CALIAS_SIZE => { + let code = value & CALIAS_MASK; + self.state.lock().calias = code; + if let Some(h) = hooks { + h.cached_alias_changed(calias_bytes(code)); + } + } + reg::CPU_ENABLE_A | reg::CPU_ENABLE_B => { + let slice = ((off - reg::CPU_ENABLE_A) / 8) as usize; + let on = value & 1 != 0; + self.state.lock().cpu_enable[slice] = on; + if slice < usize::from(self.cpus) { + if let Some(h) = hooks { + h.cpu_enable(slice, on); + } + } + self.observe(Store::CpuEnable { slice, enabled: on }); + } + reg::NMI_A | reg::NMI_B => { + let slice = ((off - reg::NMI_A) / 8) as usize; + if slice < usize::from(self.cpus) { + if let Some(h) = hooks { + h.nmi(slice); + } + } + } + reg::SOFTRESET => { + let enabled = self.state.lock().cpu_enable; + for slice in (0..usize::from(self.cpus)).filter(|&i| enabled[i]) { + if let Some(h) = hooks { + h.soft_reset(slice); + } + } + } + reg::INT_PEND_MOD => self.int_modify(value), + reg::INT_PEND0 | reg::INT_PEND1 => { + crate::dlog!(LogModule::Hub, "Hub PI: write of {value:#x} to the read-only pending register {off:#x}"); + } + reg::INT_MASK0_A..=reg::INT_MASK1_B if off % 8 == 0 => { + self.mask[((off - reg::INT_MASK0_A) / 8) as usize].store(value, Ordering::Release); + self.drive(); + } + reg::RT_COMPARE_A | reg::RT_COMPARE_B => { + let slot = ((off - reg::RT_COMPARE_A) / 8) as usize; + let mut s = self.state.lock(); + s.rt_compare[slot] = value & RT_MASK; + self.arm(&mut s, slot, now()); + drop(s); + self.schedule(); + self.observe(Store::RtCompare { slice: slot, value: value & RT_MASK }); + } + reg::RT_PEND_A | reg::RT_PEND_B => { + // The handler acknowledges by writing zero, which drops the line. + self.rt_pending[((off - reg::RT_PEND_A) / 8) as usize].store(value & 1 != 0, Ordering::Release); + self.drive(); + } + reg::RT_EN_A | reg::RT_EN_B => { + self.rt_enable[((off - reg::RT_EN_A) / 8) as usize].store(value & 1 != 0, Ordering::Release); + self.drive(); + } + reg::RT_LOCAL_CTRL => self.state.lock().rt_local_ctrl = value & RT_LOCAL_CTRL_MASK, + reg::GFX_BASE..reg::GFX_END if off % 8 == 0 => { + let n = ((off - reg::GFX_BASE) / 8) as u32; + debug_assert!(n < PI_GFX_REGS); + self.state.lock().gfx.write(n, value); + } + reg::ERR_INT_PEND => {} // write-one-to-clear; no source can set it + reg::ERR_INT_MASK_A | reg::ERR_INT_MASK_B => { + self.state.lock().err_mask[((off - reg::ERR_INT_MASK_A) / 8) as usize] = value & ERR_MASK; + } + reg::ERR_STACK_ADDR_A | reg::ERR_STACK_ADDR_B => { + self.state.lock().err_stack_addr[((off - reg::ERR_STACK_ADDR_A) / 8) as usize] = value & ERR_STACK_ADDR_MASK; + } + reg::ERR_STACK_SIZE => self.state.lock().err_stack_size = value & ERR_STACK_SIZE_MASK, + // Either clearing alias retires its processor's whole record + // (inferred, from software that clears through the status-1 alias + // and then expects status 0 to read not valid). Only a + // full-width zero store does. + reg::ERR_STATUS0_A_RCLR | reg::ERR_STATUS1_A_RCLR | reg::ERR_STATUS0_B_RCLR | reg::ERR_STATUS1_B_RCLR => { + if size == 8 && value == 0 { + let slice = usize::from(off >= reg::ERR_STATUS0_B); + self.state.lock().err_status0[slice] = 0; + } + } + reg::TIMEDOUT_A | reg::TIMEDOUT_B => { + unimp("timed-out request buffer", off, Some(value)); + } + reg::HARDRESET_BIT => { + // Zero selects doubleword credits, the only mode there is. + if value != 0 { + unimp("graphics credit mode", off, Some(value)); + } + } + reg::RT_COUNT => { + let (value, now) = (value & RT_MASK, now()); + self.rt_origin.store(now.wrapping_sub(value * TICK_NS), Ordering::Release); + // Moving the counter moves every deadline standing against it. + let mut s = self.state.lock(); + for slot in 0..2 { + self.arm(&mut s, slot, now); + } + drop(s); + self.schedule(); + } + _ => unimp("register", off, Some(value)), + } + } + + /// Whether the register state is locked now, for the `locks` monitor command. + pub fn is_locked(&self) -> bool { + self.state.is_locked() + } + + pub fn start(&self) { + self.state.lock().running = true; + self.schedule(); + } + + pub fn stop(&self) { + self.poll(); + self.state.lock().running = false; + if let Some(tm) = self.timers.get().and_then(Weak::upgrade) { + if let Some(id) = self.timer.lock().take() { + tm.remove(id); + } + } + } + + pub fn is_running(&self) -> bool { + self.state.lock().running + } +} + +impl Resettable for HubPi { + /// Everything returns to its reset value except the error stack + /// addresses, which firmware uses as a warm-start marker. Both processor + /// enables clear, so the node's processors halt until firmware releases + /// them. + fn power_on(&self) { + let now = (self.clock)(); + let mut s = self.state.lock(); + let (addr, running) = (s.err_stack_addr, s.running); + *s = State::new(); + s.err_stack_addr = addr; + s.running = running; + drop(s); + self.rt_origin.store(now, Ordering::Release); + for bank in &self.pend { + bank.store(0, Ordering::Release); + } + for m in &self.mask { + m.store(0, Ordering::Release); + } + for slot in 0..2 { + self.rt_pending[slot].store(false, Ordering::Release); + self.rt_enable[slot].store(false, Ordering::Release); + } + self.drive(); + self.schedule(); + if let Some(h) = self.hooks() { + for slice in 0..usize::from(self.cpus) { + h.cpu_enable(slice, false); + } + } + } +} + +impl BusDevice for HubPi { + fn read64(&self, addr: u64) -> BusRead64 { + BusRead64::ok(self.read(addr, true)) + } + fn write64(&self, addr: u64, value: u64) -> u32 { + self.write(addr, value); + BUS_OK + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/hub_pi/tests.rs b/src/dev/hub_pi/tests.rs new file mode 100644 index 000000000..baf813c12 --- /dev/null +++ b/src/dev/hub_pi/tests.rs @@ -0,0 +1,496 @@ +// Cases follow tests/qtest/sn0-hub-test.c, sn0-pi-error-test.c and +// sn0-pi-gfx-test.c in jamesbraid/qemu-sgi-origami: reset-values, +// guest-writes, processor-number, cpu-enable, interrupt-modify, +// interrupt-number-selects-bank, masked-pending-raises-nothing, +// pending-banks-masked-per-processor, pending-banks-reject-writes, the rt-* +// counter and comparator cases, pi-rt-local-ctrl, pi-error-register-shell, +// pi-timedout-buffers, the graphics-flow-control register cases and the +// processor-error status cases. + +use super::*; +use std::sync::atomic::AtomicU8; + +struct Fixture { + pi: Arc, + now: Arc, + lines: Arc, + hooks: Arc, +} +#[derive(Default)] +struct Lines([AtomicU8; 6]); +impl Lines { + fn get(&self, line: usize) -> bool { + self.0[line].load(Ordering::SeqCst) == 1 + } +} +impl InterruptController for Lines { + fn set_line(&self, line: u32, level: bool) { + self.0[line as usize].store(u8::from(level), Ordering::SeqCst); + } +} +#[derive(Default)] +struct Hooks(Mutex>); +impl HubHooks for Hooks { + fn cpu_enable(&self, slice: usize, on: bool) { + self.0.lock().push(format!("enable {slice} {on}")); + } + fn nmi(&self, slice: usize) { + self.0.lock().push(format!("nmi {slice}")); + } + fn soft_reset(&self, slice: usize) { + self.0.lock().push(format!("soft {slice}")); + } + fn cached_alias_changed(&self, bytes: u64) { + self.0.lock().push(format!("calias {bytes:#x}")); + } +} +impl Fixture { + fn new(cpus: u8) -> Self { + let now = Arc::new(AtomicU64::new(1_000_000)); + let clock = now.clone(); + let pi = HubPi::new(cpus, 0, Arc::new(move || clock.load(Ordering::SeqCst))); + let lines = Arc::new(Lines::default()); + let hooks = Arc::new(Hooks::default()); + pi.set_interrupt_controller(lines.clone()); + pi.set_hooks(hooks.clone()); + Self { pi, now, lines, hooks } + } + fn r(&self, off: u64) -> u64 { + self.pi.read64(off).data + } + fn w(&self, off: u64, value: u64) { + assert_eq!(self.pi.write64(off, value), BUS_OK); + } + /// Let `ticks` counter ticks pass and bring the comparators up to date. + fn advance(&self, ticks: u64) { + self.now.fetch_add(ticks * TICK_NS, Ordering::SeqCst); + self.pi.poll(); + } + fn take_hooks(&self) -> Vec { + std::mem::take(&mut *self.hooks.0.lock()) + } +} + +/// An unprogrammed Hub claims every region exists, resets its cached alias to +/// the largest size, and keeps the registers firmware uses as markers. +#[test] +fn the_pi_resets_and_stores_what_the_guest_writes() { + let t = Fixture::new(2); + assert_eq!(t.r(reg::REGION_PRESENT), !0); + assert_eq!(t.r(reg::CALIAS_SIZE), 15); + assert_eq!((t.r(reg::INT_PEND0), t.r(reg::INT_PEND1)), (0, 0)); + t.w(reg::REGION_PRESENT, 0x1234); + assert_eq!(t.r(reg::REGION_PRESENT), 0x1234); + t.w(reg::INT_MASK0_A, !0); + t.w(reg::INT_MASK1_B, 0x55); + assert_eq!((t.r(reg::INT_MASK0_A), t.r(reg::INT_MASK1_B)), (!0, 0x55)); + // The error stack address keeps bits 31:3 and survives a reset. + t.w(reg::ERR_STACK_ADDR_A, !0); + t.w(reg::ERR_STACK_ADDR_B, 0x1_2345_6789); + assert_eq!(t.r(reg::ERR_STACK_ADDR_A), 0xffff_fff8); + assert_eq!(t.r(reg::ERR_STACK_ADDR_B), 0x2345_6788); + t.pi.power_on(); + assert_eq!(t.r(reg::REGION_PRESENT), !0); + assert_eq!(t.r(reg::INT_MASK0_A), 0); + assert_eq!(t.r(reg::ERR_STACK_ADDR_A), 0xffff_fff8); + assert_eq!(t.r(reg::ERR_STACK_ADDR_B), 0x2345_6788); +} + +#[test] +fn the_processor_number_names_the_reading_processor() { + let t = Fixture::new(2); + let pi = t.pi.clone(); + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(1); + assert_eq!(pi.read64(reg::CPU_NUM).data, 1); + crate::cpu::smp::set_current_cpu(0); + assert_eq!(pi.read64(reg::CPU_NUM).data, 0); + }) + .join() + .unwrap(); + // A device thread counts as processor 0, and a store changes nothing. + t.w(reg::CPU_NUM, 1); + assert_eq!(t.r(reg::CPU_NUM), 0); + // A node whose slice A is global CPU 2. + let node = HubPi::new(2, 2, Arc::new(|| 0)); + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(3); + assert_eq!(node.read64(reg::CPU_NUM).data, 1); + crate::cpu::smp::set_current_cpu(0); + assert_eq!(node.read64(reg::CPU_NUM).data, 0); + }) + .join() + .unwrap(); +} + +#[test] +fn processor_presence_enable_nmi_and_soft_reset_reach_the_machine() { + let t = Fixture::new(2); + assert_eq!((t.r(reg::CPU_PRESENT_A), t.r(reg::CPU_PRESENT_B)), (1, 1)); + assert_eq!((t.r(reg::CPU_ENABLE_A), t.r(reg::CPU_ENABLE_B)), (0, 0)); + t.w(reg::CPU_ENABLE_B, 0xff); + assert_eq!(t.r(reg::CPU_ENABLE_B), 1); + assert_eq!(t.take_hooks(), ["enable 1 true"]); + // The soft reset reaches only the enabled processors. + t.w(reg::SOFTRESET, 1); + assert_eq!(t.take_hooks(), ["soft 1"]); + t.w(reg::NMI_A, 1); + t.w(reg::NMI_B, 0); + assert_eq!(t.take_hooks(), ["nmi 0", "nmi 1"]); + // Reset halts both processors again; the enables read zero. + t.pi.power_on(); + assert_eq!(t.r(reg::CPU_ENABLE_B), 0); + assert_eq!(t.take_hooks(), ["enable 0 false", "enable 1 false"]); +} + +#[test] +fn an_absent_processors_enable_stores_and_reaches_nothing() { + let t = Fixture::new(1); + assert_eq!((t.r(reg::CPU_PRESENT_A), t.r(reg::CPU_PRESENT_B)), (1, 0)); + t.w(reg::CPU_ENABLE_B, 1); + assert_eq!(t.r(reg::CPU_ENABLE_B), 1); + t.w(reg::NMI_B, 1); + t.w(reg::SOFTRESET, 1); + assert!(t.take_hooks().is_empty()); + t.w(reg::CPU_ENABLE_A, 1); + t.w(reg::SOFTRESET, 1); + assert_eq!(t.take_hooks(), ["enable 0 true", "soft 0"]); + // Nor does the absent slot get an interrupt line. + t.w(reg::INT_MASK0_B, !0); + t.w(reg::INT_PEND_MOD, MODIFY_SET | 3); + assert!(!t.lines.get(3)); +} + +#[test] +fn the_cached_alias_size_codes_double_from_4_kib() { + let t = Fixture::new(2); + for (code, bytes) in [(0, 0), (1, 4 << 10), (2, 8 << 10), (15, 64 << 20)] { + t.w(reg::CALIAS_SIZE, code); + assert_eq!(t.take_hooks(), [format!("calias {bytes:#x}")]); + assert_eq!(t.pi.calias_bytes(), bytes); + } + // Four bits store; a wider value wraps into them. + t.w(reg::CALIAS_SIZE, 0x31); + assert_eq!(t.r(reg::CALIAS_SIZE), 1); +} + +#[test] +fn interrupt_modify_sets_and_clears_one_bit_of_one_bank() { + let t = Fixture::new(2); + t.w(reg::INT_PEND_MOD, MODIFY_SET | 12); + assert_eq!((t.r(reg::INT_PEND0), t.r(reg::INT_PEND1)), (1 << 12, 0)); + t.w(reg::INT_PEND_MOD, 12); + assert_eq!(t.r(reg::INT_PEND0), 0); + // Number 70 is bit 6 of the second bank. + t.w(reg::INT_PEND_MOD, MODIFY_SET | 70); + assert_eq!((t.r(reg::INT_PEND0), t.r(reg::INT_PEND1)), (0, 1 << 6)); + // The modify register is an instruction: it reads zero, and the pending + // banks refuse writes. + assert_eq!(t.r(reg::INT_PEND_MOD), 0); + t.w(reg::INT_PEND0, !0); + t.w(reg::INT_PEND1, !0); + assert_eq!((t.r(reg::INT_PEND0), t.r(reg::INT_PEND1)), (0, 1 << 6)); +} + +#[test] +fn pending_bits_raise_a_line_only_through_a_mask_and_follow_it_down() { + let t = Fixture::new(2); + // Masked pending is invisible; unmasking raises the line with nothing + // written to the pending bank, which separates recomputing from + // raise-on-set. + t.w(reg::INT_PEND_MOD, MODIFY_SET | 12); + assert!(!t.lines.get(0)); + t.w(reg::INT_MASK0_A, 1 << 12); + assert!(t.lines.get(0)); + // Level-triggered: clearing the last visible bit drops it. + t.w(reg::INT_PEND_MOD, 12); + assert!(!t.lines.get(0)); + // Bank 1 has its own line: number 70 does not raise bank 0's. + t.w(reg::INT_MASK0_A, !0); + t.w(reg::INT_MASK1_A, !0); + t.w(reg::INT_PEND_MOD, MODIFY_SET | 70); + assert!(!t.lines.get(0) && t.lines.get(1)); +} + +#[test] +fn the_two_processors_have_their_own_masks() { + let t = Fixture::new(2); + t.w(reg::INT_MASK0_A, 0); + t.w(reg::INT_MASK0_B, 1 << 12); + t.w(reg::INT_PEND_MOD, MODIFY_SET | 12); + assert!(!t.lines.get(0) && t.lines.get(3)); + t.w(reg::INT_PEND_MOD, 12); + assert!(!t.lines.get(3)); + // The PROM's order for B: pend while masked, then unmask. + t.w(reg::INT_MASK0_B, 0); + t.w(reg::INT_PEND_MOD, MODIFY_SET | 12); + assert!(!t.lines.get(3)); + t.w(reg::INT_MASK0_B, 1 << 12); + assert!(t.lines.get(3)); + // B's second bank must not follow the first bank's pending bit. + t.w(reg::INT_MASK1_B, !0); + assert!(!t.lines.get(4)); +} + +#[test] +fn the_counter_runs_at_1_25_mhz_in_52_bits_and_can_be_written() { + let t = Fixture::new(1); + t.w(reg::RT_COUNT, 0); + assert_eq!(t.r(reg::RT_COUNT), 0); + // 800 ns is one tick, and the counter moves between two reads. + t.now.fetch_add(799, Ordering::SeqCst); + assert_eq!(t.r(reg::RT_COUNT), 0); + t.now.fetch_add(1, Ordering::SeqCst); + assert_eq!(t.r(reg::RT_COUNT), 1); + // A whole second is 1,250,000 ticks. + t.advance(1_250_000 - 1); + assert_eq!(t.r(reg::RT_COUNT), 1_250_000); + // A write moves the origin and the counter keeps running from it. + t.w(reg::RT_COUNT, 0xabcde_f012_3456); + assert_eq!(t.r(reg::RT_COUNT), 0xabcde_f012_3456); + t.advance(10); + assert_eq!(t.r(reg::RT_COUNT), 0xabcde_f012_3460); + // Fifty-two bits: the top of the range wraps to zero. + t.w(reg::RT_COUNT, RT_MASK); + t.advance(1); + assert_eq!(t.r(reg::RT_COUNT), 0); + t.w(reg::RT_COUNT, !0); + assert_eq!(t.r(reg::RT_COUNT), RT_MASK); + // Hours of elapsed time stay exact. + t.w(reg::RT_COUNT, 0); + t.advance(1_250_000 * 3600 * 5); + assert_eq!(t.r(reg::RT_COUNT), 1_250_000 * 3600 * 5); +} + +#[test] +fn a_comparator_pends_when_the_counter_reaches_it_and_the_enable_gates_the_line() { + let t = Fixture::new(2); + t.w(reg::RT_COUNT, 100); + t.w(reg::RT_COMPARE_A, 150); + t.advance(49); + assert_eq!((t.r(reg::RT_PEND_A), t.lines.get(2)), (0, false)); + // A match pends whether or not enabled. + t.advance(1); + assert_eq!(t.r(reg::RT_PEND_A), 1); + assert!(!t.lines.get(2)); + t.w(reg::RT_EN_A, 1); + assert!(t.lines.get(2)); + // Writing zero acknowledges and drops the line. + t.w(reg::RT_PEND_A, 0); + assert!(!t.lines.get(2)); + // A match fires once; nothing re-arms it until software writes again. + t.advance(10_000); + assert_eq!(t.r(reg::RT_PEND_A), 0); + t.w(reg::RT_COMPARE_A, 10_200); + t.advance(49); + assert_eq!(t.r(reg::RT_PEND_A), 0); + t.advance(1); + assert_eq!(t.r(reg::RT_PEND_A), 1); + // The second processor has its own comparator and line. + t.w(reg::RT_EN_B, 1); + t.w(reg::RT_COMPARE_B, t.r(reg::RT_COUNT) + 5); + t.advance(5); + assert!(t.lines.get(5)); + assert_eq!(t.r(reg::RT_PEND_B), 1); +} + +#[test] +fn a_comparator_behind_the_counter_waits_a_whole_revolution() { + let t = Fixture::new(1); + t.w(reg::RT_EN_A, 1); + // The guest kernel parks a processor's tick by writing a value behind the counter + // and depends on the silence. + t.w(reg::RT_COUNT, 1000); + t.w(reg::RT_COMPARE_A, 0); + t.advance(500_000); + assert_eq!((t.r(reg::RT_PEND_A), t.lines.get(2)), (0, false)); + // Equal to the counter is a revolution away too, not a match now. + t.w(reg::RT_COMPARE_A, t.r(reg::RT_COUNT)); + assert_eq!(t.r(reg::RT_PEND_A), 0); + // The counter wrapping round reaches the zeroed comparator. + t.w(reg::RT_COMPARE_A, 0); + t.w(reg::RT_COUNT, RT_MASK - 2); + t.advance(2); + assert_eq!(t.r(reg::RT_PEND_A), 0); + t.advance(1); + assert_eq!(t.r(reg::RT_PEND_A), 1); +} + +#[test] +fn moving_the_counter_pends_nothing_but_rearms_the_deadline() { + let t = Fixture::new(1); + t.w(reg::RT_COUNT, 0); + t.w(reg::RT_COMPARE_A, 100); + // Jumping the counter past the compare value is not a match. + t.w(reg::RT_COUNT, 500); + t.advance(1000); + assert_eq!(t.r(reg::RT_PEND_A), 0); + // Jumping to just before it re-arms against the new counter. This is + // Linux's guarded rewrite: read, add a delta, write, re-read. + t.w(reg::RT_COUNT, 90); + t.advance(9); + assert_eq!(t.r(reg::RT_PEND_A), 0); + t.advance(1); + assert_eq!(t.r(reg::RT_PEND_A), 1); + // The longest interval is a revolution minus a tick. + t.w(reg::RT_PEND_A, 0); + t.w(reg::RT_COUNT, 0); + t.w(reg::RT_COMPARE_A, RT_MASK); + t.advance(RT_MASK - 1); + assert_eq!(t.r(reg::RT_PEND_A), 0); + t.advance(1); + assert_eq!(t.r(reg::RT_PEND_A), 1); +} + +#[test] +fn the_local_control_register_stores_bit_16_and_the_low_byte() { + let t = Fixture::new(1); + assert_eq!(t.r(reg::RT_LOCAL_CTRL), 0x1_0000); + t.w(reg::RT_LOCAL_CTRL, 0x1_0047); + assert_eq!(t.r(reg::RT_LOCAL_CTRL), 0x1_0047); + t.w(reg::RT_LOCAL_CTRL, !0); + assert_eq!(t.r(reg::RT_LOCAL_CTRL), 0x1_00ff); + // Nothing written there changes the rate. + t.w(reg::RT_COUNT, 0); + t.advance(10); + assert_eq!(t.r(reg::RT_COUNT), 10); + t.pi.power_on(); + assert_eq!(t.r(reg::RT_LOCAL_CTRL), 0x1_0000); +} + +#[test] +fn the_error_registers_store_masked_values_and_nothing_times_out() { + let t = Fixture::new(2); + assert_eq!(t.r(reg::ERR_INT_PEND), 0); + t.w(reg::ERR_INT_PEND, !0); + assert_eq!(t.r(reg::ERR_INT_PEND), 0); + t.w(reg::ERR_INT_MASK_A, !0); + t.w(reg::ERR_INT_MASK_B, 0x0123456); + assert_eq!((t.r(reg::ERR_INT_MASK_A), t.r(reg::ERR_INT_MASK_B)), (0x01ff_ffff, 0x0123456)); + t.w(reg::ERR_STACK_SIZE, !0); + assert_eq!(t.r(reg::ERR_STACK_SIZE), 0xf); + for off in [reg::TIMEDOUT_A, reg::TIMEDOUT_B] { + t.w(off, !0); + assert_eq!(t.r(off), 0); + } + t.pi.power_on(); + assert_eq!((t.r(reg::ERR_INT_MASK_A), t.r(reg::ERR_INT_MASK_B), t.r(reg::ERR_STACK_SIZE)), (0, 0, 0)); + for off in (reg::ERR_STATUS0_A..=reg::ERR_STATUS1_B_RCLR).step_by(8) { + assert_eq!(t.r(off), 0, "{off:#x}"); + } +} + +#[test] +fn a_failed_read_latches_the_first_address_and_later_ones_only_set_overrun() { + let t = Fixture::new(2); + // The PROM's probe: a failed doubleword read at 0x0240_0208 of node 0. + t.pi.read_error(1, 0x0240_0208); + let first = STATUS0_VALID | ((0x0240_0208u64 >> 3) << 25); + assert_eq!(t.r(reg::ERR_STATUS0_B), first); + assert_eq!(t.r(reg::ERR_STATUS0_A), 0); + t.pi.read_error(1, 0x1234_5678); + assert_eq!(t.r(reg::ERR_STATUS0_B), first | STATUS0_OVERRUN); + // The kernel checks bits 61:25 and a 40-bit address; the type fields stay zero. + t.pi.read_error(0, 0xff_0000_0008); + assert_eq!(t.r(reg::ERR_STATUS0_A), STATUS0_VALID | ((0xff_0000_0008u64 >> 3) << 25)); + // A slice the node does not fit records nothing. + Fixture::new(1).pi.read_error(1, 8); +} + +#[test] +fn a_read_to_clear_alias_clears_only_on_a_full_width_read() { + let t = Fixture::new(1); + t.pi.read_error(0, 0x240_0208); + let value = t.r(reg::ERR_STATUS0_A); + // The 32-bit half path reads without clearing. + assert_eq!(t.pi.read(reg::ERR_STATUS0_A_RCLR, false), value); + assert_eq!(t.r(reg::ERR_STATUS0_A), value); + assert_eq!(t.r(reg::ERR_STATUS0_A_RCLR), value); + assert_eq!(t.r(reg::ERR_STATUS0_A), 0); +} + +#[test] +fn a_full_width_zero_store_to_either_alias_ends_the_record() { + let t = Fixture::new(2); + for alias in [reg::ERR_STATUS0_A_RCLR, reg::ERR_STATUS1_A_RCLR] { + t.pi.read_error(0, 0x240_0208); + // A narrow or nonzero store does not clear. + t.pi.write_sized(alias, 0, 4); + t.pi.write_sized(alias, 1, 8); + assert_ne!(t.r(reg::ERR_STATUS0_A), 0); + t.pi.write_sized(alias, 0, 8); + assert_eq!(t.r(reg::ERR_STATUS0_A), 0); + } + // B's aliases clear B alone. + t.pi.read_error(0, 8); + t.pi.read_error(1, 8); + t.pi.write_sized(reg::ERR_STATUS1_B_RCLR, 0, 8); + assert_eq!((t.r(reg::ERR_STATUS0_B), t.r(reg::ERR_STATUS0_A) != 0), (0, true)); + // Reset clears both slices. + t.pi.read_error(1, 8); + t.pi.power_on(); + assert_eq!((t.r(reg::ERR_STATUS0_A), t.r(reg::ERR_STATUS0_B)), (0, 0)); +} + +#[test] +fn the_graphics_flow_control_registers_keep_their_field_widths() { + let t = Fixture::new(2); + const PAGE_MASK: u64 = (1 << 40) | 0xff_fffe_0000; + let slot = |a: u64| reg::GFX_BASE + a * 8 * 5; + for s in 0..2 { + t.w(slot(s), 0x0123_4567_89ab_cdef); + assert_eq!(t.r(slot(s)), 0x0123_4567_89ab_cdef & PAGE_MASK); + // Credit and bias are twelve bits, the interrupt pair 26. + for (index, mask) in [(1, 0xfff), (2, 0xfff), (3, 0x3ff_ffff), (4, 0x3ff_ffff)] { + t.w(slot(s) + index * 8, !0); + assert_eq!(t.r(slot(s) + index * 8), mask); + } + } + // A and B are independent. + t.w(reg::GFX_BASE + 8, 0x65); + t.w(slot(1) + 8, 0xab); + assert_eq!((t.r(reg::GFX_BASE + 8), t.r(slot(1) + 8)), (0x65, 0xab)); + // The block shares nothing with the comparator. + t.w(reg::RT_COMPARE_A, 0x1_2345_6789_abcd); + t.w(reg::GFX_BASE, !0); + assert_eq!(t.r(reg::RT_COMPARE_A), 0x1_2345_6789_abcd); + // All ten reset to zero. + t.pi.power_on(); + for n in 0..10 { + assert_eq!(t.r(reg::GFX_BASE + n * 8), 0); + } +} + +/// The guest kernel attaches a pipe by writing its page with the enable set, the saved +/// count, and then the bias. No store is ever counted: the credit stays as +/// written. +#[test] +fn the_graphics_attach_sequence_leaves_the_credit_as_written() { + let t = Fixture::new(2); + let (page, credit, bias) = (reg::GFX_BASE, reg::GFX_BASE + 8, reg::GFX_BASE + 16); + t.w(bias, 0); + t.w(credit, 0x65); + t.w(page, 0x1234_0000 | (1 << 40)); + t.w(bias, 0x44); + assert_eq!((t.r(credit), t.r(bias), t.r(page)), (0x65, 0x44, 0x1234_0000 | (1 << 40))); + assert!(t.r(credit) >= t.r(bias)); + // No pending bit follows from the bias: the interrupt is not modelled. + assert_eq!(t.r(reg::INT_PEND0), 0); +} + +#[test] +fn the_hard_reset_bit_takes_a_zero_store_and_reads_zero() { + let t = Fixture::new(1); + t.w(reg::HARDRESET_BIT, 0); + assert_eq!(t.r(reg::HARDRESET_BIT), 0); + t.w(reg::HARDRESET_BIT, 1); + assert_eq!(t.r(reg::HARDRESET_BIT), 0); +} + +#[test] +fn unimplemented_offsets_read_zero() { + let t = Fixture::new(1); + assert_eq!(t.r(0x1f8), 0); + t.w(0x1f8, 0x5555); + assert_eq!(t.r(0x1f8), 0); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index dd945f955..a8b15baf3 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,6 +60,8 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod hub; +pub mod hub_pi; pub mod dp83843; pub mod ics1890; pub mod timekeeper; From a9669d7870a0d3f04f381c9111f12f2ee6839cc8 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:00:09 -0700 Subject: [PATCH 15/38] hub: add the memory and directory interface Firmware configures memory and reads the node's board record through this block, and the PROM reads the slot identity and writes progress codes to the indicators. The configuration register is masked storage, the error cluster reads zero because the model latches no memory error, and the DIMM registers are write-only. The one-wire master drives the shared w1 bus and completes a slot one microsecond after it starts, so a guest polling for done never sees it inside the starting write. The slot identity is whatever the board drives and is supplied by the machine. --- src/dev/hub_md.rs | 237 ++++++++++++++++++++++++++++++++++++++++ src/dev/hub_md/tests.rs | 163 +++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 401 insertions(+) create mode 100644 src/dev/hub_md.rs create mode 100644 src/dev/hub_md/tests.rs diff --git a/src/dev/hub_md.rs b/src/dev/hub_md.rs new file mode 100644 index 000000000..bd80f4f3c --- /dev/null +++ b/src/dev/hub_md.rs @@ -0,0 +1,237 @@ +//! The Hub's memory and directory interface: the memory configuration, the +//! error and DIMM registers, the one-wire master that reads the node's board +//! record, the slot identity and the diagnostic indicators. +//! +//! Offsets are from the base of the Hub window. The node's I2C controller and +//! the bring-up UART are mapped over this block by the shell, and are not here. +//! +//! # What is approximated here +//! +//! - MD_MEMORY_CONFIG is masked storage. No write resizes memory or changes +//! error reporting. Its reset value is Linux's `MMC_RESET_DEFAULTS`. +//! - The eight directory, protocol, memory and miscellaneous error registers +//! read zero: the model never latches a memory error. The two DIMM +//! mode-initialisation registers are write-only and logged. +//! - MD_PERF_CNT0 stores 20 bits and never counts, and its event selector is +//! not decoded. +//! - MD_SLOTID_USTAT reads the board signals the machine supplies. Bits 0 and +//! 2 on the Origin 200 and bit 7 on the Origin 2000 are approximations: no +//! public source gives their values on those boards. +//! - The indicator registers are write-only. Writes are logged and nothing is +//! stored; the Origin 200 pair at 0x22_0080 and 0x22_00a0 is treated like +//! them, which is an approximation (Linux names those offsets +//! microcontroller and UART registers). The stock PROM's progress codes are +//! not decoded. +//! - A one-wire slot completes one microsecond of clock time after it +//! starts. How long a slot takes on hardware is unknown; the model needs +//! only that it does not complete inside the write that starts it. A write +//! while a slot is under way starts another. +//! - The node's one-wire bus is the only net: the midplane net that an Origin +//! 2000 node's line also reaches is not wired. +//! +//! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::heart::Clock; +use crate::dev::w1::{self, Bus}; +use crate::devlog::LogModule; +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_OK}; +use parking_lot::Mutex; + +pub mod reg { + pub const CONFIG: u64 = 0x20_0018; + pub const ERROR_FIRST: u64 = 0x20_0050; + pub const ERROR_LAST: u64 = 0x20_0088; + pub const MEM_DIMM_INIT: u64 = 0x20_0090; + pub const DIR_DIMM_INIT: u64 = 0x20_0098; + pub const MLAN_CTL: u64 = 0x20_00a8; + pub const PERF_CNT0: u64 = 0x21_0010; + /// The I2C controller's S0 and S1, mapped over the Hub window by the + /// shell, and the bring-up UART after them. + pub const I2C_BASE: u64 = 0x22_0000; + pub const SLOTID_USTAT: u64 = 0x22_0048; + pub const LED0: u64 = 0x22_0050; + pub const LED1: u64 = 0x22_0058; + pub const OLD_LED0: u64 = 0x22_0080; + pub const OLD_LED1: u64 = 0x22_00a0; + pub const UART_BASE: u64 = 0x22_00c0; + pub const UART_END: u64 = 0x22_0100; +} + +/// First offset that is not the memory interface's. +pub const END: u64 = 0x40_0000; +pub const START: u64 = 0x20_0000; + +const CONFIG_MASK: u64 = 0x003f_ffff_ffff_ffff; +const CONFIG_RESET: u64 = 0x001e_7fbc_3fff_ffff; +const PERF_MASK: u64 = 0xf_ffff; + +const MLAN_RX: u64 = 1 << 0; +const MLAN_DONE: u64 = 1 << 1; +/// Sample time 9:2, pulse width 19:10, and the two timing phases 26:20 and +/// 33:27. The register is 34 bits wide; truncating it to 32 would rewrite the +/// upper phase. +const MLAN_WRITABLE: u64 = 0x3_ffff_fffc; +/// Both phases reset to 0x31, Linux's `MLAN_RESET_DEFAULTS`. +const MLAN_RESET: u64 = (0x31 << 20) | (0x31 << 27); +const MLAN_SLOT_NS: u64 = 1000; + +const SLOT_FLASH_READY_LOW: u64 = 1 << 0; +const SLOT_XJMP_ID: u64 = 1 << 2; +const SLOT_FLASH_READY_HIGH: u64 = 1 << 4; +const SLOT_MASTER_MODULE: u64 = 1 << 7; +const SLOT_ID_MASK: u64 = 7; + +/// What the board drives on the signals MD_SLOTID_USTAT samples. +#[derive(Clone, Copy, Debug, Default)] +pub struct SlotSignals { + /// The midplane's slot identity, the low three bits, or `None` where the + /// node sits in no numbered slot (the Origin 200). + pub slot: Option, + /// This processor module is the Origin 200 slave chassis. + pub slave_module: bool, +} + +struct State { + config: u64, + perf_cnt0: u64, + mlan: u64, + /// The level the slot sampled, reported once the slot's deadline passes. + level: bool, + /// Clock reading at which the slot started last completes. + deadline: Option, + bus: Option, +} +impl State { + fn new(bus: Option) -> Self { + Self { config: CONFIG_RESET, perf_cnt0: 0, mlan: MLAN_RESET, level: true, deadline: None, bus } + } +} + +pub struct HubMd { + clock: Clock, + signals: SlotSignals, + state: Mutex, +} + +fn unimp(what: &str, off: u64, value: Option) { + match value { + Some(v) => crate::dlog!(LogModule::Hub, "Hub MD: write of {v:#x} to {what} {off:#x}"), + None => crate::dlog!(LogModule::Hub, "Hub MD: read of {what} {off:#x}"), + } +} + +impl HubMd { + pub fn new(clock: Clock, signals: SlotSignals) -> Self { + Self { clock, signals, state: Mutex::new(State::new(None)) } + } + + /// Attach the node's one-wire bus to the master. + pub fn set_onewire(&self, bus: Bus) { + self.state.lock().bus = Some(bus); + } + + fn mlan_value(&self, s: &State) -> u64 { + match s.deadline { + Some(at) if (self.clock)() >= at => s.mlan | MLAN_DONE | if s.level { MLAN_RX } else { 0 }, + _ => s.mlan, + } + } + + fn slot_id(&self) -> u64 { + let mut value = SLOT_FLASH_READY_LOW | SLOT_XJMP_ID | SLOT_FLASH_READY_HIGH + | if self.signals.slave_module { 0 } else { SLOT_MASTER_MODULE }; + if let Some(bits) = self.signals.slot { + value = (value & !SLOT_ID_MASK) | (u64::from(bits) & SLOT_ID_MASK); + } + value + } + + /// A register's value without side effects, for the registers that store. + pub fn peek(&self, off: u64) -> Option { + let s = self.state.lock(); + Some(match off { + reg::CONFIG => s.config, + reg::MLAN_CTL => self.mlan_value(&s), + reg::PERF_CNT0 => s.perf_cnt0, + _ => return None, + }) + } + + pub fn read(&self, off: u64) -> u64 { + match off { + reg::ERROR_FIRST..=reg::ERROR_LAST if off % 8 == 0 => 0, + reg::SLOTID_USTAT => self.slot_id(), + _ => match self.peek(off) { + Some(v) => v, + None => { + unimp("unimplemented or write-only register", off, None); + 0 + } + }, + } + } + + pub fn write(&self, off: u64, value: u64) { + match off { + reg::CONFIG => self.state.lock().config = value & CONFIG_MASK, + reg::ERROR_FIRST..=reg::ERROR_LAST if off % 8 == 0 => { + unimp("error register (nothing latches an error to clear)", off, Some(value)) + } + reg::MEM_DIMM_INIT | reg::DIR_DIMM_INIT => { + unimp("DIMM mode initialisation register (write-only, no functional signature)", off, Some(value)) + } + reg::MLAN_CTL => { + // Programs the timing and starts a slot. The bus answers + // inside the write, but the answer is reported only from the + // slot's own deadline. A write cannot set done or the data. + let mut s = self.state.lock(); + s.mlan = value & MLAN_WRITABLE; + let (pulse, sample) = w1::slot::fields(value); + s.level = match s.bus.as_mut() { + Some(bus) => w1::slot::slot(bus, pulse, sample), + None => pulse >= w1::slot::RESET_PULSE || pulse < w1::slot::ONE_PULSE, + }; + s.deadline = Some((self.clock)().saturating_add(MLAN_SLOT_NS)); + } + reg::PERF_CNT0 => self.state.lock().perf_cnt0 = value & PERF_MASK, + reg::LED0 | reg::LED1 | reg::OLD_LED0 | reg::OLD_LED1 => { + let which = u8::from(matches!(off, reg::LED1 | reg::OLD_LED1)); + crate::dlog!(LogModule::Hub, "Hub MD: indicator {which} shows {:#04x}", value & 0xff); + } + _ => unimp("unimplemented register", off, Some(value)), + } + } +} + +impl HubMd { + /// Whether the register state is locked now, for the `locks` monitor command. + pub fn is_locked(&self) -> bool { + self.state.is_locked() + } +} + +impl Resettable for HubMd { + /// A slot started before reset must not complete after it. + fn power_on(&self) { + let mut s = self.state.lock(); + let bus = s.bus.take().map(|mut bus| { + // The part resets its own protocol state. + bus.reset(); + bus + }); + *s = State::new(bus); + } +} + +impl BusDevice for HubMd { + fn read64(&self, addr: u64) -> BusRead64 { + BusRead64::ok(self.read(addr)) + } + fn write64(&self, addr: u64, value: u64) -> u32 { + self.write(addr, value); + BUS_OK + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/hub_md/tests.rs b/src/dev/hub_md/tests.rs new file mode 100644 index 000000000..a0e3539a9 --- /dev/null +++ b/src/dev/hub_md/tests.rs @@ -0,0 +1,163 @@ +// Cases follow tests/qtest/sn0-hub-test.c in jamesbraid/qemu-sgi-origami: +// md-config, md-error-cluster, md-dimm-init, md-perf-cnt0, the slot-identity +// cases (flash-ready-at-both-bits, dual-node-master-slave-sense, +// origin2000-slot-and-processor-identity, onyx2-*), md-indicator-registers +// and the one-wire master cases. + +use super::*; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +fn md(signals: SlotSignals) -> (HubMd, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let clock = now.clone(); + (HubMd::new(Arc::new(move || clock.load(Ordering::SeqCst)), signals), now) +} +fn r(md: &HubMd, off: u64) -> u64 { + md.read64(off).data +} + +/// MD_MEMORY_CONFIG is 54 bits of storage that resets to Linux's default and +/// resizes nothing. +#[test] +fn the_memory_config_stores_54_bits_and_resets_to_linuxs_default() { + let (md, _) = md(SlotSignals::default()); + assert_eq!(r(&md, reg::CONFIG), 0x001e_7fbc_3fff_ffff); + md.write64(reg::CONFIG, !0); + assert_eq!(r(&md, reg::CONFIG), 0x003f_ffff_ffff_ffff); + md.write64(reg::CONFIG, 0x1234); + assert_eq!(r(&md, reg::CONFIG), 0x1234); + md.power_on(); + assert_eq!(r(&md, reg::CONFIG), 0x001e_7fbc_3fff_ffff); +} + +/// Firmware's memory test needs the error cluster to read zero; the DIMM +/// registers are write-only; the performance counter stores 20 bits and never +/// counts. +#[test] +fn the_error_cluster_reads_zero_and_the_performance_counter_never_counts() { + let (md, now) = md(SlotSignals::default()); + for off in (reg::ERROR_FIRST..=reg::ERROR_LAST).step_by(8) { + md.write64(off, !0); + assert_eq!(r(&md, off), 0, "{off:#x}"); + } + for off in [reg::MEM_DIMM_INIT, reg::DIR_DIMM_INIT] { + md.write64(off, 0x55); + assert_eq!(r(&md, off), 0); + } + assert_eq!(r(&md, reg::PERF_CNT0), 0); + md.write64(reg::PERF_CNT0, !0); + now.store(1_000_000, Ordering::SeqCst); + assert_eq!(r(&md, reg::PERF_CNT0), 0xf_ffff); + md.power_on(); + assert_eq!(r(&md, reg::PERF_CNT0), 0); +} + +#[test] +fn the_origin_200_reads_flash_ready_and_the_master_sense() { + let (master, _) = md(SlotSignals { slot: None, slave_module: false }); + let (slave, _) = md(SlotSignals { slot: None, slave_module: true }); + assert_eq!(r(&master, reg::SLOTID_USTAT), 0x95); + assert_eq!(r(&slave, reg::SLOTID_USTAT), 0x15); + // Read-only: a store changes nothing. + master.write64(reg::SLOTID_USTAT, 0); + assert_eq!(r(&master, reg::SLOTID_USTAT), 0x95); +} + +#[test] +fn an_origin_2000_node_reads_the_slot_the_midplane_drives() { + for slot in 0..8 { + let (md, _) = md(SlotSignals { slot: Some(slot), slave_module: false }); + assert_eq!(r(&md, reg::SLOTID_USTAT), 0x90 | u64::from(slot)); + } + // Only the low three bits are the slot. + let (md, _) = md(SlotSignals { slot: Some(0xff), slave_module: false }); + assert_eq!(r(&md, reg::SLOTID_USTAT), 0x97); +} + +#[test] +fn the_indicators_are_write_only() { + let (md, _) = md(SlotSignals::default()); + for off in [reg::LED0, reg::LED1, reg::OLD_LED0, reg::OLD_LED1] { + md.write64(off, 0x2b); + assert_eq!(r(&md, off), 0, "{off:#x}"); + } +} + +#[test] +fn unimplemented_offsets_read_zero() { + let (md, _) = md(SlotSignals::default()); + md.write64(0x20_0400, 7); + assert_eq!(r(&md, 0x20_0400), 0); +} + +/// MD_MLAN_CTL is 34 bits wide and resets to 0x31 in both phases. +#[test] +fn the_one_wire_master_keeps_its_wide_field_and_resets_to_its_timing() { + let (md, now) = md(SlotSignals::default()); + assert_eq!(r(&md, reg::MLAN_CTL), 0x1_8b10_0000); + // A write cannot set done or the received data, and truncating to 32 bits + // would rewrite the upper phase. + md.write64(reg::MLAN_CTL, (0x66 << 27) | (60 << 10) | (0x41 << 2) | 3); + assert_eq!(r(&md, reg::MLAN_CTL), (0x66 << 27) | (60 << 10) | (0x41 << 2)); + // Done and the sampled level arrive a microsecond later. With no part on + // the wire a 60-unit pulse is a write-zero, which holds the wire low, and + // a short one is a write-one, which samples high. + now.store(999, Ordering::SeqCst); + assert_eq!(r(&md, reg::MLAN_CTL) & 3, 0); + now.store(1000, Ordering::SeqCst); + assert_eq!(r(&md, reg::MLAN_CTL) & 3, 2); + md.write64(reg::MLAN_CTL, 8 << 10); + now.store(2000, Ordering::SeqCst); + assert_eq!(r(&md, reg::MLAN_CTL) & 3, 3); + // A reset cancels a slot in progress. + md.write64(reg::MLAN_CTL, 8 << 10); + md.power_on(); + now.store(10_000, Ordering::SeqCst); + assert_eq!(r(&md, reg::MLAN_CTL), 0x1_8b10_0000); +} + +/// A slot starts clear of done: a read before the deadline sees neither done +/// nor the answer, even though the bus answered inside the write. +#[test] +fn starting_a_slot_is_not_finishing_it() { + let (md, now) = md(SlotSignals::default()); + md.write64(reg::MLAN_CTL, 8 << 10); + assert_eq!(r(&md, reg::MLAN_CTL) & 3, 0); + now.fetch_add(1000, Ordering::SeqCst); + md.write64(reg::MLAN_CTL, 8 << 10); + assert_eq!(r(&md, reg::MLAN_CTL) & 3, 0, "the restart clears done"); +} + +/// Firmware reads the node's board record over the master: a reset slot, a +/// ROM command, and then the part's serial and memory, one slot a bit. +#[test] +fn the_master_reads_the_nodes_board_record() { + let (md, now) = md(SlotSignals::default()); + let record = BoardRecord { barcode: "EMUU0000", part: "013-1895-001", revision: "C", name: "PIMM_1XT5_1MB" }; + let mut bus = Bus::new(); + bus.add(Box::new(board_record_part(&record, 0x0123_4567_89ab))); + md.set_onewire(bus); + let slot = |pulse: u64| { + md.write64(reg::MLAN_CTL, pulse << 10); + now.fetch_add(MLAN_SLOT_NS, Ordering::SeqCst); + r(&md, reg::MLAN_CTL) + }; + // The part answers the reset slot with a presence pulse: the wire reads low. + let reset = slot(520); + assert_eq!((reset & MLAN_DONE != 0, reset & MLAN_RX != 0), (true, false)); + let bit = |one: bool| slot(if one { 6 } else { 80 }) & MLAN_RX != 0; + // READ ROM, then the family code and serial, least significant bit first. + for i in 0..8 { + bit(0x33u8 >> i & 1 != 0); + } + let mut rom = [0u8; 8]; + for i in 0..64 { + if bit(true) { + rom[i / 8] |= 1 << (i % 8); + } + } + assert_eq!(rom[0], 0x0b, "a DS2505's family code"); + assert_eq!(&rom[1..7], &[0xab, 0x89, 0x67, 0x45, 0x23, 0x01]); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index a8b15baf3..26b7ddcad 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -61,6 +61,7 @@ pub mod virtio; pub mod wd33c93a; pub mod z85c30; pub mod hub; +pub mod hub_md; pub mod hub_pi; pub mod dp83843; pub mod ics1890; From 5bade09a95cd1077ca1f29caa0f2dd69230d2451 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:01:00 -0700 Subject: [PATCH 16/38] hub: add the I/O interface The Hub's XIO port is where the Bridge or Crossbow hangs. This adds its widget registers, access and link control, the seven big-window translation entries and the two block transfer engines. The translation entries are the decode table for the big windows: an access reads its entry with one atomic load and the widget slots are write-once, so re-aiming a window needs no lock and no mode flag. A window aimed at an absent widget reads zero and logs, since guests disable windows that way. The engines run a transfer inside the store that launches it, through a node fabric trait the Hub implements over its claim map, and complete with a notification write or an interrupt-modify on the Hub holding the named node number. --- src/dev/hub_io.rs | 669 ++++++++++++++++++++++++++++++++++++++++ src/dev/hub_io/tests.rs | 463 +++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 1133 insertions(+) create mode 100644 src/dev/hub_io.rs create mode 100644 src/dev/hub_io/tests.rs diff --git a/src/dev/hub_io.rs b/src/dev/hub_io.rs new file mode 100644 index 000000000..d61e7d175 --- /dev/null +++ b/src/dev/hub_io.rs @@ -0,0 +1,669 @@ +//! The Hub's I/O interface: the node's one XIO port. Its widget registers, +//! access and link control, the seven big-window translation entries and the +//! windows they steer, the two block transfer engines and the scratch +//! registers firmware uses as boot state. +//! +//! Offsets are from the base of the Hub window. The translation entries are +//! the decode: a big-window access takes the entry's widget and slice with one +//! atomic load, and an entry write changes where the next access lands, so no +//! mode flag is tested per access. +//! +//! # What is approximated here +//! +//! - The model completes every request before it returns, retries nothing and +//! never fails its link, so IIO_WSTAT reads zero and the link log reads +//! zero. The link reads up from reset because a link that reads down makes +//! Linux abandon its XIO probe; the field's real reset value is not +//! published. +//! - Access protection (IIO_ILAPR, the widget access pair, IIO_IIDEM), the +//! interrupt destination and the crossbar credit are stored and enforced +//! nowhere. What a refused request does is unknown. +//! - IIO_ILCSR bit 8 sends a warm-reset pulse to the far end of the link +//! each time it is written set, and reads zero. That the bit sends the +//! pattern is inferred from its name, and that it reads zero from the stock +//! PROM's read-modify-write of the register. A link reset arriving from the +//! far end changes nothing here and is logged. +//! - A big window aimed at an absent widget reads zero and drops writes, and +//! logs the window; real behaviour is unknown, and guests disable windows by +//! aiming them at a widget that is not there. A window whose entry selects +//! I/O space is modelled only for entry 0 at slice 0, which the PROM uses to +//! read a peer Hub's identity; it becomes an XIO I/O-space packet. Any other +//! I/O-space entry reaches nothing. +//! - A peer's programmed I/O to the Hub's own widget number answers only a +//! 4-byte read of the widget identification word at offset 4, as the +//! guest kernel issues it. Every other access fails. +//! - The block transfer engines run the whole transfer inside the store to +//! IBCT that launches it, move 4096 lines (512 KiB) at a time, and work from +//! reset with no enable written. Addresses keep bits 39:7. A zero-length +//! launch completes at once; zero-fill does not read the source; poison mode +//! copies, since the directory is not modelled; a read of IBCT returns the +//! stored bits and stops nothing. The notification writes eight bytes. +//! - The Hub error interrupt is not modelled. A failed engine transfer latches +//! busy and error, and only IIO_IECLR clears the error. +//! +//! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::hub::{HubObserver, Store}; +use crate::dev::hub_pi::MODIFY_SET; +use crate::devlog::LogModule; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; + +pub mod reg { + pub const WIDGET_ID: u64 = 0x40_0000; + pub const WIDGET_STATUS: u64 = 0x40_0008; + pub const WIDGET_CONTROL: u64 = 0x40_0020; + pub const LOCAL_ACCESS: u64 = 0x40_0100; + pub const ACCESS_OVERRIDE: u64 = 0x40_0108; + pub const OUTBOUND_ACCESS: u64 = 0x40_0110; + pub const INBOUND_ACCESS: u64 = 0x40_0118; + pub const DEVICE_ERR_MASK: u64 = 0x40_0120; + pub const LINK_STATUS: u64 = 0x40_0128; + pub const LLP_LOG: u64 = 0x40_0130; + pub const INT_DEST: u64 = 0x40_0138; + pub const GFX_INIT_0: u64 = 0x40_0140; + pub const GFX_INIT_1: u64 = 0x40_0148; + pub const SCRATCH_0: u64 = 0x40_0150; + pub const SCRATCH_1: u64 = 0x40_0158; + pub const BWIN_TABLE: u64 = 0x40_0160; + pub const MISC_ENABLE: u64 = 0x40_01e8; + pub const ERROR_CLEAR: u64 = 0x40_01f8; + pub const PIO_DEALLOC: u64 = 0x40_0388; + pub const CRB_DEALLOC: u64 = 0x40_0390; + pub const IOQ_FIFO_DEPTH: u64 = 0x40_0398; + pub const CRB_MGMT: u64 = 0x40_03a8; + pub const CRB_CONTROL: u64 = 0x40_03b0; + pub const XTALK_TIMEOUT: u64 = 0x40_03b8; + pub const XTALK_PRESCALE: u64 = 0x40_03c0; + pub const ICRB_C_BASE: u64 = 0x40_04f0; + pub const BTE_BASE: u64 = 0x41_0000; +} +/// Where the block transfer engine `n`'s registers start. +pub const BTE_STRIDE: u64 = 0x1_0000; +pub mod bte { + pub const LENGTH: u64 = 0x00; + pub const SOURCE: u64 = 0x08; + pub const DEST: u64 = 0x10; + pub const CONTROL: u64 = 0x18; + pub const NOTIFY: u64 = 0x20; + pub const INTERRUPT: u64 = 0x28; +} + +pub const START: u64 = 0x40_0000; +pub const END: u64 = 0x60_0000; +/// The Hub's own widget number on the XIO port. +pub const WIDGET: u8 = 1; +/// Interrupt vectors an interrupt-modify packet can name. +pub const VECTORS: u8 = 128; +pub const BIG_WINDOWS: usize = 7; +pub const BIG_WINDOW_SIZE: u64 = 512 << 20; + +const ICRB_C_STRIDE: u64 = 0x20; +const ICRB_C_COUNT: u64 = 8; +const WIDGET_REVISION_SHIFT: u32 = 28; +const WIDGET_PART: u64 = 0xc101 << 12; +const WIDGET_CONTROL_RESET: u64 = 1 | (2 << 13); +const WIDGET_CONTROL_WRITABLE: u64 = 0x7f_ffff & !(1 << 4); +const WIDGET_ACCESS_MASK: u64 = 0xff01; +const INT_DEST_MASK: u64 = 0x1101_ff7f; +const GFX_INIT_MASK: u64 = 0x1f_ffff; +const SCRATCH_MASK: u64 = 0x0_f00f_11fff; +const TIMEOUT_MASK: u64 = 0xff; +const PRESCALE_MASK: u64 = 0xff_ffff; +const LINK_UP: u64 = 2 << 12; +const LINK_RESET: u64 = (0x3fc << 32) | (0x3ff << 16) | LINK_UP | 0xa; +const LINK_WARM_RESET: u64 = 1 << 8; +const LINK_WRITABLE: u64 = (0x3fc << 32) | (0x3ff << 16) | (1 << 10) | 0x3f; +const BWIN_MASK: u32 = 0x1fff; +const BWIN_IO_SPACE: u32 = 1 << 12; +const BWIN_WIDGET_SHIFT: u32 = 8; +const MISC_PRECISE_CLEAR: u64 = 1 << 12; +const MISC_SHUTDOWN: u64 = 0x111; +const IECLR_BTE: [u64; 2] = [1 << 17, 1 << 18]; + +const BTE_BUSY: u64 = 1 << 20; +const BTE_ERROR: u64 = 1 << 16; +const BTE_LINES: u64 = 0xffff; +const BTE_ADDRESS_MASK: u64 = 0xff_ffff_ff80; +const BTE_LINE: u64 = 128; +const BTE_POISON: u64 = 1 << 8; +const BTE_NOTIFY: u64 = 1 << 4; +const BTE_ZFILL: u64 = 1; +const BTE_CONTROL_MASK: u64 = BTE_POISON | BTE_NOTIFY | BTE_ZFILL; +const BTE_INT_LEVEL_SHIFT: u32 = 16; +const BTE_INT_LEVEL_MASK: u64 = 0x7f << BTE_INT_LEVEL_SHIFT; +const BTE_INT_NODE_MASK: u64 = 0x1ff; +const BTE_CHUNK_LINES: u64 = 4096; + +/// What the I/O interface's engines and its XIO face reach by node number: +/// memory as a bus master sees it, and the interrupt-modify register of the +/// Hub programmed with a number. The Hub implements it over its claim map. +pub trait NodeFabric: Send + Sync { + /// Read `buf.len()` bytes at the 40-bit `address`, its bits 39:32 naming + /// the node. False when any byte could not be read. + fn read(&self, address: u64, buf: &mut [u8]) -> bool; + fn write(&self, address: u64, data: &[u8]) -> bool; + /// Write `value` to the interrupt-modify register of the Hub programmed + /// with `number`. False when no reachable Hub holds the number. + fn interrupt(&self, number: u16, value: u64) -> bool; +} + +fn unimp(what: &str, off: u64, value: Option) { + match value { + Some(v) => crate::dlog!(LogModule::Hub, "Hub IO: write of {v:#x} to {what} {off:#x}"), + None => crate::dlog!(LogModule::Hub, "Hub IO: read of {what} {off:#x}"), + } +} + +#[derive(Clone, Copy, Default)] +struct Bte { + length: u64, + source: u64, + dest: u64, + control: u64, + notify: u64, + interrupt: u64, +} + +struct State { + widget_control: u64, + link_status: u64, + xtalk_timeout: u64, + xtalk_prescale: u64, + gfx_init: [u64; 2], + local_access: u64, + outbound_access: u64, + inbound_access: u64, + device_error_mask: u64, + int_dest: u64, + scratch: [u64; 2], + bte: [Bte; 2], +} +impl State { + fn new() -> Self { + Self { + widget_control: WIDGET_CONTROL_RESET, + link_status: LINK_RESET, + xtalk_timeout: 0, + xtalk_prescale: 0, + gfx_init: [0; 2], + local_access: !0, + outbound_access: 0, + inbound_access: 0, + device_error_mask: !0, + int_dest: 0, + scratch: [0; 2], + bte: [Bte::default(); 2], + } + } +} + +/// The lock-free half: what an access through a big window reads. The +/// translation entries are the decode table, written by a register store and +/// read by an access with one atomic load. +struct Windows { + bwin: [AtomicU32; BIG_WINDOWS], + widgets: [OnceLock>; 16], + /// The peer's XIO packet face, for a window aimed at I/O space. + packets: OnceLock>, +} + +pub struct HubIo { + widget_revision: u8, + manufacturer: u16, + state: Mutex, + windows: Arc, + fabric: OnceLock>, + /// The far end's reset input, where a warm reset from IIO_ILCSR goes. + reset_out: OnceLock>, + observer: OnceLock>, +} + +impl HubIo { + /// `widget_revision` is the chip revision in bits 31:28 of the widget + /// identification word and `manufacturer` its 11-bit manufacturer code. + pub fn new(widget_revision: u8, manufacturer: u16) -> Self { + assert!(widget_revision < 16 && manufacturer < 0x800); + Self { + widget_revision, + manufacturer, + state: Mutex::new(State::new()), + windows: Arc::new(Windows { + bwin: Default::default(), + widgets: Default::default(), + packets: OnceLock::new(), + }), + fabric: OnceLock::new(), + reset_out: OnceLock::new(), + observer: OnceLock::new(), + } + } + + /// Report stores to the scratch registers (see [`Store`]). + pub fn set_observer(&self, observer: Arc) { + let _ = self.observer.set(observer); + } + + pub fn set_fabric(&self, fabric: Weak) { + let _ = self.fabric.set(fabric); + } + + /// Put `dev` at crossbar widget `widget` (0 to 15) of the XIO space the + /// big windows and small windows reach. + pub fn connect_widget(&self, widget: u8, dev: Arc) { + assert!(widget < 16); + self.windows.widgets[usize::from(widget)].set(dev).unwrap_or_else(|_| panic!("widget {widget} is taken")); + } + + /// The peer's packet face: an XIO address with bit 47 set is its I/O view + /// and bits 51:48 name the destination widget. + pub fn connect_packets(&self, dev: Arc) { + self.windows.packets.set(dev).unwrap_or_else(|_| panic!("the XIO packet face is taken")); + } + + /// Where a warm reset written to IIO_ILCSR goes. + pub fn connect_reset(&self, reset: Arc) { + let _ = self.reset_out.set(reset); + } + + /// The widget identification word. + pub fn widget_id(&self) -> u64 { + (u64::from(self.widget_revision) << WIDGET_REVISION_SHIFT) | WIDGET_PART | (u64::from(self.manufacturer) << 1) | 1 + } + + /// The big windows as one device of seven 512 MiB windows, addressed from + /// the first. The machine places it at I/O-space offset 0x2000_0000. + pub fn big_windows(&self) -> Arc { + Arc::new(BigWindows(self.windows.clone())) + } + + /// The crossbar widgets' small windows, 16 MiB each, addressed from + /// widget 0's. Widget 1 is the Hub's own and is left to the caller. + pub fn small_windows(&self) -> Arc { + Arc::new(SmallWindows(self.windows.clone())) + } + + /// The peer's request for this Hub's widget identification word. The one + /// peer access with evidence is the guest kernel's 4-byte read at offset 4. + pub fn widget_face_read32(&self, offset: u64) -> Option { + (offset == 4).then(|| self.widget_id() as u32) + } + + /// Scratch register `n`; the stock PROM keeps boot state here. + pub fn scratch(&self, n: usize) -> u64 { + self.state.lock().scratch[n] + } + + /// The graphics node-widget map entry `n` (IIO_IGFX). + pub fn gfx_init(&self, n: usize) -> u64 { + self.state.lock().gfx_init[n] + } + + fn bwin_entry(off: u64) -> Option { + let rel = off.checked_sub(reg::BWIN_TABLE)?; + (rel % 8 == 0 && rel / 8 < BIG_WINDOWS as u64).then_some((rel / 8) as usize) + } + fn icrb_c(off: u64) -> bool { + off.checked_sub(reg::ICRB_C_BASE).is_some_and(|rel| rel % ICRB_C_STRIDE == 0 && rel / ICRB_C_STRIDE < ICRB_C_COUNT) + } + fn bte_entry(off: u64) -> Option<(usize, u64)> { + let rel = off.checked_sub(reg::BTE_BASE)?; + let (engine, reg) = ((rel / BTE_STRIDE) as usize, rel % BTE_STRIDE); + (engine < 2 && reg <= bte::INTERRUPT && reg % 8 == 0).then_some((engine, reg)) + } + + /// A register's value without side effects, for the registers that store. + pub fn peek(&self, off: u64) -> Option { + if let Some(n) = Self::bwin_entry(off) { + return Some(u64::from(self.windows.bwin[n].load(Ordering::Acquire))); + } + if let Some((engine, r)) = Self::bte_entry(off) { + let b = self.state.lock().bte[engine]; + return Some(match r { + bte::LENGTH => b.length, + bte::SOURCE => b.source, + bte::DEST => b.dest, + bte::CONTROL => b.control, + bte::NOTIFY => b.notify, + _ => b.interrupt, + }); + } + let s = self.state.lock(); + Some(match off { + reg::WIDGET_CONTROL => s.widget_control, + reg::LOCAL_ACCESS => s.local_access, + reg::OUTBOUND_ACCESS => s.outbound_access, + reg::INBOUND_ACCESS => s.inbound_access, + reg::DEVICE_ERR_MASK => s.device_error_mask, + reg::LINK_STATUS => s.link_status, + reg::INT_DEST => s.int_dest, + reg::GFX_INIT_0 | reg::GFX_INIT_1 => s.gfx_init[((off - reg::GFX_INIT_0) / 8) as usize], + reg::SCRATCH_0 | reg::SCRATCH_1 => s.scratch[((off - reg::SCRATCH_0) / 8) as usize], + reg::XTALK_TIMEOUT => s.xtalk_timeout, + reg::XTALK_PRESCALE => s.xtalk_prescale, + _ => return None, + }) + } + + pub fn read(&self, off: u64) -> u64 { + if Self::icrb_c(off) { + // No slot has ever tracked a transaction. + return 0; + } + match off { + reg::WIDGET_ID => self.widget_id(), + // The model completes every request before it returns. + reg::WIDGET_STATUS | reg::LLP_LOG | reg::ACCESS_OVERRIDE | reg::MISC_ENABLE => 0, + reg::PIO_DEALLOC | reg::CRB_DEALLOC | reg::IOQ_FIFO_DEPTH | reg::CRB_MGMT | reg::CRB_CONTROL => 0, + _ => match self.peek(off) { + Some(v) => v, + None => { + unimp("unimplemented or write-only register", off, None); + 0 + } + }, + } + } + + pub fn write(&self, off: u64, value: u64) { + if let Some(n) = Self::bwin_entry(off) { + // Stored: OpenBSD's window allocator reads an entry straight back. + let entry = value as u32 & BWIN_MASK; + self.windows.bwin[n].store(entry, Ordering::Release); + if entry & BWIN_IO_SPACE != 0 && (n != 0 || entry & 0x1f != 0) { + crate::dlog!(LogModule::Hub, "Hub IO: unsupported I/O big window {n}, entry {entry:#x}"); + } + return; + } + if let Some((engine, r)) = Self::bte_entry(off) { + self.bte_write(engine, r, value); + return; + } + if Self::icrb_c(off) { + unimp("read-only incoming-request-buffer register", off, Some(value)); + return; + } + let mut s = self.state.lock(); + match off { + reg::WIDGET_ID => { + crate::dlog!(LogModule::Hub, "Hub IO: write of {value:#x} to the read-only widget identification"); + } + reg::WIDGET_STATUS => unimp("widget status (firmware clears it at power-on)", off, Some(value)), + reg::WIDGET_CONTROL => { + s.widget_control = (s.widget_control & !WIDGET_CONTROL_WRITABLE) | (value & WIDGET_CONTROL_WRITABLE) + } + reg::LOCAL_ACCESS => s.local_access = value, + // The "SGIrules" password unlocks protection nothing enforces. + reg::ACCESS_OVERRIDE => {} + reg::OUTBOUND_ACCESS => s.outbound_access = value & WIDGET_ACCESS_MASK, + reg::INBOUND_ACCESS => s.inbound_access = value & WIDGET_ACCESS_MASK, + reg::DEVICE_ERR_MASK => s.device_error_mask = value, + reg::LINK_STATUS => { + // The link state and mode stay read-only: nothing may claim + // the link down. Bit 8 is a one-shot warm reset that reads zero. + s.link_status = (s.link_status & !LINK_WRITABLE) | (value & LINK_WRITABLE); + drop(s); + if value & LINK_WARM_RESET != 0 { + match self.reset_out.get() { + Some(reset) => reset(), + None => crate::dlog!(LogModule::Hub, "Hub IO: warm reset with nothing at the far end of the XIO link"), + } + } + } + reg::LLP_LOG => {} // closing a sampling interval clears zero counts + reg::INT_DEST => s.int_dest = value & INT_DEST_MASK, + reg::GFX_INIT_0 | reg::GFX_INIT_1 => s.gfx_init[((off - reg::GFX_INIT_0) / 8) as usize] = value & GFX_INIT_MASK, + reg::SCRATCH_0 | reg::SCRATCH_1 => { + let index = ((off - reg::SCRATCH_0) / 8) as usize; + s.scratch[index] = value & SCRATCH_MASK; + drop(s); + if let Some(observer) = self.observer.get() { + observer.stored(Store::IoScratch { index, value: value & SCRATCH_MASK }); + } + } + reg::MISC_ENABLE => { + if value & MISC_PRECISE_CLEAR != 0 { + unimp("precise-error latch clear (no such latch is modelled)", off, Some(value)); + } + if value & MISC_SHUTDOWN != 0 { + unimp("shutdown-on-error bits (no unit here shuts down)", off, Some(value)); + } + } + reg::ERROR_CLEAR => { + // Each bit clears a latch; only the engines' error bits exist. + for (engine, bit) in IECLR_BTE.iter().enumerate() { + if value & bit != 0 { + s.bte[engine].length &= !BTE_ERROR; + } + } + if value & !(IECLR_BTE[0] | IECLR_BTE[1]) != 0 { + unimp("error clear (only the two engine latches exist)", off, Some(value)); + } + } + reg::PIO_DEALLOC | reg::CRB_DEALLOC | reg::CRB_MGMT | reg::CRB_CONTROL => { + // Nothing outstanding: the free completes at once. + unimp("I/O-queue register", off, Some(value)); + } + reg::XTALK_TIMEOUT => s.xtalk_timeout = value & TIMEOUT_MASK, + reg::XTALK_PRESCALE => s.xtalk_prescale = value & PRESCALE_MASK, + _ => unimp("unimplemented register", off, Some(value)), + } + } + + fn bte_address(engine: usize, what: &str, value: u64) -> u64 { + if value & (BTE_LINE - 1) != 0 { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} {what} address {value:#x} is not 128-byte aligned; bits 6:0 are not stored"); + } + value & BTE_ADDRESS_MASK + } + + fn bte_write(&self, engine: usize, r: u64, value: u64) { + let mut s = self.state.lock(); + let b = &mut s.bte[engine]; + match r { + // A store arms or disarms but never absolves a latched error. + bte::LENGTH => b.length = (b.length & BTE_ERROR) | (value & (BTE_BUSY | BTE_LINES)), + bte::SOURCE => b.source = Self::bte_address(engine, "source", value), + bte::DEST => b.dest = Self::bte_address(engine, "destination", value), + bte::CONTROL => { + b.control = value & BTE_CONTROL_MASK; + // Launches if armed; busy clear does nothing. + if b.length & BTE_BUSY != 0 { + let launch = *b; + drop(s); + self.bte_launch(engine, launch); + } + } + bte::NOTIFY => b.notify = Self::bte_address(engine, "notification", value), + _ => b.interrupt = value & (BTE_INT_LEVEL_MASK | BTE_INT_NODE_MASK), + } + } + + /// Run the armed transfer through the node fabric, outside the state lock. + fn bte_launch(&self, engine: usize, b: Bte) { + let Some(fabric) = self.fabric.get().and_then(Weak::upgrade) else { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} launched with no fabric"); + self.state.lock().bte[engine].length = BTE_BUSY | BTE_ERROR | (b.length & BTE_LINES); + return; + }; + let lines = b.length & BTE_LINES; + if b.control & BTE_POISON != 0 { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} launched in poison mode; the copy runs but no directory state is poisoned"); + } + if lines == 0 { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} launched with a zero length"); + } + // Chunked through one bounce buffer. A failed chunk is retried a line + // at a time to latch the first unmoved line, and overlapping ranges go + // a line at a time to keep the engine's forward propagation. + let zfill = b.control & BTE_ZFILL != 0; + let mut chunk = BTE_CHUNK_LINES; + if !zfill && b.source < b.dest + lines * BTE_LINE && b.dest < b.source + lines * BTE_LINE { + chunk = 1; + } + let mut buf = vec![0u8; (chunk.min(lines) * BTE_LINE) as usize]; + let mut done = 0; + while done < lines { + let run = (lines - done).min(chunk); + let bytes = (run * BTE_LINE) as usize; + let ok = (zfill || fabric.read(b.source + done * BTE_LINE, &mut buf[..bytes])) + && fabric.write(b.dest + done * BTE_LINE, &buf[..bytes]); + if !ok { + if run > 1 { + chunk = 1; + continue; + } + // A failure latches error beside busy and keeps the unmoved + // count; only IIO_IECLR absolves it, and neither completion runs. + self.state.lock().bte[engine].length = BTE_BUSY | BTE_ERROR | (lines - done); + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} transfer failed at line {done} (src {:#x} dst {:#x})", + b.source + done * BTE_LINE, b.dest + done * BTE_LINE); + return; + } + done += run; + } + let status = { + let mut s = self.state.lock(); + // Busy and the count clear; a latched error survives. + s.bte[engine].length &= BTE_ERROR; + s.bte[engine].length + }; + if b.control & BTE_NOTIFY != 0 { + if !fabric.write(b.notify, &status.to_be_bytes()) { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} notification address {:#x} reaches no memory", b.notify); + } + } else { + // Completion arrives as a device interrupt through interrupt-modify + // on the Hub holding the destination number. + let level = (b.interrupt & BTE_INT_LEVEL_MASK) >> BTE_INT_LEVEL_SHIFT; + let node = (b.interrupt & BTE_INT_NODE_MASK) as u16; + if !fabric.interrupt(node, MODIFY_SET | level) { + crate::dlog!(LogModule::Hub, "Hub IO: BTE{engine} completion interrupt aimed at node {node}, which no reachable Hub holds"); + } + } + } + + /// A link reset arriving from the crossbar. The specification says it + /// clears the transmit buffer and the link protocol state and restarts + /// width negotiation; the model has none of the three, so nothing changes. + pub fn link_reset_in(&self) { + crate::dlog!(LogModule::Hub, "Hub IO: inbound link reset; no transmit buffer or protocol state is represented"); + } +} + +impl HubIo { + /// Whether the register state is locked now, for the `locks` monitor command. + pub fn is_locked(&self) -> bool { + self.state.is_locked() + } +} + +impl Resettable for HubIo { + fn power_on(&self) { + *self.state.lock() = State::new(); + // Entries reset to zero with the windows enabled: a guest calls a zero + // entry free, and zero spells widget 0, OpenBSD's only route there. + for entry in &self.windows.bwin { + entry.store(0, Ordering::Release); + } + } +} + +impl BusDevice for HubIo { + fn read64(&self, addr: u64) -> BusRead64 { + BusRead64::ok(self.read(addr)) + } + fn write64(&self, addr: u64, value: u64) -> u32 { + self.write(addr, value); + BUS_OK + } +} + +/// An access through one of the seven big windows, from the first's base. +struct BigWindows(Arc); + +impl BigWindows { + /// The widget device and the address within it that `addr` reaches. + fn target(&self, addr: u64) -> Option<(&dyn BusDevice, u64)> { + let (window, offset) = ((addr / BIG_WINDOW_SIZE) as usize, addr % BIG_WINDOW_SIZE); + let entry = self.0.bwin.get(window)?.load(Ordering::Acquire); + let (widget, slice) = ((entry >> BWIN_WIDGET_SHIFT) & 15, u64::from(entry & 0x1f)); + if entry & BWIN_IO_SPACE != 0 { + // The I/O view, modelled for entry 0 at slice 0 only: a packet + // into the peer's I/O view, the widget in bits 51:48. + if window != 0 || slice != 0 { + return None; + } + let dev = self.0.packets.get()?; + return Some((&**dev, (u64::from(widget) << 48) | (1 << 47) | offset)); + } + let dev = self.0.widgets[widget as usize].get()?; + Some((&**dev, (slice << 29) | offset)) + } + fn missing(&self, addr: u64, value: Option) { + crate::dlog!(LogModule::Hub, "Hub IO: {} through big window {} at {:#x} reaches no widget", + if value.is_some() { "write" } else { "read" }, addr / BIG_WINDOW_SIZE, addr % BIG_WINDOW_SIZE); + } +} + +macro_rules! big_window_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + match self.target(a) { + Some((dev, at)) => dev.$r(at), + None if a / BIG_WINDOW_SIZE < BIG_WINDOWS as u64 => { + self.missing(a, None); + $out::ok(0) + } + None => $out::err(), + } + } + fn $w(&self, a: u64, v: $t) -> u32 { + match self.target(a) { + Some((dev, at)) => dev.$w(at, v), + None if a / BIG_WINDOW_SIZE < BIG_WINDOWS as u64 => { + self.missing(a, Some(v as u64)); + BUS_OK + } + None => BUS_ERR, + } + } + }; +} +impl BusDevice for BigWindows { + big_window_access!(read8, write8, u8, BusRead8); + big_window_access!(read16, write16, u16, BusRead16); + big_window_access!(read32, write32, u32, BusRead32); + big_window_access!(read64, write64, u64, BusRead64); +} + +/// A crossbar widget's 16 MiB small window, addressed as widget << 24 | offset. +struct SmallWindows(Arc); + +macro_rules! small_window_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + match self.0.widgets.get((a >> 24) as usize).and_then(|w| w.get()) { + Some(dev) => dev.$r(a & 0xff_ffff), + None => $out::err(), + } + } + fn $w(&self, a: u64, v: $t) -> u32 { + match self.0.widgets.get((a >> 24) as usize).and_then(|w| w.get()) { + Some(dev) => dev.$w(a & 0xff_ffff, v), + None => BUS_ERR, + } + } + }; +} +impl BusDevice for SmallWindows { + small_window_access!(read8, write8, u8, BusRead8); + small_window_access!(read16, write16, u16, BusRead16); + small_window_access!(read32, write32, u32, BusRead32); + small_window_access!(read64, write64, u64, BusRead64); +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/hub_io/tests.rs b/src/dev/hub_io/tests.rs new file mode 100644 index 000000000..192d145e8 --- /dev/null +++ b/src/dev/hub_io/tests.rs @@ -0,0 +1,463 @@ +// Cases follow tests/qtest/sn0-hub-test.c in jamesbraid/qemu-sgi-origami: +// identifies-itself, widget-control-reset, io-access-registers, +// crosstalk-timeouts, io-scratch-registers, xio-link-reports-up, +// link-state-is-not-writable, link-log-serves-a-clean-link, +// link-enabled-bit-is-writable, the inbound-link-reset cases, +// translation-entries-decode, translation-entry-width, the big-window-* +// cases, io-queue-registers, io-icrb-c-family and the bte-* cases. + +use super::*; +use std::collections::BTreeMap; + +/// Memory as the engines see it: 1 MiB of RAM at node 0, plus an interrupt +/// log. Any address outside it fails. +struct Fabric { + ram: Mutex>, + interrupts: Mutex>, +} +impl Fabric { + fn new() -> Arc { + Arc::new(Self { ram: Mutex::new(vec![0; 1 << 20]), interrupts: Mutex::new(Vec::new()) }) + } + fn fill(&self, at: usize, bytes: &[u8]) { + self.ram.lock()[at..at + bytes.len()].copy_from_slice(bytes); + } + fn bytes(&self, at: usize, n: usize) -> Vec { + self.ram.lock()[at..at + n].to_vec() + } +} +impl NodeFabric for Fabric { + fn read(&self, address: u64, buf: &mut [u8]) -> bool { + let ram = self.ram.lock(); + let at = address as usize; + if address >= 1 << 32 || at + buf.len() > ram.len() { + return false; + } + buf.copy_from_slice(&ram[at..at + buf.len()]); + true + } + fn write(&self, address: u64, data: &[u8]) -> bool { + let mut ram = self.ram.lock(); + let at = address as usize; + if address >= 1 << 32 || at + data.len() > ram.len() { + return false; + } + ram[at..at + data.len()].copy_from_slice(data); + true + } + fn interrupt(&self, number: u16, value: u64) -> bool { + if number == 7 { + return false; + } + self.interrupts.lock().push((number, value)); + true + } +} + +fn io() -> (HubIo, Arc) { + let fabric = Fabric::new(); + let io = HubIo::new(1, 0x36); + let weak: Weak = Arc::downgrade(&(fabric.clone() as Arc)); + io.set_fabric(weak); + (io, fabric) +} +fn r(io: &HubIo, off: u64) -> u64 { + io.read64(off).data +} + +/// A widget whose registers store what is written, so a window's target +/// can be told apart. +#[derive(Default)] +struct Widget { + id: u64, + regs: Mutex>, + reads: Mutex>, +} +impl BusDevice for Widget { + fn read64(&self, a: u64) -> BusRead64 { + self.reads.lock().push(a); + BusRead64::ok(self.regs.lock().get(&a).copied().unwrap_or(self.id)) + } + fn write64(&self, a: u64, v: u64) -> u32 { + self.regs.lock().insert(a, v); + BUS_OK + } +} + +#[test] +fn the_widget_identifies_itself_and_refuses_writes() { + let (io, _) = io(); + // Revision 1, part 0xc101, manufacturer 0x036, and the always-one bit. + assert_eq!(r(&io, reg::WIDGET_ID), 0x1c10_106d); + io.write64(reg::WIDGET_ID, 0); + assert_eq!(r(&io, reg::WIDGET_ID), 0x1c10_106d); + // The revision and manufacturer are the machine's to choose and survive reset. + let other = HubIo::new(5, 0x123); + assert_eq!(other.widget_id(), (5 << 28) | (0xc101 << 12) | (0x123 << 1) | 1); + other.power_on(); + assert_eq!(other.widget_id(), (5 << 28) | (0xc101 << 12) | (0x123 << 1) | 1); + // The status register always reads zero; a write is logged. + assert_eq!(r(&io, reg::WIDGET_STATUS), 0); + io.write64(reg::WIDGET_STATUS, !0); + assert_eq!(r(&io, reg::WIDGET_STATUS), 0); +} + +#[test] +fn the_widget_control_register_resets_to_widget_one_with_a_credit_of_two() { + let (io, _) = io(); + assert_eq!(r(&io, reg::WIDGET_CONTROL), 0x4001); + // Bits 22:0 store, except bit 4 (tag mode), which reads zero. + io.write64(reg::WIDGET_CONTROL, !0); + assert_eq!(r(&io, reg::WIDGET_CONTROL), 0x7f_ffef); + io.power_on(); + assert_eq!(r(&io, reg::WIDGET_CONTROL), 0x4001); +} + +#[test] +fn the_access_registers_store_and_enforce_nothing() { + let (io, _) = io(); + assert_eq!(r(&io, reg::LOCAL_ACCESS), !0); + assert_eq!(r(&io, reg::DEVICE_ERR_MASK), !0); + assert_eq!((r(&io, reg::OUTBOUND_ACCESS), r(&io, reg::INBOUND_ACCESS)), (0, 0)); + // The PROM opens widget 0 and widgets 8 to 15. + io.write64(reg::LOCAL_ACCESS, !0); + io.write64(reg::OUTBOUND_ACCESS, 0xff01); + io.write64(reg::INBOUND_ACCESS, !0); + assert_eq!((r(&io, reg::OUTBOUND_ACCESS), r(&io, reg::INBOUND_ACCESS)), (0xff01, 0xff01)); + // The guest kernel removes and re-enables a device by clearing and setting its bit. + io.write64(reg::DEVICE_ERR_MASK, !(1 << 9)); + assert_eq!(r(&io, reg::DEVICE_ERR_MASK), !(1 << 9)); + // The interrupt destination keeps 29 bits, mask 0x1101ff7f. + io.write64(reg::INT_DEST, !0); + assert_eq!(r(&io, reg::INT_DEST), 0x1101_ff7f); + // The password write is accepted and reads zero. + io.write64(reg::ACCESS_OVERRIDE, 0x5347_4972_756c_6573); + assert_eq!(r(&io, reg::ACCESS_OVERRIDE), 0); + io.power_on(); + assert_eq!((r(&io, reg::LOCAL_ACCESS), r(&io, reg::OUTBOUND_ACCESS), r(&io, reg::INT_DEST)), (!0, 0, 0)); +} + +#[test] +fn the_crosstalk_timeouts_accept_a_write_at_reset() { + let (io, _) = io(); + // The first registers Linux writes: 0xff and 0x800. + io.write64(reg::XTALK_TIMEOUT, 0xff); + io.write64(reg::XTALK_PRESCALE, 0x800); + assert_eq!((r(&io, reg::XTALK_TIMEOUT), r(&io, reg::XTALK_PRESCALE)), (0xff, 0x800)); + io.write64(reg::XTALK_TIMEOUT, !0); + io.write64(reg::XTALK_PRESCALE, !0); + assert_eq!((r(&io, reg::XTALK_TIMEOUT), r(&io, reg::XTALK_PRESCALE)), (0xff, 0xff_ffff)); +} + +#[test] +fn the_scratch_registers_store_36_bits_and_the_graphics_map_21() { + let (io, _) = io(); + for (off, mask) in [(reg::SCRATCH_0, 0xf_00f1_1fff), (reg::SCRATCH_1, 0xf_00f1_1fff)] { + assert_eq!(r(&io, off), 0); + io.write64(off, !0); + assert_eq!(r(&io, off), mask); + } + // The PROM uses scratch 0 as boot state: bit 23 is its alive signal, and + // bits outside the mask do not store. + io.write64(reg::SCRATCH_0, 1 << 23); + assert_eq!(io.scratch(0), 1 << 23); + io.write64(reg::SCRATCH_0, 1 << 36); + assert_eq!(io.scratch(0), 0); + // Graphics node-widget map: widget 3:0, node 12:4, slot 16, valid 20. + io.write64(reg::GFX_INIT_0, 0x12_3456); + io.write64(reg::GFX_INIT_1, !0); + assert_eq!((r(&io, reg::GFX_INIT_0), io.gfx_init(1)), (0x12_3456, 0x1f_ffff)); + io.power_on(); + assert_eq!((io.scratch(0), io.gfx_init(0), io.gfx_init(1)), (0, 0, 0)); +} + +#[test] +fn the_link_reads_up_with_its_limits_and_only_the_controls_write() { + let (io, _) = io(); + // Maximum burst 0x3fc, maximum retry 0x3ff, null timeout 0xa, state 2. + assert_eq!(r(&io, reg::LINK_STATUS), 0x3fc_03ff_200a); + assert_ne!(r(&io, reg::LINK_STATUS) & (2 << 12), 0, "bit 13 alone answers is-the-link-up"); + // The state field cannot be written down. + io.write64(reg::LINK_STATUS, 0); + assert_eq!(r(&io, reg::LINK_STATUS) & (3 << 12), 2 << 12); + // Link enable (bit 10) resets clear and writes; firmware sets it by + // read-modify-write. + assert_eq!(r(&io, reg::LINK_STATUS) & (1 << 10), 0); + io.write64(reg::LINK_STATUS, r(&io, reg::LINK_STATUS) | (1 << 10)); + assert_ne!(r(&io, reg::LINK_STATUS) & (1 << 10), 0); + // The link log counts errors the link does not commit: zero, and + // closing the interval with a write changes nothing. + assert_eq!(r(&io, reg::LLP_LOG), 0); + io.write64(reg::LLP_LOG, !0); + assert_eq!(r(&io, reg::LLP_LOG), 0); +} + +#[test] +fn a_warm_reset_is_a_pulse_that_reads_zero() { + let (io, _) = io(); + let pulses = Arc::new(std::sync::atomic::AtomicU32::new(0)); + let p = pulses.clone(); + // With nothing at the far end the write is only logged. + io.write64(reg::LINK_STATUS, 1 << 8); + io.connect_reset(Arc::new(move || { + p.fetch_add(1, Ordering::SeqCst); + })); + io.write64(reg::LINK_STATUS, 1 << 8); + assert_eq!(pulses.load(Ordering::SeqCst), 1); + assert_eq!(r(&io, reg::LINK_STATUS) & (1 << 8), 0); + // Setting bit 10 by read-modify-write would send a second pulse only if + // bit 8 read back set. + io.write64(reg::LINK_STATUS, r(&io, reg::LINK_STATUS) | (1 << 10)); + assert_eq!(pulses.load(Ordering::SeqCst), 1); + // An inbound link reset leaves everything as it was. + let before = (r(&io, reg::LINK_STATUS), r(&io, reg::WIDGET_CONTROL)); + io.link_reset_in(); + assert_eq!((r(&io, reg::LINK_STATUS), r(&io, reg::WIDGET_CONTROL)), before); +} + +#[test] +fn the_translation_entries_are_thirteen_bits_and_reset_to_widget_zero() { + let (io, _) = io(); + for n in 0..7 { + let off = reg::BWIN_TABLE + n * 8; + assert_eq!(r(&io, off), 0); + io.write64(off, 0x1abc | (n << 5)); + assert_eq!(r(&io, off), (0x1abc | (n << 5)) & 0x1fff); + } + // An eighth word is not an entry. + io.write64(reg::BWIN_TABLE + 7 * 8, 5); + assert_eq!(r(&io, reg::BWIN_TABLE + 7 * 8), 0); + io.power_on(); + assert_eq!(r(&io, reg::BWIN_TABLE), 0); +} + +#[test] +fn a_big_window_follows_its_entry_to_a_widget_and_slice() { + let (io, _) = io(); + let wins = io.big_windows(); + let w3 = Arc::new(Widget { id: 3, ..Default::default() }); + let w8 = Arc::new(Widget { id: 8, ..Default::default() }); + io.connect_widget(3, w3.clone()); + io.connect_widget(8, w8.clone()); + // Entry zero names widget 0, which is not there: a read gets zero and a + // write is dropped. + assert_eq!(wins.read64(0x10).data, 0); + assert_eq!(wins.write64(0x10, 5), BUS_OK); + assert!(w3.regs.lock().is_empty()); + // Aim window 2 at widget 3: bits 11:8 the widget, 4:0 the slice. + io.write64(reg::BWIN_TABLE + 2 * 8, 3 << 8); + assert_eq!(wins.read64(2 * BIG_WINDOW_SIZE + 0x40).data, 3); + assert_eq!(w3.reads.lock().as_slice(), [0x40]); + // The slice supplies XIO address bits 33:29. + io.write64(reg::BWIN_TABLE + 2 * 8, (8 << 8) | 5); + wins.write64(2 * BIG_WINDOW_SIZE + 0x48, 0xabc); + assert_eq!(w8.regs.lock().get(&((5 << 29) | 0x48)), Some(&0xabc)); + // Re-aiming moves the next access. Widget 8 serves the window now. + assert_eq!(wins.read64(2 * BIG_WINDOW_SIZE + 0x48).data, 0xabc); + // The last byte of the window, and nothing beyond the seventh. + assert_eq!(wins.read64(3 * BIG_WINDOW_SIZE - 8).status, BUS_OK); + assert_eq!(wins.read64(7 * BIG_WINDOW_SIZE).status, BUS_ERR); + // The other windows are unaffected. + assert_eq!(wins.read64(BIG_WINDOW_SIZE).data, 0); +} + +#[test] +fn only_big_window_zero_at_slice_zero_reaches_io_space() { + let (io, _) = io(); + let wins = io.big_windows(); + let packets = Arc::new(Widget { id: 0x1234, ..Default::default() }); + io.connect_packets(packets.clone()); + // A peer Hub's identity probe: window 0, I/O space, widget 1. + io.write64(reg::BWIN_TABLE, (1 << 12) | (1 << 8)); + assert_eq!(wins.read64(0x1_0000).data, 0x1234); + assert_eq!(packets.reads.lock().as_slice(), [(1 << 48) | (1 << 47) | 0x1_0000]); + // Any other I/O-space entry reaches nothing: zero and a dropped write. + io.write64(reg::BWIN_TABLE, (1 << 12) | (1 << 8) | 1); + assert_eq!(wins.read64(0).data, 0); + io.write64(reg::BWIN_TABLE + 8, (1 << 12) | (1 << 8)); + assert_eq!(wins.read64(BIG_WINDOW_SIZE).data, 0); + assert_eq!(packets.reads.lock().len(), 1); +} + +#[test] +fn the_small_windows_reach_a_widget_by_number() { + let (io, _) = io(); + let w = Arc::new(Widget { id: 9, ..Default::default() }); + io.connect_widget(9, w.clone()); + let small = io.small_windows(); + assert_eq!(small.read64(9 << 24 | 0x20).data, 9); + assert_eq!(w.reads.lock().as_slice(), [0x20]); + assert_eq!(small.write64(9 << 24 | 0x28, 1), BUS_OK); + // An empty widget fails; widget 16 is not a widget. + assert_eq!(small.read64(10 << 24).status, BUS_ERR); + assert_eq!(small.read64(16 << 24).status, BUS_ERR); +} + +#[test] +fn a_peers_read_of_the_widget_id_is_the_one_access_the_widget_face_answers() { + let (io, _) = io(); + assert_eq!(io.widget_face_read32(4), Some(0x1c10_106d)); + for off in [0, 8, 0x20] { + assert_eq!(io.widget_face_read32(off), None); + } +} + +/// Nothing is ever in flight: the queue and request-buffer registers read +/// zero, and their writes complete at once. +#[test] +fn the_queue_and_request_buffer_registers_read_zero() { + let (io, _) = io(); + for off in [reg::PIO_DEALLOC, reg::CRB_DEALLOC, reg::IOQ_FIFO_DEPTH, reg::CRB_MGMT, reg::CRB_CONTROL] { + io.write64(off, !0); + assert_eq!(r(&io, off), 0, "{off:#x}"); + } + for n in 0..8 { + let off = reg::ICRB_C_BASE + n * 0x20; + io.write64(off, !0); + assert_eq!(r(&io, off), 0, "{off:#x}"); + } + // The error-shutdown enable reads zero. + io.write64(reg::MISC_ENABLE, !0); + assert_eq!(r(&io, reg::MISC_ENABLE), 0); +} + +// ---- the block transfer engines ---- + +fn engine(n: u64) -> u64 { + reg::BTE_BASE + n * BTE_STRIDE +} +fn start(io: &HubIo, n: u64, source: u64, dest: u64, lines: u64, control: u64) { + io.write64(engine(n) + bte::SOURCE, source); + io.write64(engine(n) + bte::DEST, dest); + io.write64(engine(n) + bte::LENGTH, BTE_BUSY | lines); + io.write64(engine(n) + bte::CONTROL, control); +} + +#[test] +fn the_engines_reset_to_zero_and_keep_their_fields() { + let (io, _) = io(); + for n in 0..2 { + for off in (0..=0x28).step_by(8) { + assert_eq!(r(&io, engine(n) + off), 0); + } + } + io.write64(engine(1) + bte::LENGTH, !0); + assert_eq!(r(&io, engine(1) + bte::LENGTH), BTE_BUSY | 0xffff); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0, "the engines are independent"); + // Addresses keep bits 39:7 and drop the rest silently. + io.write64(engine(0) + bte::SOURCE, 0xffff_ffff_ffff_ffff); + io.write64(engine(0) + bte::NOTIFY, 0x1_2345_6780 | 0x7f); + assert_eq!(r(&io, engine(0) + bte::SOURCE), 0xff_ffff_ff80); + assert_eq!(r(&io, engine(0) + bte::NOTIFY), 0x1_2345_6780); + // Interrupt: level 22:16, node 8:0. + io.write64(engine(0) + bte::INTERRUPT, !0); + assert_eq!(r(&io, engine(0) + bte::INTERRUPT), (0x7f << 16) | 0x1ff); + // Control keeps poison, notify and zero-fill only; busy clear launches nothing. + io.write64(engine(0) + bte::CONTROL, !0); + assert_eq!(r(&io, engine(0) + bte::CONTROL), 0x111); + io.power_on(); + assert_eq!(r(&io, engine(0) + bte::SOURCE), 0); +} + +#[test] +fn a_launch_copies_lines_and_interrupts_the_node_it_names() { + let (io, fabric) = io(); + let data: Vec = (0..512u32).map(|i| (i * 7) as u8).collect(); + fabric.fill(0x1000, &data); + io.write64(engine(0) + bte::INTERRUPT, (9 << 16) | 3); + start(&io, 0, 0x1000, 0x8_0000, 4, 0); + assert_eq!(fabric.bytes(0x8_0000, 512), data); + // Busy and the count clear when the transfer completes. + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0); + // Completion arrives as a device interrupt at level 9 of node 3. + assert_eq!(fabric.interrupts.lock().as_slice(), [(3, MODIFY_SET | 9)]); +} + +#[test] +fn notify_mode_writes_the_length_word_to_the_notification_address() { + let (io, fabric) = io(); + fabric.fill(0x2000, &[0xab; 256]); + io.write64(engine(1) + bte::NOTIFY, 0x4_0000); + io.write64(engine(1) + bte::SOURCE, 0x2000); + io.write64(engine(1) + bte::DEST, 0x3000); + io.write64(engine(1) + bte::LENGTH, BTE_BUSY | 2); + io.write64(engine(1) + bte::CONTROL, BTE_NOTIFY); + assert_eq!(fabric.bytes(0x3000, 256), vec![0xab; 256]); + // Exactly eight bytes, big-endian: the status after completion is zero. + assert_eq!(fabric.bytes(0x4_0000, 9), [0; 9]); + assert!(fabric.interrupts.lock().is_empty()); + // A later error shows in the word. + fabric.fill(0x4_0000, &[0xff; 8]); + io.write64(engine(1) + bte::LENGTH, BTE_BUSY | 1); + io.write64(engine(1) + bte::CONTROL, BTE_NOTIFY); + assert_eq!(fabric.bytes(0x4_0000, 8), [0; 8]); +} + +#[test] +fn zero_fill_clears_the_destination_without_reading_the_source() { + let (io, fabric) = io(); + fabric.fill(0x5000, &[0xee; 256]); + // The source is outside memory: a copy would fail, a fill does not read it. + start(&io, 0, 0xff_ffff_ff80, 0x5000, 2, BTE_ZFILL); + assert_eq!(fabric.bytes(0x5000, 256), vec![0; 256]); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0); +} + +#[test] +fn overlapping_ranges_copy_forward_a_line_at_a_time() { + let (io, fabric) = io(); + let line = |b: u8| vec![b; 128]; + for i in 0..4u8 { + fabric.fill(0x6000 + usize::from(i) * 128, &line(i + 1)); + } + // Destination one line above the source: forward propagation smears + // the first line through the range. + start(&io, 0, 0x6000, 0x6080, 3, 0); + for i in 0..4 { + assert_eq!(fabric.bytes(0x6000 + i * 128, 128), line(1), "line {i}"); + } +} + +#[test] +fn a_failed_transfer_latches_error_and_the_unmoved_count_until_it_is_cleared() { + let (io, fabric) = io(); + fabric.fill(0x7000, &[1; 128]); + // Memory ends at 1 MiB: the third line of the destination fails. + start(&io, 0, 0x7000, (1 << 20) - 256, 4, 0); + let status = r(&io, engine(0) + bte::LENGTH); + assert_eq!(status, BTE_BUSY | BTE_ERROR | 2, "two lines were not moved"); + assert!(fabric.interrupts.lock().is_empty(), "neither completion runs"); + // A store arms or disarms but never absolves. + io.write64(engine(0) + bte::LENGTH, 0); + assert_eq!(r(&io, engine(0) + bte::LENGTH), BTE_ERROR); + // Only IIO_IECLR clears it, and per engine. + io.write64(reg::ERROR_CLEAR, IECLR_BTE[1]); + assert_eq!(r(&io, engine(0) + bte::LENGTH), BTE_ERROR); + io.write64(reg::ERROR_CLEAR, IECLR_BTE[0]); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0); + // An unclaimed completion node fails soft: the interrupt is dropped. + io.write64(engine(0) + bte::INTERRUPT, 7); + start(&io, 0, 0x7000, 0x7800, 1, 0); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0); +} + +#[test] +fn a_zero_length_launch_completes_at_once_and_busy_clear_launches_nothing() { + let (io, fabric) = io(); + fabric.fill(0x9000, &[9; 128]); + start(&io, 0, 0x9000, 0x9800, 0, 0); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 0); + assert_eq!(fabric.bytes(0x9800, 128), vec![0; 128]); + // A control store with busy clear stores and does nothing. + io.write64(engine(0) + bte::LENGTH, 1); + io.write64(engine(0) + bte::CONTROL, 0); + assert_eq!(fabric.bytes(0x9800, 128), vec![0; 128]); + assert_eq!(r(&io, engine(0) + bte::LENGTH), 1); +} + +#[test] +fn an_engine_with_no_fabric_fails_the_launch() { + let io = HubIo::new(1, 0x36); + start(&io, 0, 0, 0x1000, 1, 0); + assert_eq!(r(&io, engine(0) + bte::LENGTH), BTE_BUSY | BTE_ERROR | 1); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 26b7ddcad..332f04026 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -61,6 +61,7 @@ pub mod virtio; pub mod wd33c93a; pub mod z85c30; pub mod hub; +pub mod hub_io; pub mod hub_md; pub mod hub_pi; pub mod dp83843; From 9804ff41d778bba45897c6f14d291eaec583b854 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:01:47 -0700 Subject: [PATCH 17/38] hub: add the network interface The network interface holds the node number the boot master assigns, the link state, vector PIO and the routing tables. Firmware uses vector PIO to reach a Hub before node numbers exist. Two Hubs joined by a direct cable are the only fabric: a zero route reaches the neighbour, and a nonzero route completes with the address error type because there is no Router to carry it. A request nothing answers leaves the status untouched, so the requester's poll times out. Link up, width and the never-came-out-of-reset reason follow the cable, because the guest kernel treats a down link without that reason as a lost CrayLink. The two ends never hold their locks across a call to each other. Protection, ageing and the diagnostic bits other than the two that act are stored and never consulted. --- src/dev/hub_ni.rs | 714 ++++++++++++++++++++++++++++++++++++++++ src/dev/hub_ni/tests.rs | 412 +++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 1127 insertions(+) create mode 100644 src/dev/hub_ni.rs create mode 100644 src/dev/hub_ni/tests.rs diff --git a/src/dev/hub_ni.rs b/src/dev/hub_ni.rs new file mode 100644 index 000000000..cb3172228 --- /dev/null +++ b/src/dev/hub_ni.rs @@ -0,0 +1,714 @@ +//! The Hub's network interface: the node number and mode, the link to the +//! neighbour across the CrayLink cable, vector PIO, the routing tables, the +//! scratch and port registers, and the resets. +//! +//! Offsets are from the base of the Hub window. Two Hubs joined by a cable +//! are the only fabric here: the neighbour is reached by a direct vector, and +//! a nonzero vector needs a Router the cable does not have. +//! +//! # What is approximated here +//! +//! - The node number, mode and region size reset to zero, M mode and set (or +//! to the node number the machine preprograms). The link-up and +//! eight-bit bits follow the cable; a Hub with no powered peer reads down +//! with the never-came-out-of-reset reason set, which the guest kernel +//! takes to mean no CrayLink rather than a lost one. The model stays in M mode. +//! - Failing, rather than timing out, an access to a node number no reachable +//! Hub holds is an approximation (it is made by the shell's claim map). +//! - A vector launch that reaches nothing leaves the status untouched, so the +//! requester's poll times out. Overrun never sets. A target answers from its +//! own register store with no local side effects; a register outside the set +//! a vector access decodes completes with the address error type. +//! - The routing tables are stored; an entry not written since reset routes +//! nothing. They drive vector routing and the claim map only. +//! - A link reset completes when it is written. Both ends settle the width +//! from their force-eight-bit bits (`NI_DIAG_PARMS` bit 12) and latch +//! `NI_PORT_ERROR` bit 37. How long a reset takes, and the latch on a Hub +//! with no peer, are unknown. +//! - Protection, I/O protection, ageing and the diagnostic bits other than +//! 0 and 12 are stored and never consulted: enforcing protection a firmware +//! forgot to open would leave a dead machine with no diagnostic. +//! - `NI_PORT_RESET` bit 0 restarts the whole machine, not one node, and +//! bit 7 alone is logged and does nothing; its effect alone is unknown. +//! - A Hub whose module is powered down is disabled: its port reads +//! disabled and its peer sees the link fail. +//! +//! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::hub::{HubHooks, HubObserver, Store}; +use crate::devlog::LogModule; +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_OK}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; + +pub mod reg { + pub const STATUS: u64 = 0x60_0000; + pub const PORT_RESET: u64 = 0x60_0008; + pub const PROTECTION: u64 = 0x60_0010; + pub const SCRATCH_0: u64 = 0x60_0100; + pub const SCRATCH_1: u64 = 0x60_0108; + pub const DIAG_PARMS: u64 = 0x60_0110; + pub const VECTOR_PARMS: u64 = 0x60_0200; + pub const VECTOR: u64 = 0x60_0208; + pub const VECTOR_DATA: u64 = 0x60_0210; + pub const VECTOR_STATUS: u64 = 0x60_0300; + pub const RETURN_VECTOR: u64 = 0x60_0308; + pub const VECTOR_READ_DATA: u64 = 0x60_0310; + pub const VECTOR_CLEAR: u64 = 0x60_0380; + pub const IO_PROTECT: u64 = 0x60_0400; + pub const IO_PROT_OVRRD: u64 = 0x60_0408; + pub const AGE_BASE: u64 = 0x60_0500; + pub const PORT_PARMS: u64 = 0x60_8000; + pub const PORT_ERROR: u64 = 0x60_8008; + pub const PORT_ERROR_CLEAR: u64 = 0x60_8088; + pub const META_TABLE: u64 = 0x63_8000; + pub const LOCAL_TABLE: u64 = 0x63_8100; +} + +pub const START: u64 = 0x60_0000; +pub const END: u64 = 0x80_0000; +pub const META_ROUTES: usize = 32; +pub const LOCAL_ROUTES: usize = 16; +/// Node numbers the routing tables name. +pub const NUMBERS: u16 = 256; + +/// Bit 30 eight-bit link, 29 link up, 28 down reason (0 failed, 1 never came +/// out of reset), 18 more nodes (M mode when clear), 17 region size, 16:8 the +/// node number, 7:4 the chip revision. +pub const STATUS_EIGHT_BIT: u64 = 1 << 30; +pub const STATUS_LINK_UP: u64 = 1 << 29; +pub const STATUS_NEVER_OUT_OF_RESET: u64 = 1 << 28; +const STATUS_MORE_NODES: u64 = 1 << 18; +const STATUS_REGION_SIZE: u64 = 1 << 17; +const STATUS_NODE_MASK: u64 = 0x1ff << 8; +const STATUS_RESET: u64 = STATUS_REGION_SIZE | STATUS_NEVER_OUT_OF_RESET; +const STATUS_WRITABLE: u64 = STATUS_MORE_NODES | STATUS_REGION_SIZE | STATUS_NODE_MASK; + +const PORT_RESET_LOCAL: u64 = 1 << 0; +const PORT_RESET_LINK: u64 = 1 << 1; +const PORT_RESET_WARM: u64 = 1 << 7; +const PROTECTION_MASK: u64 = 1; +const DIAG_MASK: u64 = 0x7_ffff; +const DIAG_SEND_DATA_ERROR: u64 = 1 << 0; +const DIAG_EIGHT_BIT: u64 = 1 << 12; +const AGEING_MASK: u64 = 0xfff; +const PORT_PARMS_MASK: u64 = 0xffff; +const PORT_PARMS_RESET: u64 = (6 << 10) | 0x3f0; +const PORT_ERROR_MASK: u64 = 0x3f_ffff_ffff; +const PORT_ERROR_LINK_RESET: u64 = 1 << 37; +const PORT_CHECK_ERROR_SHIFT: u32 = 8; +const PROT_OVRRD_PASSWORD: u64 = 0x5347_4972_756c_6573; +const ROUTE_MASK: u8 = 0xf; + +const VECTOR_PIOID_MASK: u64 = 0x3ff << 40; +const VECTOR_WRITEID_MASK: u64 = 0xff << 32; +const VECTOR_ADDRESS_MASK: u64 = 0xf_fff8; +const VECTOR_TYPE_MASK: u64 = 3; +const VECTOR_VALID: u64 = 1 << 63; +const VECTOR_TARGET_SHIFT: u32 = 51; +const VECTOR_STATUS_ADDR_MASK: u64 = 0xffff_fff8; +pub const VECTOR_READ: u64 = 0; +pub const VECTOR_WRITE: u64 = 1; +pub const VECTOR_EXCHANGE: u64 = 3; +pub const VECTOR_ADDRESS_ERROR: u64 = 4; + +/// One vector PIO request, as it arrives at the far end. +#[derive(Clone, Copy, Debug)] +pub struct VectorRequest { + pub op: u64, + /// Bits 19:3 of the register's address in the target's network block. + pub address: u64, + pub data: u64, + /// What is left of the route; a Hub target never forwards. + pub vector: u64, +} + +/// The far end of the cable, as this Hub's port sees it. +pub trait NetPeer: Send + Sync { + /// Perform a vector request at the target: its answer, or `None` for an + /// address error, and the answering node number. + fn receive(&self, request: &VectorRequest) -> (Option, u16); + /// The far end's programmed node number. + fn number(&self) -> u16; + /// The far end is powered and its port enabled. + fn enabled(&self) -> bool; + /// The far end forces the link to eight bits. + fn forces_eight_bit(&self) -> bool; + /// The far end reset the link: settle the width again and latch the event. + fn link_reset(&self); + /// The far end's node number or routes changed. + fn changed(&self); +} + +/// What the shell does when the network state moves. +pub trait NiEvents: Send + Sync { + /// The node number, a route or the link changed: re-derive the claim map. + fn changed(&self); +} + +fn unimp(what: &str, off: u64, value: Option) { + match value { + Some(v) => crate::dlog!(LogModule::Hub, "Hub NI: write of {v:#x} to {what} {off:#x}"), + None => crate::dlog!(LogModule::Hub, "Hub NI: read of {what} {off:#x}"), + } +} + +struct State { + status: u64, + scratch: [u64; 2], + vector: u64, + vector_data: u64, + vector_status: u64, + return_vector: u64, + vector_read_data: u64, + port_parms: u64, + port_error: u64, + ageing: [u64; 8], + io_protect: u64, + protection: u64, + diag_parms: u64, + meta_route: [u8; META_ROUTES], + local_route: [u8; LOCAL_ROUTES], + meta_written: u32, + local_written: u16, +} +impl State { + fn new(revision: u8, nasid: u16) -> Self { + Self { + status: STATUS_RESET | (u64::from(revision) << 4) | (u64::from(nasid) << 8), + scratch: [0; 2], + vector: 0, + vector_data: 0, + vector_status: 0, + return_vector: 0, + vector_read_data: 0, + port_parms: PORT_PARMS_RESET, + port_error: 0, + ageing: [0; 8], + io_protect: !0, + protection: 1, + diag_parms: 0, + meta_route: [0; META_ROUTES], + local_route: [0; LOCAL_ROUTES], + meta_written: 0, + local_written: 0, + } + } + fn number(&self) -> u16 { + ((self.status & STATUS_NODE_MASK) >> 8) as u16 + } +} + +pub struct HubNi { + revision: u8, + nasid: u16, + state: Mutex, + peer: Mutex>>, + /// The module has power. A disabled port reads down at its peer. + enabled: AtomicBool, + hooks: OnceLock>, + events: OnceLock>, + observer: OnceLock>, +} + +impl HubNi { + /// `revision` is the chip revision code in NI_STATUS_REV_ID bits 7:4 (3 is + /// Hub 2.1, 5 is Hub 2.3) and `nasid` the node number a synthesized + /// firmware preprograms, zero otherwise. + pub fn new(revision: u8, nasid: u16) -> Self { + assert!(revision < 16 && nasid < 512); + Self { + revision, + nasid, + state: Mutex::new(State::new(revision, nasid)), + peer: Mutex::new(None), + enabled: AtomicBool::new(true), + hooks: OnceLock::new(), + events: OnceLock::new(), + observer: OnceLock::new(), + } + } + + pub fn set_hooks(&self, hooks: Arc) { + let _ = self.hooks.set(hooks); + } + + /// Report stores to the scratch registers and the port reset (see + /// [`Store`]). + pub fn set_observer(&self, observer: Arc) { + let _ = self.observer.set(observer); + } + + fn observe(&self, store: Store) { + if let Some(observer) = self.observer.get() { + observer.stored(store); + } + } + + pub fn set_events(&self, events: Arc) { + let _ = self.events.set(events); + } + + /// Join this port to the far end of a cable, or take the cable off. + pub fn connect_peer(&self, peer: Option>) { + *self.peer.lock() = peer; + self.announce(); + } + + /// Power to the module comes or goes. A disabled port reads down at the + /// peer, with the reason left at "failed". + pub fn set_enabled(&self, on: bool) { + self.enabled.store(on, Ordering::Release); + if let Some(peer) = self.peer() { + peer.changed(); + } + } + + fn peer(&self) -> Option> { + self.peer.lock().as_ref().and_then(Weak::upgrade) + } + + /// The neighbour, if the cable has one that is up. + fn neighbour(&self) -> Option> { + self.peer().filter(|peer| peer.enabled()) + } + + /// The node number this Hub answers to. + pub fn number(&self) -> u16 { + self.state.lock().number() + } + + /// The direction the first Router takes for a packet to `destination`, or + /// `None` while the entry has not been written since reset. + pub fn first_turn(&self, destination: u16) -> Option { + if destination >= NUMBERS { + return None; + } + let s = self.state.lock(); + if destination >> 4 == s.number() >> 4 { + let i = usize::from(destination & 0xf); + (s.local_written & (1 << i) != 0).then(|| s.local_route[i]) + } else { + let i = usize::from(destination >> 4); + (s.meta_written & (1 << i) != 0).then(|| s.meta_route[i]) + } + } + + /// Whether the diagnostic bit that makes this Hub's vector writes count a + /// check-bit error at the first Router is set. + pub fn sends_data_error(&self) -> bool { + self.state.lock().diag_parms & DIAG_SEND_DATA_ERROR != 0 + } + + /// A Router counted a check-bit error against this Hub: bits 15:8 of the + /// port error count them, saturating at 255. + pub fn record_check_error(&self) { + let mut s = self.state.lock(); + let count = (s.port_error >> PORT_CHECK_ERROR_SHIFT) & 0xff; + if count != 0xff { + s.port_error = (s.port_error & !(0xff << PORT_CHECK_ERROR_SHIFT)) | ((count + 1) << PORT_CHECK_ERROR_SHIFT); + } + } + + pub fn latch_port_error(&self, errors: u64) { + self.state.lock().port_error |= errors & PORT_ERROR_MASK; + } + + fn notify(&self) { + if let Some(events) = self.events.get() { + events.changed(); + } + } + + /// Re-derive the link bits from the cable, then tell the shell. + pub fn refresh(&self) { + self.derive(); + self.notify(); + } + + /// This port's own state changed (the node number, a route, the cable): + /// re-derive it and tell the neighbour, which re-derives its own and does + /// not answer back. + fn announce(&self) { + self.refresh(); + if let Some(peer) = self.peer() { + peer.changed(); + } + } + + /// Whether a powered neighbour is across the cable, and its node number. + pub fn neighbour_number(&self) -> Option { + self.neighbour().map(|peer| peer.number()) + } + + /// The link bits: up when a powered neighbour is across the cable. The + /// width is what the link settled on when it came up or was last reset, + /// so a link that stays up keeps it; one that is down has none. + fn derive(&self) { + // Ask the far end before taking this state's lock: the two ends + // never hold their locks across a call to each other. + let forces = self.neighbour().map(|peer| peer.forces_eight_bit()); + let mut s = self.state.lock(); + match forces { + None => s.status &= !(STATUS_LINK_UP | STATUS_EIGHT_BIT), + Some(forces) => { + if s.status & STATUS_LINK_UP == 0 { + Self::link_up(&mut s, forces); + } + } + } + } + + /// Bring this end up at the width the two ends settle on now. The link + /// coming up clears the never-out-of-reset reason until the next reset. + fn link_up(s: &mut State, peer_forces: bool) { + s.status = (s.status & !(STATUS_NEVER_OUT_OF_RESET | STATUS_EIGHT_BIT)) | STATUS_LINK_UP; + if peer_forces || s.diag_parms & DIAG_EIGHT_BIT != 0 { + s.status |= STATUS_EIGHT_BIT; + } + } + + /// A link reset, completed as it is asked for. Node number, mode, routing, + /// vector and diagnostic state keep their values, which is what lets + /// software reset the link and keep running. + fn link_reset(&self) { + let neighbour = self.neighbour(); + let forces = neighbour.as_ref().map(|peer| peer.forces_eight_bit()); + { + let mut s = self.state.lock(); + s.port_error |= PORT_ERROR_LINK_RESET; + match forces { + None => { + s.status = (s.status & !(STATUS_LINK_UP | STATUS_EIGHT_BIT)) | STATUS_NEVER_OUT_OF_RESET; + } + Some(forces) => Self::link_up(&mut s, forces), + } + } + if let Some(peer) = neighbour { + peer.link_reset(); + } + } + + fn route_entry(off: u64) -> Option<(bool, usize)> { + if off % 8 != 0 { + return None; + } + if let Some(rel) = off.checked_sub(reg::META_TABLE).filter(|rel| rel / 8 < META_ROUTES as u64) { + return Some((true, (rel / 8) as usize)); + } + let rel = off.checked_sub(reg::LOCAL_TABLE).filter(|rel| rel / 8 < LOCAL_ROUTES as u64)?; + Some((false, (rel / 8) as usize)) + } + + fn ageing(off: u64) -> Option { + let rel = off.checked_sub(reg::AGE_BASE)?; + (rel % 8 == 0 && rel / 8 < 8).then_some((rel / 8) as usize) + } + + /// A register's value without side effects, for the registers that store. + pub fn peek(&self, off: u64) -> Option { + let s = self.state.lock(); + if let Some((meta, i)) = Self::route_entry(off) { + return Some(u64::from(if meta { s.meta_route[i] } else { s.local_route[i] })); + } + if let Some(i) = Self::ageing(off) { + return Some(s.ageing[i]); + } + Some(match off { + reg::STATUS => s.status, + reg::PROTECTION => s.protection, + reg::SCRATCH_0 | reg::SCRATCH_1 => s.scratch[((off - reg::SCRATCH_0) / 8) as usize], + reg::DIAG_PARMS => s.diag_parms, + reg::VECTOR => s.vector, + reg::VECTOR_DATA => s.vector_data, + reg::VECTOR_STATUS | reg::VECTOR_CLEAR => s.vector_status, + reg::RETURN_VECTOR => s.return_vector, + reg::VECTOR_READ_DATA => s.vector_read_data, + reg::IO_PROTECT => s.io_protect, + reg::PORT_PARMS => s.port_parms, + reg::PORT_ERROR | reg::PORT_ERROR_CLEAR => s.port_error, + _ => return None, + }) + } + + /// Read the register at `off`. `clear` is false for a 32-bit half, which + /// reads a read-to-clear register without clearing it. + pub fn read(&self, off: u64, clear: bool) -> u64 { + match off { + reg::VECTOR_CLEAR => { + let mut s = self.state.lock(); + if clear { + s.vector_status = 0; + } + // Reads zero and clears the status. + 0 + } + reg::PORT_ERROR_CLEAR => { + let mut s = self.state.lock(); + let value = s.port_error; + if clear { + s.port_error = 0; + } + value + } + reg::PORT_RESET | reg::VECTOR_PARMS | reg::IO_PROT_OVRRD => { + unimp("write-only register", off, None); + 0 + } + _ => match self.peek(off) { + Some(v) => v, + None => { + unimp("unimplemented register", off, None); + 0 + } + }, + } + } + + pub fn write(&self, off: u64, value: u64) { + self.store(off, value, false); + } + + /// A store to the register at `off`, from the bus or, when `vector` is + /// set, from another node's vector PIO. + fn store(&self, off: u64, value: u64, vector: bool) { + if let Some((meta, i)) = Self::route_entry(off) { + self.write_route(meta, i, value); + return; + } + let mut changed = false; + { + let mut s = self.state.lock(); + if let Some(i) = Self::ageing(off) { + s.ageing[i] = value & AGEING_MASK; + return; + } + match off { + reg::STATUS => { + let before = s.number(); + s.status = (s.status & !STATUS_WRITABLE) | (value & STATUS_WRITABLE); + changed = true; + let after = s.number(); + if after != before { + drop(s); + if let Some(h) = self.hooks.get() { + h.node_number_changed(after); + } + } + } + reg::PORT_RESET => { + drop(s); + self.observe(Store::PortReset { value }); + self.port_reset(value); + } + reg::PROTECTION => s.protection = value & PROTECTION_MASK, + reg::SCRATCH_0 | reg::SCRATCH_1 => { + let index = ((off - reg::SCRATCH_0) / 8) as usize; + s.scratch[index] = value; + drop(s); + self.observe(Store::NiScratch { index, value, vector }); + } + reg::DIAG_PARMS => s.diag_parms = value & DIAG_MASK, + reg::VECTOR_PARMS => { + let request = (s.vector, s.vector_data); + drop(s); + self.vector_launch(value, request); + } + reg::VECTOR => s.vector = value, + reg::VECTOR_DATA => s.vector_data = value, + reg::VECTOR_STATUS => { + if value == 0 { + s.vector_status = 0; + } + } + reg::RETURN_VECTOR => s.return_vector = value, + reg::VECTOR_READ_DATA => s.vector_read_data = value, + reg::VECTOR_CLEAR => s.vector_status = 0, + reg::IO_PROTECT => s.io_protect = value, + reg::IO_PROT_OVRRD => Self::note_override(value), + reg::PORT_PARMS => s.port_parms = value & PORT_PARMS_MASK, + reg::PORT_ERROR => s.port_error &= !(value & PORT_ERROR_MASK), + _ => unimp("unimplemented or read-only register", off, Some(value)), + } + } + if changed { + self.announce(); + } + } + + fn note_override(value: u64) { + // The password unlock is a no-op on a model that never enforces + // protection; a non-password write's effect is unstated. + if value != PROT_OVRRD_PASSWORD { + crate::dlog!(LogModule::Hub, "Hub NI: write of {value:#x} to the protection override (not the password; effect unstated)"); + } + } + + fn write_route(&self, meta: bool, i: usize, value: u64) { + { + let mut s = self.state.lock(); + let entry = value as u8 & ROUTE_MASK; + // An undefined reset entry does not establish a programmed route. + if meta { + s.meta_route[i] = entry; + s.meta_written |= 1 << i; + } else { + s.local_route[i] = entry; + s.local_written |= 1 << i; + } + } + self.announce(); + } + + fn port_reset(&self, value: u64) { + if value & PORT_RESET_WARM != 0 && value & PORT_RESET_LOCAL == 0 { + crate::dlog!(LogModule::Hub, "Hub NI: port reset bit 7 without bit 0 is not implemented"); + } + if value & PORT_RESET_LOCAL != 0 { + // Software that resets the machine writes bits 7 and 0 together. + if let Some(h) = self.hooks.get() { + h.machine_restart(); + } + } else if value & PORT_RESET_LINK != 0 { + self.link_reset(); + self.notify(); + } + } + + /// Vector PIO: launch a request from this Hub's own port and perform the + /// access on whatever the route ends at. + fn vector_launch(&self, parms: u64, (vector, data): (u64, u64)) { + let address = parms & VECTOR_ADDRESS_MASK; + let op = parms & VECTOR_TYPE_MASK; + let Some(peer) = self.neighbour() else { + // A route nothing answers latches nothing: the poll times out. + crate::dlog!(LogModule::Hub, "Hub NI: vector launch (parms {parms:#x}) reached nothing that answers"); + return; + }; + let request = VectorRequest { op, address, data, vector }; + // The neighbour is on the wire and answers, but a route past it needs + // a Router the cable does not have: the address-error response, not + // the timeout a cable with nothing on it leaves. + let (answer, source) = if vector != 0 { (None, peer.number()) } else { peer.receive(&request) }; + let mut s = self.state.lock(); + let mut response = op; + match answer { + Some(value) => { + if op != VECTOR_WRITE { + s.vector_read_data = value; + } + } + None => response = VECTOR_ADDRESS_ERROR, + } + s.vector_status = VECTOR_VALID + | (u64::from(source) << VECTOR_TARGET_SHIFT) + | (parms & (VECTOR_PIOID_MASK | VECTOR_WRITEID_MASK)) + | (address & VECTOR_STATUS_ADDR_MASK) + | response; + } + + /// The target's half of a vector operation. A read or write reaches the + /// registers a vector access decodes: status, + /// protection, scratch, diagnostics, I/O protection, ageing, the port + /// registers and both routing tables. An exchange writes only over a zero. + fn vector_access(&self, request: &VectorRequest) -> Option { + let off = reg::STATUS + request.address; + match request.op { + VECTOR_READ => self.vector_read(off), + VECTOR_WRITE => self.vector_write(off, request.data).then_some(0), + VECTOR_EXCHANGE => { + let old = self.vector_read(off)?; + if old == 0 && !self.vector_write(off, request.data) { + return None; + } + Some(old) + } + _ => None, + } + } + + fn vector_read(&self, off: u64) -> Option { + match off { + reg::PORT_ERROR_CLEAR => Some(self.read(off, true)), + reg::STATUS | reg::PROTECTION | reg::SCRATCH_0 | reg::SCRATCH_1 | reg::DIAG_PARMS | reg::IO_PROTECT + | reg::PORT_PARMS | reg::PORT_ERROR => self.peek(off), + _ if Self::ageing(off).is_some() || Self::route_entry(off).is_some() => self.peek(off), + _ => None, + } + } + + fn vector_write(&self, off: u64, value: u64) -> bool { + match off { + reg::STATUS | reg::PROTECTION | reg::SCRATCH_0 | reg::SCRATCH_1 | reg::DIAG_PARMS | reg::IO_PROTECT + | reg::PORT_PARMS | reg::PORT_ERROR => self.store(off, value, true), + reg::IO_PROT_OVRRD => Self::note_override(value), + _ if Self::ageing(off).is_some() || Self::route_entry(off).is_some() => self.store(off, value, true), + _ => return false, + } + true + } +} + +impl NetPeer for HubNi { + fn receive(&self, request: &VectorRequest) -> (Option, u16) { + let answer = if self.enabled.load(Ordering::Acquire) { self.vector_access(request) } else { None }; + (answer, self.number()) + } + fn number(&self) -> u16 { + HubNi::number(self) + } + fn enabled(&self) -> bool { + self.enabled.load(Ordering::Acquire) + } + fn forces_eight_bit(&self) -> bool { + self.state.lock().diag_parms & DIAG_EIGHT_BIT != 0 + } + fn link_reset(&self) { + // The peer reset the link: settle the width again and latch the event. + let forces = self.neighbour().map(|peer| peer.forces_eight_bit()); + let mut s = self.state.lock(); + s.port_error |= PORT_ERROR_LINK_RESET; + if let Some(forces) = forces { + Self::link_up(&mut s, forces); + } + } + fn changed(&self) { + self.refresh(); + } +} + +impl HubNi { + /// Whether the register state is locked now, for the `locks` monitor command. + pub fn is_locked(&self) -> bool { + self.state.is_locked() + } +} + +impl Resettable for HubNi { + fn power_on(&self) { + { + let mut s = self.state.lock(); + *s = State::new(self.revision, self.nasid); + } + // The link comes out of reset now: its width is settled from both + // ends' force bits as they stand. + self.announce(); + if let Some(h) = self.hooks.get() { + h.node_number_changed(self.nasid); + } + } +} + +impl BusDevice for HubNi { + fn read64(&self, addr: u64) -> BusRead64 { + BusRead64::ok(self.read(addr, true)) + } + fn write64(&self, addr: u64, value: u64) -> u32 { + self.write(addr, value); + BUS_OK + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/hub_ni/tests.rs b/src/dev/hub_ni/tests.rs new file mode 100644 index 000000000..0e217ec6f --- /dev/null +++ b/src/dev/hub_ni/tests.rs @@ -0,0 +1,412 @@ +// Cases follow tests/qtest/sn0-hub-test.c in jamesbraid/qemu-sgi-origami: +// status-field-access, network-link-presence, +// a-node-with-no-link-reads-down, peerless-link-reset, +// local-port-reset-resets-the-node, network-scratch-registers, +// vector-pio-direct-peer, vector-launch-no-fabric-stays-silent, +// llp-port-registers, llp-port-error-clear-contract, +// vector-writes-land-only-on-rows-that-store, the *-answer-vector-decode +// cases, io-protect-registers, ni-protection-config, ni-diag-parms, +// network-routes-reset-to-zero and network-ageing-controls. + +use super::*; +use std::sync::atomic::AtomicU32; + +#[derive(Default)] +struct Calls(Mutex>); +impl HubHooks for Calls { + fn node_number_changed(&self, number: u16) { + self.0.lock().push(format!("number {number}")); + } + fn machine_restart(&self) { + self.0.lock().push("restart".into()); + } +} +#[derive(Default)] +struct Moves(AtomicU32); +impl NiEvents for Moves { + fn changed(&self) { + self.0.fetch_add(1, Ordering::SeqCst); + } +} + +struct Node { + ni: Arc, + calls: Arc, + moves: Arc, +} +fn node(revision: u8, nasid: u16) -> Node { + let ni = Arc::new(HubNi::new(revision, nasid)); + let calls = Arc::new(Calls::default()); + let moves = Arc::new(Moves::default()); + ni.set_hooks(calls.clone()); + ni.set_events(moves.clone()); + Node { ni, calls, moves } +} +/// Two Hubs on a direct cable, the two-module Origin 200. +fn pair() -> (Node, Node) { + let (a, b) = (node(3, 0), node(3, 0)); + let (wa, wb): (Weak, Weak) = (Arc::downgrade(&a.ni) as _, Arc::downgrade(&b.ni) as _); + a.ni.connect_peer(Some(wb)); + b.ni.connect_peer(Some(wa)); + (a, b) +} +fn r(ni: &HubNi, off: u64) -> u64 { + ni.read64(off).data +} +fn w(ni: &HubNi, off: u64, value: u64) { + assert_eq!(ni.write64(off, value), BUS_OK); +} + +/// Run a vector operation from `ni` to the Hub across the cable and return +/// the completion status. +fn vector(ni: &HubNi, route: u64, off: u64, ty: u64, data: u64) -> u64 { + w(ni, reg::VECTOR, route); + w(ni, reg::VECTOR_DATA, data); + w(ni, reg::VECTOR_PARMS, (off - reg::STATUS) | ty); + r(ni, reg::VECTOR_STATUS) +} +const VALID: u64 = 1 << 63; + +#[test] +fn the_status_register_names_the_node_the_mode_and_the_chip_revision() { + let n = node(3, 0); + let status = r(&n.ni, reg::STATUS); + // Region size set, M mode (bit 18 clear), never out of reset, revision 3. + assert_eq!(status, STATUS_REGION_SIZE | STATUS_NEVER_OUT_OF_RESET | (3 << 4)); + // Firmware assigns node numbers through this register. + w(&n.ni, reg::STATUS, STATUS_NODE_MASK); + assert_eq!(r(&n.ni, reg::STATUS) & STATUS_NODE_MASK, STATUS_NODE_MASK); + assert_eq!(n.ni.number(), 0x1ff); + // The revision and the link bits are not writable. + w(&n.ni, reg::STATUS, !0); + assert_eq!(r(&n.ni, reg::STATUS) & 0xff, 3 << 4); + assert_eq!(r(&n.ni, reg::STATUS) & (STATUS_LINK_UP | STATUS_EIGHT_BIT), 0); + // Mode: bit 18 writes (N mode), region size writes. + w(&n.ni, reg::STATUS, 0); + assert_eq!(r(&n.ni, reg::STATUS) & (STATUS_MORE_NODES | STATUS_REGION_SIZE), 0); + // A preprogrammed node number survives reset. + let preset = node(5, 7); + assert_eq!(r(&preset.ni, reg::STATUS), STATUS_RESET | (5 << 4) | (7 << 8)); + w(&preset.ni, reg::STATUS, 0); + preset.ni.power_on(); + assert_eq!(preset.ni.number(), 7); +} + +#[test] +fn a_new_node_number_tells_the_machine_and_the_claim_map() { + let n = node(3, 0); + let moved = n.moves.0.load(Ordering::SeqCst); + w(&n.ni, reg::STATUS, 5 << 8); + assert_eq!(n.calls.0.lock().as_slice(), ["number 5"]); + assert!(n.moves.0.load(Ordering::SeqCst) > moved); + // Writing the number it already has is not a change. + w(&n.ni, reg::STATUS, 5 << 8); + assert_eq!(n.calls.0.lock().len(), 1); +} + +#[test] +fn a_node_with_no_link_never_came_out_of_reset() { + // The guest kernel treats down with the never-out-of-reset reason clear as a lost + // CrayLink on every bus error, so the reason must read one. + let n = node(3, 0); + let down = |n: &Node| r(&n.ni, reg::STATUS) & (STATUS_LINK_UP | STATUS_NEVER_OUT_OF_RESET); + assert_eq!(down(&n), STATUS_NEVER_OUT_OF_RESET); + n.ni.power_on(); + assert_eq!(down(&n), STATUS_NEVER_OUT_OF_RESET); + // Read-only: a guest write cannot claim the link failed. + w(&n.ni, reg::STATUS, 0); + assert_eq!(down(&n), STATUS_NEVER_OUT_OF_RESET); +} + +#[test] +fn a_link_reset_with_no_peer_latches_the_event_and_leaves_the_link_down() { + let n = node(3, 0); + w(&n.ni, reg::STATUS, 9 << 8); + w(&n.ni, reg::SCRATCH_0, 0x55); + assert_eq!(r(&n.ni, reg::PORT_ERROR), 0); + w(&n.ni, reg::PORT_RESET, PORT_RESET_LINK); + assert_eq!(r(&n.ni, reg::PORT_ERROR), PORT_ERROR_LINK_RESET); + assert_eq!(r(&n.ni, reg::STATUS) & (STATUS_LINK_UP | STATUS_NEVER_OUT_OF_RESET), STATUS_NEVER_OUT_OF_RESET); + // The node number, scratch state and routing keep their values. + assert_eq!((n.ni.number(), r(&n.ni, reg::SCRATCH_0)), (9, 0x55)); + // The kernel's power-off path reads the clearing alias until bit 37 clears: a + // single clearing read sees it set once and then clear. + assert_eq!(r(&n.ni, reg::PORT_ERROR_CLEAR), PORT_ERROR_LINK_RESET); + assert_eq!(r(&n.ni, reg::PORT_ERROR_CLEAR), 0); + // None of that restarted the machine. + assert!(n.calls.0.lock().iter().all(|c| c.starts_with("number"))); +} + +#[test] +fn the_local_bit_of_the_port_reset_restarts_the_machine_with_either_companion() { + for value in [0x81, 0x03, 0x01] { + let n = node(3, 0); + w(&n.ni, reg::PORT_RESET, value); + assert_eq!(n.calls.0.lock().as_slice(), ["restart"], "{value:#x}"); + // Nothing is stored and the link reset is superseded. + assert_eq!(r(&n.ni, reg::PORT_ERROR), 0); + } + // Bit 7 alone is logged and does nothing. + let n = node(3, 0); + w(&n.ni, reg::PORT_RESET, PORT_RESET_WARM); + assert!(n.calls.0.lock().is_empty()); +} + +#[test] +fn two_cabled_hubs_read_link_up_and_a_lost_peer_reads_failed() { + let (a, b) = pair(); + let up = |n: &Node| r(&n.ni, reg::STATUS) & (STATUS_LINK_UP | STATUS_NEVER_OUT_OF_RESET | STATUS_EIGHT_BIT); + // Coming up clears the never-out-of-reset reason until the next reset. + assert_eq!((up(&a), up(&b)), (STATUS_LINK_UP, STATUS_LINK_UP)); + a.ni.power_on(); + assert_eq!(up(&a), STATUS_LINK_UP); + // The peer's module loses power: this end reads down, with the reason + // left at "failed". + b.ni.set_enabled(false); + assert_eq!(r(&a.ni, reg::STATUS) & (STATUS_LINK_UP | STATUS_NEVER_OUT_OF_RESET), 0); + b.ni.set_enabled(true); + assert_eq!(up(&a), STATUS_LINK_UP); +} + +#[test] +fn a_link_reset_settles_the_width_from_both_ends_force_bits() { + let (a, b) = pair(); + let eight = |n: &Node| r(&n.ni, reg::STATUS) & STATUS_EIGHT_BIT != 0; + // Writing the force bit alone changes nothing until the next link reset. + w(&b.ni, reg::DIAG_PARMS, DIAG_EIGHT_BIT); + assert_eq!((eight(&a), eight(&b)), (false, false)); + w(&a.ni, reg::PORT_RESET, PORT_RESET_LINK); + // Eight bits if either end forces it, reported at both ends. + assert_eq!((eight(&a), eight(&b)), (true, true)); + // Link up stays set, and both Hub ends latch the link-reset bit. + assert_eq!(r(&a.ni, reg::STATUS) & STATUS_LINK_UP, STATUS_LINK_UP); + assert_eq!((r(&a.ni, reg::PORT_ERROR), r(&b.ni, reg::PORT_ERROR)), (PORT_ERROR_LINK_RESET, PORT_ERROR_LINK_RESET)); + // Sixteen again when nobody forces eight. + w(&b.ni, reg::DIAG_PARMS, 0); + w(&a.ni, reg::PORT_RESET, PORT_RESET_LINK); + assert_eq!((eight(&a), eight(&b)), (false, false)); +} + +#[test] +fn vector_pio_reaches_the_direct_peer() { + let (a, b) = pair(); + // Read the peer's scratch register. + w(&b.ni, reg::SCRATCH_0, 0x0123_4567_89ab_cdef); + let status = vector(&a.ni, 0, reg::SCRATCH_0, VECTOR_READ, 0); + assert_eq!(status & (VALID | 3), VALID | VECTOR_READ); + assert_eq!(r(&a.ni, reg::VECTOR_READ_DATA), 0x0123_4567_89ab_cdef); + // Reading the clearing alias drops the completion. + assert_eq!(r(&a.ni, reg::VECTOR_CLEAR), 0); + assert_eq!(r(&a.ni, reg::VECTOR_STATUS) & VALID, 0); + // Write. + let status = vector(&a.ni, 0, reg::SCRATCH_1, VECTOR_WRITE, 0xfedc_ba98_7654_3210); + assert_eq!(status & (VALID | 3), VALID | VECTOR_WRITE); + assert_eq!(r(&b.ni, reg::SCRATCH_1), 0xfedc_ba98_7654_3210); + // An exchange returns the old value and writes only over a zero. + w(&b.ni, reg::SCRATCH_1, 0); + let status = vector(&a.ni, 0, reg::SCRATCH_1, VECTOR_EXCHANGE, 0x1122_3344_5566_7788); + assert_eq!(status & (VALID | 3), VALID | VECTOR_EXCHANGE); + assert_eq!((r(&a.ni, reg::VECTOR_READ_DATA), r(&b.ni, reg::SCRATCH_1)), (0, 0x1122_3344_5566_7788)); + vector(&a.ni, 0, reg::SCRATCH_1, VECTOR_EXCHANGE, 0x8877_6655_4433_2211); + assert_eq!((r(&a.ni, reg::VECTOR_READ_DATA), r(&b.ni, reg::SCRATCH_1)), (0x1122_3344_5566_7788, 0x1122_3344_5566_7788)); + // The routing tables answer: a local entry keeps four bits. + assert_eq!(r(&b.ni, reg::LOCAL_TABLE), 0); + vector(&a.ni, 0, reg::LOCAL_TABLE, VECTOR_WRITE, 0x1b); + assert_eq!(r(&b.ni, reg::LOCAL_TABLE), 0xb); + vector(&a.ni, 0, reg::LOCAL_TABLE, VECTOR_READ, 0); + assert_eq!(r(&a.ni, reg::VECTOR_READ_DATA), 0xb); + let last = reg::META_TABLE + 8 * 31; + vector(&a.ni, 0, last, VECTOR_WRITE, 0x16); + assert_eq!(r(&b.ni, last), 6); +} + +#[test] +fn a_vector_completion_carries_the_answering_node_the_ids_and_the_address() { + let (a, b) = pair(); + w(&b.ni, reg::STATUS, 0x1a5 << 8); + let ids = (0x2a5 << 40) | (0x5c << 32); + w(&a.ni, reg::VECTOR_DATA, 0); + w(&a.ni, reg::VECTOR, 0); + w(&a.ni, reg::VECTOR_PARMS, ids | (reg::SCRATCH_0 - reg::STATUS) | VECTOR_READ); + // Valid, the target in 60:51, the identifiers, address bits 31:3, the type. + assert_eq!(r(&a.ni, reg::VECTOR_STATUS), VALID | (0x1a5 << 51) | ids | 0x100 | VECTOR_READ); + // Only a zero write clears the completion. + w(&a.ni, reg::VECTOR_STATUS, 5); + assert_ne!(r(&a.ni, reg::VECTOR_STATUS), 0); + w(&a.ni, reg::VECTOR_STATUS, 0); + assert_eq!(r(&a.ni, reg::VECTOR_STATUS), 0); +} + +#[test] +fn a_vector_that_the_cable_cannot_carry_or_the_target_does_not_decode_errors_out() { + let (a, b) = pair(); + // A nonzero route needs a Router. + let status = vector(&a.ni, 3, reg::SCRATCH_0, VECTOR_READ, 0); + assert_eq!(status & (VALID | 7), VALID | VECTOR_ADDRESS_ERROR); + // A register outside the vector decode, a type the protocol lacks, and a + // write to a read-to-clear alias all answer with the address error. + for (off, ty) in [(reg::VECTOR_PARMS, VECTOR_READ), (reg::SCRATCH_0, 2), (reg::PORT_ERROR_CLEAR, VECTOR_WRITE)] { + let status = vector(&a.ni, 0, off, ty, !0); + assert_eq!(status & (VALID | 7), VALID | VECTOR_ADDRESS_ERROR, "{off:#x} type {ty}"); + } + assert_eq!(r(&b.ni, reg::SCRATCH_0), 0, "a refused request wrote nothing"); + // A powered-down peer answers nothing: the poll times out. + w(&a.ni, reg::VECTOR_STATUS, 0); + b.ni.set_enabled(false); + assert_eq!(vector(&a.ni, 0, reg::SCRATCH_0, VECTOR_READ, 0), 0); +} + +#[test] +fn a_vector_launch_with_no_peer_latches_nothing() { + let n = node(3, 0); + w(&n.ni, reg::VECTOR_DATA, 0x0123_4567_89ab_cdef); + assert_eq!(vector(&n.ni, 0, reg::SCRATCH_0, VECTOR_READ, 0), 0); + assert_eq!(vector(&n.ni, 0, reg::SCRATCH_1, VECTOR_WRITE, 5), 0); + assert_eq!(r(&n.ni, reg::VECTOR_READ_DATA), 0); +} + +#[test] +fn vector_writes_land_only_on_rows_that_store() { + let (a, b) = pair(); + // The port error answers the vector decode with its write-one-to-clear rule. + b.ni.latch_port_error(0x300); + let status = vector(&a.ni, 0, reg::PORT_ERROR, VECTOR_WRITE, !0); + assert_eq!(status & (VALID | 7), VALID | VECTOR_WRITE); + assert_eq!(r(&b.ni, reg::PORT_ERROR), 0); + // The clearing alias returns and clears on a vector read. + b.ni.latch_port_error(0x5); + vector(&a.ni, 0, reg::PORT_ERROR_CLEAR, VECTOR_READ, 0); + assert_eq!((r(&a.ni, reg::VECTOR_READ_DATA), r(&b.ni, reg::PORT_ERROR)), (5, 0)); + // The remote vector write of the node number moves the claim map there. + let before = b.moves.0.load(Ordering::SeqCst); + vector(&a.ni, 0, reg::STATUS, VECTOR_WRITE, 3 << 8); + assert_eq!(b.ni.number(), 3); + assert!(b.moves.0.load(Ordering::SeqCst) > before); +} + +#[test] +fn the_protection_and_diagnostic_registers_store_and_answer_the_vector_decode() { + let (a, b) = pair(); + // Protection: one permissive bit, resetting set, never consulted. + assert_eq!(r(&a.ni, reg::PROTECTION), 1); + w(&a.ni, reg::PROTECTION, !0); + assert_eq!(r(&a.ni, reg::PROTECTION), 1); + w(&a.ni, reg::PROTECTION, 0); + assert_eq!(r(&a.ni, reg::PROTECTION), 0); + // Link diagnostics: 19 bits stored. + w(&a.ni, reg::DIAG_PARMS, !0); + assert_eq!(r(&a.ni, reg::DIAG_PARMS), 0x7_ffff); + // PIO protection resets all ones and stores the whole word; the override + // is write-only and stores nothing. + assert_eq!(r(&a.ni, reg::IO_PROTECT), !0); + w(&a.ni, reg::IO_PROTECT, 0x0123_4567_89ab_cdef); + w(&a.ni, reg::IO_PROT_OVRRD, 0x5347_4972_756c_6573); + assert_eq!((r(&a.ni, reg::IO_PROTECT), r(&a.ni, reg::IO_PROT_OVRRD)), (0x0123_4567_89ab_cdef, 0)); + // The stock PROM's link diagnostic vector-writes a peer's PIO protection + // and vector-reads its port error; the kernel vector-accesses the diagnostics. + for off in [reg::IO_PROTECT, reg::PROTECTION, reg::DIAG_PARMS, reg::PORT_PARMS, reg::PORT_ERROR, reg::AGE_BASE] { + let status = vector(&a.ni, 0, off, VECTOR_READ, 0); + assert_eq!(status & (VALID | 7), VALID | VECTOR_READ, "{off:#x}"); + } + vector(&a.ni, 0, reg::IO_PROTECT, VECTOR_WRITE, 0xfedc_ba98_7654_3210); + assert_eq!(r(&b.ni, reg::IO_PROTECT), 0xfedc_ba98_7654_3210); + // The override is write-only: its vector read keeps the undecoded answer. + let status = vector(&a.ni, 0, reg::IO_PROT_OVRRD, VECTOR_READ, 0); + assert_eq!(status & (VALID | 7), VALID | VECTOR_ADDRESS_ERROR); + let status = vector(&a.ni, 0, reg::IO_PROT_OVRRD, VECTOR_WRITE, 0x5347_4972_756c_6573); + assert_eq!(status & (VALID | 7), VALID | VECTOR_WRITE); +} + +#[test] +fn the_port_registers_reset_to_the_models_values() { + let n = node(3, 0); + // The null-packet timeout in bits 15:10 and the maximum burst in 9:0. + assert_eq!(r(&n.ni, reg::PORT_PARMS), 0x1bf0); + assert_eq!((r(&n.ni, reg::PORT_PARMS) >> 10) & 0x3f, 6); + w(&n.ni, reg::PORT_PARMS, !0); + assert_eq!(r(&n.ni, reg::PORT_PARMS), 0xffff); + // The error register holds 38 bits; writing ones clears them, and the + // clearing alias returns and clears. + n.ni.latch_port_error(!0); + assert_eq!(r(&n.ni, reg::PORT_ERROR), 0x3f_ffff_ffff); + w(&n.ni, reg::PORT_ERROR, 0x1); + assert_eq!(r(&n.ni, reg::PORT_ERROR), 0x3f_ffff_fffe); + assert_eq!(r(&n.ni, reg::PORT_ERROR_CLEAR), 0x3f_ffff_fffe); + assert_eq!(r(&n.ni, reg::PORT_ERROR), 0); + // A 32-bit half reads the alias without clearing it. + n.ni.latch_port_error(0x10); + assert_eq!(n.ni.read(reg::PORT_ERROR_CLEAR, false), 0x10); + assert_eq!(n.ni.read(reg::PORT_ERROR_CLEAR, true), 0x10); + n.ni.power_on(); + assert_eq!(r(&n.ni, reg::PORT_PARMS), 0x1bf0); +} + +#[test] +fn a_check_bit_error_count_saturates_at_255() { + let n = node(3, 0); + assert!(!n.ni.sends_data_error()); + w(&n.ni, reg::DIAG_PARMS, DIAG_SEND_DATA_ERROR); + assert!(n.ni.sends_data_error()); + for _ in 0..300 { + n.ni.record_check_error(); + } + assert_eq!(r(&n.ni, reg::PORT_ERROR) >> 8 & 0xff, 255); + // Nothing else moved. + assert_eq!(r(&n.ni, reg::PORT_ERROR) & !0xff00, 0); +} + +#[test] +fn the_routing_tables_hold_four_bits_and_a_fresh_entry_routes_nothing() { + let n = node(3, 0); + w(&n.ni, reg::STATUS, 0x23 << 8); + for i in 0..32 { + assert_eq!(r(&n.ni, reg::META_TABLE + i * 8), 0); + } + for i in 0..16 { + assert_eq!(r(&n.ni, reg::LOCAL_TABLE + i * 8), 0); + } + // An entry not written since reset routes nothing, even though it reads zero. + assert_eq!(n.ni.first_turn(0x25), None); + assert_eq!(n.ni.first_turn(0x41), None); + // A destination in this Hub's own group of sixteen uses the local entry + // for its low four bits; any other uses the meta entry for its high bits. + w(&n.ni, reg::LOCAL_TABLE + 8 * 5, 0xfe); + w(&n.ni, reg::META_TABLE + 8 * 4, 0x7); + assert_eq!((r(&n.ni, reg::LOCAL_TABLE + 8 * 5), r(&n.ni, reg::META_TABLE + 8 * 4)), (0xe, 7)); + assert_eq!(n.ni.first_turn(0x25), Some(0xe)); + assert_eq!(n.ni.first_turn(0x41), Some(7)); + assert_eq!(n.ni.first_turn(0x26), None); + assert_eq!(n.ni.first_turn(256), None); + // A written route tells the machine; reset forgets it. + let moved = n.moves.0.load(Ordering::SeqCst); + w(&n.ni, reg::META_TABLE, 1); + assert!(n.moves.0.load(Ordering::SeqCst) > moved); + n.ni.power_on(); + assert_eq!(n.ni.first_turn(0x41), None); +} + +#[test] +fn the_scratch_ageing_and_vector_registers_store_what_is_written() { + let n = node(3, 0); + w(&n.ni, reg::SCRATCH_0, 0x1111_2222_3333_4444); + w(&n.ni, reg::SCRATCH_1, !0); + assert_eq!((r(&n.ni, reg::SCRATCH_0), r(&n.ni, reg::SCRATCH_1)), (0x1111_2222_3333_4444, !0)); + for i in 0..8 { + assert_eq!(r(&n.ni, reg::AGE_BASE + i * 8), 0); + } + w(&n.ni, reg::AGE_BASE, !0); + assert_eq!(r(&n.ni, reg::AGE_BASE), 0xfff); + for i in 1..8 { + w(&n.ni, reg::AGE_BASE + i * 8, i); + assert_eq!(r(&n.ni, reg::AGE_BASE + i * 8), i); + } + for off in [reg::VECTOR, reg::VECTOR_DATA, reg::RETURN_VECTOR, reg::VECTOR_READ_DATA] { + w(&n.ni, off, 0xa5a5_a5a5_a5a5_a5a5); + assert_eq!(r(&n.ni, off), 0xa5a5_a5a5_a5a5_a5a5); + } + n.ni.power_on(); + assert_eq!((r(&n.ni, reg::SCRATCH_0), r(&n.ni, reg::AGE_BASE), r(&n.ni, reg::VECTOR)), (0, 0, 0)); + // Unimplemented offsets read zero. + w(&n.ni, reg::STATUS + 0x1f8, 3); + assert_eq!(r(&n.ni, reg::STATUS + 0x1f8), 0); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 332f04026..0fd53a75c 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -63,6 +63,7 @@ pub mod z85c30; pub mod hub; pub mod hub_io; pub mod hub_md; +pub mod hub_ni; pub mod hub_pi; pub mod dp83843; pub mod ics1890; From 0ae9a391c3cc2c82847662752b026baa2930e980 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:02:41 -0700 Subject: [PATCH 18/38] hub: add the register window and the node's address decode The sections only become a Hub behind one 8 MiB window. Doubleword accesses dispatch by section, a 32-bit access names half of the aligned doubleword as on a big-endian bus, and the I2C controller and bring-up UART answer narrow accesses over the memory block. A node's address decode is rebuilt when something programs it, never tested per access. The PROM numbers the nodes after reset, so each Hub keeps a table from node number to the Hub that answers it (itself first, then the neighbour across the cable). The claim, processor memory and I/O views read that table, and the cached alias is a size compare. For the Bridge the Hub offers the minimum: an inbound XIO face for DMA and interrupt-modify writes, and an interrupt target whose lines are interrupt numbers. Failed processor reads are recorded through a wrapper over the downstream I/O space. The observer reports the stores a processor rendezvous would watch without changing any value. --- src/dev/hub.rs | 857 ++++++++++++++++++++++++++++++++++++++++++- src/dev/hub/tests.rs | 774 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1628 insertions(+), 3 deletions(-) create mode 100644 src/dev/hub/tests.rs diff --git a/src/dev/hub.rs b/src/dev/hub.rs index e2691e9be..d89d05eb0 100644 --- a/src/dev/hub.rs +++ b/src/dev/hub.rs @@ -2,10 +2,20 @@ //! Onyx2): one per node, joining the node's processors, memory and directory, //! one XIO port and one CrayLink port. The register window is split by //! section into `hub_pi`, `hub_md`, `hub_io` and `hub_ni`, as the Bedrock -//! files are. +//! files are; this module is the window itself and what the sections share. //! -//! This module holds what the sections share: the machine-facing hooks and -//! the observer of the stores a processor rendezvous depends on. +//! Address decode is data, not a flag tested per access. Each Hub keeps a +//! table from node number to the Hub that answers it, rebuilt when a node +//! number or the cable changes (the PROM numbers the nodes after reset); +//! the big windows are decoded by their translation entries and the cached +//! alias by its size. A machine places the views [`Hub::claim`], +//! [`Hub::cpu_memory`] and [`Hub::io_space`] in its address map. +//! +//! The Bridge reaches the Hub through [`Hub::inbound`] for DMA and +//! interrupt-modify writes, and [`Hub::interrupt_target`] for a controller +//! whose lines are interrupt numbers. Both are the minimum a Bridge needs +//! and are meant to be replaced by whatever shared host interface the +//! widgets settle on. //! //! # What is approximated here //! @@ -14,6 +24,13 @@ //! chip, and no Hub specification is public: almost every register behaviour //! is a software expectation or an approximation. The sections name theirs. //! +//! - A Hub whose module is powered down is the machine's to express: it +//! unmaps the register window. The network port reads disabled at the peer. +//! - Failing, rather than timing out, an access to a node number no reachable +//! Hub holds is an approximation. +//! - A processor rendezvous hold is not modelled. [`HubObserver`] reports the +//! stores such a mechanism would watch, and changes no value. +//! //! Ported from hw/misc/sgi-hub.c in jamesbraid/qemu-sgi-origami. /// Length of the register window: 8 MiB. @@ -78,3 +95,837 @@ pub trait HubHooks: Send + Sync { /// NI_PORT_RESET bit 0: restart the whole machine. fn machine_restart(&self) {} } + +use crate::dev::heart::Clock; +use crate::dev::hub_io::{self, HubIo, NodeFabric}; +use crate::dev::hub_md::{self, HubMd, SlotSignals}; +use crate::dev::hub_ni::{self, HubNi, NetPeer, NiEvents}; +use crate::dev::hub_pi::{self, HubPi}; +use crate::devlog::LogModule; +use crate::irq::InterruptController; +use crate::physical::{read_bytes, write_bytes}; +use crate::traits::{ + BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Device, Resettable, BUS_ERR, BUS_OK, +}; +use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; + +/// What differs between Hubs: the processors the node populates, where its +/// slice A sits in the machine's numbering, the preprogrammed node number and +/// the revision fields. The widget identification and network status carry +/// separate revisions, and the model sets them separately. +#[derive(Clone, Copy, Debug)] +pub struct HubConfig { + /// Populated processor sockets: 1 or 2. + pub cpus: u8, + /// Global index of this node's slice A, which PI_CPU_NUM subtracts. + pub cpu_base: usize, + /// The node number a synthesized firmware preprograms; zero otherwise. + pub nasid: u16, + /// NI_STATUS_REV_ID bits 7:4. The model reports 3 (Hub 2.1) on the + /// Origin 200 and 5 (Hub 2.3) on the Origin 2000 rack and deskside and the + /// Onyx2. + pub ni_revision: u8, + /// Widget identification bits 31:28. No capture of the field is known, so + /// 1 is an approximation. + pub widget_revision: u8, + /// The 11-bit XIO manufacturer: 0x36. + pub manufacturer: u16, + /// What the board drives on the slot-identity signals. + pub slot: SlotSignals, +} +impl Default for HubConfig { + fn default() -> Self { + Self { cpus: 1, cpu_base: 0, nasid: 0, ni_revision: 3, widget_revision: 1, manufacturer: 0x36, slot: SlotSignals::default() } + } +} + +/// The four uncached address spaces a node-qualified processor address +/// selects, in the order of their attribute. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Space { + /// Node memory, cached or uncached. + Memory = 0, + /// The reader's own Hub-special resources. + HubSpecial = 1, + /// Widgets and Hub registers. + Io = 2, + /// Fetch-and-op memory. + Special = 3, +} +const SPACES: usize = 4; + +/// Node numbers are bits 39:32 of a physical address in M mode, so each node +/// owns a 4 GiB window. +pub const NODE_SHIFT: u32 = 32; +const NODE_MASK: u64 = 0xff; +const NODE_SPAN: u64 = 1 << NODE_SHIFT; + +/// Offsets in the I/O space of a node, from the base of its address range. +pub const IO_REGISTERS_LOCAL: u64 = 0x0100_0000; +pub const IO_REGISTERS_NAMED: u64 = 0x0180_0000; +pub const IO_BIG_WINDOWS: u64 = 0x2000_0000; +/// A doubleword read here fails at the issuing Hub whatever node it names. +pub const IO_BUS_ERROR_PROBE: u64 = 0x0240_0208; + +/// Where in the XIO address a packet is taken as an interrupt-modify write: +/// bit 47 selects the I/O view, the node number is bits 45:38. +const XIO_IO_VIEW: u64 = 1 << 47; +const XIO_NODE_SHIFT: u32 = 38; +const XIO_MEMORY_SIZE: u64 = 1 << 40; +const XIO_INT_MODIFY: u64 = IO_REGISTERS_NAMED + hub_pi::reg::INT_PEND_MOD; +const XIO_IDENTITY: u64 = 0x0160_0000; + +// A slot of the claim map: which Hub answers a node number. +const NOBODY: u8 = 0; +const THIS_HUB: u8 = 1; +const PEER_HUB: u8 = 2; + +/// The shell's end of the hooks: it keeps the cached-alias limit current, and +/// passes everything on to the machine's. +struct Hooks { + hub: Weak, + machine: OnceLock>, +} +impl HubHooks for Hooks { + fn cpu_enable(&self, slice: usize, enabled: bool) { + if let Some(h) = self.machine.get() { + h.cpu_enable(slice, enabled); + } + } + fn nmi(&self, slice: usize) { + if let Some(h) = self.machine.get() { + h.nmi(slice); + } + } + fn soft_reset(&self, slice: usize) { + if let Some(h) = self.machine.get() { + h.soft_reset(slice); + } + } + fn cached_alias_changed(&self, bytes: u64) { + if let Some(hub) = self.hub.upgrade() { + hub.calias.store(bytes, Ordering::Release); + } + if let Some(h) = self.machine.get() { + h.cached_alias_changed(bytes); + } + } + fn node_number_changed(&self, number: u16) { + if let Some(h) = self.machine.get() { + h.node_number_changed(number); + } + } + fn machine_restart(&self) { + if let Some(h) = self.machine.get() { + h.machine_restart(); + } + } +} + +/// The network section tells the shell when the claim map must move. +struct Remap(Weak); +impl NiEvents for Remap { + fn changed(&self) { + if let Some(hub) = self.0.upgrade() { + hub.remap(); + } + } +} + +/// What another Hub needs of this node: its spaces and its interrupt +/// banks. It holds no reference back to the Hub, so a cable can share it. +struct NodeView { + spaces: [OnceLock>; SPACES], + pi: Arc, +} + +/// The register window's sections. Kept apart from the Hub's address decode +/// so the node's I/O space can hold the registers without holding the Hub: +/// the Hub owns the spaces, and a space that held the Hub would be a cycle +/// that frees neither. +pub struct Regs { + pi: Arc, + md: HubMd, + io: HubIo, + ni: Arc, + /// The node's I2C controller and bring-up UART, mapped over the MD block. + i2c: OnceLock>, + uart: OnceLock>, +} + +impl Regs { + fn section_read(&self, off: u64, clear: bool) -> u64 { + match off { + ..hub_pi::END => self.pi.read(off, clear), + hub_md::START..hub_md::END => self.md.read(off), + hub_io::START..hub_io::END => self.io.read(off), + hub_ni::START..hub_ni::END => self.ni.read(off, clear), + _ => { + crate::dlog!(LogModule::Hub, "Hub: read of unimplemented register {off:#x}"); + 0 + } + } + } + + fn section_peek(&self, off: u64) -> Option { + match off { + ..hub_pi::END => self.pi.peek(off), + hub_md::START..hub_md::END => self.md.peek(off), + hub_io::START..hub_io::END => self.io.peek(off), + hub_ni::START..hub_ni::END => self.ni.peek(off), + _ => None, + } + } + + fn section_write(&self, off: u64, value: u64, size: u32) { + match off { + ..hub_pi::END => self.pi.write_sized(off, value, size), + hub_md::START..hub_md::END => self.md.write(off, value), + hub_io::START..hub_io::END => self.io.write(off, value), + hub_ni::START..hub_ni::END => self.ni.write(off, value), + _ => crate::dlog!(LogModule::Hub, "Hub: write of {value:#x} to unimplemented register {off:#x}"), + } + } + + /// The narrow device over an offset, for the I2C controller and UART + /// mapped over the MD block. + fn overlay(&self, off: u64) -> Option<(&dyn BusDevice, u64)> { + const I2C: std::ops::Range = hub_md::reg::I2C_BASE..hub_md::reg::I2C_BASE + 0x10; + const UART: std::ops::Range = hub_md::reg::UART_BASE..hub_md::reg::UART_END; + if I2C.contains(&off) { + self.i2c.get().map(|d| (&**d, off - I2C.start)) + } else if UART.contains(&off) { + self.uart.get().map(|d| (&**d, off - UART.start)) + } else { + None + } + } +} + +pub struct Hub { + regs: Arc, + /// What answers each space of this node, addressed from the node's base. + view: Arc, + /// Which Hub answers each node number, derived from the programmed node + /// numbers when one changes, never from a flag tested per access. + claims: [AtomicU8; 256], + /// The Hub across the CrayLink cable, as its spaces and interrupts. Written + /// once when the cable is fitted, so an access to it takes no lock and + /// touches no reference count. + peer: OnceLock>, + /// Bytes the cached alias spans: the processor view's node-zero memory + /// below it stays on this node. + calias: AtomicU64, + hooks: Arc, + weak: Weak, +} + +impl std::ops::Deref for Hub { + type Target = Regs; + fn deref(&self) -> &Regs { + &self.regs + } +} + +impl Hub { + pub fn new(cfg: HubConfig, clock: Clock) -> Arc { + let hub = Arc::new_cyclic(|weak: &Weak| { + let hooks = Arc::new(Hooks { hub: weak.clone(), machine: OnceLock::new() }); + let pi = HubPi::new(cfg.cpus, cfg.cpu_base, clock.clone()); + let view = Arc::new(NodeView { spaces: Default::default(), pi: pi.clone() }); + let ni = Arc::new(HubNi::new(cfg.ni_revision, cfg.nasid)); + pi.set_hooks(hooks.clone()); + ni.set_hooks(hooks.clone()); + ni.set_events(Arc::new(Remap(weak.clone()))); + let io = HubIo::new(cfg.widget_revision, cfg.manufacturer); + io.set_fabric(weak.clone() as Weak); + let regs = Arc::new(Regs { + pi, + md: HubMd::new(clock, cfg.slot), + io, + ni, + i2c: OnceLock::new(), + uart: OnceLock::new(), + }); + Self { + regs, + view, + claims: std::array::from_fn(|_| AtomicU8::new(NOBODY)), + peer: OnceLock::new(), + calias: AtomicU64::new(hub_pi::calias_bytes(15)), + hooks, + weak: weak.clone(), + } + }); + hub.remap(); + hub + } + + pub fn pi(&self) -> &Arc { + &self.pi + } + pub fn md(&self) -> &HubMd { + &self.md + } + pub fn io(&self) -> &HubIo { + &self.io + } + pub fn ni(&self) -> &Arc { + &self.ni + } + + /// The machine's side of the node: processors, the cached alias and + /// restart. + pub fn set_hooks(&self, hooks: Arc) { + let _ = self.hooks.machine.set(hooks); + } + + /// Register the sections' state locks with the `locks` monitor command. + pub fn register_locks(self: &Arc) { + crate::locks::register_mutex_fn("hub-pi", self, |h| h.pi.is_locked()); + crate::locks::register_mutex_fn("hub-md", self, |h| h.md.is_locked()); + crate::locks::register_mutex_fn("hub-io", self, |h| h.io.is_locked()); + crate::locks::register_mutex_fn("hub-ni", self, |h| h.ni.is_locked()); + } + + /// Observe the stores a processor rendezvous depends on. Connected by the + /// board; with none connected the stores cost an atomic load. + pub fn set_observer(&self, observer: Arc) { + self.pi.set_observer(observer.clone()); + self.io.set_observer(observer.clone()); + self.ni.set_observer(observer); + } + + /// Where this node's four spaces answer, for the node-qualified claims of + /// every Hub that reaches this one and for its own engines. + pub fn set_space(&self, space: Space, dev: Arc) { + self.view.spaces[space as usize].set(dev).unwrap_or_else(|_| panic!("{space:?} is wired twice")); + } + + /// The node's I2C controller, answering at 0x22_0000 of the window. + pub fn attach_i2c(&self, dev: Arc) { + self.i2c.set(dev).unwrap_or_else(|_| panic!("the I2C controller is wired twice")); + } + + /// The bring-up UART, answering at 0x22_00c0 of the window. + pub fn attach_uart(&self, dev: Arc) { + self.uart.set(dev).unwrap_or_else(|_| panic!("the bring-up UART is wired twice")); + } + + /// Join the Hubs across a direct CrayLink cable, as on a two-module + /// Origin 200. + pub fn cable(a: &Arc, b: &Arc) { + assert!(a.peer.set(b.view.clone()).is_ok() && b.peer.set(a.view.clone()).is_ok(), "a Hub is cabled once"); + a.ni.connect_peer(Some(Arc::downgrade(&b.ni) as Weak)); + b.ni.connect_peer(Some(Arc::downgrade(&a.ni) as Weak)); + } + + /// Re-derive which Hub answers each node number. This Hub always answers + /// for its own first: while two Hubs both hold the reset value zero, each + /// keeps zero for itself. A number no reachable Hub holds fails. + pub fn remap(&self) { + let own = self.ni.number(); + let peer = self.ni.neighbour_number(); + for number in 0..256u16 { + let slot = if number == own { + THIS_HUB + } else if peer == Some(number) { + PEER_HUB + } else { + NOBODY + }; + self.claims[usize::from(number)].store(slot, Ordering::Release); + } + } + + /// Run `f` on the Hub the node number names. The Hub is itself in the + /// common case, and the peer is looked up only across the cable. + fn with_node(&self, number: u8, f: impl FnOnce(&NodeView) -> R) -> Option { + match self.claims[usize::from(number)].load(Ordering::Acquire) { + THIS_HUB => Some(f(&self.view)), + PEER_HUB => self.peer.get().map(|peer| f(peer)), + _ => None, + } + } + + /// A node-qualified address of `space`, claimed by the Hub holding its + /// number. Addresses are the 40-bit physical address with the node in bits + /// 39:32. + pub fn claim(self: &Arc, space: Space) -> Arc { + Arc::new(View { hub: self.clone(), space, local: Local::Never }) + } + + /// The processor's view of memory: a node-zero address below the cached + /// alias size stays on this node and any other goes to the claim map. + pub fn cpu_memory(self: &Arc) -> Arc { + Arc::new(View { hub: self.clone(), space: Space::Memory, local: Local::Calias }) + } + + /// The processor's view of the Hub-special space: always this node's own. + pub fn cpu_special(self: &Arc) -> Arc { + Arc::new(View { hub: self.clone(), space: Space::HubSpecial, local: Local::Always }) + } + + /// This node's I/O space, addressed from the node's base: the crossbar + /// widgets' small windows, the Hub's own registers at both offsets, and + /// the seven big windows. + pub fn io_space(self: &Arc) -> Arc { + Arc::new(IoSpace { regs: self.regs.clone(), small: self.io.small_windows(), big: self.io.big_windows() }) + } + + /// The Hub as a bus master's target for interrupts: a line is the + /// interrupt number 0 to 127, set and cleared through interrupt-modify. A + /// Bridge that reaches the Hub by an XIO write is equivalent. + pub fn interrupt_target(&self) -> Arc { + Arc::new(InterruptTarget(self.pi.clone())) + } + + /// The XIO port's packet face: what a widget's bus-master requests reach, + /// a memory address below 2^40, an interrupt-modify write or the + /// identity probe. + pub fn inbound(&self) -> Arc { + Arc::new(Inbound(self.weak.clone())) + } + + /// A peer's programmed I/O to the Hub's own widget number. + pub fn widget_face(&self) -> Arc { + Arc::new(WidgetFace(self.weak.clone())) + } + + /// Wrap the downstream I/O space a processor reaches through this Hub, so + /// a failed read records in that slice's error status. + pub fn processor_io(self: &Arc, downstream: Arc) -> Arc { + Arc::new(ProcessorIo { hub: self.clone(), downstream }) + } + + /// Record a failed processor read in slice `slice`'s error status. + pub fn read_error(&self, slice: usize, physical: u64) { + self.pi.read_error(slice, physical); + } + + /// Whether the node is powered down is the machine's to express: it + /// unmaps the window, and the network port reads down at the peer. + pub fn set_powered(&self, on: bool) { + self.ni.set_enabled(on); + } + +} + +/// How a view treats a node-zero address. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Local { + /// Every address is claimed by its node number. + Never, + /// Node-zero addresses below the cached-alias size stay on this node. + Calias, + /// Node-zero addresses are always this node's own. + Always, +} + +/// A node-qualified address space as a processor, an engine or a widget sees +/// it: the node number picks the Hub, and the rest the address within it. +struct View { + hub: Arc, + space: Space, + local: Local, +} +impl View { + fn run(&self, a: u64, f: impl FnOnce(&dyn BusDevice, u64) -> R) -> Option { + let offset = a & (NODE_SPAN - 1); + let number = ((a >> NODE_SHIFT) & NODE_MASK) as u8; + let stays = number == 0 + && match self.local { + Local::Never => false, + Local::Calias => a < self.hub.calias.load(Ordering::Acquire), + Local::Always => true, + }; + if stays { + return self.hub.view.spaces[self.space as usize].get().map(|d| f(&**d, offset)); + } + self.hub.with_node(number, |node| node.spaces[self.space as usize].get().map(|d| f(&**d, offset))).flatten() + } + fn unclaimed(&self, a: u64, write: bool) { + crate::dlog!(LogModule::Hub, "Hub: {} of {:?} {a:#x}: no Hub holds node {} (or its space is unwired)", + if write { "write" } else { "read" }, self.space, (a >> NODE_SHIFT) & NODE_MASK); + } +} +macro_rules! view_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + self.run(a, |d, at| d.$r(at)).unwrap_or_else(|| { + self.unclaimed(a, false); + $out::err() + }) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.run(a, |d, at| d.$w(at, v)).unwrap_or_else(|| { + self.unclaimed(a, true); + BUS_ERR + }) + } + }; +} +impl BusDevice for View { + view_access!(read8, write8, u8, BusRead8); + view_access!(read16, write16, u16, BusRead16); + view_access!(read32, write32, u32, BusRead32); + view_access!(read64, write64, u64, BusRead64); +} + +/// The node's I/O space. +struct IoSpace { + regs: Arc, + small: Arc, + big: Arc, +} +impl IoSpace { + /// The register window offset a Hub-register address names. + fn registers(a: u64) -> Option { + let off = a.checked_sub(IO_REGISTERS_LOCAL)? & !(1 << 23); + (a - IO_REGISTERS_LOCAL < 2 * WINDOW_SIZE).then_some(off & (WINDOW_SIZE - 1)) + } +} +macro_rules! io_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + if let Some(off) = Self::registers(a) { + self.regs.$r(off) + } else if a >= IO_BIG_WINDOWS { + self.big.$r(a - IO_BIG_WINDOWS) + } else { + self.small.$r(a) + } + } + fn $w(&self, a: u64, v: $t) -> u32 { + if let Some(off) = Self::registers(a) { + self.regs.$w(off, v) + } else if a >= IO_BIG_WINDOWS { + self.big.$w(a - IO_BIG_WINDOWS, v) + } else { + self.small.$w(a, v) + } + } + }; +} +impl BusDevice for IoSpace { + io_access!(read8, write8, u8, BusRead8); + io_access!(read16, write16, u16, BusRead16); + io_access!(read32, write32, u32, BusRead32); + io_access!(read64, write64, u64, BusRead64); +} + +/// Every I/O-space request a processor makes passes through its Hub. A read +/// that fails there, or downstream, is recorded in the issuing slice's error +/// status 0; a debugger's access or a device's DMA does not come this way. +struct ProcessorIo { + hub: Arc, + downstream: Arc, +} +impl ProcessorIo { + /// Record a failed read for the issuing slice. A debugger's access, or + /// any thread that is not one of the node's processors, records nothing. + fn failed(&self, a: u64) { + if let Some(slice) = self.hub.pi.reader_slice() { + self.hub.read_error(slice, a); + } + } +} +macro_rules! processor_read { + ($r:ident, $out:ident) => { + fn $r(&self, a: u64) -> $out { + let r = self.downstream.$r(a); + if r.status != BUS_OK { + self.failed(a); + } + r + } + }; +} +impl BusDevice for ProcessorIo { + processor_read!(read8, BusRead8); + processor_read!(read16, BusRead16); + processor_read!(read32, BusRead32); + fn read64(&self, a: u64) -> BusRead64 { + // The Hub fails one address itself: a doubleword read at node offset + // 0x0240_0208 (IIO_IIBUSERR, "reads here cause a bus error"), whatever + // node it names. Nothing goes out on the XIO link. + let r = if a & (NODE_SPAN - 1) == IO_BUS_ERROR_PROBE { BusRead64::err() } else { self.downstream.read64(a) }; + if r.status != BUS_OK { + self.failed(a); + } + r + } + fn write8(&self, a: u64, v: u8) -> u32 { + self.downstream.write8(a, v) + } + fn write16(&self, a: u64, v: u16) -> u32 { + self.downstream.write16(a, v) + } + fn write32(&self, a: u64, v: u32) -> u32 { + self.downstream.write32(a, v) + } + fn write64(&self, a: u64, v: u64) -> u32 { + self.downstream.write64(a, v) + } +} + +/// A line is an interrupt number; a rising level sets its pending bit and a +/// falling one clears it. +struct InterruptTarget(Arc); +impl InterruptController for InterruptTarget { + fn set_line(&self, line: u32, level: bool) { + if line < 128 { + self.0.int_modify(u64::from(line) | if level { hub_pi::MODIFY_SET } else { 0 }); + } + } +} + +/// The XIO packet face: a 48-bit address, bit 47 selecting the I/O view. +struct Inbound(Weak); +impl Inbound { + fn memory(hub: &Hub, a: u64, size: usize, value: Option) -> BusRead64 { + if a >= XIO_MEMORY_SIZE || size as u64 > XIO_MEMORY_SIZE - a { + return BusRead64::err(); + } + let mut bytes = [0u8; 8]; + match value { + Some(v) => { + bytes[..size].copy_from_slice(&v.to_be_bytes()[8 - size..]); + BusRead64 { status: if hub.write(a, &bytes[..size]) { BUS_OK } else { BUS_ERR }, data: 0 } + } + None => { + if !hub.read(a, &mut bytes[..size]) { + return BusRead64::err(); + } + let mut word = [0u8; 8]; + word[8 - size..].copy_from_slice(&bytes[..size]); + BusRead64::ok(u64::from_be_bytes(word)) + } + } + } + fn access(&self, a: u64, size: usize, value: Option) -> BusRead64 { + let Some(hub) = self.0.upgrade() else { return BusRead64::err() }; + if a & XIO_IO_VIEW == 0 { + return Self::memory(&hub, a, size, value); + } + let number = ((a >> XIO_NODE_SHIFT) & NODE_MASK) as u16; + let offset = a & ((1 << XIO_NODE_SHIFT) - 1); + match value { + // A peer's identity probe: node field zero, the status register. + None if size == 8 && a == (XIO_IO_VIEW | XIO_IDENTITY) => BusRead64::ok(hub.ni.read(hub_ni::reg::STATUS, false)), + // A device interrupt: a doubleword write to a node's + // interrupt-modify register lands on that node's Hub. + Some(v) if size == 8 && offset == XIO_INT_MODIFY => { + let reached = NodeFabric::interrupt(&*hub, number, v); + BusRead64 { status: if reached { BUS_OK } else { BUS_ERR }, data: 0 } + } + _ => { + crate::dlog!(LogModule::Hub, "Hub: unsupported inbound XIO {} at {a:#x} size {size}", + if value.is_some() { "write" } else { "read" }); + BusRead64::err() + } + } + } +} +macro_rules! inbound_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + let r = self.access(a, std::mem::size_of::<$t>(), None); + $out { status: r.status, data: r.data as $t } + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.access(a, std::mem::size_of::<$t>(), Some(u64::from(v))).status + } + }; +} +impl BusDevice for Inbound { + inbound_access!(read8, write8, u8, BusRead8); + inbound_access!(read16, write16, u16, BusRead16); + inbound_access!(read32, write32, u32, BusRead32); + inbound_access!(read64, write64, u64, BusRead64); +} + +/// A peer's programmed I/O to this Hub's widget number answers only the +/// 4-byte read of the identification word at offset 4. +struct WidgetFace(Weak); +impl BusDevice for WidgetFace { + fn read32(&self, a: u64) -> BusRead32 { + match self.0.upgrade().and_then(|hub| hub.io.widget_face_read32(a)) { + Some(v) => BusRead32::ok(v), + None => { + crate::dlog!(LogModule::Hub, "Hub: unsupported peer read at {a:#x} on the Hub's widget face"); + BusRead32::err() + } + } + } + fn write32(&self, a: u64, v: u32) -> u32 { + crate::dlog!(LogModule::Hub, "Hub: unsupported peer write of {v:#x} at {a:#x} on the Hub's widget face"); + BUS_ERR + } +} + +impl NodeFabric for Hub { + fn read(&self, address: u64, buf: &mut [u8]) -> bool { + let number = ((address >> NODE_SHIFT) & NODE_MASK) as u8; + let offset = address & (NODE_SPAN - 1); + self.with_node(number, |node| { + node.spaces[Space::Memory as usize].get().is_some_and(|mem| read_bytes(&**mem, offset, buf) == BUS_OK) + }) + .unwrap_or(false) + } + fn write(&self, address: u64, data: &[u8]) -> bool { + let number = ((address >> NODE_SHIFT) & NODE_MASK) as u8; + let offset = address & (NODE_SPAN - 1); + self.with_node(number, |node| { + node.spaces[Space::Memory as usize].get().is_some_and(|mem| write_bytes(&**mem, offset, data) == BUS_OK) + }) + .unwrap_or(false) + } + fn interrupt(&self, number: u16, value: u64) -> bool { + number < 256 + && self + .with_node(number as u8, |node| node.pi.int_modify(value)) + .is_some() + } +} + +/// The register window. The I2C controller and the bring-up UART take every +/// width, one register access each: the stock PROM polls the controller's +/// status with 8-byte loads. Elsewhere it takes 4- and 8-byte accesses. A 32-bit +/// access names half of the aligned doubleword, offset 0 carrying bits 63:32 +/// as on a big-endian bus. A 32-bit write replaces its half and takes the +/// ordinary write path; for the interrupt-modify register, the port error +/// register and any register that stores nothing the other half is zero. A +/// 32-bit read of a read-to-clear register returns its half without clearing +/// it: what hardware does for the narrower access is unknown, and this keeps +/// a latch until software has read all of it. +impl BusDevice for Regs { + fn read64(&self, a: u64) -> BusRead64 { + if a & 7 != 0 || a >= WINDOW_SIZE { + return BusRead64::err(); + } + if let Some((dev, at)) = self.overlay(a) { + return dev.read64(at); + } + BusRead64::ok(self.section_read(a, true)) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a & 7 != 0 || a >= WINDOW_SIZE { + return BUS_ERR; + } + if let Some((dev, at)) = self.overlay(a) { + return dev.write64(at, v); + } + self.section_write(a, v, 8); + BUS_OK + } + fn read32(&self, a: u64) -> BusRead32 { + if a & 3 != 0 || a >= WINDOW_SIZE { + return BusRead32::err(); + } + if let Some((dev, at)) = self.overlay(a & !7) { + return dev.read32(at + (a & 7)); + } + let value = self.section_read(a & !7, false); + BusRead32::ok(if a & 4 != 0 { value as u32 } else { (value >> 32) as u32 }) + } + fn write32(&self, a: u64, v: u32) -> u32 { + if a & 3 != 0 || a >= WINDOW_SIZE { + return BUS_ERR; + } + if let Some((dev, at)) = self.overlay(a & !7) { + return dev.write32(at + (a & 7), v); + } + let aligned = a & !7; + let mut merged = if matches!(aligned, hub_pi::reg::INT_PEND_MOD | hub_ni::reg::PORT_ERROR) { + 0 + } else { + self.section_peek(aligned).unwrap_or(0) + }; + merged = if a & 4 != 0 { + (merged & !0xffff_ffff) | u64::from(v) + } else { + (merged & 0xffff_ffff) | (u64::from(v) << 32) + }; + self.section_write(aligned, merged, 4); + BUS_OK + } + fn read8(&self, a: u64) -> BusRead8 { + match self.overlay(a & !7) { + Some((dev, at)) => dev.read8(at + (a & 7)), + None => BusRead8::err(), + } + } + fn write8(&self, a: u64, v: u8) -> u32 { + match self.overlay(a & !7) { + Some((dev, at)) => dev.write8(at + (a & 7), v), + None => BUS_ERR, + } + } + fn read16(&self, a: u64) -> BusRead16 { + match self.overlay(a & !7) { + Some((dev, at)) => dev.read16(at + (a & 7)), + None => BusRead16::err(), + } + } + fn write16(&self, a: u64, v: u16) -> u32 { + match self.overlay(a & !7) { + Some((dev, at)) => dev.write16(at + (a & 7), v), + None => BUS_ERR, + } + } +} + +macro_rules! delegate { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + self.regs.$r(a) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.regs.$w(a, v) + } + }; +} +/// The Hub as the register window a machine maps. +impl BusDevice for Hub { + delegate!(read8, write8, u8, BusRead8); + delegate!(read16, write16, u16, BusRead16); + delegate!(read32, write32, u32, BusRead32); + delegate!(read64, write64, u64, BusRead64); +} + +impl Resettable for Hub { + /// Every register returns to its reset value and the claim map re-derives + /// from the node numbers. The error stack addresses, the memory behind the + /// node and the one-wire part's contents survive. + fn power_on(&self) { + self.pi.power_on(); + self.md.power_on(); + self.io.power_on(); + self.ni.power_on(); + self.calias.store(self.pi.calias_bytes(), Ordering::Release); + self.remap(); + } +} + +impl Device for Hub { + fn step(&self, _cycles: u64) { + self.pi.poll(); + } + fn stop(&self) { + self.pi.stop(); + } + fn start(&self) { + self.pi.start(); + } + fn is_running(&self) -> bool { + self.pi.is_running() + } + fn get_clock(&self) -> u64 { + self.pi.rt_count() + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/hub/tests.rs b/src/dev/hub/tests.rs new file mode 100644 index 000000000..803ec4770 --- /dev/null +++ b/src/dev/hub/tests.rs @@ -0,0 +1,774 @@ +// Cases follow tests/qtest/sn0-hub-test.c, sn0-hub-inbound-test.c, +// sn0-pi-error-test.c and sn0-widget-decode-test.c in +// jamesbraid/qemu-sgi-origami: 32-bit-access-selects-the-big-endian-half, +// 32-bit-vector-clear-read-leaves-the-status, the junk-uart and I2C overlay +// cases, stock-prom-hubs-start-unnumbered, +// qualified-window-follows-the-programmed-number, unclaimed-node-number-fails, +// qualified-memory-follows-the-programmed-number, the calias-* cases, +// hub-special-window-stays-reader-local, the bte-routing and +// bte-physical-node-ownership cases, the inbound XIO cases, both-windows, +// prom-probe-latches-issuing-slice, narrow-access-matches-bridge-alias and +// status1-clear-alias-retires-record. + +use super::*; +use parking_lot::Mutex; +use std::sync::atomic::AtomicBool; + +/// A node's memory: big-endian bytes, failing past its end. +struct Ram(Mutex>); +impl Ram { + fn new(size: usize, seed: u8) -> Arc { + Arc::new(Self(Mutex::new((0..size).map(|i| seed ^ (i as u8)).collect()))) + } +} +macro_rules! ram_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + let m = self.0.lock(); + const N: usize = std::mem::size_of::<$t>(); + match m.get(a as usize..a as usize + N) { + Some(b) => $out::ok(<$t>::from_be_bytes(b.try_into().unwrap())), + None => $out::err(), + } + } + fn $w(&self, a: u64, v: $t) -> u32 { + let mut m = self.0.lock(); + const N: usize = std::mem::size_of::<$t>(); + match m.get_mut(a as usize..a as usize + N) { + Some(b) => { + b.copy_from_slice(&v.to_be_bytes()); + BUS_OK + } + None => BUS_ERR, + } + } + }; +} +impl BusDevice for Ram { + ram_access!(read8, write8, u8, BusRead8); + ram_access!(read16, write16, u16, BusRead16); + ram_access!(read32, write32, u32, BusRead32); + ram_access!(read64, write64, u64, BusRead64); +} + +/// A few byte registers, standing in for the I2C controller and the UART. +struct Bytes(Mutex<[u8; 64]>); +impl Default for Bytes { + fn default() -> Self { + Self(Mutex::new([0; 64])) + } +} +impl BusDevice for Bytes { + fn read64(&self, a: u64) -> BusRead64 { + BusRead64::ok(u64::from(self.0.lock()[a as usize])) + } + fn write64(&self, a: u64, v: u64) -> u32 { + self.0.lock()[a as usize] = v as u8; + BUS_OK + } + fn read8(&self, a: u64) -> BusRead8 { + BusRead8::ok(self.0.lock()[a as usize]) + } + fn write8(&self, a: u64, v: u8) -> u32 { + self.0.lock()[a as usize] = v; + BUS_OK + } +} + +#[derive(Default)] +struct Machine(Mutex>); +impl HubHooks for Machine { + fn cpu_enable(&self, slice: usize, on: bool) { + self.0.lock().push(format!("enable {slice} {on}")); + } + fn nmi(&self, slice: usize) { + self.0.lock().push(format!("nmi {slice}")); + } + fn soft_reset(&self, slice: usize) { + self.0.lock().push(format!("soft {slice}")); + } + fn cached_alias_changed(&self, bytes: u64) { + self.0.lock().push(format!("calias {bytes:#x}")); + } + fn node_number_changed(&self, number: u16) { + self.0.lock().push(format!("number {number}")); + } + fn machine_restart(&self) { + self.0.lock().push("restart".into()); + } +} + +const RAM_SIZE: usize = 1 << 20; + +fn hub(cpus: u8, ni_revision: u8) -> Arc { + let cfg = HubConfig { cpus, ni_revision, ..Default::default() }; + let hub = Hub::new(cfg, Arc::new(|| 0)); + hub.set_space(Space::Memory, Ram::new(RAM_SIZE, 0)); + hub +} +/// Two Hubs on a direct cable, each with a node memory that tells it apart. +fn pair() -> (Arc, Arc) { + let a = hub(2, 3); + let b = Hub::new(HubConfig { cpus: 2, cpu_base: 2, ..Default::default() }, Arc::new(|| 0)); + b.set_space(Space::Memory, Ram::new(RAM_SIZE, 0x80)); + Hub::cable(&a, &b); + (a, b) +} +fn r64(d: &dyn BusDevice, a: u64) -> u64 { + d.read64(a).data +} +const NODE: fn(u64) -> u64 = |n| n << NODE_SHIFT; +fn number(hub: &Hub, n: u64) { + assert_eq!(hub.write64(hub_ni::reg::STATUS, n << 8), BUS_OK); +} + +#[test] +fn a_32_bit_access_selects_the_big_endian_half() { + let hub = hub(1, 3); + let status = r64(&*hub, hub_ni::reg::STATUS); + // The reset value is all in the low word, so a swap would show. + assert_ne!(status, 0); + assert_eq!(u64::from(hub.read32(hub_ni::reg::STATUS).data), status >> 32); + assert_eq!(u64::from(hub.read32(hub_ni::reg::STATUS + 4).data), status & 0xffff_ffff); + // A register with reset bits in both halves separates them sharply. + assert_eq!(hub.read32(hub_md::reg::CONFIG).data, 0x001e_7fbc); + assert_eq!(hub.read32(hub_md::reg::CONFIG + 4).data, 0x3fff_ffff); + // A 32-bit write replaces its own half and leaves the other alone. + hub.write64(hub_ni::reg::SCRATCH_0, 0x1111_2222_3333_4444); + hub.write32(hub_ni::reg::SCRATCH_0, 0xaaaa_bbbb); + assert_eq!(r64(&*hub, hub_ni::reg::SCRATCH_0), 0xaaaa_bbbb_3333_4444); + hub.write32(hub_ni::reg::SCRATCH_0 + 4, 0xcccc_dddd); + assert_eq!(r64(&*hub, hub_ni::reg::SCRATCH_0), 0xaaaa_bbbb_cccc_dddd); + // A half write still passes through the field mask (bits 31:3). + hub.write32(hub_pi::reg::ERR_STACK_ADDR_A + 4, 0xdead_beef); + assert_eq!(r64(&*hub, hub_pi::reg::ERR_STACK_ADDR_A), 0xdead_bee8); + hub.write32(hub_pi::reg::ERR_STACK_ADDR_A, 0xffff_ffff); + assert_eq!(r64(&*hub, hub_pi::reg::ERR_STACK_ADDR_A), 0xdead_bee8); + // Only whole words, and only inside the window. + assert_eq!(hub.read32(2).status, BUS_ERR); + assert_eq!(hub.write32(WINDOW_SIZE, 0), BUS_ERR); + assert_eq!(hub.read64(4).status, BUS_ERR); + assert_eq!(hub.read64(WINDOW_SIZE).status, BUS_ERR); + assert_eq!(hub.read8(0x18).status, BUS_ERR); +} + +/// The vector clear alias reads zero and clears the status, but a 32-bit +/// read must leave the status for the other half. +#[test] +fn a_32_bit_read_of_a_read_to_clear_register_does_not_clear_it() { + let (a, b) = pair(); + b.write64(hub_ni::reg::SCRATCH_0, 7); + a.write64(hub_ni::reg::VECTOR, 0); + a.write64(hub_ni::reg::VECTOR_PARMS, hub_ni::reg::SCRATCH_0 - hub_ni::reg::STATUS); + let status = r64(&*a, hub_ni::reg::VECTOR_STATUS); + assert_ne!(status, 0); + assert_eq!(a.read32(hub_ni::reg::VECTOR_CLEAR).data, 0); + assert_eq!(r64(&*a, hub_ni::reg::VECTOR_STATUS), status); + assert_eq!(r64(&*a, hub_ni::reg::VECTOR_CLEAR), 0); + assert_eq!(r64(&*a, hub_ni::reg::VECTOR_STATUS), 0); + // And a half write of the write-one-to-clear port error does not merge + // with the old value, which would clear more than was asked. + a.ni().latch_port_error(0x6); + a.write32(hub_ni::reg::PORT_ERROR + 4, 0x2); + assert_eq!(r64(&*a, hub_ni::reg::PORT_ERROR), 0x4); + // The interrupt-modify register stores nothing: the other half is zero. + a.write32(hub_pi::reg::INT_PEND_MOD + 4, (hub_pi::MODIFY_SET | 9) as u32); + assert_eq!(r64(&*a, hub_pi::reg::INT_PEND0), 1 << 9); +} + +#[test] +fn every_section_answers_through_the_window_and_the_rest_reads_zero() { + let hub = hub(2, 5); + assert_eq!(r64(&*hub, hub_pi::reg::CALIAS_SIZE), 15); + assert_eq!(r64(&*hub, hub_md::reg::CONFIG), 0x001e_7fbc_3fff_ffff); + assert_eq!(r64(&*hub, hub_io::reg::WIDGET_ID), 0x1c10_106d); + assert_eq!(r64(&*hub, hub_ni::reg::STATUS) & 0xff, 5 << 4); + assert_eq!(r64(&*hub, hub_pi::reg::RT_COUNT) < 1_000, true); + for off in [0x10_0000, 0x30_0000, 0x50_0000, 0x70_0000, 0x7f_fff8] { + assert_eq!(hub.write64(off, !0), BUS_OK); + assert_eq!(r64(&*hub, off), 0, "{off:#x}"); + } +} + +#[test] +fn the_i2c_controller_and_the_uart_take_narrow_accesses_over_the_md_block() { + let hub = hub(1, 3); + // With nothing fitted, narrow accesses fail like any other. + assert_eq!(hub.read8(hub_md::reg::UART_BASE).status, BUS_ERR); + let (i2c, uart) = (Arc::new(Bytes::default()), Arc::new(Bytes::default())); + hub.attach_i2c(i2c.clone()); + hub.attach_uart(uart.clone()); + // The PCF8584's S0 and S1, eight bytes apart. + assert_eq!(hub.write8(hub_md::reg::I2C_BASE, 0x5a), BUS_OK); + assert_eq!(hub.write8(hub_md::reg::I2C_BASE + 8, 0x3c), BUS_OK); + let (s0, s1) = { + let regs = i2c.0.lock(); + (regs[0], regs[8]) + }; + assert_eq!((s0, s1), (0x5a, 0x3c)); + assert_eq!(hub.read8(hub_md::reg::I2C_BASE).data, 0x5a); + // The UART's eight-byte register stride, the range exact. + hub.write8(hub_md::reg::UART_BASE + 8 * 3, 0x77); + assert_eq!(uart.0.lock()[24], 0x77); + assert_eq!(hub.read8(hub_md::reg::UART_END).status, BUS_ERR); + assert_eq!(hub.read8(hub_md::reg::UART_BASE - 8).status, BUS_ERR); + // The registers between them are still the MD block's. + assert_eq!(r64(&*hub, hub_md::reg::SLOTID_USTAT), 0x95); + // A doubleword access to the overlay is one register access too: the + // PROM polls the controller's status with 8-byte loads. + assert_eq!(hub.write64(hub_md::reg::I2C_BASE, 1), BUS_OK); + assert_eq!(i2c.0.lock()[0], 1); + assert_eq!(hub.read64(hub_md::reg::I2C_BASE + 8).data, 0x3c); + i2c.0.lock()[0] = 0x81; + assert_eq!(hub.read64(hub_md::reg::I2C_BASE).data, 0x81, "an 8-byte read reaches the PCF8584 status"); +} + +#[test] +fn hooks_carry_the_nodes_processor_controls_to_the_machine() { + let hub = hub(2, 3); + let machine = Arc::new(Machine::default()); + hub.set_hooks(machine.clone()); + let take = || std::mem::take(&mut *machine.0.lock()); + hub.write64(hub_pi::reg::CPU_ENABLE_A, 1); + hub.write64(hub_pi::reg::NMI_B, 1); + hub.write64(hub_pi::reg::SOFTRESET, 1); + hub.write64(hub_pi::reg::CALIAS_SIZE, 2); + hub.write64(hub_ni::reg::PORT_RESET, 0x81); + assert_eq!(take(), ["enable 0 true", "nmi 1", "soft 0", "calias 0x2000", "restart"]); + number(&hub, 4); + assert_eq!(take(), ["number 4"]); + hub.power_on(); + assert_eq!(take(), ["enable 0 false", "enable 1 false", "number 0"]); +} + +#[test] +fn reset_restores_the_registers_and_keeps_what_firmware_parks() { + let hub = hub(2, 3); + hub.write64(hub_pi::reg::ERR_STACK_ADDR_B, 0xabcd_0000); + hub.write64(hub_pi::reg::CALIAS_SIZE, 1); + hub.write64(hub_md::reg::CONFIG, 5); + hub.write64(hub_io::reg::SCRATCH_0, 6); + number(&hub, 3); + hub.power_on(); + assert_eq!(r64(&*hub, hub_pi::reg::ERR_STACK_ADDR_B), 0xabcd_0000); + assert_eq!(r64(&*hub, hub_pi::reg::CALIAS_SIZE), 15); + assert_eq!(hub.calias.load(Ordering::SeqCst), 64 << 20); + assert_eq!(r64(&*hub, hub_md::reg::CONFIG), 0x001e_7fbc_3fff_ffff); + assert_eq!(r64(&*hub, hub_io::reg::SCRATCH_0), 0); + assert_eq!(hub.ni().number(), 0); + assert_eq!(hub.claims[0].load(Ordering::SeqCst), THIS_HUB); + assert_eq!(hub.claims[3].load(Ordering::SeqCst), NOBODY); +} + +// ---- the claim map ---- + +#[test] +fn an_unprogrammed_hub_claims_node_zero_and_a_programmed_one_moves() { + let hub = hub(1, 3); + let mem = hub.claim(Space::Memory); + // Stock PROM Hubs start unnumbered: every Hub holds zero. + assert_eq!(mem.read8(0x10).data, 0x10); + assert_eq!(mem.read8(NODE(1) + 0x10).status, BUS_ERR, "no Hub holds node 1"); + // The boot master numbers the Hub: node 5 answers, and node 0 no longer does. + number(&hub, 5); + assert_eq!(mem.read8(NODE(5) + 0x10).data, 0x10); + assert_eq!(mem.read8(0x10).status, BUS_ERR); + assert_eq!(mem.read8(NODE(255) + 0x10).status, BUS_ERR); + // The window is the node's, from its base, whatever its number. + assert_eq!(mem.write32(NODE(5) + 0x20, 0xdead_beef), BUS_OK); + assert_eq!(mem.read32(NODE(5) + 0x20).data, 0xdead_beef); + assert_eq!(mem.read8(NODE(5) + RAM_SIZE as u64).status, BUS_ERR, "past the node's memory"); + // A space nobody wired fails without taking the others down. + assert_eq!(hub.claim(Space::Special).read8(NODE(5)).status, BUS_ERR); +} + +#[test] +fn two_cabled_hubs_each_claim_the_others_programmed_number() { + let (a, b) = pair(); + let (ma, mb) = (a.claim(Space::Memory), b.claim(Space::Memory)); + // While both hold zero, each keeps zero for itself. + assert_eq!(ma.read8(0x10).data, 0x10); + assert_eq!(mb.read8(0x10).data, 0x90); + // Numbering the peer makes it reachable by number from this side. + number(&b, 1); + assert_eq!(ma.read8(NODE(1) + 0x10).data, 0x90); + assert_eq!(ma.read8(0x10).data, 0x10); + // b no longer holds zero: node zero is a's, which b reaches by a's number. + assert_eq!(mb.read8(0x10).data, 0x10); + assert_eq!(mb.read8(NODE(1) + 0x10).data, 0x90); + // Writes land in the Hub the number names. + assert_eq!(ma.write8(NODE(1) + 0x40, 0x5a), BUS_OK); + assert_eq!(mb.read8(NODE(1) + 0x40).data, 0x5a); + // Renumbering moves the claim. + number(&b, 2); + assert_eq!(ma.read8(NODE(1) + 0x40).status, BUS_ERR); + assert_eq!(ma.read8(NODE(2) + 0x40).data, 0x5a); + // A powered-down peer is not reachable. + b.set_powered(false); + assert_eq!(ma.read8(NODE(2) + 0x40).status, BUS_ERR); + b.set_powered(true); + assert_eq!(ma.read8(NODE(2) + 0x40).data, 0x5a); +} + +#[test] +fn the_cached_alias_keeps_node_zero_local_until_the_numbering_moves_it() { + let hub = hub(1, 3); + let cpu = hub.cpu_memory(); + hub.write64(hub_pi::reg::CALIAS_SIZE, 1); + number(&hub, 6); + // Within the 4 KiB window a node-zero address stays on this node even + // though node 0 is held by nobody now. + assert_eq!(cpu.read8(0x10).data, 0x10); + assert_eq!(cpu.read8(0xfff).data, 0xff); + // Beyond it the access goes to the claim map, which fails. + assert_eq!(cpu.read8(0x1000).status, BUS_ERR); + assert_eq!(cpu.read8(NODE(6) + 0x1000).data, 0x00); + // The window tracks the register: 8 KiB, then none. + hub.write64(hub_pi::reg::CALIAS_SIZE, 2); + assert_eq!(cpu.read8(0x1000).data, 0x00); + assert_eq!(cpu.read8(0x2000).status, BUS_ERR); + hub.write64(hub_pi::reg::CALIAS_SIZE, 0); + assert_eq!(cpu.read8(0x10).status, BUS_ERR); + // Reset sets it back to its 64 MiB sentinel, and node zero is this + // node's own again before anything is numbered. + hub.power_on(); + assert_eq!(cpu.read8(RAM_SIZE as u64 - 1).data, 0xff); + // The plain claim never honours the alias. + hub.write64(hub_pi::reg::CALIAS_SIZE, 0); + assert_eq!(hub.claim(Space::Memory).read8(0x10).data, 0x10); +} + +#[test] +fn the_hub_special_window_always_stays_reader_local() { + let (a, b) = pair(); + a.set_space(Space::HubSpecial, Ram::new(64, 0x11)); + b.set_space(Space::HubSpecial, Ram::new(64, 0x22)); + number(&a, 3); + number(&b, 4); + let (sa, sb) = (a.cpu_special(), b.cpu_special()); + // Each reader's node-zero address names its own node, not the number's holder. + assert_eq!((sa.read8(1).data, sb.read8(1).data), (0x10, 0x23)); + // A node-qualified address is claimed by number. + assert_eq!(sa.read8(NODE(4) + 1).data, 0x23); +} + +#[test] +fn the_node_view_of_a_peer_cannot_reach_a_number_nobody_holds() { + let (a, b) = pair(); + number(&a, 1); + number(&b, 2); + for number in [0, 3, 200] { + assert_eq!(a.claim(Space::Memory).read8(NODE(number) + 8).status, BUS_ERR); + assert_eq!(b.claim(Space::Memory).read8(NODE(number) + 8).status, BUS_ERR); + } + assert_eq!(a.claim(Space::Memory).read8(NODE(2) + 8).data, 0x88); + assert_eq!(b.claim(Space::Memory).read8(NODE(1) + 8).data, 0x08); +} + +// ---- the XIO port ---- + +#[test] +fn inbound_memory_reaches_the_nodes_memory_by_node_number() { + let (a, b) = pair(); + number(&a, 1); + number(&b, 2); + let xio = a.inbound(); + // A widget's DMA of every size lands in node memory. The address carries + // the node number the Hub was given, as a processor's does: node zero is + // held by nobody now. + let local = NODE(1) + 0x100; + assert_eq!(xio.write64(local, 0x0123_4567_89ab_cdef), BUS_OK); + assert_eq!(xio.read64(local).data, 0x0123_4567_89ab_cdef); + assert_eq!(xio.read8(local).data, 0x01); + assert_eq!(xio.read16(local + 2).data, 0x4567); + assert_eq!(xio.read32(local + 4).data, 0x89ab_cdef); + assert_eq!(xio.write16(local + 6, 0xfeed), BUS_OK); + assert_eq!(xio.read64(local).data, 0x0123_4567_89ab_feed); + assert_eq!(xio.read64(0x100).status, BUS_ERR); + // A node-qualified address goes to the Hub holding the number. + assert_eq!(xio.read8(NODE(2) + 8).data, 0x88); + assert_eq!(xio.write8(NODE(2) + 9, 0x42), BUS_OK); + assert_eq!(b.claim(Space::Memory).read8(NODE(2) + 9).data, 0x42); + // Memory is below 2^40; the rest of the address is the I/O view. + assert_eq!(xio.read8(1 << 40).status, BUS_ERR); + assert_eq!(xio.read8(NODE(7)).status, BUS_ERR); + assert_eq!(xio.read8(1 << 48).status, BUS_ERR); +} + +#[test] +fn an_interrupt_modify_write_lands_on_the_hub_the_packet_names() { + let (a, b) = pair(); + number(&a, 1); + number(&b, 2); + let xio = a.inbound(); + let packet = |node: u64| (1 << 47) | (node << 38) | 0x0180_0090; + // Local: set interrupt 70, which is bit 6 of the second bank. + assert_eq!(xio.write64(packet(1), hub_pi::MODIFY_SET | 70), BUS_OK); + assert_eq!(a.pi().pending(1), 1 << 6); + assert_eq!(b.pi().pending(1), 0); + // Across the cable, by the peer's programmed number. + assert_eq!(xio.write64(packet(2), hub_pi::MODIFY_SET | 9), BUS_OK); + assert_eq!(b.pi().pending(0), 1 << 9); + assert_eq!(xio.write64(packet(2), 9), BUS_OK); + assert_eq!(b.pi().pending(0), 0); + // A number nobody holds, a narrow write and any other offset fail. + assert_eq!(xio.write64(packet(3), hub_pi::MODIFY_SET | 1), BUS_ERR); + assert_eq!(xio.write32(packet(1), 1), BUS_ERR); + assert_eq!(xio.write64(packet(1) + 8, 1), BUS_ERR); + assert_eq!(a.pi().pending(0), 0); +} + +#[test] +fn a_peers_identity_probe_reads_the_network_status_and_nothing_else_is_answered() { + let (a, _) = pair(); + let xio = a.inbound(); + let probe = (1u64 << 47) | 0x0160_0000; + assert_eq!(xio.read64(probe).data, a.ni().read64(hub_ni::reg::STATUS).data); + assert_eq!(xio.read32(probe).status, BUS_ERR, "a doubleword read only"); + assert_eq!(xio.read64(probe + 8).status, BUS_ERR); + assert_eq!(xio.write64(probe, 0).status_ok(), false); +} + +trait StatusOk { + fn status_ok(self) -> bool; +} +impl StatusOk for u32 { + fn status_ok(self) -> bool { + self == BUS_OK + } +} + +#[test] +fn a_peers_programmed_io_to_the_hubs_widget_reads_only_its_identity() { + let hub = hub(1, 3); + let face = hub.widget_face(); + assert_eq!(face.read32(4).data, 0x1c10_106d); + for off in [0, 8, 0x20] { + assert_eq!(face.read32(off).status, BUS_ERR); + } + assert_eq!(face.read64(4).status, BUS_ERR); + assert_eq!(face.write32(4, 0), BUS_ERR); +} + +#[test] +fn the_io_space_holds_the_registers_at_both_offsets_and_the_widgets_behind_them() { + let hub = hub(1, 3); + let io = hub.io_space(); + // Both the reader's own alias and the node-explicit window. + let regs = [IO_REGISTERS_LOCAL, IO_REGISTERS_NAMED]; + for base in regs { + assert_eq!(io.read64(base + hub_md::reg::CONFIG).data, 0x001e_7fbc_3fff_ffff); + } + io.write64(IO_REGISTERS_LOCAL + hub_io::reg::SCRATCH_1, 0x1234); + assert_eq!(io.read64(IO_REGISTERS_NAMED + hub_io::reg::SCRATCH_1).data, 0x1234); + assert_eq!(io.read32(IO_REGISTERS_NAMED + hub_io::reg::SCRATCH_1 + 4).data, 0x1234); + // Past the second window the space is widget 2's. + assert_eq!(io.read64(0x0200_0000).status, BUS_ERR); + // A widget at its small window and through a big window. + let widget = Arc::new(Bytes::default()); + hub.io().connect_widget(9, widget.clone()); + io.write8(9 << 24 | 5, 0x77); + assert_eq!(widget.0.lock()[5], 0x77); + hub.write64(hub_io::reg::BWIN_TABLE + 8, 9 << 8); + io.write8(IO_BIG_WINDOWS + hub_io::BIG_WINDOW_SIZE + 6, 0x66); + assert_eq!(widget.0.lock()[6], 0x66); + assert_eq!(io.read8(IO_BIG_WINDOWS + hub_io::BIG_WINDOW_SIZE + 6).data, 0x66); + assert_eq!(io.read8(IO_BIG_WINDOWS + 7 * hub_io::BIG_WINDOW_SIZE).status, BUS_ERR); +} + +// ---- the engines through the fabric ---- + +#[test] +fn an_engine_addresses_memory_by_node_number_and_interrupts_the_node_it_names() { + let (a, b) = pair(); + number(&a, 1); + number(&b, 2); + let (engine, bte) = (hub_io::reg::BTE_BASE, hub_io::bte::LENGTH); + // Copy two lines from this node's memory into the peer's. + a.write64(hub_io::reg::BTE_BASE + hub_io::bte::SOURCE, NODE(1) + 0x1000); + a.write64(hub_io::reg::BTE_BASE + hub_io::bte::DEST, NODE(2) + 0x2000); + a.write64(hub_io::reg::BTE_BASE + hub_io::bte::INTERRUPT, (17 << 16) | 2); + a.write64(engine + bte, (1 << 20) | 2); + a.write64(engine + hub_io::bte::CONTROL, 0); + for i in 0..256 { + assert_eq!(b.claim(Space::Memory).read8(NODE(2) + 0x2000 + i).data, a.claim(Space::Memory).read8(NODE(1) + 0x1000 + i).data); + } + // Busy and the count clear, and the completion interrupted node 2's Hub. + assert_eq!(a.read64(engine + bte).data, 0); + assert_eq!(b.pi().pending(0), 1 << 17); + // An engine's addresses ignore the cached alias: node zero is whichever + // Hub holds zero, which nobody does now. + a.write64(hub_pi::reg::CALIAS_SIZE, 15); + a.write64(engine + hub_io::bte::SOURCE, 0x1000); + a.write64(engine + hub_io::bte::DEST, NODE(1) + 0x3000); + a.write64(engine + bte, (1 << 20) | 1); + a.write64(engine + hub_io::bte::CONTROL, 0); + assert_eq!(a.read64(engine + bte).data, (1 << 20) | (1 << 16) | 1); + // Completion aimed at a node nobody holds drops the interrupt softly. + a.write64(engine + hub_io::bte::LENGTH, 0); + a.write64(hub_io::reg::ERROR_CLEAR, 1 << 17); + a.write64(engine + hub_io::bte::SOURCE, NODE(1) + 0x1000); + a.write64(engine + hub_io::bte::INTERRUPT, 9); + a.write64(engine + bte, (1 << 20) | 1); + a.write64(engine + hub_io::bte::CONTROL, 0); + assert_eq!(a.read64(engine + bte).data, 0); +} + +// ---- the processor's path ---- + +#[derive(Default)] +struct Downstream { + fail: AtomicBool, +} +impl BusDevice for Downstream { + fn read64(&self, a: u64) -> BusRead64 { + if self.fail.load(Ordering::SeqCst) { BusRead64::err() } else { BusRead64::ok(a) } + } + fn read32(&self, a: u64) -> BusRead32 { + if self.fail.load(Ordering::SeqCst) { BusRead32::err() } else { BusRead32::ok(a as u32) } + } + fn write64(&self, _a: u64, _v: u64) -> u32 { + if self.fail.load(Ordering::SeqCst) { BUS_ERR } else { BUS_OK } + } +} + +#[test] +fn a_failed_processor_read_is_recorded_for_the_slice_that_issued_it() { + let hub = hub(2, 3); + let down = Arc::new(Downstream::default()); + let io = hub.processor_io(down.clone()); + let on_cpu = |cpu: u8, f: Box| { + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(cpu); + f(); + }) + .join() + .unwrap() + }; + // A good read records nothing. + let i = io.clone(); + on_cpu(0, Box::new(move || assert_eq!(i.read64(0x0100_0000).data, 0x0100_0000))); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_A).data, 0); + // The PROM's probe: the Hub fails 0x0240_0208 itself, whatever node + // it names, and the first failure latches the address. + let i = io.clone(); + on_cpu(0, Box::new(move || { + assert_eq!(i.read64(IO_BUS_ERROR_PROBE).status, BUS_ERR); + assert_eq!(i.read64(NODE(9) + IO_BUS_ERROR_PROBE).status, BUS_ERR); + })); + let record = hub.read64(hub_pi::reg::ERR_STATUS0_A).data; + assert_eq!(record >> 62, 0b11, "valid, and overrun from the second"); + assert_eq!((record >> 25) & ((1 << 37) - 1), IO_BUS_ERROR_PROBE >> 3); + // A narrower read of that address goes downstream instead. + let i = io.clone(); + on_cpu(0, Box::new(move || assert_eq!(i.read32(IO_BUS_ERROR_PROBE).status, BUS_OK))); + // A read that fails downstream records too, in the slice that issued it. + down.fail.store(true, Ordering::SeqCst); + let i = io.clone(); + on_cpu(1, Box::new(move || assert_eq!(i.read32(0x0300_0004).status, BUS_ERR))); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_B).data, (1 << 63) | ((0x0300_0004u64 >> 3) << 25)); + // Writes are not reads: a failing store records nothing. + hub.read64(hub_pi::reg::ERR_STATUS0_B_RCLR); + let i = io.clone(); + on_cpu(1, Box::new(move || assert_eq!(i.write64(0x10, 0), BUS_ERR))); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_B).data, 0); + // A full-width zero store to the status-1 alias ends the record + // (inferred). + hub.write64(hub_pi::reg::ERR_STATUS1_A_RCLR, 0); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_A).data, 0); +} + +/// A debugger's access, a device's DMA and a processor of another node record +/// nothing. A thread that never set its CPU is a device or monitor thread. +#[test] +fn a_failed_read_from_a_thread_that_is_not_one_of_the_nodes_processors_records_nothing() { + let hub = hub(2, 3); + let down = Arc::new(Downstream::default()); + down.fail.store(true, Ordering::SeqCst); + let io = hub.processor_io(down); + // This test thread never calls set_current_cpu. + assert_eq!(io.read64(0x0300_0000).status, BUS_ERR); + assert_eq!(io.read64(IO_BUS_ERROR_PROBE).status, BUS_ERR); + assert_eq!(hub.pi().reader_slice(), None); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_A).data, 0); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_B).data, 0); + // Nor does a processor numbered outside this node's. + let (h, i) = (hub.clone(), io.clone()); + std::thread::spawn(move || { + crate::cpu::smp::set_current_cpu(5); + assert_eq!(h.pi().reader_slice(), None); + assert_eq!(i.read64(0x0300_0000).status, BUS_ERR); + }) + .join() + .unwrap(); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_A).data, 0); + assert_eq!(hub.read64(hub_pi::reg::ERR_STATUS0_B).data, 0); +} + +// ---- the Bridge's side ---- + +#[test] +fn the_interrupt_target_sets_and_clears_pending_bits_by_vector() { + let hub = hub(1, 3); + let target = hub.interrupt_target(); + target.set_line(70, true); + target.set_line(3, true); + assert_eq!((hub.pi().pending(0), hub.pi().pending(1)), (1 << 3, 1 << 6)); + target.set_line(70, false); + assert_eq!(hub.pi().pending(1), 0); + // Vectors are seven bits. + target.set_line(128, true); + assert_eq!((hub.pi().pending(0), hub.pi().pending(1)), (1 << 3, 0)); +} + +#[test] +fn the_hub_is_a_device_whose_counter_is_its_clock() { + let now = Arc::new(std::sync::atomic::AtomicU64::new(0)); + let clock = now.clone(); + let hub = Hub::new(HubConfig::default(), Arc::new(move || clock.load(Ordering::SeqCst))); + assert!(!hub.is_running()); + hub.start(); + assert!(hub.is_running()); + now.store(800 * 1234, Ordering::SeqCst); + assert_eq!(hub.get_clock(), 1234); + hub.step(1); + hub.stop(); + assert!(!hub.is_running()); +} + +// ---- the rendezvous observer ---- + +#[derive(Default)] +struct Seen(Mutex>); +impl HubObserver for Seen { + fn stored(&self, store: Store) { + self.0.lock().push(store); + } +} + +/// A mechanism that holds a processor until a partner has run needs to see the +/// registers the processors post to. The Hub reports each store after it has +/// taken effect and changes no value on its account. +#[test] +fn the_observer_sees_the_stores_a_processor_rendezvous_posts() { + let (a, b) = pair(); + let seen = Arc::new(Seen::default()); + a.set_observer(seen.clone()); + let take = || std::mem::take(&mut *seen.0.lock()); + // The pair handshake: a compare post, and the failure tail's enable clear. + a.write64(hub_pi::reg::RT_COMPARE_A, 0x24); + a.write64(hub_pi::reg::RT_COMPARE_B, !0); + a.write64(hub_pi::reg::CPU_ENABLE_B, 0); + a.write64(hub_pi::reg::CPU_ENABLE_A, 0xff); + assert_eq!( + take(), + [ + Store::RtCompare { slice: 0, value: 0x24 }, + Store::RtCompare { slice: 1, value: (1 << 52) - 1 }, + Store::CpuEnable { slice: 1, enabled: false }, + Store::CpuEnable { slice: 0, enabled: true }, + ] + ); + // The values are the registers': nothing was held back. + assert_eq!(a.read64(hub_pi::reg::RT_COMPARE_A).data, 0x24); + // II scratch 0 holds the pair's outcome bits; the mask applies. + a.write64(hub_io::reg::SCRATCH_0, (1 << 23) | (1 << 40)); + a.write64(hub_io::reg::SCRATCH_1, 5); + assert_eq!(take(), [Store::IoScratch { index: 0, value: 1 << 23 }, Store::IoScratch { index: 1, value: 5 }]); + // The node barrier and numbering posts, on the bus. + a.write64(hub_ni::reg::SCRATCH_0, !0); + a.write64(hub_ni::reg::SCRATCH_1, 1 << 17); + assert_eq!( + take(), + [ + Store::NiScratch { index: 0, value: !0, vector: false }, + Store::NiScratch { index: 1, value: 1 << 17, vector: false }, + ] + ); + // And from another node over the cable, by vector PIO. + b.write64(hub_ni::reg::VECTOR, 0); + b.write64(hub_ni::reg::VECTOR_DATA, 0x77); + b.write64(hub_ni::reg::VECTOR_PARMS, (hub_ni::reg::SCRATCH_1 - hub_ni::reg::STATUS) | 1); + assert_eq!(take(), [Store::NiScratch { index: 1, value: 0x77, vector: true }]); + // The launch poke: a pending bit raised, one cleared, and one set twice. + let poke = (hub_pi::MODIFY_SET | 12) as u64; + a.write64(hub_pi::reg::INT_PEND_MOD, poke); + a.write64(hub_pi::reg::INT_PEND_MOD, poke); + a.write64(hub_pi::reg::INT_PEND_MOD, 12); + assert_eq!( + take(), + [ + Store::IntPendMod { value: poke, changed: true }, + Store::IntPendMod { value: poke, changed: false }, + Store::IntPendMod { value: 12, changed: true }, + ] + ); + // A device interrupt arrives through the same register. + a.interrupt_target().set_line(3, true); + assert_eq!(take(), [Store::IntPendMod { value: hub_pi::MODIFY_SET | 3, changed: true }]); + // The port reset is reported before it acts. + a.write64(hub_ni::reg::PORT_RESET, 0x81); + assert_eq!(take(), [Store::PortReset { value: 0x81 }]); +} + +#[test] +fn the_observer_does_not_see_reads_or_other_stores_and_is_optional() { + let hub = hub(2, 3); + // Nothing connected: every one of those stores simply works. + hub.write64(hub_pi::reg::RT_COMPARE_A, 5); + hub.write64(hub_pi::reg::INT_PEND_MOD, hub_pi::MODIFY_SET | 1); + hub.write64(hub_ni::reg::SCRATCH_0, 1); + hub.write64(hub_io::reg::SCRATCH_0, 1); + hub.write64(hub_ni::reg::PORT_RESET, 0x80); + let seen = Arc::new(Seen::default()); + hub.set_observer(seen.clone()); + hub.read64(hub_pi::reg::RT_COMPARE_A); + hub.read64(hub_pi::reg::RT_COUNT); + hub.read64(hub_pi::reg::INT_PEND0); + hub.read64(hub_ni::reg::SCRATCH_0); + hub.write64(hub_pi::reg::INT_MASK0_A, !0); + hub.write64(hub_pi::reg::RT_COUNT, 0); + hub.write64(hub_md::reg::CONFIG, 0); + hub.write64(hub_ni::reg::STATUS, 0); + hub.write64(hub_io::reg::SCRATCH_0 + 0x100, 0); + assert!(seen.0.lock().is_empty()); +} + +// ---- ownership ---- + +/// A node that is built, wired and dropped must free its Hub and the decode +/// table of its memory. The Hub owns its spaces, so no space may hold the Hub: +/// the I/O space once did, and a machine's every Hub and decode table leaked +/// (the process has only 64 tables). The memory's own routing back into the +/// Hub is released the way a machine's drop releases it. +#[test] +fn a_wired_hub_with_a_peer_and_attachments_is_freed_when_dropped() { + use crate::physical::{Physical, RamBank}; + let (a, b) = (Hub::new(HubConfig::default(), Arc::new(|| 0)), Hub::new(HubConfig::default(), Arc::new(|| 0))); + Hub::cable(&a, &b); + let phys = Physical::new(vec![RamBank::new(1)]); + a.set_space(Space::Memory, phys.clone()); + // What a machine does: the node's own I/O space as a space of the node, the + // Hub's claim view routed into the node's memory, the Hub's handles given + // to widgets, and attachments on the window. + a.set_space(Space::Io, a.io_space()); + phys.map(1 << 32, 2 << 32, a.claim(Space::Memory), "node-qualified memory"); + // The manager's callbacks reach the processor and its bus, which reach the + // Hub: the interface must not own the manager. + let timers = Arc::new(crate::hptimer::TimerManager::new()); + a.pi().set_timer_manager(timers.clone()); + assert_eq!(Arc::strong_count(&timers), 1, "the interface holds the timer manager strongly"); + a.attach_i2c(Arc::new(Bytes::default())); + a.attach_uart(Arc::new(Bytes::default())); + a.set_observer(Arc::new(Seen::default())); + a.set_hooks(Arc::new(Machine::default())); + a.io().connect_widget(9, Arc::new(Bytes::default())); + a.io().connect_widget(8, a.widget_face()); + let held = (a.inbound(), a.widget_face(), a.interrupt_target(), a.cpu_memory(), a.cpu_special(), a.processor_io(Arc::new(Downstream::default()))); + let (weak_a, weak_b, weak_phys) = (Arc::downgrade(&a), Arc::downgrade(&b), Arc::downgrade(&phys)); + phys.release_devices(); + drop((held, phys, a, b)); + assert!(weak_a.upgrade().is_none(), "the Hub is still referenced"); + assert!(weak_b.upgrade().is_none(), "the peer Hub is still referenced"); + assert!(weak_phys.upgrade().is_none(), "the node's decode table is still referenced"); +} From 929d2e4c5154325b0003dc4ec8dc02ece50c2237 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:52:11 -0700 Subject: [PATCH 19/38] i2c: add an untimed I2C bus The Origin 200 Hub reaches its system controller and the controller's NVRAM over I2C, and nothing in iris models that bus. Add a small one: a master starts at an address, the bus selects the target that claims it (every target that takes a general call for address 0), and bytes then move one call at a time. A repeated start to the selected address keeps the selection. Targets lock their own state and never call back into the master, so the master's lock is the only one held across a transfer. A target can also ask its master to put an address-only transaction on the wire or to end the open transfer, which the controller's arbitration grants and its deferred reset and power commands need. --- src/dev/i2c.rs | 253 +++++++++++++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 2 files changed, 254 insertions(+) create mode 100644 src/dev/i2c.rs diff --git a/src/dev/i2c.rs b/src/dev/i2c.rs new file mode 100644 index 000000000..db551abd1 --- /dev/null +++ b/src/dev/i2c.rs @@ -0,0 +1,253 @@ +//! A small untimed I2C bus: the targets on one master's wire. +//! +//! A transfer is not clocked bit by bit. The master starts at an address, the +//! bus finds the target that claims it (every target that takes a general +//! call, for address 0) and the bytes then move one call at a time. A repeated +//! start to the address already selected does not rescan, as on the wire. +//! +//! The bus holds no lock of its own; the controller that owns it serialises +//! access. Targets lock their own state, so the owner's lock is held across +//! target calls and a target must never call back into the controller. + +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +/// Whether a transfer is open on a bus, readable without the controller's +/// lock. The controller stores it after every access; a target that decides +/// something by "is the bus free" (the system controller's grant ring and its +/// operator command line) loads it. +pub type BusBusy = Arc; + +/// What a target asks of its master when serviced. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Service { + /// An address byte (seven-bit address and direction) to put on the wire as + /// an address-only transaction nobody acknowledges. + pub grant: Option, + /// End the open transfer, as a target does that resets or powers down + /// something while still selected. + pub release_bus: bool, +} + +/// One device on the bus. +pub trait I2cTarget: Send + Sync { + /// Whether the target answers a directed start at `addr7`. + fn claims(&self, addr7: u8) -> bool; + + /// Whether the target takes part in a general call (address 0). + fn answers_general_call(&self) -> bool { + true + } + + /// A start or repeated start. Answering false refuses the address. + fn start(&self, _addr7: u8, _recv: bool) -> bool { + true + } + + /// A byte from the master; false is a NAK. + fn send(&self, byte: u8) -> bool; + + /// The next byte for the master. + fn recv(&self) -> u8; + + /// The master NAKed the last byte it read. + fn nack(&self) {} + + /// The transfer ended, with a stop or a start to another address. + fn finish(&self) {} + + /// Let time pass for the target and collect what it wants of its master. + /// Called around every register access of the controller. + fn service(&self) -> Service { + Service::default() + } +} + +#[derive(Default)] +pub struct I2cBus { + targets: Vec>, + current: Vec>, + broadcast: bool, +} + +impl I2cBus { + pub fn new() -> Self { + Self::default() + } + + pub fn add(&mut self, target: Arc) { + self.targets.push(target); + } + + pub fn targets(&self) -> &[Arc] { + &self.targets + } + + /// A target is selected. + pub fn busy(&self) -> bool { + !self.current.is_empty() + } + + pub fn is_broadcast(&self) -> bool { + self.broadcast + } + + /// Select the targets for a new transfer. Does nothing while a transfer + /// is open, since a repeated start keeps the selection. + pub fn scan(&mut self, addr7: u8) { + if !self.current.is_empty() { + return; + } + self.broadcast = addr7 == 0; + if self.broadcast { + self.current = self + .targets + .iter() + .filter(|t| t.answers_general_call()) + .cloned() + .collect(); + } else if let Some(t) = self.targets.iter().find(|t| t.claims(addr7)) { + self.current.push(t.clone()); + } + } + + /// Deliver the start to the selected targets. A refusal ends the transfer + /// when `fresh` (this start opened it); a repeated start that is refused + /// leaves the earlier transfer for the caller to end. A general call is + /// never refused. + pub fn start_selected(&mut self, addr7: u8, recv: bool, fresh: bool) -> bool { + let mut refused = false; + for t in &self.current { + if !t.start(addr7, recv) && !self.broadcast { + refused = true; + break; + } + } + if refused && fresh { + self.end(); + } + !refused + } + + /// Send a byte to every selected target; false if any NAKed. + pub fn send(&self, byte: u8) -> bool { + let mut ok = true; + for t in &self.current { + ok = t.send(byte) && ok; + } + ok + } + + /// A byte from the first selected target; a general call reads 0xff. + pub fn recv(&self) -> u8 { + match self.current.first() { + Some(t) if !self.broadcast => t.recv(), + _ => 0xff, + } + } + + pub fn nack(&self) { + for t in &self.current { + t.nack(); + } + } + + /// End the transfer. + pub fn end(&mut self) { + for t in self.current.drain(..) { + t.finish(); + } + self.broadcast = false; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use parking_lot::Mutex; + + #[derive(Default)] + struct Probe { + addr: u8, + general: bool, + log: Mutex>, + } + + impl I2cTarget for Probe { + fn claims(&self, addr7: u8) -> bool { + addr7 == self.addr + } + fn answers_general_call(&self) -> bool { + self.general + } + fn start(&self, addr7: u8, recv: bool) -> bool { + self.log.lock().push(format!("start {addr7:#x} {recv}")); + true + } + fn send(&self, byte: u8) -> bool { + self.log.lock().push(format!("send {byte:#x}")); + true + } + fn recv(&self) -> u8 { + 0x5a + } + fn finish(&self) { + self.log.lock().push("finish".into()); + } + } + + fn probe(addr: u8, general: bool) -> Arc { + Arc::new(Probe { addr, general, ..Default::default() }) + } + + #[test] + fn directed_start_selects_the_claiming_target_only() { + let (a, b) = (probe(0x08, true), probe(0x50, false)); + let mut bus = I2cBus::new(); + bus.add(a.clone()); + bus.add(b.clone()); + bus.scan(0x50); + assert!(bus.busy()); + assert!(bus.start_selected(0x50, false, true)); + assert!(bus.send(1)); + bus.end(); + assert!(!bus.busy()); + assert!(a.log.lock().is_empty()); + assert_eq!(*b.log.lock(), ["start 0x50 false", "send 0x1", "finish"]); + } + + #[test] + fn general_call_reaches_only_targets_that_answer_it() { + let (a, b) = (probe(0x08, true), probe(0x50, false)); + let mut bus = I2cBus::new(); + bus.add(a.clone()); + bus.add(b.clone()); + bus.scan(0); + assert!(bus.is_broadcast()); + assert!(bus.send(7)); + assert_eq!(bus.recv(), 0xff); + assert_eq!(*a.log.lock(), ["send 0x7"]); + assert!(b.log.lock().is_empty()); + } + + #[test] + fn unclaimed_address_selects_nothing() { + let mut bus = I2cBus::new(); + bus.add(probe(0x08, true)); + bus.scan(0x2b); + assert!(!bus.busy()); + } + + #[test] + fn rescan_is_skipped_while_a_transfer_is_open() { + let (a, b) = (probe(0x08, true), probe(0x09, true)); + let mut bus = I2cBus::new(); + bus.add(a); + bus.add(b.clone()); + bus.scan(0x08); + bus.scan(0x09); + assert!(b.log.lock().is_empty()); + assert!(!bus.targets()[1].claims(0x08)); + assert_eq!(bus.recv(), 0x5a); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 0fd53a75c..d0a343470 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -44,6 +44,7 @@ pub mod ds1x86; pub mod eeprom_93c56; pub mod hal2; pub mod hpc3; +pub mod i2c; pub mod ioc; pub mod mc; pub mod mc_vdma; From d2faac8f837f380c72d4423d48c924526823f224 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:52:11 -0700 Subject: [PATCH 20/38] elsc: add the module controller's configuration NVRAM The module system controller keeps a 2048-byte store on the module's I2C bus. It answers eight consecutive addresses, one 256-byte block each, with a one-byte word address, as a 24C16-class part does. The top block holds the controller's own fields: validity marker, password, debug switches, configuration byte, module number and clock select. A store with no backing file starts as a master controller leaves it after first power-up. A backing file must be exactly 2048 bytes and its contents win; a missing one is created holding the fresh image. The file is replaced whole when a transfer ends with changed bytes, so the store survives the process. The controller reads and writes its own fields through a direct access that moves the address counter as a real transfer would. --- src/dev/elsc_nvram.rs | 463 ++++++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 2 files changed, 464 insertions(+) create mode 100644 src/dev/elsc_nvram.rs diff --git a/src/dev/elsc_nvram.rs b/src/dev/elsc_nvram.rs new file mode 100644 index 000000000..44fdb1bc5 --- /dev/null +++ b/src/dev/elsc_nvram.rs @@ -0,0 +1,463 @@ +//! Configuration NVRAM of a module system controller: one 2048-byte store on +//! the module's management I2C bus. +//! +//! Address `A` answers at seven-bit I2C address `0x50 + (A >> 8)` with the +//! one-byte word address `A & 0xff`, as a 24C16-class EEPROM does. The +//! controller owns the top block, `0x700`-`0x7ff`; the rest is the board +//! inventory log the PROM keeps. +//! +//! Model choices, not measured behaviour: the store is one device answering +//! eight addresses; the address counter wraps inside its 256-byte block +//! (a real 24C16 wraps a write inside a 16-byte page); writes complete at once +//! and are never refused as busy; the part does not answer a general call; a +//! fresh store holds what a master controller writes at its first power-up. +//! +//! The backing file is replaced whole through `nv_storage` when a transfer +//! ends with changed bytes, not on every byte. +//! +//! Ported from hw/nvram/sgi-elsc-nvram.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::i2c::I2cTarget; +use crate::devlog::LogModule; +use crate::dlog; +use crate::nv_storage; +use crate::traits::Resettable; +use parking_lot::Mutex; +use std::io; +use std::path::{Path, PathBuf}; + +/// Seven-bit address of block 0. +pub const FIRST_ADDR: u8 = 0x50; +pub const BLOCKS: usize = 8; +pub const BLOCK_BYTES: usize = 256; +pub const BYTES: usize = BLOCKS * BLOCK_BYTES; + +pub const MARKER: usize = 0x700; +pub const PASSWORD: usize = 0x701; +pub const DEBUG: usize = 0x705; +pub const CONFIG: usize = 0x707; +pub const MODULE: usize = 0x708; +pub const CLOCK: usize = 0x70b; +pub const MARKER_VALUE: u8 = 0x37; + +/// Configuration byte: bit 0 fans high, bit 1 automatic power-up. +pub const CONFIG_FAN_HIGH: u8 = 0x01; +pub const CONFIG_AUTO_POWER: u8 = 0x02; + +/// Clock-select default a controller writes on first power-up for the +/// full-clock midplane type, which the Origin 200 build always assumes. +pub const CLOCK_FULL: u8 = 0x78; + +/// The state a master controller leaves after its first power-up: zeroes, +/// then at 0x700 the validity marker, the password "none", zero debug-switch +/// values, `config`, `module`, zero self-test flags and partition, and `clock`. +pub fn fresh_image(config: u8, module: u8, clock: u8) -> [u8; BYTES] { + let mut image = [0u8; BYTES]; + image[MARKER] = MARKER_VALUE; + image[PASSWORD..PASSWORD + 4].copy_from_slice(b"none"); + image[CONFIG] = config; + image[MODULE] = module; + image[CLOCK] = clock; + image +} + +/// The controller store an Origin 200 machine supplies at power-on. The PROM +/// refuses the store unless `0x700` holds `0x37`; the password `none` at +/// `0x701`, `0x02` at `0x707` and `0x78` at `0x70b` are copied as they are +/// without the PROM's use of them being established, and every other byte is +/// zero. The machine calls this (or [`ElscNvram::origin200`]) and does not +/// build the contents itself. +pub fn origin200_image() -> [u8; BYTES] { + fresh_image(CONFIG_AUTO_POWER, 0, CLOCK_FULL) +} + +struct State { + mem: [u8; BYTES], + /// Block the current transfer addressed. + block: usize, + /// Eleven-bit address the next data byte reads or writes. + cur: usize, + /// A write transfer whose word address has not arrived. + want_word: bool, + /// Bytes changed since the backing file was last replaced; a failed + /// replacement leaves it set so the next transfer's end retries. + dirty: bool, + path: Option, +} + +impl State { + fn advance(&mut self) { + self.cur = (self.cur & !0xff) | ((self.cur + 1) & 0xff); + } + + fn store(&mut self, data: u8) { + if self.mem[self.cur] != data { + self.mem[self.cur] = data; + self.dirty = self.path.is_some(); + } + } + + /// Replace the backing file if bytes changed. A failure is reported and + /// the bytes stay in the store. + fn flush(&mut self) { + let Some(path) = self.path.as_ref().filter(|_| self.dirty) else { + return; + }; + match nv_storage::save(&path.to_string_lossy(), &self.mem) { + Ok(()) => self.dirty = false, + Err(err) => dlog!(LogModule::Bedrock, "elsc nvram: write to {} failed: {err}", path.display()), + } + } + + fn start(&mut self, addr7: u8, recv: bool) { + self.block = (addr7 - FIRST_ADDR) as usize; + if recv { + // A read continues from the counter inside the block this + // transfer addressed. + self.cur = (self.block << 8) | (self.cur & 0xff); + self.want_word = false; + } else { + self.want_word = true; + } + } + + fn send(&mut self, byte: u8) { + if self.want_word { + self.cur = (self.block << 8) | byte as usize; + self.want_word = false; + return; + } + self.store(byte); + self.advance(); + } + + fn recv(&mut self) -> u8 { + let byte = self.mem[self.cur]; + self.advance(); + byte + } +} + +pub struct ElscNvram { + state: Mutex, +} + +impl ElscNvram { + fn from_image(mem: [u8; BYTES], path: Option) -> Self { + Self { + state: Mutex::new(State { + mem, + block: 0, + cur: 0, + want_word: false, + dirty: false, + path, + }), + } + } + + /// The Origin 200 machine's store: [`origin200_image`], held in memory + /// when `path` is `None`, or backed by that file as [`Self::open`] does. + pub fn origin200(path: Option<&Path>) -> io::Result { + match path { + None => Ok(Self::from_image(origin200_image(), None)), + Some(p) => Self::open(p, CONFIG_AUTO_POWER, 0, CLOCK_FULL), + } + } + + /// A store with no backing file, holding a fresh image. + pub fn new(config: u8, module: u8, clock: u8) -> Self { + Self::from_image(fresh_image(config, module, clock), None) + } + + /// A store backed by `path`. A file that exists must be exactly + /// [`BYTES`] long and its contents win over the fresh-image values; + /// a missing file is created holding the fresh image. + pub fn open(path: &Path, config: u8, module: u8, clock: u8) -> io::Result { + let mem = if path.exists() { + let bytes = std::fs::read(path)?; + <[u8; BYTES]>::try_from(bytes.as_slice()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "{}: the controller NVRAM file is {} bytes, not {BYTES}", + path.display(), + bytes.len() + ), + ) + })? + } else { + let mem = fresh_image(config, module, clock); + nv_storage::save(&path.to_string_lossy(), &mem)?; + mem + }; + Ok(Self::from_image(mem, Some(path.to_path_buf()))) + } + + /// One byte of the store, without moving the address counter. + pub fn peek(&self, addr: usize) -> u8 { + self.state.lock().mem[addr % BYTES] + } + + pub fn contents(&self) -> [u8; BYTES] { + self.state.lock().mem + } + + /// One complete transfer as the controller makes it to a part of its own: + /// the word address written, then a byte written or read. Answers `None` + /// when no block answers `addr7`. The address counter ends where a real + /// access leaves it. + pub fn access(&self, addr7: u8, offset: u8, write: Option) -> Option { + if !self.claims(addr7) { + return None; + } + let mut st = self.state.lock(); + st.start(addr7, false); + st.send(offset); + let value = match write { + Some(byte) => { + st.send(byte); + byte + } + None => { + st.start(addr7, true); + st.recv() + } + }; + st.want_word = false; + st.flush(); + Some(value) + } +} + +impl I2cTarget for ElscNvram { + fn claims(&self, addr7: u8) -> bool { + (FIRST_ADDR..FIRST_ADDR + BLOCKS as u8).contains(&addr7) + } + + fn answers_general_call(&self) -> bool { + false + } + + fn start(&self, addr7: u8, recv: bool) -> bool { + self.state.lock().start(addr7, recv); + true + } + + fn send(&self, byte: u8) -> bool { + self.state.lock().send(byte); + true + } + + fn recv(&self) -> u8 { + self.state.lock().recv() + } + + fn finish(&self) { + let mut st = self.state.lock(); + st.want_word = false; + st.flush(); + } +} + +impl Resettable for ElscNvram { + /// Only the transfer state resets; the store is non-volatile. + fn power_on(&self) { + let mut st = self.state.lock(); + st.block = 0; + st.cur = 0; + st.want_word = false; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::i2c::I2cBus; + use std::sync::Arc; + + /// A bus with the store and a master that does what the PROM does. + struct Rig { + bus: I2cBus, + nvram: Arc, + } + + impl Rig { + fn new(nvram: ElscNvram) -> Self { + let nvram = Arc::new(nvram); + let mut bus = I2cBus::new(); + bus.add(nvram.clone()); + Self { bus, nvram } + } + + fn begin_write(&mut self, addr: usize, word: u8) { + let a7 = FIRST_ADDR + (addr >> 8) as u8; + self.bus.scan(a7); + assert!(self.bus.start_selected(a7, false, true)); + assert!(self.bus.send(word)); + } + + fn write(&mut self, addr: usize, data: &[u8]) { + self.begin_write(addr, addr as u8); + for b in data { + assert!(self.bus.send(*b)); + } + self.bus.end(); + } + + fn read(&mut self, addr: usize) -> u8 { + let a7 = FIRST_ADDR + (addr >> 8) as u8; + self.begin_write(addr, addr as u8); + assert!(self.bus.start_selected(a7, true, false)); + let v = self.bus.recv(); + self.bus.nack(); + self.bus.end(); + v + } + } + + fn fresh() -> Rig { + Rig::new(ElscNvram::new(CONFIG_AUTO_POWER, 0, CLOCK_FULL)) + } + + #[test] + fn new_store_defaults() { + let mut rig = fresh(); + assert_eq!(rig.read(MARKER), 0x37); + for (i, ch) in b"none".iter().enumerate() { + assert_eq!(rig.read(PASSWORD + i), *ch); + } + assert_eq!(rig.read(0x705), 0); + assert_eq!(rig.read(0x706), 0); + assert_eq!(rig.read(CONFIG), 0x02); + assert_eq!(rig.read(MODULE), 0); + assert_eq!(rig.read(0x709), 0); + assert_eq!(rig.read(0x70a), 0); + assert_eq!(rig.read(CLOCK), 0x78); + assert_eq!(rig.read(0x70c), 0); + assert_eq!(rig.read(0x70d), 0); + // Everything outside the controller's block is zero. + assert_eq!(rig.read(0x000), 0); + assert_eq!(rig.read(0x6ff), 0); + assert_eq!(rig.read(0x70e), 0); + } + + #[test] + fn the_origin_200_image_is_the_marker_and_the_copied_bytes_only() { + let image = origin200_image(); + let mut want = [0u8; BYTES]; + want[0x700] = 0x37; + want[0x701..0x705].copy_from_slice(b"none"); + want[0x707] = 0x02; + want[0x70b] = 0x78; + assert_eq!(image, want); + assert_eq!(ElscNvram::origin200(None).unwrap().contents(), want); + } + + #[test] + fn blocks_are_addressed_by_the_i2c_address() { + let mut rig = fresh(); + for block in 0..BLOCKS { + rig.write(block * 0x100 + 0x10, &[0xa0 + block as u8]); + } + for block in 0..BLOCKS { + assert_eq!(rig.read(block * 0x100 + 0x10), 0xa0 + block as u8); + // The same word address in the neighbouring block is untouched. + assert_ne!(rig.read(block * 0x100 + 0x11), 0xa0 + block as u8); + } + assert!(!rig.nvram.claims(0x4f)); + assert!(!rig.nvram.claims(0x58)); + } + + #[test] + fn twelve_byte_block_write_is_one_transaction() { + let mut rig = fresh(); + let data: Vec = (1..=12).collect(); + rig.write(MARKER, &data); + for (i, b) in data.iter().enumerate() { + assert_eq!(rig.read(MARKER + i), *b); + } + } + + #[test] + fn sequential_access_wraps_inside_the_block() { + let mut rig = fresh(); + rig.write(0x1fe, &[0xaa, 0xbb, 0xcc]); + assert_eq!(rig.read(0x1fe), 0xaa); + assert_eq!(rig.read(0x1ff), 0xbb); + // The third byte wrapped to the first of the same block. + assert_eq!(rig.read(0x100), 0xcc); + assert_eq!(rig.read(0x200), 0); + } + + #[test] + fn survives_a_reset_and_does_not_answer_a_general_call() { + let mut rig = fresh(); + rig.write(0x123, &[0x42]); + rig.nvram.power_on(); + assert_eq!(rig.read(0x123), 0x42); + assert_eq!(rig.read(MARKER), 0x37); + rig.bus.scan(0); + assert!(!rig.bus.busy()); + } + + fn scratch_path(tag: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("iris-elsc-nvram-{tag}-{}.raw", std::process::id())) + } + + #[test] + fn caller_image_wins_over_the_defaults() { + let path = scratch_path("wins"); + let mut image = [0u8; BYTES]; + image[CONFIG] = 0x01; + image[MODULE] = 9; + std::fs::write(&path, image).unwrap(); + let nv = ElscNvram::open(&path, CONFIG_AUTO_POWER, 0, CLOCK_FULL).unwrap(); + // The file's zeros stand: no marker, no password, no clock byte. + assert_eq!(nv.peek(MARKER), 0); + assert_eq!(nv.peek(CONFIG), 0x01); + assert_eq!(nv.peek(MODULE), 9); + assert_eq!(nv.peek(CLOCK), 0); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn a_missing_file_is_created_fresh_and_a_wrong_size_is_refused() { + let path = scratch_path("size"); + let _ = std::fs::remove_file(&path); + let nv = ElscNvram::open(&path, CONFIG_AUTO_POWER, 3, CLOCK_FULL).unwrap(); + assert_eq!(nv.peek(MODULE), 3); + assert_eq!(std::fs::read(&path).unwrap(), nv.contents().to_vec()); + drop(nv); + std::fs::write(&path, [0u8; 100]).unwrap(); + let err = ElscNvram::open(&path, 0, 0, 0).err().unwrap(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn persists_across_instances() { + let path = scratch_path("persist"); + let _ = std::fs::remove_file(&path); + { + let mut rig = Rig::new(ElscNvram::open(&path, CONFIG_AUTO_POWER, 0, CLOCK_FULL).unwrap()); + rig.write(0x42, &[0x99]); + rig.write(MODULE, &[5]); + } + let nv = ElscNvram::open(&path, 0, 0, 0).unwrap(); + assert_eq!(nv.peek(0x42), 0x99); + assert_eq!(nv.peek(MODULE), 5); + // The fresh-store values it was not given this time do not apply. + assert_eq!(nv.peek(CLOCK), CLOCK_FULL); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn the_controllers_own_access_moves_the_counter_like_a_transfer() { + let nv = ElscNvram::new(CONFIG_AUTO_POWER, 0, CLOCK_FULL); + assert_eq!(nv.access(0x57, 0x07, None), Some(0x02)); + assert_eq!(nv.access(0x57, 0x07, Some(0x03)), Some(0x03)); + assert_eq!(nv.peek(CONFIG), 0x03); + assert_eq!(nv.access(0x08, 0, None), None); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index d0a343470..0a425e328 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -42,6 +42,7 @@ pub mod cdmc; pub mod daynaport; pub mod ds1x86; pub mod eeprom_93c56; +pub mod elsc_nvram; pub mod hal2; pub mod hpc3; pub mod i2c; From ca7622b37fd4048904e1c0a86b55bf57efc059ac Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:52:11 -0700 Subject: [PATCH 21/38] pcf8584: add the Hub's I2C controller The Origin 200 Hub drives its I2C bus through a Philips PCF8584, two registers eight bytes apart. Model its register file, the ESO/ES1/ES2 selection, the receive pipeline that returns the held byte before fetching the next, repeated starts, the programmed own address and the general call, in front of an untimed private bus. A byte completes inside the register access that starts it. Every access width is one register access, since the PROM reads the data register with 8-byte loads and a split would advance the pipeline twice. Targets are serviced around each access, so a grant a target publishes lands in the data register the way the arbitration protocol expects. The interrupt output is driven with no lock held. --- src/dev/mod.rs | 1 + src/dev/pcf8584.rs | 531 +++++++++++++++++++++++++++++++++++++++ src/dev/pcf8584/tests.rs | 423 +++++++++++++++++++++++++++++++ 3 files changed, 955 insertions(+) create mode 100644 src/dev/pcf8584.rs create mode 100644 src/dev/pcf8584/tests.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 0a425e328..599be6a30 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -50,6 +50,7 @@ pub mod ioc; pub mod mc; pub mod mc_vdma; pub mod mem; +pub mod pcf8584; pub mod pit8254; pub mod prom; pub mod ps2; diff --git a/src/dev/pcf8584.rs b/src/dev/pcf8584.rs new file mode 100644 index 000000000..ee7163073 --- /dev/null +++ b/src/dev/pcf8584.rs @@ -0,0 +1,531 @@ +//! Philips PCF8584 I2C-bus controller, as the Origin 200's Hub wires it: two +//! registers eight bytes apart (data and its neighbours at A0 low, control on +//! write and status on read at A0 high) in front of a private untimed bus. +//! +//! A byte completes inside the register access that starts it and no time +//! passes. Several adapters sharing one clocked wire, as on an Origin 2000 +//! module, are not modelled; `Pcf8584` owns its `I2cBus` and a shared wire +//! would replace that bus. +//! +//! Model choices, not measured behaviour: the control register reads back its +//! bits plus the live PIN with the serial interface off; ES1 and ES2 both set +//! with ESO clear selects the clock register; the general call is +//! acknowledged without raising AAS; a PIN write clears BER, LAB and AAS +//! without disturbing a transfer; the last unmatched address byte in +//! `0xe0`-`0xef` is copied into the data register when ESO is set and the +//! own address is nonzero, which is how a processor reads its arbitration +//! grant. +//! +//! The chip's slave half (the general call, and the programmed own address) +//! is handled inside this file, not as a target on the bus. +//! +//! Migration and snapshot state are not saved yet. +//! +//! Ported from hw/i2c/pcf8584.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::i2c::{BusBusy, I2cBus, Service}; +use crate::devlog::LogModule; +use crate::dlog; +use crate::irq::IrqLine; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::sync::atomic::Ordering; + +/// The two addresses the A0 pin selects, spaced as the Hub wires them. +pub const A0_STRIDE: u64 = 8; +pub const MMIO_SIZE: u64 = 2 * A0_STRIDE; + +pub mod ctl { + pub const PIN: u8 = 0x80; + pub const ESO: u8 = 0x40; + pub const ES1: u8 = 0x20; + pub const ES2: u8 = 0x10; + pub const ENI: u8 = 0x08; + pub const STA: u8 = 0x04; + pub const STO: u8 = 0x02; + pub const ACK: u8 = 0x01; +} + +pub mod sts { + pub const PIN: u8 = 0x80; + pub const STS: u8 = 0x20; + pub const BER: u8 = 0x10; + pub const LRB: u8 = 0x08; + pub const AAS: u8 = 0x04; + pub const LAB: u8 = 0x02; + /// Set means the bus is not busy. + pub const BB: u8 = 0x01; +} + +enum Reg { + OwnAddress, + Clock, + Vector, + Data, +} + +struct State { + bus: I2cBus, + /// S0', seven bits, right-justified. + own_address: u8, + /// S2, oscillator divider and bus speed. Stored only. + clock: u8, + /// S3, interrupt vector. Stored only. + vector: u8, + /// S0, the byte shifted in or out. + data: u8, + control: u8, + status: u8, + /// The core accepted the start and the open transfer must be ended. + transfer: bool, + /// Current direction is slave to master. + recv: bool, + /// A repeated start awaits its address byte. + start_pending: bool, + /// An idle data-register write awaits an immediate first start. + start_address_staged: bool, + /// Outbound address byte kept apart from the readable S0. + start_address: u8, + /// The matched seven-bit address while `transfer` is set. + cur_addr7: u8, + /// The chip's own slave half is part of the open transfer, and whether + /// that is a directed hit on the own address rather than a general call. + rx_current: bool, + rx_directed: bool, + /// Interrupt output level last reported to the line. + irq_level: bool, +} + +impl State { + fn new() -> Self { + Self { + bus: I2cBus::new(), + own_address: 0, + clock: 0, + vector: 0, + data: 0, + control: 0, + // Out of reset the chip is idle and the bus is free. + status: sts::PIN | sts::BB, + transfer: false, + recv: false, + start_pending: false, + start_address_staged: false, + start_address: 0, + cur_addr7: 0, + rx_current: false, + rx_directed: false, + irq_level: false, + } + } + + /// Which register the A0-low address selects. ES2 keeps selecting even + /// with the serial interface enabled; only ES1 stops meaning anything. + fn selected(&self) -> Reg { + let c = self.control; + if c & ctl::ES2 != 0 && c & ctl::ES1 == 0 { + Reg::Vector + } else if c & ctl::ESO != 0 { + if c & ctl::ES2 != 0 { + Reg::Vector + } else { + Reg::Data + } + } else if c & ctl::ES1 != 0 { + Reg::Clock + } else { + Reg::OwnAddress + } + } + + /// PIN is active low in the chip's naming: clear means completed. + fn irq_wanted(&self) -> bool { + self.control & ctl::ENI != 0 && self.status & sts::PIN == 0 + } + + /// Mark the byte transfer finished, recording whether the far end acked. + fn complete(&mut self, nacked: bool) { + self.status &= !sts::PIN; + if nacked { + self.status |= sts::LRB; + } else { + self.status &= !sts::LRB; + } + } + + fn bus_busy(&self) -> bool { + self.bus.busy() || self.rx_current + } + + fn bus_end(&mut self) { + self.bus.end(); + self.rx_current = false; + self.rx_directed = false; + } + + /// Copy an address byte no target answered into S0, when the grant + /// protocol asks for it. + fn observe_address(&mut self, addr_byte: u8) { + if self.control & ctl::ESO == 0 + || self.own_address == 0 + || addr_byte >> 1 == self.own_address + || addr_byte == 0 + { + return; + } + self.data = addr_byte; + } + + /// Start a transfer on the bus, the way the I2C core does. Answers + /// whether the address was acknowledged. + fn bus_start(&mut self, addr7: u8, recv: bool) -> bool { + let fresh = !self.bus_busy(); + if fresh { + self.bus.scan(addr7); + // The chip's slave half joins a general call, and answers its own + // address when no target claimed it first. + if self.bus.is_broadcast() { + self.rx_current = true; + self.rx_directed = false; + } else if !self.bus.busy() && addr7 == self.own_address { + self.rx_current = true; + self.rx_directed = true; + } + } + if !self.bus.busy() && !self.rx_current { + if fresh && (0xe0..=0xef).contains(&((addr7 << 1) | recv as u8)) { + self.observe_address((addr7 << 1) | recv as u8); + } + return false; + } + if !self.bus.start_selected(addr7, recv, fresh) { + if fresh { + self.rx_current = false; + self.rx_directed = false; + } + return false; + } + // Only a directed hit on the programmed address raises AAS. + if self.rx_directed { + self.status |= sts::AAS; + } + true + } + + fn bus_send(&mut self, byte: u8) -> bool { + let ok = self.bus.send(byte); + if self.rx_current { + self.data = byte; + } + ok + } + + fn bus_recv(&mut self) -> u8 { + if self.bus.busy() { + self.bus.recv() + } else if self.rx_current && !self.bus.is_broadcast() { + self.data + } else { + 0xff + } + } + + fn stop(&mut self) { + if self.transfer { + self.bus_end(); + } + self.transfer = false; + self.recv = false; + self.start_pending = false; + self.start_address_staged = false; + self.status |= sts::BB; + } + + /// Issue a start with `addr` as the address-and-direction byte. + fn start(&mut self, addr: u8) { + let recv = addr & 1 != 0; + let addr7 = addr >> 1; + + self.start_address_staged = false; + // The core rescans only when nothing is selected, so an address change + // must end the old selection here; ending unconditionally would tell a + // target mid-command it was finished. + if self.transfer && addr7 != self.cur_addr7 { + self.bus_end(); + self.transfer = false; + } + self.cur_addr7 = addr7; + self.recv = recv; + self.status &= !sts::BB; + + let acked = self.bus_start(addr7, recv); + if !acked && self.transfer { + // A refused same-address repeated start is left open for the + // caller to end; held open it would keep the bus busy for ever. + self.bus_end(); + } + self.transfer = acked; + // Normal: firmware probes an invalid address in order to be refused. + self.complete(!acked); + } + + fn write_data(&mut self, val: u8) { + if self.start_pending { + self.start_pending = false; + self.start(val); + return; + } + if self.status & sts::BB != 0 { + // Bus free: the address byte staged ahead of the control write. + self.data = val; + self.start_address = val; + self.start_address_staged = true; + return; + } + if self.recv { + dlog!(LogModule::Bedrock, "pcf8584: data written during a receive transfer, ignored"); + return; + } + self.status |= sts::PIN; + // A refused start leaves nothing selected; the refusal is remembered + // here because sending over an empty selection would report success. + let acked = self.transfer && self.bus_send(val); + self.data = val; + self.complete(!acked); + } + + fn read_data(&mut self) -> u8 { + let val = self.data; + if self.transfer && self.recv { + // The held byte goes out first, then the fetch: fetching first + // would be off by one for the whole packet. + if self.control & ctl::ACK == 0 { + // ACK cleared means the driver wants this to be the last byte. + self.bus.nack(); + } + self.data = self.bus_recv(); + self.complete(false); + } + val + } + + fn write_control(&mut self, val: u8) { + self.control = val; + + if val & ctl::PIN != 0 { + // PIN from the host is the idle request. It is also written alone + // to select a configuration register, so it must not disturb a + // transfer. + self.status |= sts::PIN; + self.status &= !(sts::BER | sts::LAB | sts::AAS); + } + if val & ctl::STA != 0 { + if self.status & sts::BB == 0 { + // Repeated start: the address byte has not been written yet. + self.start_pending = true; + self.status |= sts::PIN; + } else { + let addr = if self.start_address_staged { + self.start_address + } else { + self.data + }; + // First start: the address is already staged in the register. + self.status |= sts::PIN; + self.start(addr); + } + return; + } + if val & ctl::STO != 0 { + self.stop(); + self.status |= sts::PIN; + } + } + + // The Hub decodes only bit 3 of the offset (the A0 pin): the bus window + // bounds the offset, and bit 3 alone picks the register pair. + fn read(&mut self, addr: u64) -> u8 { + if addr & A0_STRIDE != 0 { + // Write-only in the documentation with the serial interface off; + // real parts answer with the control bits plus the PIN state. + if self.control & ctl::ESO != 0 { + return self.status; + } + return (self.control & !sts::PIN) | (self.status & sts::PIN); + } + match self.selected() { + Reg::OwnAddress => self.own_address, + Reg::Clock => self.clock, + Reg::Vector => self.vector, + Reg::Data => self.read_data(), + } + } + + fn write(&mut self, addr: u64, val: u8) { + if addr & A0_STRIDE != 0 { + self.write_control(val); + return; + } + match self.selected() { + // Seven bits, right-justified; stored as written so the + // firmware's read-back liveness check sees what it wrote. + Reg::OwnAddress => self.own_address = val & 0x7f, + Reg::Clock => self.clock = val & 0x1f, + Reg::Vector => self.vector = val, + Reg::Data => self.write_data(val), + } + } + + /// Reset releases an untimed transfer before forgetting that it owns it. + fn reset(&mut self) { + if self.transfer && self.bus_busy() { + self.bus_end(); + } + // The line level survives so the access that follows reports a drop. + let (bus, irq_level) = (std::mem::take(&mut self.bus), self.irq_level); + *self = Self { bus, irq_level, ..Self::new() }; + } +} + +pub struct Pcf8584 { + state: Mutex, + busy: BusBusy, + irq: Mutex, +} + +impl Pcf8584 { + /// An adapter on a bus of its own. `busy` is the flag the bus's targets + /// read; the adapter keeps it current. + pub fn new(bus: I2cBus, busy: BusBusy) -> Self { + let mut state = State::new(); + state.bus = bus; + Self { + state: Mutex::new(state), + busy, + irq: Mutex::new(IrqLine::unconnected()), + } + } + + /// Where the chip's interrupt output leads. The Origin 200 Hub does not + /// connect it; software polls. + pub fn set_irq(&self, irq: IrqLine) { + *self.irq.lock() = irq; + } + + /// Run `f` with the bus servicing around it: targets are let advance + /// first (a grant they publish lands before the access), and what they + /// ask afterwards (releasing the bus) is carried out once it is done. + fn access(&self, f: impl FnOnce(&mut State) -> R) -> R { + let (value, level) = { + let mut st = self.state.lock(); + Self::service(&mut st); + let value = f(&mut st); + Self::service(&mut st); + self.busy.store(st.bus_busy(), Ordering::Relaxed); + let level = st.irq_wanted(); + let changed = level != st.irq_level; + st.irq_level = level; + (value, changed.then_some(level)) + }; + // Drive the line with no lock held, ours or the line slot's: the + // controller behind it may call back into the Hub. + if let Some(level) = level { + let irq = self.irq.lock().clone(); + irq.set(level); + } + value + } + + fn service(st: &mut State) { + for i in 0..st.bus.targets().len() { + let service: Service = st.bus.targets()[i].service(); + if service.release_bus && st.bus_busy() { + st.bus_end(); + } + if let Some(byte) = service.grant { + // An address-only transaction nobody acknowledges; it cannot + // open while a transfer holds the bus. + if !st.bus_busy() { + st.bus_start(byte >> 1, byte & 1 != 0); + st.bus_end(); + } + } + } + } + + pub fn read_register(&self, addr: u64) -> u8 { + self.access(|st| st.read(addr)) + } + + pub fn write_register(&self, addr: u64, val: u8) { + self.access(|st| st.write(addr, val)); + } +} + +impl Resettable for Pcf8584 { + fn power_on(&self) { + self.access(|st| st.reset()); + } +} + +/// Every width is one register access: the firmware reads the data register +/// with 8-byte loads, and splitting one would advance the receive pipeline +/// twice. The register value is the low byte of the access. +impl BusDevice for Pcf8584 { + fn read8(&self, addr: u64) -> BusRead8 { + if addr >= MMIO_SIZE { + return BusRead8::err(); + } + BusRead8::ok(self.read_register(addr)) + } + + fn write8(&self, addr: u64, val: u8) -> u32 { + self.write_checked(addr, val) + } + + fn read16(&self, addr: u64) -> BusRead16 { + if addr >= MMIO_SIZE { + return BusRead16::err(); + } + BusRead16::ok(self.read_register(addr) as u16) + } + + fn write16(&self, addr: u64, val: u16) -> u32 { + self.write_checked(addr, val as u8) + } + + fn read32(&self, addr: u64) -> BusRead32 { + if addr >= MMIO_SIZE { + return BusRead32::err(); + } + BusRead32::ok(self.read_register(addr) as u32) + } + + fn write32(&self, addr: u64, val: u32) -> u32 { + self.write_checked(addr, val as u8) + } + + fn read64(&self, addr: u64) -> BusRead64 { + if addr >= MMIO_SIZE { + return BusRead64::err(); + } + BusRead64::ok(self.read_register(addr) as u64) + } + + fn write64(&self, addr: u64, val: u64) -> u32 { + self.write_checked(addr, val as u8) + } +} + +impl Pcf8584 { + fn write_checked(&self, addr: u64, val: u8) -> u32 { + if addr >= MMIO_SIZE { + return BUS_ERR; + } + self.write_register(addr, val); + BUS_OK + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/pcf8584/tests.rs b/src/dev/pcf8584/tests.rs new file mode 100644 index 000000000..4c207744b --- /dev/null +++ b/src/dev/pcf8584/tests.rs @@ -0,0 +1,423 @@ +//! Register-contract tests of the PCF8584: the selection matrix, the own +//! address, the slave half, and master transfers against a stand-in target. + +use super::*; +use crate::dev::i2c::I2cTarget; +use crate::irq::LevelProbe; +use std::sync::Arc; + +const DATA: u64 = 0; +const CTRL: u64 = A0_STRIDE; + +/// A target that gives back what it was sent, in order, once read. +struct Echo { + address: u8, + state: Mutex<(Vec, usize)>, +} + +impl Echo { + fn new(address: u8) -> Arc { + Arc::new(Self { address, state: Mutex::new((Vec::new(), 0)) }) + } +} + +impl I2cTarget for Echo { + fn claims(&self, addr7: u8) -> bool { + addr7 == self.address + } + + fn start(&self, _addr7: u8, recv: bool) -> bool { + let mut st = self.state.lock(); + if recv { + st.1 = 0; + } else { + st.0.clear(); + } + true + } + + fn send(&self, byte: u8) -> bool { + self.state.lock().0.push(byte); + true + } + + fn recv(&self) -> u8 { + let mut st = self.state.lock(); + let byte = st.0.get(st.1).copied().unwrap_or(0xff); + st.1 += 1; + byte + } +} + +/// A target that publishes a grant when serviced. +struct Granter(Mutex>); + +impl I2cTarget for Granter { + fn claims(&self, _addr7: u8) -> bool { + false + } + + fn answers_general_call(&self) -> bool { + false + } + + fn send(&self, _byte: u8) -> bool { + true + } + + fn recv(&self) -> u8 { + 0xff + } + + fn service(&self) -> Service { + Service { grant: self.0.lock().take(), release_bus: false } + } +} + +fn chip(targets: Vec>) -> Pcf8584 { + let mut bus = I2cBus::new(); + for t in targets { + bus.add(t); + } + Pcf8584::new(bus, BusBusy::default()) +} + +fn status(p: &Pcf8584) -> u8 { + p.read_register(CTRL) +} + +#[test] +fn reset_state_is_idle_with_the_bus_free() { + let p = chip(vec![]); + // ESO clear: control reads back its bits and the live PIN. + assert_eq!(status(&p), sts::PIN); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + assert_eq!(status(&p), sts::PIN | sts::BB); +} + +#[test] +fn register_selection_matrix() { + let p = chip(vec![]); + // ESO=0 ES1=0 ES2=0: own address. + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x11); + assert_eq!(p.read_register(DATA), 0x11); + + // ESO=0 ES1=1 ES2=0: clock control. A model conflating this with own + // address would return 0x11 here instead. + p.write_register(CTRL, ctl::PIN | ctl::ES1); + p.write_register(DATA, 0x1c); + assert_eq!(p.read_register(DATA), 0x1c); + + // ESO=0 ES1=0 ES2=1: interrupt vector. + p.write_register(CTRL, ctl::PIN | ctl::ES2); + p.write_register(DATA, 0x33); + assert_eq!(p.read_register(DATA), 0x33); + + // ESO=1 ES1=0 ES2=0: the working data register. Outside any transfer, a + // plain byte store. + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x55); + assert_eq!(p.read_register(DATA), 0x55); + + // ESO=1 ES2=1: still the vector. A model treating "ESO set" as "always + // data" gets this row wrong. + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ES2); + assert_eq!(p.read_register(DATA), 0x33); + + // Own address and clock control are undisturbed by any of the above. + p.write_register(CTRL, ctl::PIN); + assert_eq!(p.read_register(DATA), 0x11); + p.write_register(CTRL, ctl::PIN | ctl::ES1); + assert_eq!(p.read_register(DATA), 0x1c); +} + +#[test] +fn own_address_round_trips() { + let p = chip(vec![]); + p.write_register(CTRL, ctl::PIN); + // The init sequence reads back what it wrote. + p.write_register(DATA, 0x7f); + assert_eq!(p.read_register(DATA), 0x7f); + // A constant-answering register would pass the half above. + p.write_register(DATA, 0x0a); + assert_eq!(p.read_register(DATA), 0x0a); + // Seven bits only. + p.write_register(DATA, 0xff); + assert_eq!(p.read_register(DATA), 0x7f); +} + +#[test] +fn clock_control_stores_what_was_written() { + let p = chip(vec![]); + p.write_register(CTRL, ctl::PIN | ctl::ES1); + // 12 MHz / 90 kHz is the only combination the PROM writes. + p.write_register(DATA, 0x1c); + assert_eq!(p.read_register(DATA), 0x1c); + // 6 MHz / 11 kHz: a model that hard-coded 0x1c would fail only here. + p.write_register(DATA, 0x0a); + assert_eq!(p.read_register(DATA), 0x0a); +} + +#[test] +fn the_prom_init_sequence_leaves_eso_and_the_address() { + let p = chip(vec![]); + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x7f); + p.write_register(CTRL, ctl::PIN | ctl::ES1); + p.write_register(DATA, 0x1c); + p.write_register(CTRL, ctl::PIN); + assert_eq!(p.read_register(DATA), 0x7f); + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x0a); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + + // The serial interface is enabled: the control address now answers the + // status half, idle. + assert_eq!(status(&p), sts::PIN | sts::BB); + // And the real address is still there once ESO is cleared to look. + p.write_register(CTRL, ctl::PIN); + assert_eq!(p.read_register(DATA), 0x0a); +} + +#[test] +fn a_silent_chip_reads_stably() { + let p = chip(vec![]); + // Stability tells a real register from writes leaking straight back. + let first = p.read_register(DATA); + assert_eq!(p.read_register(DATA), first); + assert_eq!(p.read_register(DATA), first); +} + +#[test] +fn the_programmed_own_address_answers_as_a_slave() { + let p = chip(vec![]); + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x2a); + assert_eq!(p.read_register(DATA), 0x2a); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + + // Control: a neighbouring unprogrammed address still finds nobody. + p.write_register(DATA, 0x2b << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(status(&p) & sts::LRB, sts::LRB); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + + // The programmed address is acknowledged and reports AAS. + p.write_register(DATA, 0x2a << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(status(&p) & sts::LRB, 0); + assert_eq!(status(&p) & sts::AAS, sts::AAS); + + // The PIN written with the stop clears the addressed-as-slave state. + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert_eq!(status(&p) & sts::AAS, 0); +} + +#[test] +fn a_general_call_is_acknowledged_without_addressing_the_chip_as_a_slave() { + let p = chip(vec![]); + // Program an own address so a directed hit would be the AAS case. + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x2a); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + + // A start to the general-call address reaches the broadcast receiver. + p.write_register(DATA, 0x00); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + // The general call is acknowledged: a receiver exists... + assert_eq!(status(&p) & sts::LRB, 0); + // ...but it must not report the chip addressed as a slave: only a + // directed hit on the programmed address is an AAS event. When a + // broadcast raised AAS too, the firmware's arbitration poll wedged. + assert_eq!(status(&p) & sts::AAS, 0); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); +} + +#[test] +fn a_general_call_reaches_the_targets_that_answer_it_and_data_lands_in_s0() { + let echo = Echo::new(0x08); + let p = chip(vec![echo.clone()]); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x00); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + p.write_register(DATA, 0x42); + assert_eq!(status(&p) & sts::LRB, 0); + assert_eq!(echo.state.lock().0, [0x42]); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); +} + +#[test] +fn a_send_after_a_refused_start_stays_nacked() { + let p = chip(vec![]); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + + // Start to an address nothing answers. + p.write_register(DATA, 0x2b << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(status(&p) & sts::LRB, sts::LRB); + // The transaction is open at the chip level: the bus reads busy. + assert_eq!(status(&p) & sts::BB, 0); + + // A data byte into the void must stay NAKed. + p.write_register(DATA, 0x99); + assert_eq!(status(&p) & sts::LRB, sts::LRB); + + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert_eq!(status(&p) & sts::BB, sts::BB); +} + +/// The closing invalid-address step, checked by identity: the ACK bit alone +/// reads 0 for bug and no-bug alike, so an echo target at 0x7f must hand back +/// the bytes only this test sends. +#[test] +fn the_closing_invalid_address_step_shifts_and_orders_correctly() { + let p = chip(vec![Echo::new(0x7f)]); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + + // Open a transfer to an unanswered address (not 0x08: the ELSC ACKs). + p.write_register(DATA, 0x12); // addr 0x09, write + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(status(&p) & sts::LRB, sts::LRB); + + // Control first with PIN absent, then the address, then the payload. + p.write_register(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + p.write_register(DATA, 0xfe); + p.write_register(DATA, 0xaa); + p.write_register(DATA, 0xbb); + p.write_register(DATA, 0xcc); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + + // One discard first: the fetch attaches to the read, not the start. + p.write_register(DATA, 0xff); // addr 0x7f, read + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(p.read_register(DATA), 0xff); // discarded + assert_eq!(p.read_register(DATA), 0xaa); + assert_eq!(p.read_register(DATA), 0xbb); + p.write_register(CTRL, ctl::ESO); // clear ACK: last byte + assert_eq!(p.read_register(DATA), 0xcc); + + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert_eq!(status(&p) & sts::BB, sts::BB); +} + +#[test] +fn a_repeated_start_to_another_address_ends_the_old_match_first() { + let a = Echo::new(0x10); + let b = Echo::new(0x11); + let p = chip(vec![a.clone(), b.clone()]); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x10 << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + p.write_register(DATA, 1); + // Repeated start, new target: the first sees a clean end, the second a + // clean start. + p.write_register(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + p.write_register(DATA, 0x11 << 1); + assert_eq!(status(&p) & sts::LRB, 0); + p.write_register(DATA, 2); + assert_eq!(a.state.lock().0, [1]); + assert_eq!(b.state.lock().0, [2]); +} + +#[test] +fn an_unmatched_grant_address_lands_in_s0_only_with_eso_and_a_nonzero_own_address() { + let granter = Arc::new(Granter(Mutex::new(None))); + let p = chip(vec![granter.clone()]); + + // Own address zero: passive buffering only. + p.write_register(CTRL, ctl::PIN | ctl::ESO); + *granter.0.lock() = Some(0xe3); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x77); + assert_ne!(p.read_register(DATA), 0xe3); + + // Program an own address, enable the interface. + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x09); + // ESO still clear: nothing is copied. + *granter.0.lock() = Some(0xe4); + assert_eq!(p.read_register(DATA), 0x09); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + // The grant publishes at the start of the next access. + *granter.0.lock() = Some(0xe5); + assert_eq!(p.read_register(DATA), 0xe5); + // A byte outside 0xe0-0xef is not a grant. + *granter.0.lock() = Some(0x44); + assert_eq!(p.read_register(DATA), 0xe5); + // The chip's own address on the wire is a directed hit, not a grant. + *granter.0.lock() = Some(0x09 << 1); + assert_eq!(p.read_register(DATA), 0xe5); +} + +#[test] +fn a_grant_does_not_interrupt_an_open_transfer() { + let granter = Arc::new(Granter(Mutex::new(None))); + let p = chip(vec![Echo::new(0x10), granter.clone()]); + p.write_register(CTRL, ctl::PIN); + p.write_register(DATA, 0x09); + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x10 << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + *granter.0.lock() = Some(0xe2); + // The bus is busy, so the address is dropped. + assert_ne!(p.read_register(DATA), 0xe2); +} + +#[test] +fn the_interrupt_output_follows_eni_and_pin() { + let p = chip(vec![Echo::new(0x10)]); + let (probe, line) = LevelProbe::line(); + p.set_irq(line); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI); + // Idle: PIN set, no interrupt. + assert!(!probe.level()); + p.write_register(DATA, 0x10 << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI | ctl::STA | ctl::ACK); + // A completed byte clears PIN and raises the line. + assert!(probe.level()); + // Writing PIN clears the request. + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI); + assert!(!probe.level()); + // ENI off masks it. + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI | ctl::STO); + p.write_register(CTRL, ctl::ESO); + assert!(!probe.level()); +} + +#[test] +fn a_reset_releases_an_open_transfer_and_drops_a_raised_line() { + let echo = Echo::new(0x10); + let p = chip(vec![echo]); + let (probe, line) = LevelProbe::line(); + p.set_irq(line); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI); + p.write_register(DATA, 0x10 << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::ENI | ctl::STA | ctl::ACK); + assert!(probe.level()); + p.power_on(); + assert!(!probe.level()); + assert_eq!(status(&p), sts::PIN); +} + +#[test] +fn every_access_width_is_one_register_access() { + let p = chip(vec![Echo::new(0x10)]); + assert_eq!(p.write64(CTRL, (ctl::PIN | ctl::ESO) as u64), BUS_OK); + assert_eq!(p.write64(DATA, 0x10 << 1), BUS_OK); + assert_eq!(p.write64(CTRL, (ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK) as u64), BUS_OK); + assert_eq!(p.write32(DATA, 0x5a), BUS_OK); + assert_eq!(p.write64(CTRL, (ctl::PIN | ctl::ESO | ctl::STO) as u64), BUS_OK); + assert_eq!(p.write64(DATA, (0x10 << 1) | 1), BUS_OK); + assert_eq!( + p.write64(CTRL, (ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK) as u64), + BUS_OK + ); + // One 8-byte load of the data register: the held byte, then the fetch + // happens once, not twice. + assert_eq!(p.read64(DATA).data, ((0x10 << 1) | 1) as u64); + assert_eq!(p.read16(DATA).data, 0x5a); + assert_eq!(p.read8(CTRL).data & sts::LRB, 0); + assert!(!p.read32(MMIO_SIZE).is_ok()); + assert_eq!(p.write8(MMIO_SIZE, 0), BUS_ERR); +} From d1bc5c036f278c1f44789e600343ad1cf7372fe0 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 00:52:11 -0700 Subject: [PATCH 22/38] elsc: add the Origin 200 module system controller The Origin 200's PROM and kernel talk to a module system controller over I2C: checksummed ASCII commands for power, reset, NMI, the heartbeat watchdog and console selection, arbitration grants that tell each processor when it may use the bus, and an AUX serial port with an operator command line. Without it a guest cannot even learn the module number or power the machine off. No specification of the protocol exists, so every timing, the version text, the grant schedule and the reply text are model choices, labelled as such in the module doc. Time is virtual and run lazily from a Clock, so tests drive it with a manual clock and never wait. Power, reset and NMI are reported as events for the machine to drain. The Origin 2000 build, the second module's controller and the shared four-adapter bus are left for a module-management device. --- src/dev/elsc.rs | 2087 +++++++++++++++++++++++++++++++++++++++++ src/dev/elsc/tests.rs | 1879 +++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 3967 insertions(+) create mode 100644 src/dev/elsc.rs create mode 100644 src/dev/elsc/tests.rs diff --git a/src/dev/elsc.rs b/src/dev/elsc.rs new file mode 100644 index 000000000..6d7c0e1c0 --- /dev/null +++ b/src/dev/elsc.rs @@ -0,0 +1,2087 @@ +//! Module system controller (MSC, the ELSC) of an Origin 200 module: a +//! checksummed ASCII command and reply protocol over I2C, the arbitration +//! grants the processors wait for, and the AUX serial port. +//! +//! The controller is an I2C target at seven-bit address 0x08 on the Hub's +//! bus, next to its [`ElscNvram`]. The module's power, reset and NMI are +//! outputs, reported as [`ElscEvent`]s the machine drains with +//! [`Elsc::take_events`]. +//! +//! No specification of the controller's protocol, firmware or microcontroller +//! is public and no controller has been measured. Every timing, the version +//! text, the grant schedule, the queue depths and the reply text below are +//! the model's own choices, approximations unless a comment says otherwise: +//! +//! * the version field `V 3.1S` and its padding; +//! * the one-millisecond grant tick, the five ring positions (processor codes +//! 0 to 3, then a fifth published as code 4's byte), and that a busy tick +//! publishes nothing and does not advance; +//! * the 4 s power-up reset hold, the 1 s waits of `rst` and `nmi`, the +//! 100 ms NMI pulse, the 1 s life of a controller message, the 5 s warning +//! and the 5 s off interval of the heartbeat watchdog; +//! * the 256-byte console input queue that overwrites its oldest byte; +//! * that cold-power latch `pwr q` clears on read; +//! * that a deferred destructive command acts once its `ok` has been read; +//! * that `nic` reads 1 (no device on the one-wire line) and `sla` reports +//! from a slave are stored or logged only. +//! +//! Time is read from a `heart::Clock`, as the Bedrock devices read it, and +//! timers run lazily, whenever the bus, the serial line or [`Elsc::poll`] +//! touches the controller, so a test sets the clock by hand and never waits. +//! A request is attributed to the CPU thread that made it +//! ([`crate::cpu::smp::current_cpu`]); other threads count as CPU 0. +//! +//! Migration and snapshot state are not saved yet. +//! +//! Not modelled: the Origin 2000 build, the second module's controller and +//! the master's forwarding to it, sensors, and the Origin 2000 four-adapter +//! bus (a module-management device owns that and would drive the same +//! [`I2cTarget`] and event interfaces). +//! +//! Ported from hw/misc/sgi-elsc.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::elsc_nvram::{self, ElscNvram}; +use crate::dev::heart::Clock; +use crate::dev::i2c::{BusBusy, I2cTarget, Service}; +use crate::dev::serial_host::{null_serial_backend, SerialBackend}; +use crate::devlog::LogModule; +use crate::dlog; +use crate::traits::Resettable; +use parking_lot::Mutex; +use std::collections::VecDeque; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +/// Seven-bit I2C address the controller answers at. +pub const I2C_ADDR: u8 = 0x08; + +/// Length, text, checksum summing to zero. A length above 94 would overrun the +/// firmware's own 96-byte buffers; never emit one. +pub const MAX_TEXT: usize = 94; +pub const MAX_FRAME: usize = MAX_TEXT + 2; + +pub const RX_QUEUE_SIZE: usize = 256; +pub const DELAY_MAX: u32 = 0x258; +pub const PROCESSORS: usize = 8; + +const VERSION: &str = "V 3.1S "; +const SEE_ALL: u8 = 0xff; +const CTRL_T: u8 = 0x14; +const TAS_FREE: u8 = b'0'; +const CYCLE_DEFAULT_SECONDS: u32 = 5; +const HBT_CYCLE_SECONDS: u64 = 5; +const HBT_DEFAULT_DELAY: u32 = 30; +const HBT_WARNING: u64 = 5; +const POWER_UP_HOLD_MS: u64 = 4000; +const MESSAGE_LIFETIME_MS: u64 = 1000; +const RESET_WAIT_MS: u64 = 1000; +const NMI_WAIT_MS: u64 = 1000; +const NMI_PULSE_MS: u64 = 100; +const ARB_TICK_NS: u64 = NS_PER_MS; +const ARB_POSITIONS: u32 = 5; +const SIGNAL_BASE: u8 = 0xe0; +const NS_PER_MS: u64 = 1_000_000; + +/// What the controller does to its module and to the processors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ElscEvent { + /// Module power output: low as the module loses power, high as it + /// returns. A power-down is repeated when the controller learns that a + /// pending power-up was cancelled, so the machine asks again whether + /// anything is left to bring a module back. + Power(bool), + /// A pulse on the module's reset line (instantaneous in the model). + Reset, + /// The NMI output, high for the 100 ms of a pulse. + Nmi(bool), +} + +#[derive(Clone, Copy, Debug)] +pub struct ElscConfig { + /// Front-panel DIP switches as the `dip-switches` property gives them + /// (inverted when read back by `rsw`). + pub dip_switches: u8, + /// Whether this is the master controller, which publishes the grants. + /// A slave's role bit in `rsw` is clear and it runs no serial commands. + pub master: bool, + /// Milliseconds after start that the controller powers its module up on + /// its own (when the NVRAM says automatic power-on); zero starts the + /// module powered. + pub start_delay_ms: u32, +} + +impl Default for ElscConfig { + fn default() -> Self { + Self { dip_switches: 0, master: true, start_delay_ms: 0 } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Action { + None, + Reset, + Cycle, + Shutdown, + PowerUp, +} + +impl Action { + fn name(self) -> &'static str { + match self { + Action::Reset => "reset", + Action::Cycle => "power-cycle", + Action::Shutdown => "shutdown", + Action::PowerUp => "power-up", + Action::None => "deferred action", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Message { + None, + PdRemote, + PdCycle, + Rst, +} + +impl Message { + fn text(self) -> &'static str { + match self { + Message::PdRemote => "pd remote", + Message::PdCycle => "pd cycle", + Message::Rst => "rst", + Message::None => "", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Select { + Auto, + None, + Explicit, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Heartbeat { + Idle, + Waiting, + Warned, + Cycling, +} + +// Timer slots. +const T_ARB: usize = 0; +const T_PWR_DOWN: usize = 1; +const T_MESSAGE: usize = 2; +const T_RST: usize = 3; +const T_NMI: usize = 4; +const T_NMI_PULSE: usize = 5; +const T_HBT: usize = 6; +const T_POWER_UP: usize = 7; +const T_START: usize = 8; +const TIMERS: usize = 9; + +// Commands whose missing effect is logged once. +const UNIMP_FSC: u8 = 1 << 0; +const UNIMP_TST: u8 = 1 << 1; +const UNIMP_NIC: u8 = 1 << 3; + +fn checksum(buf: &[u8]) -> u8 { + buf.iter().fold(0u8, |s, b| s.wrapping_add(*b)) +} + +/// One request as the controller parses it. The firmware keeps the text and +/// puts a carriage return where the checksum byte was, so every command ends +/// in one: the last byte of `text` is always that carriage return. `pos` is +/// the next unread byte and the readers never move it past the end. +struct Req<'a> { + text: &'a [u8], + pos: usize, +} + +impl Req<'_> { + fn peek(&self) -> u8 { + self.text.get(self.pos).copied().unwrap_or(b'\r') + } + + fn skip(&mut self) { + if self.pos < self.text.len() { + self.pos += 1; + } + } + + /// The character reader most commands use: skip spaces and take the next + /// character. A carriage return or line feed is no argument. + fn read_char(&mut self) -> Option { + while self.peek() == b' ' { + self.skip(); + } + let c = self.peek(); + if c == b'\r' || c == b'\n' { + return None; + } + self.skip(); + Some(c) + } + + /// The hex reader: up to four digits, either case, with no leading spaces + /// skipped. A number shorter than four digits consumes the character that + /// ends it, so a following number must come after exactly one space; a + /// four-digit number consumes nothing after it. No digits at all reads as + /// no value, and the character that ended the attempt is still consumed. + fn read_hex(&mut self) -> Option { + let mut value = 0u32; + let mut digits = 0; + while digits < 4 { + let Some(d) = (self.peek() as char).to_digit(16) else { + break; + }; + value = value << 4 | d; + self.skip(); + digits += 1; + } + if digits < 4 { + self.skip(); + } + (digits > 0).then_some(value) + } + + /// A run of non-space characters, up to the end of the line. + fn read_word(&mut self) -> &[u8] { + while self.peek() == b' ' { + self.skip(); + } + let start = self.pos; + while self.pos < self.text.len() && !matches!(self.text[self.pos], b' ' | b'\r' | b'\n' | 0) { + self.pos += 1; + } + &self.text[start..self.pos] + } + + /// The two characters `sel` and `see` take, folded to uppercase. The + /// second is 0 when there is none. + fn read_selection(&mut self) -> Option<(u8, u8)> { + let first = self.read_char()?.to_ascii_uppercase(); + let second = self.read_char().map_or(0, |c| c.to_ascii_uppercase()); + Some((first, second)) + } +} + +/// A slot digit and `A` or `B` name one processor. +fn selection_processor(first: u8, second: u8) -> Option { + if !(b'1'..=b'4').contains(&first) || !matches!(second, b'A' | b'B') { + return None; + } + Some((first - b'1') * 2 + (second == b'B') as u8) +} + +fn processor_name(p: u8) -> [u8; 2] { + [b'1' + p / 2, if p & 1 != 0 { b'b' } else { b'a' }] +} + +type Handler = fn(&mut State, &mut Req); + +struct Command { + name: &'static [u8; 3], + handler: Handler, + on_i2c: bool, + on_serial: bool, +} + +const fn cmd(name: &'static [u8; 3], handler: Handler, on_i2c: bool, on_serial: bool) -> Command { + Command { name, handler, on_i2c, on_serial } +} + +/// What the Origin 200 build answers, and on which channel. A mnemonic it +/// lacks answers `err cmd` with no other effect, and so does a command on a +/// channel that does not take it: `acp` and `get` belong to the Hubs, and the +/// last six to the operator. +static COMMANDS: &[Command] = &[ + cmd(b"acp", State::handle_acp, true, false), + cmd(b"aut", State::handle_aut, true, true), + cmd(b"clr", State::handle_clr, true, true), + cmd(b"fan", State::handle_fan, true, true), + cmd(b"fsc", State::handle_fsc, true, true), + cmd(b"get", State::handle_get, true, false), + cmd(b"hbt", State::handle_hbt, true, true), + cmd(b"nic", State::handle_nic, true, true), + cmd(b"nmi", State::handle_nmi, true, true), + cmd(b"pwr", State::handle_pwr, true, true), + cmd(b"rst", State::handle_rst, true, true), + cmd(b"rsw", State::handle_rsw, true, true), + cmd(b"see", State::handle_see, true, true), + cmd(b"sel", State::handle_sel, true, true), + cmd(b"sla", State::handle_sla, true, true), + cmd(b"tas", State::handle_tas, true, true), + cmd(b"tmp", State::handle_tmp, true, true), + cmd(b"tst", State::handle_tst, true, true), + cmd(b"ver", State::handle_ver, true, true), + cmd(b"cfg", State::handle_cfg, false, true), + cmd(b"dbg", State::handle_dbg, false, true), + cmd(b"ech", State::handle_ech, false, true), + cmd(b"mod", State::handle_mod, false, true), + cmd(b"nvm", State::handle_nvm, false, true), + cmd(b"pas", State::handle_pas, false, true), +]; + +struct State { + cfg: ElscConfig, + nvram: Arc, + bus_busy: BusBusy, + + /// Virtual time of the event being handled, nanoseconds. + now: u64, + deadlines: [Option; TIMERS], + + /// The request, one byte per I2C send; one over the legal maximum so an + /// over-long guest frame lands here, not in adjacent state. + rx: Vec, + /// The response being read back, built once the request is complete. + tx: Vec, + txpos: usize, + /// The pending response's owner: the processor code whose request framed + /// it, -1 when none. While it is unconsumed the owner's grant uses + /// signal 2. + resp_owner: i32, + + // Grant ring. + arb_code: u32, + arb_signal: u8, + pending_grant: Option, + + // Power. + module_off: bool, + power_sequence: bool, + cycle_seconds: u32, + cold_power_on: bool, + + // Heartbeat watchdog. + hbt_interval: u32, + hbt_delay: u32, + hbt_stage: Heartbeat, + nmi_pulse_active: bool, + + // AUX serial port. + rx_queue: VecDeque, + serial_command: bool, + serial_line: Vec, + serial_term: u8, + serial_pending: bool, + serial_echo: bool, + supervisor: bool, + slave_addressed: bool, + + // A serial command in flight. + serial_running: bool, + serial_raw: bool, + serial_reply: Vec, + serial_action: Action, + + // Deferred destructive command. + deferred_action: Action, + deferred_committed: bool, + deferred_scheduled: bool, + + tas_lock: u8, + fan_high: bool, + fsc_present: bool, + short_timeouts: bool, + unimp_logged: u8, + slave_link: bool, + slave_temp: u8, + slave_report: u8, + + message: Message, + message_mask: u8, + + sel_mode: Select, + sel_processor: u8, + see_processor: u8, + console_sender: i32, + + /// Bytes for the AUX port, sent once the lock is released. + out: Vec, + events: Vec, + release_bus: bool, +} + +impl State { + fn new(cfg: ElscConfig, nvram: Arc, bus_busy: BusBusy) -> Self { + Self { + cfg, + nvram, + bus_busy, + now: 0, + deadlines: [None; TIMERS], + rx: Vec::new(), + tx: Vec::new(), + txpos: 0, + resp_owner: -1, + arb_code: 0, + arb_signal: 0, + pending_grant: None, + module_off: false, + power_sequence: false, + cycle_seconds: CYCLE_DEFAULT_SECONDS, + // Set at the controller's first power-up and again whenever its + // module powers up; no reset re-arms it. + cold_power_on: true, + hbt_interval: 0, + hbt_delay: 0, + hbt_stage: Heartbeat::Idle, + nmi_pulse_active: false, + rx_queue: VecDeque::new(), + serial_command: false, + serial_line: Vec::new(), + serial_term: 0, + serial_pending: false, + serial_echo: true, + supervisor: false, + slave_addressed: false, + serial_running: false, + serial_raw: false, + serial_reply: Vec::new(), + serial_action: Action::None, + deferred_action: Action::None, + deferred_committed: false, + deferred_scheduled: false, + tas_lock: TAS_FREE, + fan_high: false, + fsc_present: false, + short_timeouts: false, + unimp_logged: 0, + slave_link: false, + slave_temp: b'n', + slave_report: 0, + message: Message::None, + message_mask: 0, + sel_mode: Select::Auto, + sel_processor: 0, + see_processor: SEE_ALL, + console_sender: -1, + out: Vec::new(), + events: Vec::new(), + release_bus: false, + } + } + + // ---- time ----------------------------------------------------------- + + fn arm_ms(&mut self, timer: usize, ms: u64) { + self.deadlines[timer] = Some(self.now + ms * NS_PER_MS); + } + + fn arm_secs(&mut self, timer: usize, secs: u64) { + self.arm_ms(timer, secs * 1000); + } + + fn pending(&self, timer: usize) -> bool { + self.deadlines[timer].is_some() + } + + fn cancel(&mut self, timer: usize) { + self.deadlines[timer] = None; + } + + /// Run every timer due by `clock_now`, oldest first, each at its own + /// deadline so a long gap replays the same sequence a steady clock gives. + fn advance(&mut self, clock_now: u64) { + if clock_now < self.now { + return; + } + while let Some((deadline, timer)) = self + .deadlines + .iter() + .enumerate() + .filter_map(|(i, d)| d.filter(|d| *d <= clock_now).map(|d| (d, i))) + .min() + { + self.deadlines[timer] = None; + self.now = deadline.max(self.now); + self.fire(timer, deadline, clock_now); + } + self.now = clock_now; + } + + fn fire(&mut self, timer: usize, deadline: u64, clock_now: u64) { + match timer { + T_ARB => self.arb_tick(deadline, clock_now), + T_PWR_DOWN => self.pwr_down_expired(), + T_MESSAGE => { + self.message = Message::None; + self.message_mask = 0; + } + T_RST => self.reset_wait_expired(), + T_NMI => self.pulse_nmi(), + T_NMI_PULSE => { + self.nmi_pulse_active = false; + self.events.push(ElscEvent::Nmi(false)); + self.tas_lock = TAS_FREE; + self.reinitialize(); + } + T_HBT => self.hbt_expired(), + T_POWER_UP => self.power_up_expired(), + T_START => self.start_expired(), + _ => unreachable!(), + } + } + + // ---- serial line ---------------------------------------------------- + + fn serial_write(&mut self, bytes: &[u8]) { + self.out.extend_from_slice(bytes); + } + + fn serial_puts(&mut self, text: &str) { + self.serial_write(text.as_bytes()); + } + + // ---- framing -------------------------------------------------------- + + /// A frame above `MAX_TEXT` overruns the firmware's buffer; a model + /// invariant, not guest-triggerable. + fn respond(&mut self, text: &[u8]) { + assert!(text.len() <= MAX_TEXT); + // A serial command's reply goes to the serial line instead. + if self.serial_running { + self.serial_reply = text.to_vec(); + return; + } + self.tx.clear(); + self.tx.push(text.len() as u8); + self.tx.extend_from_slice(text); + self.tx.push(checksum(&self.tx).wrapping_neg()); + self.txpos = 0; + } + + fn respond_str(&mut self, text: &str) { + self.respond(text.as_bytes()); + } + + /// A success with no value: the firmware's three bytes `ok `, trailing + /// space included. IRIX accepts `ok` followed by a space or the end. + fn respond_ok(&mut self) { + self.respond_str("ok "); + } + + // ---- power ---------------------------------------------------------- + + fn powered(&self) -> bool { + !self.module_off + } + + /// A power-up that was to come may have just been cancelled. On an + /// unpowered module say again that the power is off, so the machine asks + /// whether anything is left to power a module up. + fn recheck_shutdown(&mut self) { + if !self.powered() { + self.events.push(ElscEvent::Power(false)); + } + } + + /// Whether module power is due back without anyone asking: a power-up + /// countdown, or a heartbeat watchdog that will power-cycle the module. + fn power_return_pending(&self) -> bool { + if self.pending(T_POWER_UP) || self.pending(T_START) { + return true; + } + if !self.pending(T_HBT) { + return false; + } + match self.hbt_stage { + Heartbeat::Waiting | Heartbeat::Warned => self.hbt_delay != 0, + Heartbeat::Cycling => true, + Heartbeat::Idle => false, + } + } + + /// Clear the protocol queues and nothing else. An in-flight request is + /// abandoned; an action already acknowledged to a guest still happens. + fn reinitialize(&mut self) { + self.rx.clear(); + self.tx.clear(); + self.txpos = 0; + self.resp_owner = -1; + self.rx_queue.clear(); + self.slave_addressed = false; + } + + /// Count down to power-up: the off interval of a power cycle, measured + /// from now and replacing any countdown already running. + fn power_countdown(&mut self, seconds: u32) { + self.power_sequence = false; + self.arm_secs(T_POWER_UP, seconds as u64); + } + + /// The supplies come on with reset held, the lock is freed and queued + /// serial input goes. The module runs again when the hold ends. + fn power_sequence_start(&mut self) { + self.power_sequence = true; + self.tas_lock = TAS_FREE; + self.reinitialize(); + self.arm_ms(T_POWER_UP, POWER_UP_HOLD_MS); + } + + /// Remove module power. The controller keeps running on standby power. + fn remove_power(&mut self) { + self.cancel(T_PWR_DOWN); + if !self.powered() { + return; + } + self.module_off = true; + self.reinitialize(); + self.release_bus = true; + self.events.push(ElscEvent::Power(false)); + } + + fn power_up_complete(&mut self) { + self.power_sequence = false; + self.cold_power_on = true; + self.module_off = false; + self.events.push(ElscEvent::Power(true)); + } + + fn power_up_expired(&mut self) { + if !self.power_sequence { + self.power_sequence_start(); + return; + } + self.power_up_complete(); + } + + /// Power goes now and comes back after the interval and the power-up + /// sequence. On a module already off it only restarts the countdown. + fn power_cycle(&mut self, seconds: u32) { + self.cancel(T_PWR_DOWN); + self.power_countdown(seconds); + self.remove_power(); + } + + /// The controller's own start-up power-up: an Origin 200 master does it + /// when its NVRAM says automatic power-on; a slave waits for its master. + fn start_expired(&mut self) { + if self.powered() { + return; + } + let auto = self + .nvram + .access(elsc_nvram::FIRST_ADDR + 7, 7, None) + .is_some_and(|c| c & elsc_nvram::CONFIG_AUTO_POWER != 0); + if !auto { + self.recheck_shutdown(); + return; + } + self.power_sequence_start(); + } + + fn pwr_down_expired(&mut self) { + // Its "ok" was read long ago, so the shutdown commits at once. It + // overrides an action whose ack is still unread; one already + // committed fires instead and the shutdown is lost. + if self.deferred_committed { + dlog!( + LogModule::Bedrock, + "elsc: \"pwr d\" countdown expired while a {} is committed; that action fires instead", + self.deferred_action.name() + ); + return; + } + self.deferred_action = Action::Shutdown; + self.deferred_committed = true; + self.fire_committed(); + } + + fn pulse_nmi(&mut self) { + if !self.powered() { + // The pulse reaches no processor of an unpowered module. + self.tas_lock = TAS_FREE; + self.reinitialize(); + return; + } + if self.nmi_pulse_active { + dlog!(LogModule::Bedrock, "elsc: NMI requested during an NMI pulse"); + return; + } + self.nmi_pulse_active = true; + self.events.push(ElscEvent::Nmi(true)); + self.arm_ms(T_NMI_PULSE, NMI_PULSE_MS); + } + + fn reset_wait_expired(&mut self) { + self.tas_lock = TAS_FREE; + self.release_bus = true; + self.events.push(ElscEvent::Reset); + } + + fn hbt_arm(&mut self, seconds: u64) { + self.arm_secs(T_HBT, seconds); + } + + fn hbt_stop(&mut self) { + self.hbt_interval = 0; + self.hbt_stage = Heartbeat::Idle; + self.cancel(T_HBT); + } + + /// The watchdog's timer ran out. Waiting: warn, and give the host five + /// more seconds. Warned: pulse the NMI at once and start the delay. + /// Cycling: power the module off and on and go idle with the interval + /// still set. Nothing here looks at module power, so an armed watchdog + /// brings back a module `pwr d` turned off. + fn hbt_expired(&mut self) { + match self.hbt_stage { + Heartbeat::Waiting => { + self.hbt_stage = Heartbeat::Warned; + self.hbt_arm(HBT_WARNING); + } + Heartbeat::Warned => { + self.pulse_nmi(); + self.hbt_stage = Heartbeat::Cycling; + if self.hbt_delay != 0 { + self.hbt_arm(self.hbt_delay as u64); + } + } + Heartbeat::Cycling => { + self.hbt_stage = Heartbeat::Idle; + self.power_cycle(HBT_CYCLE_SECONDS as u32); + } + Heartbeat::Idle => {} + } + } + + // ---- controller messages ------------------------------------------- + + /// Queue a message for the Hubs. It is announced to every processor code + /// until that processor fetches it with `get` or the second runs out; a + /// later message replaces it. The Origin 200 build also prints each one on + /// its serial line. + fn post_message(&mut self, message: Message) { + self.message = message; + self.serial_puts("\x14\r\nMSC: "); + self.serial_puts(message.text()); + self.serial_puts("\x14\r"); + self.arm_ms(T_MESSAGE, MESSAGE_LIFETIME_MS); + self.message_mask = 0xff; + } + + /// Whether the message is announced to a processor, which then has it. + fn take_message(&mut self, processor: i32) -> bool { + if self.message == Message::None || !(0..PROCESSORS as i32).contains(&processor) { + return false; + } + let bit = 1u8 << processor; + if self.message_mask & bit == 0 { + return false; + } + self.message_mask &= !bit; + true + } + + // ---- console selection --------------------------------------------- + + /// The processor queued console input is announced to, or -1 with input + /// selection disabled. + fn console_target(&self) -> i32 { + match self.sel_mode { + Select::Explicit => self.sel_processor as i32, + Select::None => -1, + Select::Auto if (0..PROCESSORS as i32).contains(&self.console_sender) => self.console_sender, + Select::Auto => -1, + } + } + + /// A new input selection discards queued serial input not yet fetched. + fn select_input(&mut self, mode: Select, processor: u8) { + self.sel_mode = mode; + if mode == Select::Explicit { + self.sel_processor = processor; + } + self.rx_queue.clear(); + } + + fn queue_input(&mut self, c: u8) { + if self.rx_queue.len() == RX_QUEUE_SIZE { + self.rx_queue.pop_front(); + } + self.rx_queue.push_back(c); + } + + // ---- grants --------------------------------------------------------- + + /// Whether something waits for the given code's processor: a controller + /// message, console input for the console processor, or a response its + /// requester has not yet consumed. + fn signal2(&self, code: u32) -> bool { + (code < PROCESSORS as u32 && self.message_mask & (1 << code) != 0) + || (!self.rx_queue.is_empty() && self.console_target() == code as i32) + || (self.resp_owner >= 0 && code == self.resp_owner as u32 && self.txpos < self.tx.len()) + } + + /// Whether a tick would publish nothing: the bus is busy, or a serial + /// command is waiting for it (which also stops the grants). + fn arb_blocked(&self) -> bool { + self.bus_busy.load(Ordering::Relaxed) || (self.serial_pending && self.message_mask == 0) + } + + /// Publish the next position's grant, which stays readable until the next + /// tick replaces it. + fn arb_publish(&mut self) { + let code = self.arb_code; + let signal1 = SIGNAL_BASE + code as u8; + self.arb_signal = if self.signal2(code) { signal1 + 8 } else { signal1 }; + self.pending_grant = Some(self.arb_signal); + self.arb_code = (code + 1) % ARB_POSITIONS; + } + + /// One periodic tick. A tick that finds the bus busy publishes nothing and + /// does not advance, and the controller waits for its next tick rather + /// than publishing as soon as the bus frees. + /// + /// The ring is replayed on its one-millisecond grid when a gap is long, + /// but nothing between two events changes what a tick sees, so whole + /// revolutions (or, while blocked, every tick) up to the next other timer + /// are skipped in one step. + fn arb_tick(&mut self, deadline: u64, clock_now: u64) { + let blocked = self.arb_blocked(); + let horizon = self + .deadlines + .iter() + .flatten() + .copied() + .filter(|d| *d <= clock_now) + .min() + .map_or(clock_now, |d| d.min(clock_now)); + let whole = horizon.saturating_sub(deadline) / ARB_TICK_NS; + let skip = if blocked { whole } else { whole / ARB_POSITIONS as u64 * ARB_POSITIONS as u64 }; + let at = deadline + skip * ARB_TICK_NS; + self.now = self.now.max(at); + if !blocked { + self.arb_publish(); + } + self.deadlines[T_ARB] = Some(at + ARB_TICK_NS); + } + + // ---- deferred destructive commands --------------------------------- + + /// Arm a destructive action whose `ok` has just been framed. Clearing + /// `deferred_committed` would demote an acknowledged action to armed and + /// lose the commit after the last text byte is fetched. The committed + /// action still fires; log if this request's `ok` therefore describes + /// another action. + fn arm_action(&mut self, action: Action, command: &str) { + // A serial command's action runs once its reply is printed. + if self.serial_running { + self.serial_action = action; + return; + } + if !self.deferred_committed { + self.deferred_action = action; + } else { + dlog!( + LogModule::Bedrock, + "elsc: \"{command}\" while a {} is already committed; that action fires and this \"ok\" may not match it", + self.deferred_action.name() + ); + } + } + + /// The fetch of the last frame byte: the whole ack is delivered. End the + /// announcement and commit any deferred action. Committing here, not at a + /// STOP or NACK, keeps the action when a master NAKs that byte and then + /// repeats the start with no STOP. + fn response_fully_read(&mut self) { + self.resp_owner = -1; + if self.deferred_action != Action::None { + self.deferred_committed = true; + } + } + + /// Mark a committed action to fire. Called at a transaction boundary (a + /// NACK, the STOP, or a following start) so it runs after the guest has + /// taken the whole frame, not from inside the read that completed the ack. + fn fire_committed(&mut self) { + if self.deferred_action != Action::None && self.deferred_committed { + self.deferred_scheduled = true; + } + } + + fn run_committed_action(&mut self) { + if !self.deferred_committed { + self.deferred_scheduled = false; + return; + } + let action = self.deferred_action; + self.deferred_action = Action::None; + self.deferred_committed = false; + self.deferred_scheduled = false; + self.run_action(action); + } + + fn run_action(&mut self, action: Action) { + // Release the bus for every destructive action, before it fires: all + // three leave this controller selected (the NACK-without-STOP path + // never ended the transfer) and none ends it themselves. + self.release_bus = true; + match action { + Action::Cycle => { + // The Hubs hear "pd cycle" before the power goes. + self.post_message(Message::PdCycle); + self.power_cycle(self.cycle_seconds); + } + Action::Reset => { + // The reset comes a second later; another "rst" inside that + // second starts the wait again. + self.post_message(Message::Rst); + self.arm_ms(T_RST, RESET_WAIT_MS); + } + Action::Shutdown => self.remove_power(), + Action::PowerUp => { + // "pwr u" on an unpowered module starts the power-up sequence + // now, cutting short any countdown. + if !self.powered() && !(self.power_sequence && self.pending(T_POWER_UP)) { + self.power_sequence_start(); + } + } + Action::None => unreachable!("only a real action can be committed"), + } + } + + // ---- request handling ---------------------------------------------- + + /// The direction switch to read marks the request complete. + fn request_complete(&mut self) { + if self.rx.is_empty() && self.txpos < self.tx.len() { + // No request this transaction: a fetch of the pending response. + return; + } + // A new request supersedes any pending response. An armed action (its + // ack not fully read) is cancelled; a committed one is past the point + // of no return and survives. + if !self.deferred_committed { + self.deferred_action = Action::None; + } + self.resp_owner = crate::cpu::smp::current_cpu() as i32; + self.record_console_sender(); + + let n = self.rx.first().map_or(0, |b| *b as usize); + if self.rx.len() < 2 || self.rx.len() != n + 2 || checksum(&self.rx) != 0 { + // The protocol names no framing error distinct from "err csum". + self.respond_str("err csum"); + return; + } + let text = self.rx[1..=n].to_vec(); + self.dispatch_i2c(&text); + } + + /// Console output names its sender in the character after `acp `, which + /// the controller turns into a number by subtracting the code of `0`, with + /// no range check. It is recorded as the request is parsed, before the + /// checksum is looked at, so a corrupted request still records it. + fn record_console_sender(&mut self) { + if self.rx.len() >= 7 && self.rx[0] >= 5 && &self.rx[1..5] == b"acp " { + self.console_sender = self.rx[5] as i32 - b'0' as i32; + } + } + + /// The first four bytes are a three-letter lowercase mnemonic and its + /// separator, a space or the end. Matching is case-sensitive, and a + /// request that is only the terminator is no command. Whatever a handler + /// leaves unread is discarded. + fn dispatch_i2c(&mut self, text: &[u8]) { + let mut line = text.to_vec(); + line.push(b'\r'); + if text.len() < 3 || (line[3] != b' ' && line[3] != b'\r') { + self.respond_str("err cmd"); + return; + } + let pos = if line[3] == b'\r' { 3 } else { 4 }; + let mut req = Req { text: &line, pos }; + match COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_i2c) { + Some(c) => (c.handler)(self, &mut req), + None => self.respond_str("err cmd"), + } + } + + // ---- commands ------------------------------------------------------- + + fn handle_ver(&mut self, _r: &mut Req) { + self.respond_str(&format!("ok {VERSION}")); + } + + /// The physical switches, active-low, as hex with no leading zero. The + /// Origin 200 build reads its role from bit 0x04 of the same byte: set on + /// the master, clear on a slave. The topology decides the role, so the bit + /// follows it whatever the configured switches say. + fn handle_rsw(&mut self, _r: &mut Req) { + let mut raw = 0xff ^ self.cfg.dip_switches; + raw = if self.cfg.master { raw | 0x04 } else { raw & !0x04 }; + self.respond_str(&format!("ok {raw:x}")); + } + + /// The console-input poll. A pending controller message comes first, as + /// its bare text. Then console input: the firmware checks four of the six + /// header bytes and copies from byte six, so the shape must be exact. The + /// fifth byte names the input selection mode, not who is reading. + fn handle_get(&mut self, _r: &mut Req) { + if self.resp_owner >= 0 && self.take_message(self.resp_owner) { + self.respond_str(self.message.text()); + return; + } + let n = self.rx_queue.len().min(MAX_TEXT - 6); + let mut buf = b"acp 0 ".to_vec(); + buf[4] = match self.sel_mode { + Select::Explicit => b'0' + self.sel_processor, + Select::None => b'/', + Select::Auto => b'.', + }; + buf.extend(self.rx_queue.drain(..n)); + self.respond(&buf); + } + + /// Console output. A bare `ok` parses as zero accepted and livelocks the + /// firmware, so the reply is always `ok `. The selector and payload + /// are taken from fixed offsets, as the firmware's output parser does, + /// with no validation of either. + fn handle_acp(&mut self, r: &mut Req) { + let len = r.text.len() - 1; + if len < 6 { + self.respond_str("err arg"); + return; + } + let n = (len - 6).min(86); + // Output from a processor the "see" filter excludes is accepted and + // counted but not copied to the serial port. While an operator types + // a command line, all output is accepted and lost. + if !self.serial_command + && (self.see_processor == SEE_ALL || self.see_processor as i32 == self.console_sender) + { + let text = r.text[6..6 + n].to_vec(); + self.serial_write(&text); + } + self.respond_str(&format!("ok {n:x}")); + } + + /// `sel [|all|none|auto]`: the console input selection. With no + /// argument it reports `ok auto`, `ok none` or `ok` and a processor name. + /// `all` and `none` (the first two letters are what count) disable input + /// selection, and `auto` makes it follow console output. + fn handle_sel(&mut self, r: &mut Req) { + let Some((first, second)) = r.read_selection() else { + let name = match self.sel_mode { + Select::Auto => "auto".to_string(), + Select::None => "none".to_string(), + Select::Explicit => String::from_utf8_lossy(&processor_name(self.sel_processor)).into_owned(), + }; + self.respond_str(&format!("ok {name}")); + return; + }; + if let Some(p) = selection_processor(first, second) { + self.select_input(Select::Explicit, p); + } else if matches!((first, second), (b'A', b'L') | (b'N', b'O')) { + self.select_input(Select::None, 0); + } else if (first, second) == (b'A', b'U') { + self.select_input(Select::Auto, 0); + } else { + self.respond_str("err arg"); + return; + } + self.respond_ok(); + } + + /// `see [|all|none]`: the console output filter. A processor + /// name copies only that processor's output to the serial port and also + /// selects it for input. + fn handle_see(&mut self, r: &mut Req) { + let Some((first, second)) = r.read_selection() else { + let name = if self.see_processor == SEE_ALL { + "all".to_string() + } else { + String::from_utf8_lossy(&processor_name(self.see_processor)).into_owned() + }; + self.respond_str(&format!("ok {name}")); + return; + }; + if let Some(p) = selection_processor(first, second) { + self.see_processor = p; + self.select_input(Select::Explicit, p); + } else if matches!((first, second), (b'A', b'L') | (b'N', b'O')) { + self.see_processor = SEE_ALL; + } else { + self.respond_str("err arg"); + return; + } + self.respond_ok(); + } + + /// `pwr d []`: power down after a hex delay. Exactly one character + /// after the `d` is skipped, then the hex reader runs. 1 to 0x258 + /// (re)loads the countdown from this request, so a repeated request moves + /// the deadline either way. 0 powers down at once, as does no argument + /// while a countdown is pending; no argument otherwise starts a + /// three-second countdown. Above 0x258 is `err arg` and changes nothing. + /// An immediate power-down replies `ok` first and acts once the ack is + /// read, as `rst` does. A countdown's expiry needs no ack. + fn handle_pwr_down(&mut self, r: &mut Req) { + if !self.powered() { + // Nothing to power down: a bare "ok", with no range check. + self.respond_ok(); + return; + } + r.skip(); + let delay = r.read_hex(); + if delay.is_some_and(|s| s > DELAY_MAX) { + self.respond_str("err arg"); + return; + } + self.respond_ok(); + let immediate = match delay { + Some(s) => s == 0, + None => self.pending(T_PWR_DOWN), + }; + if !immediate { + // A timed request tells the Hubs: IRIX starts its + // controller-initiated shutdown on "pd remote". + self.arm_secs(T_PWR_DOWN, delay.unwrap_or(3) as u64); + self.post_message(Message::PdRemote); + return; + } + // A pending countdown keeps running: the firmware removes power + // whether or not this ack is read. + self.arm_action(Action::Shutdown, "pwr d"); + } + + fn handle_pwr(&mut self, r: &mut Req) { + let Some(sub) = r.read_char() else { + self.respond_str(if self.powered() { "ok u" } else { "ok d" }); + return; + }; + match sub { + b'u' => { + // Power-up runs the module's power sequence, which also + // reinitialises this controller's bus interface, so like the + // destructive commands it waits until the "ok" has been read. + // A powered module ignores the request. + self.respond_ok(); + if !self.powered() { + self.arm_action(Action::PowerUp, "pwr u"); + } + } + b'q' => { + // Power-on query: the PROM issues it once at power-on to + // detect a cold start. "ok 1" on the first read after a cold + // power-on, "ok 0" thereafter; clear-on-read is unconfirmed. + let v = self.cold_power_on as u8; + self.cold_power_on = false; + self.respond_str(&format!("ok {v:x}")); + } + b'd' => self.handle_pwr_down(r), + b'c' => { + // The optional off interval is read like the "pwr d" delay + // and runs from 5 to 0x258 seconds, 5 without one. Replies + // "ok" before the power goes and acts once the ack is read. + r.skip(); + let interval = r.read_hex(); + if interval.is_some_and(|s| !(5..=DELAY_MAX).contains(&s)) { + self.respond_str("err arg"); + return; + } + self.respond_ok(); + self.cycle_seconds = interval.unwrap_or(CYCLE_DEFAULT_SECONDS); + self.arm_action(Action::Cycle, "pwr c"); + } + _ => self.respond_str("err arg"), + } + } + + /// `hbt [ []]`: the heartbeat watchdog. `hbt 0` stops it. + /// An interval of 5 to 0x258 seconds arms it: the stage goes back to + /// waiting, the timer starts at the interval, and the delay from the NMI + /// to the power cycle is the second value, zero allowed, or 30 seconds + /// without one. Any other interval is `err arg`. With no readable value + /// the request is a heartbeat, which only reloads the timer: it never + /// returns the watchdog to an earlier stage. + fn handle_hbt(&mut self, r: &mut Req) { + let Some(interval) = r.read_hex() else { + if self.hbt_interval != 0 { + self.hbt_arm(self.hbt_interval as u64); + } + self.respond_ok(); + return; + }; + if interval == 0 { + self.hbt_stop(); + self.recheck_shutdown(); + self.respond_ok(); + return; + } + if !(5..=DELAY_MAX).contains(&interval) { + self.respond_str("err arg"); + return; + } + self.hbt_interval = interval; + self.hbt_delay = r.read_hex().unwrap_or(HBT_DEFAULT_DELAY); + self.hbt_stage = Heartbeat::Waiting; + self.hbt_arm(interval as u64); + self.respond_ok(); + } + + /// `tas []`: the test-and-set cell, free when it holds ASCII `0`. The + /// argument is the next non-space character, a literal byte, so a space + /// can never be an owner. `tas 0` frees the cell; any other byte, NUL + /// included, takes it only if it is free; no argument only reports. Every + /// reply is `ok ` and the byte the cell held before the request, framed + /// with an explicit length so a NUL owner round-trips. + fn handle_tas(&mut self, r: &mut Req) { + let before = self.tas_lock; + if let Some(ch) = r.read_char() { + if ch == TAS_FREE { + self.tas_lock = TAS_FREE; + } else if self.tas_lock == TAS_FREE { + self.tas_lock = ch; + } + } + self.respond(&[b'o', b'k', b' ', before]); + } + + fn log_unimp_once(&mut self, bit: u8, what: &str) { + if self.unimp_logged & bit == 0 { + self.unimp_logged |= bit; + dlog!(LogModule::Bedrock, "elsc: {what}"); + } + } + + /// `nic `: one-wire identity passthrough. Three + /// values from the hex reader, any of them missing a bad argument. No + /// device is modelled on the line, which idles high. + fn handle_nic(&mut self, r: &mut Req) { + if r.read_hex().is_none() || r.read_hex().is_none() || r.read_hex().is_none() { + self.respond_str("err arg"); + return; + } + self.log_unimp_once( + UNIMP_NIC, + "\"nic\" with no one-wire device on this module; the line idles high, an approximation", + ); + self.respond_str("ok 1"); + } + + /// Module reset: panic reboot and fatal-error recovery only. A caller + /// treats `ok` as proof the reset happened and does not fall back to its + /// own hardware reset, so the reset must fire, once the guest has read + /// `ok`. + fn handle_rst(&mut self, _r: &mut Req) { + if !self.powered() { + self.respond_str("err cmd"); + return; + } + self.respond_ok(); + self.arm_action(Action::Reset, "rst"); + } + + fn nvram_byte(&self, offset: u8, write: Option) -> Option { + self.nvram.access(elsc_nvram::FIRST_ADDR + 7, offset, write) + } + + /// `aut [1|0]`: query, set or clear the automatic power-on bit in the + /// NVRAM configuration byte. The bit takes effect only when the controller + /// next starts, which the model does not distinguish from the machine's + /// own start, so it is only kept in the NVRAM. + fn handle_aut(&mut self, r: &mut Req) { + let arg = r.read_char(); + if arg.is_some_and(|c| c != b'1' && c != b'0') { + self.respond_str("err arg"); + return; + } + let mut config = self.nvram_byte(7, None).unwrap_or(0); + let Some(c) = arg else { + self.respond_str(if config & elsc_nvram::CONFIG_AUTO_POWER != 0 { "ok 1" } else { "ok 0" }); + return; + }; + if c == b'1' { + config |= elsc_nvram::CONFIG_AUTO_POWER; + } else { + config &= !elsc_nvram::CONFIG_AUTO_POWER; + } + self.nvram_byte(7, Some(config)); + self.respond_ok(); + } + + /// `clr`: cancel a pending power-down countdown, the heartbeat watchdog + /// and the fan-high request. Accepted with the module on or off. It leaves + /// the power-up countdown, the `tas` byte, the console selection, the + /// `fsc` and `tst` flags, the NVRAM and module power alone. It turns + /// serial echo on and clears supervisor mode. + fn handle_clr(&mut self, _r: &mut Req) { + self.cancel(T_PWR_DOWN); + // The watchdog stops with its interval zeroed and its stage left as + // it was, so a later heartbeat loads a zero timer and does nothing. + self.hbt_interval = 0; + self.cancel(T_HBT); + self.fan_high = false; + self.serial_echo = true; + self.supervisor = false; + self.recheck_shutdown(); + self.respond_ok(); + } + + /// `fan [h|n]`: the fan-high request. The model has no fans or + /// temperature inputs, so the request alone decides. Over I2C neither + /// form writes the NVRAM; on the serial line the request is also kept + /// there. + fn handle_fan(&mut self, r: &mut Req) { + let Some(c) = r.read_char() else { + self.respond_str(if self.fan_high { "ok h" } else { "ok n" }); + return; + }; + if c != b'h' && c != b'n' { + self.respond_str("err arg"); + return; + } + self.fan_high = c == b'h'; + if self.serial_running { + if let Some(config) = self.nvram_byte(7, None) { + let config = if self.fan_high { + config | elsc_nvram::CONFIG_FAN_HIGH + } else { + config & !elsc_nvram::CONFIG_FAN_HIGH + }; + self.nvram_byte(7, Some(config)); + } + } + self.respond_ok(); + } + + /// `fsc [1|0]`: declare a multimodule controller present or absent. The + /// query form and any other argument answer a plain `ok ` over I2C (the + /// firmware writes the flag where the reply's separator then goes). The + /// flag only changes what an Origin 2000 build does, so it is only stored. + fn handle_fsc(&mut self, r: &mut Req) { + match r.read_char() { + Some(c @ (b'1' | b'0')) => { + self.fsc_present = c == b'1'; + if self.fsc_present { + self.log_unimp_once( + UNIMP_FSC, + "\"fsc 1\" is stored only; the serial display mirror and the key-switch power-up rule are not modelled", + ); + } + } + _ if self.serial_running => { + // The serial reply keeps the digit the I2C reply loses. + self.respond_str(if self.fsc_present { "ok1" } else { "ok0" }); + return; + } + _ => {} + } + self.respond_ok(); + } + + /// `tst `: the factory's short timeouts, set by `1` and cleared by any + /// other character. They shorten waits the model does not have, so the + /// flag is only stored. + fn handle_tst(&mut self, r: &mut Req) { + let Some(c) = r.read_char() else { + self.respond_str("err arg"); + return; + }; + self.short_timeouts = c == b'1'; + if self.short_timeouts { + self.log_unimp_once( + UNIMP_TST, + "\"tst 1\" is stored only; the shortened fan-check and power-up waits are not modelled", + ); + } + self.respond_ok(); + } + + /// `tmp`: `ok n` while neither temperature input is active, which in a + /// model with no thermal sensors is always. A master that has heard from + /// its slave appends the slave's last reported letter. + fn handle_tmp(&mut self, _r: &mut Req) { + let mut resp = b"ok n".to_vec(); + if self.slave_link { + resp.extend_from_slice(&[b' ', self.slave_temp]); + } + self.respond(&resp); + } + + /// `sla `: the report a slave controller sends its master. The + /// controller believes whoever sends it. A master takes `n`, `h` and `o` + /// as its slave's temperature and 0x07 as the slave announcing itself, + /// after which `tmp` reports the slave too. The shutdowns and fan reports + /// a master starts on `o`, 0x04, 0x05 and 0x06 need the + /// controller-to-controller link, which the model does not have. + fn handle_sla(&mut self, r: &mut Req) { + if let Some(c) = r.read_char() { + if !self.cfg.master { + self.slave_report = c; + } else if matches!(c, b'n' | b'h' | b'o') { + self.slave_temp = c; + } else if c == 0x07 { + self.slave_link = true; + } + if self.cfg.master && (c == b'o' || (0x04..=0x06).contains(&c)) { + dlog!( + LogModule::Bedrock, + "elsc: \"sla\" report {c:#04x} from a slave controller; the shutdown or fan report it starts is not modelled" + ); + } + } + self.respond_ok(); + } + + /// `nmi`: an unpowered module answers `err cmd`. Otherwise `ok`, and a + /// second later the module's processors get a 100 ms NMI pulse; another + /// `nmi` inside that second starts the wait again. The wait starts as the + /// command is answered, as the firmware's does. + fn handle_nmi(&mut self, _r: &mut Req) { + if !self.powered() { + self.respond_str("err cmd"); + return; + } + self.arm_ms(T_NMI, NMI_WAIT_MS); + self.respond_ok(); + } + + // ---- serial-only commands ------------------------------------------ + + /// `ech`: set echo, `1` on, `0` off; with no argument it toggles. + fn handle_ech(&mut self, r: &mut Req) { + match r.read_char() { + None => self.serial_echo = !self.serial_echo, + Some(c @ (b'1' | b'0')) => self.serial_echo = c == b'1', + Some(_) => { + self.respond_str("err arg"); + return; + } + } + self.respond_ok(); + } + + /// `pas`: with no argument, leave supervisor mode. `pas ` enters it + /// when the word is exactly four characters and matches the password at + /// offsets 1 to 4 of the NVRAM page. `pas s `, in supervisor mode + /// already, stores the first four characters of a word at least that + /// long, which must follow exactly one space. + fn handle_pas(&mut self, r: &mut Req) { + const LEN: usize = 4; + let word = r.read_word().to_vec(); + if word.is_empty() { + self.supervisor = false; + self.respond_ok(); + return; + } + if word == b"s" && r.peek() == b' ' { + if !self.supervisor { + self.respond_str("err perm"); + return; + } + // The word follows exactly one space: no more are skipped. + r.skip(); + let new: Vec = r.text[r.pos..] + .iter() + .copied() + .take_while(|c| !matches!(c, b' ' | b'\r' | b'\n' | 0)) + .collect(); + if new.len() < LEN { + self.respond_str("err arg"); + return; + } + for (i, b) in new[..LEN].iter().enumerate() { + if self.nvram_byte(1 + i as u8, Some(*b)).is_none() { + break; + } + } + self.supervisor = true; + self.respond_ok(); + return; + } + if word.len() != LEN { + self.respond_str("err arg"); + return; + } + for (i, b) in word.iter().enumerate() { + if self.nvram_byte(1 + i as u8, None) != Some(*b) { + self.respond_str("err arg"); + return; + } + } + self.supervisor = true; + self.respond_ok(); + } + + fn reply_nvram_byte(&mut self, offset: u8) { + let v = self.nvram_byte(offset, None).unwrap_or(0); + self.respond_str(&format!("ok {v:x}")); + } + + /// `mod []`: the module number. A non-hex argument is the query. + fn handle_mod(&mut self, r: &mut Req) { + match r.read_hex() { + None => self.reply_nvram_byte(8), + Some(v) => { + self.nvram_byte(8, Some(v as u8)); + self.respond_ok(); + } + } + } + + /// `dbg [ []]`: the two debug-switch bytes. + fn handle_dbg(&mut self, r: &mut Req) { + let Some(a) = r.read_hex() else { + let v5 = self.nvram_byte(5, None).unwrap_or(0); + let v6 = self.nvram_byte(6, None).unwrap_or(0); + self.respond_str(&format!("ok {v5:x} {v6:x}")); + return; + }; + self.nvram_byte(5, Some(a as u8)); + if let Some(b) = r.read_hex() { + self.nvram_byte(6, Some(b as u8)); + } + self.respond_ok(); + } + + /// `nvm []`: one byte of the management NVRAM. The address's + /// low byte is the offset and its high byte picks the device, the firmware + /// forming the device address byte as 0xa0 ORed with twice the high byte: + /// 000 to 7ff reach 0x50 to 0x57, and higher addresses alias back onto + /// those. A device that does not answer gives a bare `err `. + fn handle_nvm(&mut self, r: &mut Req) { + let Some(addr) = r.read_hex() else { + self.respond_str("err arg"); + return; + }; + let device = ((0xa0 | ((addr >> 8) << 1)) as u8) >> 1; + let data = r.read_hex(); + match self.nvram.access(device, addr as u8, data.map(|d| d as u8)) { + None => self.respond_str("err "), + Some(_) if data.is_some() => self.respond_ok(), + Some(v) => self.respond_str(&format!("ok {v:x}")), + } + } + + /// `cfg`: the management NVRAM's raw bytes from device 0x50 offset 0, up + /// to and including the first zero byte or 0x700 bytes, then a line end. + /// No Ctrl+T and no `ok`. + fn handle_cfg(&mut self, _r: &mut Req) { + const LIMIT: usize = 0x700; + self.serial_raw = true; + for offset in 0..LIMIT { + let b = self + .nvram + .access(elsc_nvram::FIRST_ADDR + (offset >> 8) as u8, offset as u8, None) + .unwrap_or(0); + self.serial_write(&[b]); + if b == 0 { + break; + } + } + self.serial_puts("\r\n"); + } + + // ---- serial line ---------------------------------------------------- + + /// Whether a serial command needs supervisor mode. `pas s` checks for + /// itself; `nmi` and `rst` check module power before this. + fn serial_gated(&self, name: &[u8], r: &Req) -> bool { + let mut probe = Req { text: r.text, pos: r.pos }; + match name { + b"clr" | b"hbt" | b"nmi" | b"rst" => true, + b"pwr" => probe.read_char().is_some_and(|c| matches!(c, b'u' | b'd' | b'c')), + b"fan" | b"aut" => probe.read_char().is_some(), + b"mod" | b"dbg" => probe.read_hex().is_some(), + b"nvm" => probe.read_hex().is_some() && probe.read_hex().is_some(), + _ => false, + } + } + + /// Run one serial line. It is parsed as an I2C request is, with the byte + /// that ended it in place of the I2C carriage return, so only a carriage + /// return separates a bare mnemonic; then the supervisor gate. + fn serial_dispatch(&mut self, line: &[u8]) { + // Ctrl+T and Enter alone. + if line == b"\r" { + self.respond_str(if self.serial_echo { "ok " } else { "err cmd" }); + return; + } + if line.len() < 4 || (line[3] != b' ' && line[3] != b'\r') { + self.respond_str("err cmd"); + return; + } + let pos = if line[3] == b'\r' { 3 } else { 4 }; + let mut req = Req { text: line, pos }; + let Some(c) = COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_serial) else { + self.respond_str("err cmd"); + return; + }; + if matches!(c.name, b"nmi" | b"rst") && !self.powered() { + self.respond_str("err cmd"); + return; + } + if !self.supervisor && self.serial_gated(c.name, &req) { + self.respond_str("err perm"); + return; + } + (c.handler)(self, &mut req); + } + + /// Run the waiting serial line and print its reply: Ctrl+T, the reply as + /// the I2C contract gives it but with no separator space after a bare + /// `ok`, and a line end. No prompt follows. An action the command asked + /// for runs once the reply is out. + fn serial_run(&mut self) { + let mut line = std::mem::take(&mut self.serial_line); + line.push(self.serial_term); + self.serial_pending = false; + + self.serial_running = true; + self.serial_raw = false; + self.serial_reply.clear(); + self.serial_action = Action::None; + self.serial_dispatch(&line); + self.serial_running = false; + + if !self.serial_raw { + let mut reply = std::mem::take(&mut self.serial_reply); + if reply == b"ok " { + reply.pop(); + } + self.serial_write(&[CTRL_T]); + self.serial_write(&reply); + self.serial_puts("\r\n"); + } + let action = std::mem::replace(&mut self.serial_action, Action::None); + if action != Action::None { + self.run_action(action); + } + } + + /// A completed line runs once the bus is free, this controller's own I2C + /// target is idle and no Hub message is pending; an I2C command arriving + /// at the same moment goes first. + fn serial_poll(&mut self) { + if self.serial_pending + && self.message_mask == 0 + && !self.slave_addressed + && !self.bus_busy.load(Ordering::Relaxed) + { + self.serial_run(); + } + } + + /// Ctrl+T in pass-through: the prompt, or `Slave!` from a slave. + fn serial_enter_command(&mut self) { + if self.serial_echo { + self.serial_puts("\r\nMSC> "); + if !self.cfg.master { + self.serial_puts("Slave!\r\n"); + return; + } + } + self.serial_command = true; + } + + /// One byte from the operator. In pass-through everything but Ctrl+T is + /// console input. In command mode bytes build the line; a carriage + /// return, line feed or NUL ends it, and a second Ctrl+T abandons it, + /// keeping what was typed for the next line and passing one Ctrl+T on as + /// input. + fn serial_input(&mut self, c: u8) { + if !self.serial_command { + if c == CTRL_T { + self.serial_enter_command(); + } else { + self.queue_input(c); + } + return; + } + match c { + 0x08 | 0x7f => { + if self.serial_line.pop().is_some() && self.serial_echo { + self.serial_puts("\x08 \x08"); + } + } + b'\r' | b'\n' | 0 => { + self.serial_term = c; + if self.serial_echo { + if c == b'\r' { + self.serial_puts("\r\n"); + } else { + self.serial_write(&[c]); + } + } + self.serial_command = false; + if !self.cfg.master { + // A slave never runs serial commands. + self.serial_line.clear(); + return; + } + self.serial_pending = true; + } + CTRL_T => { + self.serial_command = false; + self.queue_input(CTRL_T); + if self.serial_echo { + self.serial_puts("\x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"); + } + } + _ => { + if self.serial_line.len() < MAX_TEXT { + self.serial_line.push(c); + } + if self.serial_echo { + self.serial_write(&[c]); + } + } + } + } + + // ---- start ---------------------------------------------------------- + + /// The controller's own start, which the machine's reset stands for (an + /// AC cycle). + fn start(&mut self, clock_now: u64) { + self.reset_queues(); + self.cancel(T_PWR_DOWN); + self.serial_command = false; + self.serial_line.clear(); + self.serial_pending = false; + self.serial_echo = true; + self.supervisor = false; + self.cancel(T_POWER_UP); + self.cancel(T_START); + self.module_off = false; + self.power_sequence = false; + if self.cfg.start_delay_ms != 0 { + // The module starts off. A master counts the delay; a slave + // waits for its master. + self.module_off = true; + if self.cfg.master { + self.arm_ms(T_START, self.cfg.start_delay_ms as u64); + } + } + self.cycle_seconds = CYCLE_DEFAULT_SECONDS; + self.cancel(T_RST); + self.cancel(T_NMI); + self.cancel(T_NMI_PULSE); + if self.nmi_pulse_active { + self.nmi_pulse_active = false; + self.events.push(ElscEvent::Nmi(false)); + } + self.hbt_stop(); + self.hbt_delay = 0; + self.fan_high = false; + self.fsc_present = false; + self.short_timeouts = false; + self.slave_link = false; + self.slave_temp = b'n'; + self.slave_report = 0; + self.sel_mode = Select::Auto; + self.see_processor = SEE_ALL; + // No processor has sent console output since the start. + self.console_sender = -1; + + // The start announces the controller on its serial line. + self.serial_puts("\x14MSC "); + self.serial_puts(VERSION); + self.serial_puts("\r\n"); + // The rotation starts with code 0, and a later start leaves a running + // rotation on its schedule. + if self.cfg.master && !self.pending(T_ARB) { + if !self.arb_blocked() { + self.arb_publish(); + } + self.deadlines[T_ARB] = Some(clock_now + ARB_TICK_NS); + } + } + + /// The part of a start that a module event reaches through the bus the + /// controller shares with module parts, which it rides through. + fn reset_queues(&mut self) { + self.rx.clear(); + self.tx.clear(); + self.txpos = 0; + self.resp_owner = -1; + self.arb_code = 0; + self.arb_signal = 0; + self.rx_queue.clear(); + self.slave_addressed = false; + self.deferred_action = Action::None; + self.deferred_committed = false; + self.deferred_scheduled = false; + self.cancel(T_MESSAGE); + self.message = Message::None; + self.message_mask = 0; + self.tas_lock = TAS_FREE; + } + + fn describe(&self) -> String { + use std::fmt::Write; + let mut s = String::new(); + let resp_pending = self.resp_owner >= 0 && self.txpos < self.tx.len(); + let code0_signal2 = (!self.rx_queue.is_empty() && self.console_target() == 0) + || self.message_mask & 1 != 0 + || (resp_pending && self.resp_owner == 0); + let power = if self.powered() { + "on" + } else if self.power_sequence { + "off, powering up" + } else if self.pending(T_POWER_UP) { + "off, power-up counting down" + } else { + "off" + }; + let _ = writeln!(s, "elsc"); + let _ = writeln!(s, " module power: {power}"); + if self.pending(T_START) { + let _ = writeln!(s, " start-up power-up pending"); + } + let _ = writeln!(s, " rx queue: {} byte(s) waiting for \"get\"", self.rx_queue.len()); + if !self.rx_queue.is_empty() { + let _ = write!(s, " input: \""); + for c in self.rx_queue.iter().take(64) { + if (0x20..0x7f).contains(c) { + s.push(*c as char); + } else { + let _ = write!(s, "\\x{c:02x}"); + } + } + let _ = writeln!(s, "{}", if self.rx_queue.len() > 64 { "\"..." } else { "\"" }); + } + if self.message != Message::None { + let _ = writeln!(s, " controller message: \"{}\"", self.message.text()); + } + if resp_pending { + let _ = writeln!( + s, + " pending response: {} byte(s) for processor code {}", + self.tx.len() - self.txpos, + self.resp_owner + ); + } else { + let _ = writeln!(s, " pending response: none"); + } + match self.sel_mode { + Select::Explicit => { + let _ = write!(s, " console input: processor {}", self.sel_processor); + } + Select::None => { + let _ = write!(s, " console input: disabled"); + } + Select::Auto => { + let _ = write!(s, " console input: automatic, last output from processor {}", self.console_sender); + } + } + if self.see_processor == SEE_ALL { + let _ = writeln!(s, "; output: all"); + } else { + let _ = writeln!(s, "; output: processor {}", self.see_processor); + } + let _ = writeln!(s, " arbitration rotation: next position {} of {}", self.arb_code, ARB_POSITIONS); + let _ = writeln!( + s, + " fan high: {}, fsc: {}, short timeouts: {}, slave report: {:#04x}", + self.fan_high, self.fsc_present, self.short_timeouts, self.slave_report + ); + let _ = writeln!( + s, + " console signal (code 0): {:#04x} = signal{}", + SIGNAL_BASE + if code0_signal2 { 8 } else { 0 }, + if code0_signal2 { 2 } else { 1 } + ); + s + } +} + +pub struct Elsc { + state: Mutex, + clock: Clock, + backend: Mutex>, +} + +impl Elsc { + /// A controller on the bus whose busy flag is `busy`, keeping its + /// configuration in `nvram`. Call [`Resettable::power_on`] to start it. + pub fn new(cfg: ElscConfig, nvram: Arc, clock: Clock, busy: BusBusy) -> Self { + Self { + state: Mutex::new(State::new(cfg, nvram, busy)), + clock, + backend: Mutex::new(null_serial_backend()), + } + } + + /// Where the AUX connector's transmit bytes go. + pub fn set_serial_backend(&self, backend: Arc) { + *self.backend.lock() = backend; + } + + /// Run `f` with the timers brought up to the present, then send what it + /// wrote to the AUX port with the lock released. + fn run(&self, f: impl FnOnce(&mut State) -> R) -> R { + let (value, out) = { + let mut st = self.state.lock(); + st.advance((self.clock)()); + let value = f(&mut st); + (value, std::mem::take(&mut st.out)) + }; + if !out.is_empty() { + let backend = self.backend.lock().clone(); + for byte in out { + backend.send_byte(byte); + } + } + value + } + + /// Let time pass and carry out what is due. The bus does this around + /// every register access; a machine whose guest may not touch the bus for + /// a long time (a countdown expiring, an operator typing) also calls it + /// from its own tick. + pub fn poll(&self) { + self.run(|st| { + if st.deferred_scheduled { + st.run_committed_action(); + } + st.serial_poll(); + }); + } + + /// One byte from the operator on the AUX connector. + pub fn serial_input(&self, byte: u8) { + self.run(|st| { + st.serial_input(byte); + st.serial_poll(); + }); + } + + /// The module-power, reset and NMI outputs, in the order they happened, + /// with time brought up to the present first. + pub fn take_events(&self) -> Vec { + self.run(|st| { + if st.deferred_scheduled { + st.run_committed_action(); + } + st.serial_poll(); + std::mem::take(&mut st.events) + }) + } + + pub fn module_powered(&self) -> bool { + self.run(|st| st.powered()) + } + + /// Whether module power is due back without anyone asking: a power-up + /// countdown, or a heartbeat watchdog that will power-cycle the module. + pub fn power_return_pending(&self) -> bool { + self.run(|st| st.power_return_pending()) + } + + /// A module event that reaches the controller only through a bus it + /// shares with module parts: the controller runs on standby power and + /// keeps its state, reinitialising its bus interface and queues, freeing + /// the lock and dropping what it had queued for the Hubs. + pub fn ride_through_reset(&self) { + self.run(|st| st.reset_queues()); + } + + /// A state dump for the monitor. + pub fn describe(&self) -> String { + self.run(|st| st.describe()) + } +} + +impl Resettable for Elsc { + /// The controller's own start, which the machine's reset stands for (an + /// AC cycle): every timer and flag starts over. + fn power_on(&self) { + let now = (self.clock)(); + self.run(|st| st.start(now)); + } +} + +impl I2cTarget for Elsc { + fn claims(&self, addr7: u8) -> bool { + addr7 == I2C_ADDR + } + + fn start(&self, _addr7: u8, recv: bool) -> bool { + self.run(|st| { + st.slave_addressed = true; + if recv { + // Direction switch: the request just written is complete. + st.request_complete(); + st.fire_committed(); + } else { + // A new request is arriving; its start is a boundary too. + st.fire_committed(); + st.rx.clear(); + } + }); + true + } + + fn send(&self, byte: u8) -> bool { + self.run(|st| { + if st.rx.len() > MAX_FRAME { + dlog!(LogModule::Bedrock, "elsc: request longer than the protocol allows, byte dropped"); + } else { + st.rx.push(byte); + } + }); + true + } + + fn recv(&self) -> u8 { + self.run(|st| { + if st.txpos < st.tx.len() { + let byte = st.tx[st.txpos]; + st.txpos += 1; + if st.txpos == st.tx.len() { + st.response_fully_read(); + } + byte + } else { + // The driver's own length byte said stop; this is over-reading. + 0xff + } + }) + } + + fn nack(&self) { + self.run(|st| st.fire_committed()); + } + + fn finish(&self) { + self.run(|st| { + st.slave_addressed = false; + st.rx.clear(); + // An unconsumed response stays pending for its requester until + // read to its end or superseded; a committed action fires here + // for a response the master ends with a STOP. + st.fire_committed(); + if st.txpos >= st.tx.len() { + st.tx.clear(); + st.txpos = 0; + st.resp_owner = -1; + } + }); + } + + fn service(&self) -> Service { + self.run(|st| { + if st.deferred_scheduled { + st.run_committed_action(); + } + st.serial_poll(); + Service { + grant: st.pending_grant.take(), + release_bus: std::mem::take(&mut st.release_bus), + } + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/elsc/tests.rs b/src/dev/elsc/tests.rs new file mode 100644 index 000000000..59c9632ea --- /dev/null +++ b/src/dev/elsc/tests.rs @@ -0,0 +1,1879 @@ +//! Register-level tests of the controller, driven through the PCF8584 the way +//! the PROM drives it, on a manual clock so none of them waits. + +use super::*; +use crate::dev::elsc_nvram::ElscNvram; +use crate::dev::i2c::I2cBus; +use crate::dev::pcf8584::{ctl, sts, Pcf8584}; +use std::io; +use std::sync::atomic::AtomicU64; + +const DATA: u64 = 0; +const CTRL: u64 = 8; +const ELSC_ADDR: u8 = I2C_ADDR; +const NVRAM_PAGE7: u8 = 0x57; + +const SIGNAL1: u8 = 0xe0; +const SIGNAL2: u8 = 0xe8; +const STEP_NS: u64 = 10_000; +const SCAN_NS: u64 = 100_000; +const TICK_NS: u64 = 1_000_000; +const SECOND: u64 = 1000; + +/// What a default "pwr c" takes on an Origin 200: five seconds off, then four +/// of power-up with reset held. +const CYCLE_SECS: u64 = 9; + +const BANNER: &str = "\x14MSC V 3.1S \r\n"; + +/// The AUX connector's far end. +#[derive(Default)] +struct Aux(Mutex>); + +impl SerialBackend for Aux { + fn send_byte(&self, byte: u8) { + self.0.lock().push(byte); + } + + fn recv_byte(&self) -> io::Result { + Err(io::Error::new(io::ErrorKind::WouldBlock, "none")) + } +} + +impl Aux { + fn take(&self) -> String { + String::from_utf8_lossy(&std::mem::take(&mut *self.0.lock())).into_owned() + } +} + +/// Virtual time, set by hand. +#[derive(Clone, Default)] +struct TestClock(Arc); + +impl TestClock { + fn now_ns(&self) -> u64 { + self.0.load(Ordering::SeqCst) + } + + fn advance_ns(&self, ns: u64) { + self.0.fetch_add(ns, Ordering::SeqCst); + } + + fn advance_ms(&self, ms: u64) { + self.advance_ns(ms * 1_000_000); + } + + fn advance_secs(&self, s: u64) { + self.advance_ms(s * 1000); + } + + fn clock(&self) -> Clock { + let now = self.0.clone(); + Arc::new(move || now.load(Ordering::SeqCst)) + } +} + +/// The sum of a frame's bytes modulo 256, written out here rather than shared +/// with the controller: a valid frame sums to zero. +fn byte_sum(buf: &[u8]) -> u8 { + (buf.iter().map(|b| *b as u32).sum::() % 256) as u8 +} + +struct Rig { + busy: BusBusy, + clock: TestClock, + elsc: Arc, + nvram: Arc, + pcf: Pcf8584, + aux: Arc, +} + +fn frame(text: &[u8]) -> Vec { + let mut req = vec![text.len() as u8]; + req.extend_from_slice(text); + req.push(((256 - byte_sum(&req) as u32) % 256) as u8); + req +} + +impl Rig { + fn new() -> Self { + Self::with(ElscConfig::default()) + } + + fn with(cfg: ElscConfig) -> Self { + let clock = TestClock::default(); + let busy = BusBusy::default(); + let nvram = Arc::new(ElscNvram::origin200(None).unwrap()); + let elsc = Arc::new(Elsc::new(cfg, nvram.clone(), clock.clock(), busy.clone())); + let mut bus = I2cBus::new(); + bus.add(elsc.clone()); + bus.add(nvram.clone()); + let pcf = Pcf8584::new(bus, busy.clone()); + let aux = Arc::new(Aux::default()); + elsc.set_serial_backend(aux.clone()); + pcf.power_on(); + elsc.power_on(); + Self { busy, clock, elsc, nvram, pcf, aux } + } + + fn w(&self, off: u64, v: u8) { + self.pcf.write_register(off, v); + } + + fn r(&self, off: u64) -> u8 { + self.pcf.read_register(off) + } + + fn step_ms(&self, ms: u64) { + self.clock.advance_ms(ms); + } + + fn advance_to_ms(&self, ms: u64) { + let now = self.clock.now_ns(); + assert!(ms * 1_000_000 >= now, "already past {ms} ms"); + self.clock.advance_ns(ms * 1_000_000 - now); + } + + fn step_secs(&self, s: u64) { + self.clock.advance_secs(s); + } + + fn events(&self) -> Vec { + self.elsc.take_events() + } + + /// The Hub's side of a module reset: the adapter resets and the + /// controller rides through. + fn module_reset(&self) { + self.elsc.ride_through_reset(); + self.pcf.power_on(); + } + + // ---- transactions --------------------------------------------------- + + /// One combined transaction, the way the PROM makes it. The read discards + /// the chip's stale data register first. + fn transact(&self, req: &[u8]) -> Vec { + self.w(CTRL, ctl::PIN | ctl::ESO); + self.w(DATA, ELSC_ADDR << 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(self.r(CTRL) & sts::LRB, 0); + for b in req { + self.w(DATA, *b); + } + // Repeated start, switching direction: control first, then address. + self.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(DATA, (ELSC_ADDR << 1) | 1); + let _ = self.r(DATA); + self.read_frame(true) + } + + /// The reply text for a request, after checking the frame. + fn reply_bytes(&self, text: &[u8]) -> Vec { + let resp = self.transact(&frame(text)); + assert_eq!(byte_sum(&resp), 0, "bad checksum in {resp:?}"); + resp[1..1 + resp[0] as usize].to_vec() + } + + fn reply(&self, text: &str) -> String { + String::from_utf8_lossy(&self.reply_bytes(text.as_bytes())).into_owned() + } + + #[track_caller] + fn assert_reply(&self, text: &str, want: &str) { + assert_eq!(self.reply(text), want, "reply to {text:?}"); + } + + /// A request completed through the direction switch, then abandoned with + /// the response entirely unread. + fn request_without_read(&self, text: &str) { + let req = frame(text.as_bytes()); + self.w(CTRL, ctl::PIN | ctl::ESO); + self.w(DATA, ELSC_ADDR << 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(self.r(CTRL) & sts::LRB, 0); + for b in req { + self.w(DATA, b); + } + self.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(DATA, (ELSC_ADDR << 1) | 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + } + + /// Start a read-only transaction and discard the stale first byte. + fn begin_read(&self) { + self.w(DATA, (ELSC_ADDR << 1) | 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(self.r(CTRL) & sts::LRB, 0); + let _ = self.r(DATA); + } + + /// Read a whole frame from a started read, NAKing the last byte. + fn read_frame(&self, stop: bool) -> Vec { + let mut resp = vec![self.r(DATA)]; + let total = resp[0] as usize + 2; + for i in 1..total { + if i == total - 1 { + self.w(CTRL, ctl::ESO); + } + resp.push(self.r(DATA)); + } + if stop { + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + } + resp + } + + /// A read-only transaction: fetch a response with no request written. + fn read_response(&self) -> Vec { + self.begin_read(); + self.read_frame(true) + } + + // ---- arbitration ---------------------------------------------------- + + fn observer(&self) { + self.w(CTRL, ctl::PIN); + self.w(DATA, 0x7f); + self.w(CTRL, ctl::PIN | ctl::ES1); + self.w(DATA, 0x1c); + self.w(CTRL, ctl::PIN); + assert_eq!(self.r(DATA), 0x7f); + self.w(DATA, 0x09); + self.w(CTRL, ctl::PIN | ctl::ESO); + } + + fn is_signal(byte: u8) -> bool { + (0xe0..=0xef).contains(&byte) + } + + /// Step the clock until the ring publishes a code different from the one + /// currently latched, and return the new grant. + fn next_code(&self) -> u8 { + let start = self.r(DATA); + for _ in 0..2000 { + let d = self.r(DATA); + if Self::is_signal(d) && d != start { + return d; + } + self.clock.advance_ns(SCAN_NS); + } + panic!("the ring never moved"); + } + + /// Step until a new grant appears, leaving the clock no more than + /// `STEP_NS` past the tick that published it. + fn anchor_on_tick(&self) -> u8 { + let start = self.r(DATA); + for _ in 0..32 * TICK_NS / STEP_NS { + self.clock.advance_ns(STEP_NS); + let d = self.r(DATA); + if Self::is_signal(d) && d != start { + return d; + } + } + panic!("no tick"); + } + + /// Loop 1: wait for another processor's grant, with the bus free. + fn wait_for_absence(&self) { + for _ in 0..2000 { + let d = self.r(DATA); + let s = self.r(CTRL); + if s & sts::BB != 0 && Self::is_signal(d) && d & 7 != 0 { + return; + } + self.clock.advance_ns(SCAN_NS); + } + panic!("no other processor's grant"); + } + + /// Loop 2: step until the bus is free and the data register holds + /// processor 0's signal 1 or signal 2, and report which. + fn wait_for_signal(&self) -> u8 { + for _ in 0..2000 { + let d = self.r(DATA); + let s = self.r(CTRL); + if s & sts::BB != 0 && (d == SIGNAL1 || d == SIGNAL2) { + return d; + } + self.clock.advance_ns(SCAN_NS); + } + panic!("no grant for processor 0"); + } + + /// Arbitrate for processor 0's slot. + fn arbitrate(&self) { + self.wait_for_absence(); + self.wait_for_signal(); + } + + /// One NVRAM byte as firmware reads it: arbitrated, offset-write then + /// data-read, closed with the invalid-address step. + fn firmware_nvram_read(&self, offset: u8) -> u8 { + self.arbitrate(); + self.w(DATA, NVRAM_PAGE7 << 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(self.r(CTRL) & sts::LRB, 0); + self.w(DATA, offset); + self.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(DATA, (NVRAM_PAGE7 << 1) | 1); + let _ = self.r(DATA); + self.w(CTRL, ctl::ESO); + let value = self.r(DATA); + self.close_with_invalid_address(); + value + } + + /// The closing invalid-address step, still inside the transfer: control + /// first, PIN deliberately absent, then the address byte. + fn close_with_invalid_address(&self) { + self.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(DATA, 0xfe); + assert_eq!(self.r(CTRL) & sts::LRB, sts::LRB); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + } + + /// One full command as firmware drives it: arbitrate, the combined + /// transaction, then the closing repeated start to the invalid address. + fn firmware_command(&self, text: &str) -> Vec { + self.arbitrate(); + self.w(DATA, ELSC_ADDR << 1); + self.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(self.r(CTRL) & sts::LRB, 0); + for b in frame(text.as_bytes()) { + self.w(DATA, b); + } + self.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(DATA, (ELSC_ADDR << 1) | 1); + let _ = self.r(DATA); + let resp = self.read_frame(false); + self.close_with_invalid_address(); + resp + } + + // ---- serial --------------------------------------------------------- + + fn type_bytes(&self, bytes: &[u8]) { + for b in bytes { + self.elsc.serial_input(*b); + } + } + + /// One serial command line: Ctrl+T, the text and the reply it prints. + #[track_caller] + fn serial_command(&self, line: &str, reply: &str) { + self.type_bytes(format!("\x14{line}\r").as_bytes()); + assert_eq!( + self.aux.take(), + format!("\r\nMSC> {line}\r\n\x14{reply}\r\n"), + "serial {line:?}" + ); + } +} + +/// Text of a framed response. +fn text(resp: &[u8]) -> String { + String::from_utf8_lossy(&resp[1..1 + resp[0] as usize]).into_owned() +} + +fn power_events(events: &[ElscEvent]) -> Vec { + events + .iter() + .filter_map(|e| if let ElscEvent::Power(on) = e { Some(*on) } else { None }) + .collect() +} + +fn count_nmi(events: &[ElscEvent]) -> usize { + events.iter().filter(|e| **e == ElscEvent::Nmi(true)).count() +} + +// ---- framing and the request grammar ------------------------------------- + +#[test] +fn ver_command_succeeds() { + let rig = Rig::new(); + let resp = rig.transact(&frame(b"ver")); + assert_eq!(byte_sum(&resp), 0); + assert!(resp[0] >= 4); + let t = text(&resp); + assert!(t.starts_with("ok "), "{t:?}"); + // The synthetic version field stays within the eight-byte limit. + let version = t[3..].split(' ').next().unwrap(); + assert!(!version.is_empty() && version.len() < 8); + assert_eq!(t, "ok V 3.1S "); +} + +#[test] +fn bad_request_checksum_gets_err_csum() { + let rig = Rig::new(); + let mut req = frame(b"ver"); + *req.last_mut().unwrap() ^= 0xff; + let resp = rig.transact(&req); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(resp[0], 8); + assert_eq!(text(&resp), "err csum"); +} + +#[test] +fn a_short_or_long_request_is_err_csum() { + let rig = Rig::new(); + // The length byte says three, but four text bytes and a checksum follow. + let mut req = frame(b"verx"); + req[0] = 3; + assert_eq!(text(&rig.transact(&req)), "err csum"); + // Nothing but a length. + assert_eq!(text(&rig.transact(&[0])), "err csum"); +} + +#[test] +fn an_unimplemented_command_completes_with_err_cmd() { + let rig = Rig::new(); + let resp = rig.transact(&frame(b"xyz")); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(resp[0], 7); + assert_eq!(text(&resp), "err cmd"); +} + +#[test] +fn rsw_reports_the_switches_active_low_with_the_role_from_topology() { + let rig = Rig::new(); + assert_eq!(rig.transact(&frame(b"rsw"))[0], 5); + rig.assert_reply("rsw", "ok ff"); + // A configured value comes back inverted. + let rig = Rig::with(ElscConfig { dip_switches: 1, ..Default::default() }); + rig.assert_reply("rsw", "ok fe"); + // Bit 0x04 is the role, and a lone module's controller is the master. + let rig = Rig::with(ElscConfig { dip_switches: 4, ..Default::default() }); + rig.assert_reply("rsw", "ok ff"); + let rig = Rig::with(ElscConfig { dip_switches: 16, ..Default::default() }); + rig.assert_reply("rsw", "ok ef"); + // A slave reads the role bit clear. + let rig = Rig::with(ElscConfig { dip_switches: 16, master: false, ..Default::default() }); + rig.assert_reply("rsw", "ok eb"); +} + +#[test] +fn request_grammar() { + let rig = Rig::with(ElscConfig { dip_switches: 240, ..Default::default() }); + // The mnemonic and its separator. + assert_eq!(text(&rig.transact(&frame(b""))), "err cmd"); + rig.assert_reply("ve", "err cmd"); + rig.assert_reply("VER", "err cmd"); + rig.assert_reply("verx", "err cmd"); + rig.assert_reply("ver", "ok V 3.1S "); + rig.assert_reply("ver junk", "ok V 3.1S "); + // Hex replies carry no leading zero. + rig.assert_reply("rsw", "ok f"); + rig.assert_reply("rsw 12", "ok f"); + // The character reader skips spaces and leaves the rest unread. + rig.assert_reply("pwr q", "ok 1"); + rig.assert_reply("pwr q junk", "ok 0"); + // An Origin 200 module is powered whenever the machine runs. + rig.assert_reply("pwr", "ok u"); + rig.assert_reply("pwr u", "ok "); + rig.assert_reply("pwr q", "ok 0"); +} + +#[test] +fn the_origin_200_build_has_no_display_commands() { + let rig = Rig::new(); + for c in ["dsc 4 E", "dsc 1 A", "dsp testing", "dsp", "dsc", "key", "vlm 5", "fre", "mod"] { + let resp = rig.transact(&frame(c.as_bytes())); + assert_eq!(text(&resp), "err cmd", "{c}"); + } + let resp = rig.transact(&frame(b"ver")); + assert_eq!(&resp[1..12], b"ok V 3.1S "); + assert!(!rig.elsc.describe().contains("display:")); +} + +#[test] +fn no_supervisor_on_i2c() { + let rig = Rig::new(); + for c in ["pas", "pas none", "pas ab", "pas s abcd"] { + let resp = rig.transact(&frame(c.as_bytes())); + assert_eq!(resp[0], 7); + assert_eq!(text(&resp), "err cmd"); + } + // A destructive command with no "pas" ever sent. + rig.assert_reply("rst", "ok "); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +// ---- arbitration ----------------------------------------------------------- + +#[test] +fn arbitration_observes_a_grant_and_keeps_the_staged_start() { + let rig = Rig::new(); + rig.observer(); + rig.wait_for_absence(); + let signal = rig.wait_for_signal(); + + // A grant is an unmatched address phase, not a payload byte. Its address + // replaces readable S0 while START retains the staged controller address. + rig.w(DATA, ELSC_ADDR << 1); + let grant = rig.next_code(); + assert_ne!(rig.r(CTRL) & sts::BB, 0); + assert_eq!(grant & 7, ((signal & 7) + 1) % 5); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(rig.r(CTRL) & sts::LRB, 0); + for b in frame(b"ver") { + rig.w(DATA, b); + } + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, (ELSC_ADDR << 1) | 1); + let _ = rig.r(DATA); + let resp = rig.read_frame(false); + assert_eq!(byte_sum(&resp), 0); + assert!(text(&resp).starts_with("ok")); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + rig.arbitrate(); + + // Reset also releases an address staged without a following START. + rig.w(DATA, ELSC_ADDR << 1); + rig.pcf.power_on(); + rig.observer(); + rig.arbitrate(); + + // A benign control change must not abandon the staged address. + rig.w(DATA, ELSC_ADDR << 1); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::ACK); + rig.next_code(); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(rig.r(CTRL) & sts::LRB, 0); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + rig.arbitrate(); +} + +#[test] +fn each_ring_position_lasts_one_millisecond() { + let rig = Rig::new(); + rig.observer(); + let mut grant = rig.anchor_on_tick(); + for _ in 0..10 { + let next = SIGNAL1 + ((grant & 7) + 1) % 5; + // From just past one tick the grant stands until the next one, a + // millisecond later, which publishes the next. + rig.clock.advance_ns(TICK_NS - 2 * STEP_NS); + assert_eq!(rig.r(DATA), grant); + rig.clock.advance_ns(2 * STEP_NS); + assert_eq!(rig.r(DATA), next); + grant = next; + } +} + +#[test] +fn the_ring_turns_through_five_positions() { + let rig = Rig::new(); + rig.observer(); + loop { + if rig.anchor_on_tick() == SIGNAL1 { + break; + } + } + for code in 1..4 { + rig.clock.advance_ns(TICK_NS); + assert_eq!(rig.r(DATA), SIGNAL1 + code); + } + // The fifth position stands for one tick like the others, with a byte no + // processor of this module owns. + rig.clock.advance_ns(TICK_NS); + assert_eq!(rig.r(DATA), SIGNAL1 + 4); + rig.clock.advance_ns(TICK_NS); + assert_eq!(rig.r(DATA), SIGNAL1); +} + +#[test] +fn a_busy_tick_publishes_nothing_and_does_not_advance() { + let rig = Rig::new(); + rig.observer(); + let signal = rig.anchor_on_tick(); + + rig.w(DATA, ELSC_ADDR << 1); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(rig.r(CTRL) & sts::LRB, 0); + assert_eq!(rig.r(CTRL) & sts::BB, 0); + + // Three eligible ticks pass while the bus is busy. + rig.clock.advance_ns(3 * TICK_NS + TICK_NS / 2); + assert_eq!(rig.r(CTRL) & sts::BB, 0); + + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert_ne!(rig.r(CTRL) & sts::BB, 0); + + // The bus is free, but nothing is published before the next tick. + rig.clock.advance_ns(TICK_NS / 2 - 2 * STEP_NS); + assert!(!Rig::is_signal(rig.r(DATA))); + + // That tick publishes the code the busy ticks skipped. + rig.clock.advance_ns(2 * STEP_NS); + assert_eq!(rig.r(DATA) & 7, ((signal & 7) + 1) % 5); + assert_eq!(rig.next_code() & 7, ((signal & 7) + 2) % 5); +} + +#[test] +fn a_late_clock_replays_the_ring_on_its_grid() { + let rig = Rig::new(); + rig.observer(); + let grant = rig.anchor_on_tick(); + // 5 000 ticks is a whole number of revolutions. Just short of the last + // tick the ring stands one position back; at it, it is where it began. + rig.clock.advance_ns(5000 * TICK_NS - STEP_NS); + assert_eq!(rig.r(DATA), SIGNAL1 + ((grant & 7) + 4) % 5); + rig.clock.advance_ns(STEP_NS); + assert_eq!(rig.r(DATA), grant); +} + +#[test] +fn a_pending_response_announces_signal2_to_its_requester() { + let rig = Rig::new(); + rig.observer(); + rig.request_without_read("ver"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + let info = rig.elsc.describe(); + assert!(info.contains("pending response"), "{info}"); + assert!(info.contains("for processor code 0"), "{info}"); +} + +#[test] +fn a_consumed_response_reverts_to_signal1() { + let rig = Rig::new(); + rig.observer(); + rig.reply("ver"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); +} + +#[test] +fn an_announced_response_is_fetched_after_the_stop() { + let rig = Rig::new(); + rig.observer(); + rig.request_without_read("ver"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + + let resp = rig.read_response(); + assert_eq!(byte_sum(&resp), 0); + assert!(text(&resp).starts_with("ok ")); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); +} + +#[test] +fn a_new_request_supersedes_a_pending_response() { + let rig = Rig::new(); + rig.observer(); + rig.request_without_read("ver"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + rig.assert_reply("rsw", "ok ff"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); +} + +#[test] +fn the_requesting_cpu_is_announced_in_its_own_slot() { + let rig = Rig::new(); + rig.observer(); + crate::cpu::smp::set_current_cpu(2); + rig.request_without_read("ver"); + crate::cpu::smp::set_current_cpu(u8::MAX); + // Code 0 has nothing waiting; code 2 does. + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + let mut saw = false; + for _ in 0..50 { + if rig.r(DATA) == SIGNAL2 + 2 { + saw = true; + break; + } + rig.clock.advance_ns(TICK_NS); + } + assert!(saw, "no signal 2 for code 2"); +} + +#[test] +fn frames_match_literal_bytes() { + // "ver" is 0x76 0x65 0x72: with the length 3 they sum to 0x50, so the + // checksum that brings the frame to zero is 0xb0. + assert_eq!(frame(b"ver"), [0x03, 0x76, 0x65, 0x72, 0xb0]); + let rig = Rig::new(); + let resp = rig.transact(&[0x03, 0x76, 0x65, 0x72, 0xb0]); + assert_eq!( + resp, + [0x0b, b'o', b'k', b' ', b'V', b' ', b'3', b'.', b'1', b'S', b' ', b' ', 0x60] + ); + // "err csum" for a request whose last byte is wrong by one. + let resp = rig.transact(&[0x03, 0x76, 0x65, 0x72, 0xb1]); + assert_eq!( + resp, + [0x08, b'e', b'r', b'r', b' ', b'c', b's', b'u', b'm', 0xd7] + ); +} + +// ---- console --------------------------------------------------------------- + +#[test] +fn acp_writes_console_output_to_the_aux_port() { + let rig = Rig::new(); + let resp = rig.transact(&frame(b"acp 0 hi")); + assert_eq!(byte_sum(&resp), 0); + // "hi" is two characters; the accepted count in hex is "2". + assert_eq!(resp[0], 4); + assert_eq!(text(&resp), "ok 2"); + // The controller's start announcement comes first. + assert_eq!(rig.aux.take(), format!("{BANNER}hi")); +} + +#[test] +fn acp_with_no_backend_still_answers() { + let rig = Rig::new(); + rig.elsc.set_serial_backend(null_serial_backend()); + rig.assert_reply("acp 0 hi", "ok 2"); + // Too short to carry output is a bad argument, never a bare ok. + rig.assert_reply("acp 0", "err arg"); + rig.assert_reply("acp 0 ", "ok 0"); +} + +#[test] +fn acp_takes_at_most_86_characters() { + let rig = Rig::new(); + let long = format!("acp 0 {}", "x".repeat(88)); + rig.assert_reply(&long, "ok 56"); + assert_eq!(rig.aux.take(), format!("{BANNER}{}", "x".repeat(86))); +} + +#[test] +fn get_returns_queued_console_input() { + let rig = Rig::new(); + // Input is announced only once a processor has sent output. + rig.assert_reply("acp 0 ", "ok 0"); + rig.type_bytes(b"hello"); + rig.observer(); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + let resp = rig.transact(&frame(b"get")); + assert_eq!(byte_sum(&resp), 0); + assert!(resp[0] >= 6 + 5); + assert_eq!(text(&resp), "acp . hello"); +} + +#[test] +fn a_command_escape_byte_is_not_queued() { + let rig = Rig::new(); + rig.assert_reply("acp 0 ", "ok 0"); + // What ends an escaped command is the model's own choice: a single \r. + rig.type_bytes(b"AB\x14zzz\rCD"); + rig.observer(); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + let resp = rig.transact(&frame(b"get")); + assert_eq!(resp[0], 6 + 4); + assert_eq!(text(&resp), "acp . ABCD"); +} + +#[test] +fn console_selection_commands() { + let rig = Rig::new(); + rig.aux.take(); + rig.assert_reply("sel", "ok auto"); + rig.assert_reply("see", "ok all"); + rig.assert_reply("get", "acp . "); + + rig.assert_reply("sel 2b", "ok "); + rig.assert_reply("sel", "ok 2b"); + rig.assert_reply("get", "acp 3 "); + rig.assert_reply("sel all", "ok "); + rig.assert_reply("sel", "ok none"); + rig.assert_reply("get", "acp / "); + rig.assert_reply("sel 4 A", "ok "); + rig.assert_reply("sel", "ok 4a"); + rig.assert_reply("sel AUTO", "ok "); + rig.assert_reply("sel", "ok auto"); + rig.assert_reply("sel no", "ok "); + rig.assert_reply("sel", "ok none"); + + // A bad name changes nothing. + for bad in ["sel 5a", "sel 1", "sel 1c", "sel x", "see au"] { + rig.assert_reply(bad, "err arg"); + } + rig.assert_reply("sel", "ok none"); + + // "see" with a processor filters output and selects that input. + rig.assert_reply("see 1b", "ok "); + rig.assert_reply("see", "ok 1b"); + rig.assert_reply("sel", "ok 1b"); + // Output from processor 0 is counted, not copied. + rig.assert_reply("acp 0 hi", "ok 2"); + assert_eq!(rig.aux.take(), ""); + // The filter goes by the frame's selector, "1" being processor 1. + rig.assert_reply("acp 1 ab", "ok 2"); + assert_eq!(rig.aux.take(), "ab"); + rig.assert_reply("see 1a", "ok "); + rig.assert_reply("acp 0 yo", "ok 2"); + assert_eq!(rig.aux.take(), "yo"); + rig.assert_reply("see none", "ok "); + rig.assert_reply("see", "ok all"); + rig.assert_reply("sel", "ok 1a"); + + // "clr" leaves the selection; a start restores the default. + rig.assert_reply("clr", "ok "); + rig.assert_reply("sel", "ok 1a"); + rig.elsc.power_on(); + rig.assert_reply("sel", "ok auto"); +} + +#[test] +fn queued_input_is_announced_to_the_selected_processor_only() { + let rig = Rig::new(); + rig.observer(); + rig.assert_reply("sel 1b", "ok "); + rig.type_bytes(b"hi"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + + // A new selection discards the queue; fresh input is announced. + rig.assert_reply("sel 1a", "ok "); + assert!(rig.elsc.describe().contains("rx queue: 0 byte(s)")); + rig.type_bytes(b"yo"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + rig.assert_reply("get", "acp 0 yo"); + + // Automatic selection follows the selector in the last output frame, not + // the processor that sent it. No output yet: nobody is announced to. + rig.assert_reply("sel auto", "ok "); + rig.type_bytes(b"zz"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + rig.assert_reply("acp 0 ", "ok 0"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + // A frame with a corrupted checksum still records its sender. + let mut req = frame(b"acp 1 "); + *req.last_mut().unwrap() ^= 0xff; + assert_eq!(text(&rig.transact(&req)), "err csum"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); +} + +#[test] +fn info_dump_names_the_empty_and_the_waiting_queue() { + let rig = Rig::new(); + let empty = rig.elsc.describe(); + assert!(empty.contains("rx queue: 0 byte(s)"), "{empty}"); + assert!(empty.contains("pending response: none"), "{empty}"); + assert!(empty.contains("signal1"), "{empty}"); + + rig.assert_reply("acp 0 ", "ok 0"); + rig.type_bytes(b"hello"); + let full = rig.elsc.describe(); + assert!(full.contains("rx queue: 5 byte(s)"), "{full}"); + assert!(full.contains("hello"), "{full}"); + assert!(full.contains("signal2"), "{full}"); +} + +// ---- boot sequence --------------------------------------------------------- + +#[test] +fn boot_acceptance_sequence() { + const INITIAL: [u8; 16] = [0x37, b'n', b'o', b'n', b'e', 0, 0, 0x02, 0, 0, 0, 0x78, 0, 0, 0, 0]; + let rig = Rig::new(); + rig.observer(); + let block: Vec = (0..16).map(|i| rig.firmware_nvram_read(i)).collect(); + // The controller's block as its first power-up leaves it: the marker, the + // password "none", automatic power-up, no module number and the + // full-clock midplane's clock-select byte. + assert_eq!(block, INITIAL); + + let resp = rig.firmware_command("rsw"); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(resp[0], 5); + assert_eq!(text(&resp), "ok ff"); + + let resp = rig.firmware_command("ver"); + assert_eq!(byte_sum(&resp), 0); + assert!(text(&resp).starts_with("ok")); + if resp[0] != 2 { + assert_eq!(resp[3], b' '); + } +} + +// ---- tas, pwr q, nic, flags ------------------------------------------------ + +#[test] +fn tas_lock_cell() { + let rig = Rig::new(); + // Free cell: the pre-acquisition value is 0, then 'A' installs. + rig.assert_reply("tas A", "ok 0"); + // Re-acquiring returns the literal owner. + rig.assert_reply("tas A", "ok A"); + // Held by 'A': a different caller reads the holder. + rig.assert_reply("tas z", "ok A"); + // Release: the pre-release value is the holder, and the cell frees. + rig.assert_reply("tas 0", "ok A"); + // 'z' is a valid non-hex channel. + rig.assert_reply("tas z", "ok 0"); + rig.assert_reply("tas z", "ok z"); + // No argument only reports the holder. + rig.assert_reply("tas", "ok z"); + rig.assert_reply("tas 0", "ok z"); + rig.assert_reply("tas", "ok 0"); + // The owner is the next non-space character and the rest is discarded. + rig.assert_reply("tas AB", "ok 0"); + rig.assert_reply("tas B", "ok A"); + rig.assert_reply("tas", "ok A"); + rig.assert_reply("tas 0", "ok A"); + rig.assert_reply("tasx", "err cmd"); +} + +#[test] +fn a_nul_tas_owner_holds_the_cell_and_round_trips() { + let rig = Rig::new(); + let nul = frame(b"tas \0"); + let reply = |req: &[u8]| { + let resp = rig.transact(req); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(resp[0], 4); + resp + }; + assert_eq!(text(&reply(&nul)), "ok 0"); + // Another caller finds the cell held, and the NUL owner survives framing. + let resp = reply(&frame(b"tas z")); + assert_eq!(&resp[1..5], b"ok \0"); + // And 'z' did not steal it. + assert_eq!(reply(&nul)[4], 0); + // Release frees it; the pre-release value is the NUL owner. + assert_eq!(reply(&frame(b"tas 0"))[4], 0); + assert_eq!(text(&reply(&frame(b"tas z"))), "ok 0"); +} + +#[test] +fn pwr_q_reports_the_cold_power_on_latch_once() { + let rig = Rig::new(); + rig.assert_reply("pwr q", "ok 1"); + rig.assert_reply("pwr q", "ok 0"); + rig.assert_reply("pwr z", "err arg"); + rig.assert_reply("pwr q x", "ok 0"); + // The redundant-supply query belongs to later firmware. + rig.assert_reply("pwr r", "err arg"); +} + +#[test] +fn nic_reads_an_idle_one_wire_line() { + let rig = Rig::new(); + rig.assert_reply("nic 1 2 3", "ok 1"); + rig.assert_reply("nic 1 2", "err arg"); + rig.assert_reply("nic 1 2 zz", "err arg"); + // A value shorter than four digits consumes the character after it. + rig.assert_reply("nic 1 2 3", "err arg"); + // A four-digit value consumes nothing: the next follows with no space. + rig.assert_reply("nic 000100020003", "ok 1"); + rig.assert_reply("nic 0001 2 3", "err arg"); + rig.assert_reply("nicx", "err cmd"); +} + +#[test] +fn stored_flag_commands() { + let rig = Rig::new(); + rig.assert_reply("fan", "ok n"); + rig.assert_reply("fan h", "ok "); + rig.assert_reply("fan", "ok h"); + rig.assert_reply("fan H", "err arg"); + rig.assert_reply("fan x", "err arg"); + rig.assert_reply("fan", "ok h"); + rig.assert_reply("fan n junk", "ok "); + rig.assert_reply("fan", "ok n"); + + // The "fsc" query loses its answer over I2C. + for c in ["fsc", "fsc 1", "fsc", "fsc 0", "fsc x"] { + rig.assert_reply(c, "ok "); + } + // "tst" needs a character; "1" sets and anything else clears. + rig.assert_reply("tst", "err arg"); + rig.assert_reply("tst 1", "ok "); + rig.assert_reply("tst x", "ok "); + + // No thermal model: normal, and the slave's letter once it reports. + rig.assert_reply("tmp", "ok n"); + rig.assert_reply("tmp junk", "ok n"); + rig.assert_reply("sla h", "ok "); + rig.assert_reply("tmp", "ok n"); + rig.assert_reply("sla \x07", "ok "); + rig.assert_reply("tmp", "ok n h"); + rig.assert_reply("sla n", "ok "); + rig.assert_reply("tmp", "ok n n"); + + // "clr" drops the fan request and a pending power-down countdown. + rig.assert_reply("fan h", "ok "); + rig.assert_reply("pwr d 2", "ok "); + rig.assert_reply("clr", "ok "); + rig.assert_reply("fan", "ok n"); + rig.step_secs(3); + rig.elsc.poll(); + assert!(rig.elsc.module_powered()); +} + +#[test] +fn a_slave_stores_the_report_it_would_forward_and_publishes_no_grants() { + let rig = Rig::with(ElscConfig { master: false, ..Default::default() }); + rig.assert_reply("sla o", "ok "); + rig.assert_reply("tmp", "ok n"); + rig.observer(); + rig.step_ms(20); + assert!(!Rig::is_signal(rig.r(DATA))); +} + +#[test] +fn aut_reads_and_writes_the_nvram_bit() { + let rig = Rig::new(); + rig.observer(); + rig.assert_reply("aut", "ok 1"); + rig.assert_reply("aut 0", "ok "); + rig.assert_reply("aut", "ok 0"); + rig.assert_reply("aut 1", "ok "); + rig.assert_reply("aut", "ok 1"); + assert_eq!(rig.firmware_nvram_read(7), 0x02); + // The password's last byte, not the configuration byte. + assert_eq!(rig.firmware_nvram_read(4), b'e'); + rig.assert_reply("aut x", "err arg"); + rig.assert_reply("aut", "ok 1"); + + // The NVRAM, not the controller, holds it: a start leaves it set. + rig.elsc.power_on(); + rig.assert_reply("aut", "ok 1"); + rig.assert_reply("aut 0 junk", "ok "); + rig.assert_reply("aut", "ok 0"); + assert_eq!(rig.firmware_nvram_read(7), 0); + assert_eq!(rig.firmware_nvram_read(0), 0x37); +} + +// ---- hbt ------------------------------------------------------------------- + +#[test] +fn hbt_forms() { + let rig = Rig::new(); + // The hex reader takes no digits from "xy" and so reads no value: a + // heartbeat, not a bad argument. "hbt 00045" is the four digits 0004. + for ok in [ + "hbt", "hbt 0", "hbt 30 20", "hbt 5", "hbt 258", "hbt 1e 0", "hbt xy", "hbt 30 xy", "hbt 1e 14", + "hbt 025814", + ] { + let resp = rig.transact(&frame(ok.as_bytes())); + assert_eq!(resp[0], 3, "{ok}"); + assert_eq!(text(&resp), "ok "); + } + for bad in ["hbt 4", "hbt 259", "hbt 1000 5", "hbt 00045"] { + let resp = rig.transact(&frame(bad.as_bytes())); + assert_eq!(resp[0], 7, "{bad}"); + assert_eq!(text(&resp), "err arg"); + } + rig.assert_reply("hbtx", "err cmd"); +} + +#[test] +fn hbt_stages_warn_pulse_and_cycle() { + let rig = Rig::new(); + rig.assert_reply("pwr q", "ok 1"); + rig.assert_reply("hbt 5 a", "ok "); + rig.events(); + rig.clock.advance_ms(9999); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 0); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 1); + rig.assert_reply("pwr q", "ok 0"); + + // The second value, 0xa, is the delay to the power cycle. + rig.clock.advance_ms(9999); + rig.assert_reply("pwr q", "ok 0"); + rig.step_ms(1); + rig.assert_reply("pwr", "ok d"); + assert_eq!(power_events(&rig.events()), [false]); + rig.step_secs(CYCLE_SECS); + rig.assert_reply("pwr", "ok u"); + assert_eq!(power_events(&rig.events()), [true]); + rig.assert_reply("pwr q", "ok 1"); + + // Idle now: neither time nor a heartbeat brings another NMI. + rig.step_secs(60); + rig.assert_reply("hbt", "ok "); + rig.step_secs(60); + assert_eq!(count_nmi(&rig.events()), 0); + rig.assert_reply("pwr q", "ok 0"); +} + +#[test] +fn a_heartbeat_in_the_warning_postpones_the_nmi() { + let rig = Rig::new(); + rig.assert_reply("hbt 5 0", "ok "); + rig.events(); + rig.step_secs(6); + rig.assert_reply("hbt", "ok "); + rig.clock.advance_ms(4999); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 0); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 1); + // A delay of zero never power-cycles. + rig.step_secs(120); + assert!(rig.elsc.module_powered()); + rig.assert_reply("pwr q", "ok 1"); +} + +#[test] +fn hbt_is_stopped_only_by_clr_or_zero() { + let rig = Rig::new(); + rig.assert_reply("hbt 5", "ok "); + rig.step_secs(3); + rig.assert_reply("clr", "ok "); + rig.step_secs(20); + rig.assert_reply("hbt 5", "ok "); + rig.assert_reply("hbt 0", "ok "); + rig.step_secs(20); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 0); + + // A "rst" does not stop it: the controller rides through the reset. + rig.assert_reply("hbt 5", "ok "); + rig.step_secs(1); + rig.assert_reply("rst", "ok "); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); + rig.module_reset(); + rig.clock.advance_ms(7999); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 0); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(count_nmi(&rig.events()), 1); + rig.assert_reply("hbt 0", "ok "); +} + +// ---- rst and the deferred-action rules -------------------------------------- + +#[test] +fn rst_resets_the_module_not_the_controller() { + let rig = Rig::new(); + // Leave a mark the reset must clear: take the test-and-set lock. + rig.assert_reply("tas A", "ok 0"); + rig.assert_reply("rst", "ok "); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); + rig.module_reset(); + // It freed the lock the controller was holding. + rig.assert_reply("tas B", "ok 0"); +} + +#[test] +fn rst_waits_for_the_ack_to_be_read() { + let rig = Rig::new(); + // Write "rst" and end the transaction without reading the response. + rig.request_without_read("rst"); + rig.step_secs(5); + rig.elsc.poll(); + assert!(!rig.events().contains(&ElscEvent::Reset)); + + // The ack survives to be fetched in a separate read transaction. + let resp = rig.read_response(); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(resp[0], 3); + assert_eq!(text(&resp), "ok "); + // Only now, once the ack has been read, does the reset wait begin. + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn rst_is_dropped_when_its_ack_is_superseded_unread() { + let rig = Rig::new(); + rig.assert_reply("tas A", "ok 0"); + rig.request_without_read("rst"); + // A new request supersedes the ack; its own response reads back clean. + rig.assert_reply("ver", "ok V 3.1S "); + // No reset ran: the lock is still held. + rig.step_secs(5); + rig.assert_reply("tas B", "ok A"); + assert!(!rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn rst_fires_without_a_stop() { + let rig = Rig::new(); + rig.request_without_read("rst"); + // Read the whole ack in a read transaction, issuing no STOP. + rig.begin_read(); + let resp = rig.read_frame(false); + assert_eq!(byte_sum(&resp), 0); + assert_eq!(text(&resp), "ok "); + // Reading the last ack byte committed the reset. + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn rst_survives_a_nack_on_the_last_text_byte() { + let rig = Rig::new(); + rig.request_without_read("rst"); + // Read through the last text byte, NAKing it; the checksum is unread. + rig.begin_read(); + assert_eq!(rig.r(DATA), 3); + assert_eq!(rig.r(DATA), b'o'); + assert_eq!(rig.r(DATA), b'k'); + rig.w(CTRL, ctl::ESO); + assert_eq!(rig.r(DATA), b' '); + + // No STOP: a same-address repeated START carrying an unrelated "ver" + // must not cancel the reset the guest already read the "ok" of. + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, ELSC_ADDR << 1); + for b in frame(b"ver") { + rig.w(DATA, b); + } + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, (ELSC_ADDR << 1) | 1); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn rst_releases_the_i2c_bus() { + let rig = Rig::new(); + rig.request_without_read("rst"); + // Read the whole ack, NAK the last byte, issue no STOP. + rig.begin_read(); + let resp = rig.read_frame(false); + assert_eq!(text(&resp), "ok "); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); + rig.module_reset(); + + // Re-enable and read identity-NVRAM page 7, offset 0, directly. With the + // bus released this routes to the NVRAM and byte 0 is the 0x37 marker; with + // the controller left selected it would answer as the controller. + rig.w(CTRL, ctl::PIN | ctl::ESO); + rig.w(DATA, NVRAM_PAGE7 << 1); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, 0); + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, (NVRAM_PAGE7 << 1) | 1); + let _ = rig.r(DATA); + rig.w(CTRL, ctl::ESO); + assert_eq!(rig.r(DATA), 0x37); + rig.close_with_invalid_address(); +} + +#[test] +fn a_committed_action_ends_the_transfer_that_selected_the_controller() { + // No adapter reset follows: the action itself must end the transfer, or + // the grant ring would see a busy bus for ever. + let rig = Rig::new(); + rig.request_without_read("pwr d 0"); + rig.begin_read(); + assert!(rig.busy.load(Ordering::Relaxed)); + // The "ok" is read in full and the last byte NAKed; no STOP follows. + let resp = rig.read_frame(false); + assert_eq!(text(&resp), "ok "); + assert_eq!(power_events(&rig.events()), [false]); + rig.r(CTRL); + assert!(!rig.busy.load(Ordering::Relaxed)); +} + +#[test] +fn rst_waits_one_second_and_a_second_rst_restarts_the_wait() { + let rig = Rig::new(); + rig.assert_reply("tas A", "ok 0"); + rig.assert_reply("fan h", "ok "); + rig.assert_reply("rst", "ok "); + rig.clock.advance_ms(999); + rig.assert_reply("tas", "ok A"); + rig.assert_reply("rst", "ok "); + rig.clock.advance_ms(999); + rig.assert_reply("tas", "ok A"); + rig.step_ms(1); + rig.elsc.poll(); + assert!(rig.events().contains(&ElscEvent::Reset)); + rig.module_reset(); + rig.assert_reply("tas", "ok 0"); + // The controller rides through the reset it caused. + rig.assert_reply("fan", "ok h"); + + // The countdown keeps running through the reset. + rig.assert_reply("pwr d 3", "ok "); + rig.assert_reply("rst", "ok "); + rig.step_secs(1); + rig.module_reset(); + assert!(rig.elsc.module_powered()); + rig.step_secs(2); + assert_eq!(power_events(&rig.events()), [false]); + + // A reset from outside is the controller's own start and clears the fan + // request. + let rig = Rig::new(); + rig.assert_reply("fan h", "ok "); + rig.elsc.power_on(); + rig.assert_reply("fan", "ok n"); +} + +#[test] +fn a_power_cycle_inside_the_rst_wait_does_not_cancel_it() { + let rig = Rig::new(); + rig.assert_reply("rst", "ok "); + rig.clock.advance_ms(300); + rig.assert_reply("pwr c", "ok "); + rig.assert_reply("tas A", "ok 0"); + rig.clock.advance_ms(699); + rig.assert_reply("tas", "ok A"); + rig.step_ms(1); + rig.elsc.poll(); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn a_committed_action_survives_a_second_destructive_command() { + let rig = Rig::new(); + rig.request_without_read("pwr d 0"); + // Read the "ok" through its last text byte and keep the frame open. + rig.begin_read(); + assert_eq!(rig.r(DATA), 3); + assert_eq!(rig.r(DATA), b'o'); + assert_eq!(rig.r(DATA), b'k'); + assert_eq!(rig.r(DATA), b' '); + // A "rst" arrives while the power-down is committed: it still answers + // "ok" but the power-down fires. + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, ELSC_ADDR << 1); + for b in frame(b"rst") { + rig.w(DATA, b); + } + rig.w(CTRL, ctl::ESO | ctl::STA | ctl::ACK); + rig.w(DATA, (ELSC_ADDR << 1) | 1); + assert_eq!(power_events(&rig.events()), [false]); + assert!(!rig.elsc.module_powered()); +} + +// ---- pwr ------------------------------------------------------------------- + +#[test] +fn pwr_refusals_take_no_action() { + let rig = Rig::new(); + for bad in ["pwr d 259", "pwr d ffff", "pwr c 4", "pwr c 259", "pwr r"] { + let resp = rig.transact(&frame(bad.as_bytes())); + assert_eq!(resp[0], 7, "{bad}"); + assert_eq!(text(&resp), "err arg"); + } + rig.step_secs(1000); + rig.elsc.poll(); + assert!(rig.events().is_empty()); +} + +#[test] +fn an_immediate_pwr_d_powers_down_once_the_ack_is_read() { + let rig = Rig::new(); + rig.assert_reply("pwr d 0", "ok "); + assert_eq!(power_events(&rig.events()), [false]); + assert_eq!(rig.reply("pwr"), "ok d"); +} + +#[test] +fn pwr_c_powers_off_then_back_after_the_interval_and_hold() { + let rig = Rig::new(); + rig.assert_reply("pwr c", "ok "); + assert_eq!(power_events(&rig.events()), [false]); + rig.clock.advance_ms(CYCLE_SECS * SECOND - 1); + rig.elsc.poll(); + assert!(power_events(&rig.events()).is_empty()); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(power_events(&rig.events()), [true]); +} + +#[test] +fn pwr_d_range_is_checked() { + let rig = Rig::new(); + for ok in ["pwr d 1", "pwr d 4", "pwr d 258"] { + let resp = rig.transact(&frame(ok.as_bytes())); + assert_eq!(resp[0], 3); + assert_eq!(text(&resp), "ok "); + } + rig.assert_reply("pwr d 259", "err arg"); +} + +#[test] +fn pwr_d_countdown_expires() { + let rig = Rig::new(); + rig.assert_reply("pwr d 1", "ok "); + rig.clock.advance_ms(999); + rig.elsc.poll(); + assert!(power_events(&rig.events()).is_empty()); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(power_events(&rig.events()), [false]); +} + +#[test] +fn each_timed_pwr_d_reloads_the_countdown() { + let rig = Rig::new(); + rig.assert_reply("pwr d 8", "ok "); + rig.step_secs(7); + rig.assert_reply("pwr d 8", "ok "); + // The deadline is now 15 s, not the first request's 8 s. + rig.clock.advance_ms(7999); + rig.elsc.poll(); + assert!(power_events(&rig.events()).is_empty()); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(power_events(&rig.events()), [false]); + + let rig = Rig::new(); + rig.assert_reply("pwr d 8", "ok "); + rig.step_secs(1); + rig.assert_reply("pwr d 2", "ok "); + // Pulled in to 3 s from 8 s. + rig.clock.advance_ms(1999); + rig.elsc.poll(); + assert!(power_events(&rig.events()).is_empty()); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(power_events(&rig.events()), [false]); +} + +#[test] +fn pwr_d_argument_parsing() { + let rig = Rig::new(); + // 0x1234 is above the limit and arms nothing. + rig.assert_reply("pwr d 12345", "err arg"); + rig.assert_reply("pwr d 259", "err arg"); + rig.step_secs(700); + rig.elsc.poll(); + assert!(rig.elsc.module_powered()); + + // One separator of any kind, then the digits: "pwr d-1x" is one second. + rig.assert_reply("pwr d-1x", "ok "); + rig.clock.advance_ms(999); + rig.elsc.poll(); + assert!(rig.elsc.module_powered()); + rig.step_ms(1); + rig.elsc.poll(); + assert!(!rig.elsc.module_powered()); + + // "pwr d 00001" reads 0000 and powers down at once. + let rig = Rig::new(); + rig.assert_reply("pwr d 00001", "ok "); + assert_eq!(power_events(&rig.events()), [false]); +} + +#[test] +fn pwr_d_without_a_delay() { + // Three-second countdown when none is pending... + let rig = Rig::new(); + rig.assert_reply("pwr d", "ok "); + rig.step_secs(2); + rig.elsc.poll(); + assert!(rig.elsc.module_powered()); + rig.step_secs(1); + rig.elsc.poll(); + assert!(!rig.elsc.module_powered()); + + // ...and immediate when one is; text that is not hex counts as no delay. + let rig = Rig::new(); + rig.assert_reply("pwr d 3c", "ok "); + assert!(rig.elsc.module_powered()); + rig.assert_reply("pwr d zz", "ok "); + assert!(!rig.elsc.module_powered()); +} + +#[test] +fn pwr_c_re_arms_the_cold_power_latch() { + let rig = Rig::new(); + rig.assert_reply("pwr q", "ok 1"); + rig.assert_reply("pwr q", "ok 0"); + rig.assert_reply("pwr c", "ok "); + rig.assert_reply("pwr q", "ok 0"); + rig.step_secs(CYCLE_SECS); + // The cycle re-armed the latch: the first post-cycle query reads 1. + let resp = rig.transact(&frame(b"pwr q")); + assert_eq!(resp[0], 4); + assert_eq!(text(&resp), "ok 1"); +} + +#[test] +fn an_unpowered_module_keeps_answering_and_refuses_what_needs_power() { + let rig = Rig::new(); + rig.assert_reply("pwr c 6", "ok "); + rig.step_ms(1); + rig.assert_reply("pwr", "ok d"); + rig.assert_reply("rst", "err cmd"); + rig.assert_reply("nmi", "err cmd"); + // "pwr d" has nothing to do and does no range check. + rig.assert_reply("pwr d 1", "ok "); + rig.assert_reply("pwr d 259", "ok "); + rig.assert_reply("clr", "ok "); + assert!(rig.elsc.describe().contains("module power: off")); + // The Origin 200 build keeps publishing while the module is off. + rig.observer(); + rig.wait_for_absence(); + rig.wait_for_signal(); + + // Six seconds off, then four of power-up with reset held. + rig.advance_to_ms(9999); + rig.assert_reply("pwr", "ok d"); + rig.advance_to_ms(10_001); + rig.assert_reply("pwr", "ok u"); + + // "pwr u" cuts the off interval short; power is back four seconds on. + rig.assert_reply("pwr c 258", "ok "); + rig.step_secs(1); + rig.assert_reply("pwr u", "ok "); + rig.clock.advance_ms(3999); + rig.assert_reply("pwr", "ok d"); + rig.step_ms(1); + rig.assert_reply("pwr", "ok u"); +} + +#[test] +fn pwr_d_posts_pd_remote_to_the_hubs() { + let rig = Rig::new(); + rig.observer(); + rig.assert_reply("pwr d 259", "err arg"); + assert!(!rig.elsc.describe().contains("controller message")); + + rig.assert_reply("pwr d 8", "ok "); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + rig.assert_reply("get", "pd remote"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + rig.assert_reply("get", "acp . "); + + // Unfetched, it lapses after a second. + rig.assert_reply("pwr d 8", "ok "); + rig.step_ms(1000); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + rig.assert_reply("get", "acp . "); + + // The soft power-down tells the Hubs too. + rig.assert_reply("clr", "ok "); + rig.assert_reply("pwr d", "ok "); + assert!(rig.elsc.describe().contains("controller message: \"pd remote\"")); + rig.assert_reply("clr", "ok "); +} + +#[test] +fn nmi_pulses_after_one_second_for_100_ms_and_frees_the_lock() { + let rig = Rig::new(); + rig.assert_reply("tas A", "ok 0"); + rig.assert_reply("nmi", "ok "); + rig.clock.advance_ms(999); + rig.assert_reply("nmi junk", "ok "); + rig.clock.advance_ms(999); + assert_eq!(count_nmi(&rig.events()), 0); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(rig.events(), [ElscEvent::Nmi(true)]); + rig.assert_reply("tas", "ok A"); + rig.clock.advance_ms(99); + rig.elsc.poll(); + assert!(rig.events().is_empty()); + rig.step_ms(1); + rig.elsc.poll(); + assert_eq!(rig.events(), [ElscEvent::Nmi(false)]); + rig.assert_reply("tas", "ok 0"); + + // A power cycle inside the wait does not cancel it, but the pulse then + // reaches no processor of the unpowered module: the lock is still freed. + rig.assert_reply("nmi", "ok "); + rig.clock.advance_ms(500); + rig.assert_reply("pwr c", "ok "); + rig.assert_reply("tas C", "ok 0"); + rig.clock.advance_ms(500); + rig.assert_reply("tas", "ok 0"); + assert_eq!(count_nmi(&rig.events()), 0); +} + +// ---- start-up power-up ------------------------------------------------------ + +#[test] +fn start_delay_waits_then_powers_up_when_the_nvram_says_so() { + let rig = Rig::with(ElscConfig { start_delay_ms: 500, ..Default::default() }); + rig.assert_reply("pwr", "ok d"); + assert!(rig.elsc.power_return_pending()); + rig.clock.advance_ms(499); + rig.assert_reply("pwr", "ok d"); + rig.step_ms(1); + // The 4 s power-up reset hold follows. + rig.clock.advance_ms(3999); + rig.assert_reply("pwr", "ok d"); + rig.step_ms(1); + rig.assert_reply("pwr", "ok u"); + assert_eq!(power_events(&rig.events()), [true]); + assert!(!rig.elsc.power_return_pending()); +} + +#[test] +fn start_delay_stays_off_without_automatic_power_on() { + let rig = Rig::with(ElscConfig { start_delay_ms: 100, ..Default::default() }); + rig.assert_reply("aut 0", "ok "); + rig.step_secs(10); + rig.assert_reply("pwr", "ok d"); + // The controller says again that the power is off, so the machine asks + // whether anything is left to bring a module back. + assert_eq!(power_events(&rig.events()), [false]); + assert!(!rig.elsc.power_return_pending()); +} + +// ---- AUX serial line --------------------------------------------------------- + +#[test] +fn the_start_announces_the_version_on_the_serial_line() { + let rig = Rig::new(); + assert_eq!(rig.aux.take(), BANNER); +} + +#[test] +fn serial_command_line() { + let rig = Rig::new(); + rig.aux.take(); + rig.serial_command("ver", "ok V 3.1S "); + // Delete and backspace remove the last character. + rig.type_bytes(b"\x14verx\x7f\r"); + assert_eq!(rig.aux.take(), "\r\nMSC> verx\x08 \x08\r\n\x14ok V 3.1S \r\n"); + // Enter alone answers "ok" with echo on. + rig.serial_command("", "ok"); + // Only a carriage return separates a bare mnemonic. + rig.type_bytes(b"\x14ver\n"); + assert_eq!(rig.aux.take(), "\r\nMSC> ver\n\x14err cmd\r\n"); + rig.type_bytes(b"\x14pwr q\n"); + assert_eq!(rig.aux.take(), "\r\nMSC> pwr q\n\x14ok 1\r\n"); + rig.serial_command("acp 0 x", "err cmd"); + rig.serial_command("get", "err cmd"); + // The serial "fsc" keeps the digit the I2C reply loses. + rig.serial_command("fsc", "ok0"); + rig.serial_command("fsc 1", "ok"); + rig.serial_command("fsc x", "ok1"); + + // A second Ctrl+T abandons the line, keeps it and passes one on. + rig.type_bytes(b"\x14ve\x14"); + assert_eq!(rig.aux.take(), "\r\nMSC> ve\x08\x08 \x08\x08 \x08\x08 \x08\x08 \x08"); + rig.type_bytes(b"\x14r\r"); + assert_eq!(rig.aux.take(), "\r\nMSC> r\r\n\x14ok V 3.1S \r\n"); + // A terminal's line feed after the return goes to the processors. + rig.type_bytes(b"\x14tmp\r\n"); + assert_eq!(rig.aux.take(), "\r\nMSC> tmp\r\n\x14ok n\r\n"); + assert!(rig.elsc.describe().contains("input: \"\\x14\\x0a\"")); + + // Processor output arriving while a line is typed is lost. + rig.type_bytes(b"\x14v"); + assert_eq!(rig.aux.take(), "\r\nMSC> v"); + rig.assert_reply("acp 0 hi", "ok 2"); + assert_eq!(rig.aux.take(), ""); + rig.type_bytes(b"er\r"); + assert_eq!(rig.aux.take(), "er\r\n\x14ok V 3.1S \r\n"); + rig.assert_reply("acp 0 yo", "ok 2"); + assert_eq!(rig.aux.take(), "yo"); + + // "cfg" prints the NVRAM raw, here its first byte, a zero. + rig.type_bytes(b"\x14cfg\r"); + assert_eq!(rig.aux.take(), "\r\nMSC> cfg\r\n\0\r\n"); + + // Echo off: no prompt, no echo, and Enter alone is "err cmd". + rig.serial_command("ech 0", "ok"); + rig.type_bytes(b"\x14\r"); + assert_eq!(rig.aux.take(), "\x14err cmd\r\n"); + rig.type_bytes(b"\x14ech\r"); + assert_eq!(rig.aux.take(), "\x14ok\r\n"); + rig.serial_command("ech 2", "err arg"); +} + +#[test] +fn a_serial_line_waits_for_the_bus() { + let rig = Rig::new(); + rig.aux.take(); + // An I2C transfer to the controller is open: the line queues behind it. + rig.w(CTRL, ctl::PIN | ctl::ESO); + rig.w(DATA, ELSC_ADDR << 1); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + rig.type_bytes(b"\x14ver\r"); + assert_eq!(rig.aux.take(), "\r\nMSC> ver\r\n"); + rig.w(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + rig.elsc.poll(); + assert_eq!(rig.aux.take(), "\x14ok V 3.1S \r\n"); +} + +#[test] +fn a_slave_answers_ctrl_t_with_the_prompt_and_takes_no_command() { + let rig = Rig::with(ElscConfig { master: false, ..Default::default() }); + rig.aux.take(); + rig.type_bytes(b"\x14"); + assert_eq!(rig.aux.take(), "\r\nMSC> Slave!\r\n"); + rig.type_bytes(b"ver\r"); + assert_eq!(rig.aux.take(), ""); +} + +#[test] +fn serial_supervisor_gates_the_destructive_commands() { + let rig = Rig::new(); + rig.aux.take(); + rig.serial_command("hbt 0", "err perm"); + rig.serial_command("fan h", "err perm"); + rig.serial_command("pwr d 8", "err perm"); + rig.serial_command("pwr q", "ok 1"); + rig.serial_command("mod 7", "err perm"); + rig.serial_command("mod", "ok 0"); + rig.serial_command("pas abc", "err arg"); + rig.serial_command("pas nope", "err arg"); + rig.serial_command("pas s abcd", "err perm"); + rig.serial_command("pas none", "ok"); + rig.serial_command("hbt 0", "ok"); + rig.serial_command("mod 7", "ok"); + rig.serial_command("mod", "ok 7"); + rig.serial_command("nvm 708", "ok 7"); + rig.serial_command("nvm 705 3", "ok"); + rig.serial_command("dbg", "ok 3 0"); + rig.serial_command("dbg 1 2", "ok"); + rig.serial_command("dbg", "ok 1 2"); + rig.serial_command("fre", "err cmd"); + + // On the serial line the fan request is kept in the NVRAM too. + rig.serial_command("fan h", "ok"); + rig.serial_command("nvm 707", "ok 3"); + + // The Hub message goes out on the line as it is queued. + rig.type_bytes(b"\x14pwr d 258\r"); + assert_eq!( + rig.aux.take(), + "\r\nMSC> pwr d 258\r\n\x14\r\nMSC: pd remote\x14\r\x14ok\r\n" + ); + // A line waits while a Hub message is pending; it lapses after a second. + rig.step_secs(1); + + rig.serial_command("pas s ab", "err arg"); + rig.serial_command("pas s abcd", "err arg"); + rig.serial_command("pas s wxyz12", "ok"); + rig.serial_command("nvm 701", "ok 77"); + rig.serial_command("pas", "ok"); + rig.serial_command("hbt 0", "err perm"); + rig.serial_command("pas none", "err arg"); + rig.serial_command("pas wxyz", "ok"); + // A host's "clr" over I2C ends the session. + rig.assert_reply("clr", "ok "); + rig.aux.take(); + rig.serial_command("hbt 0", "err perm"); + rig.serial_command("rst", "err perm"); + assert_eq!(&rig.nvram.contents()[0x701..0x705], b"wxyz"); +} + +#[test] +fn a_serial_nvm_to_a_missing_device_prints_a_bare_err() { + let rig = Rig::new(); + rig.aux.take(); + // Device 0xa0 | (8 << 1) is 0x58, which no part answers. + rig.serial_command("nvm 800", "err "); +} + +#[test] +fn a_serial_reset_runs_once_its_reply_is_printed() { + let rig = Rig::new(); + rig.aux.take(); + rig.serial_command("pas none", "ok"); + rig.type_bytes(b"\x14rst\r"); + let out = rig.aux.take(); + assert!(out.ends_with("\x14ok\r\n\x14\r\nMSC: rst\x14\r"), "{out:?}"); + rig.step_secs(1); + assert!(rig.events().contains(&ElscEvent::Reset)); +} + +#[test] +fn the_serial_input_queue_overwrites_its_oldest_byte() { + let rig = Rig::new(); + rig.observer(); + // Input waits for the first processor to write before it is announced. + rig.type_bytes(b"x"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL1); + rig.assert_reply("acp 0 ", "ok 0"); + rig.wait_for_absence(); + assert_eq!(rig.wait_for_signal(), SIGNAL2); + + let mut line = vec![b'a'; 300]; + // The first byte of the overflow is the one that must be lost. + line[0] = b'b'; + rig.type_bytes(&line); + rig.type_bytes(b"z"); + let info = rig.elsc.describe(); + assert!(info.contains("rx queue: 256 byte(s)"), "{info}"); + assert!(info.contains("input: \"aaaa"), "{info}"); +} + +/// The exchanges the Origin 200 PROM makes before the System Maintenance +/// Menu, in order, with the frames spelled out byte for byte: a request and +/// the reply it accepts. Replies it discards (`dsp`, `dsc`) are `err cmd`, +/// which it tolerates. +#[test] +fn the_prom_exchanges_get_the_frames_it_accepts() { + const ERR_CMD: [u8; 9] = [0x07, 0x65, 0x72, 0x72, 0x20, 0x63, 0x6d, 0x64, 0x5c]; + let exchanges: &[(&[u8], &[u8])] = &[ + // dsp and eight blanks; dsp P 0 M 0 + (&[0x0b, 0x64, 0x73, 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xae], &ERR_CMD), + (&[0x0c, 0x64, 0x73, 0x70, 0x20, 0x50, 0x20, 0x30, 0x20, 0x4d, 0x20, 0x30, 0x20, 0x10], &ERR_CMD), + // rsw: ok ff + (&[0x03, 0x72, 0x73, 0x77, 0xa1], &[0x05, 0x6f, 0x6b, 0x20, 0x66, 0x66, 0x35]), + // pwr q: ok 1 + (&[0x05, 0x70, 0x77, 0x72, 0x20, 0x71, 0x11], &[0x04, 0x6f, 0x6b, 0x20, 0x31, 0xd1]), + // rsw again, then ver: ok V 3.1S + (&[0x03, 0x72, 0x73, 0x77, 0xa1], &[0x05, 0x6f, 0x6b, 0x20, 0x66, 0x66, 0x35]), + ( + &[0x03, 0x76, 0x65, 0x72, 0xb0], + &[0x0b, 0x6f, 0x6b, 0x20, 0x56, 0x20, 0x33, 0x2e, 0x31, 0x53, 0x20, 0x20, 0x60], + ), + // dsc 1 e; dsc 2 0 + (&[0x07, 0x64, 0x73, 0x63, 0x20, 0x31, 0x20, 0x65, 0xe9], &ERR_CMD), + (&[0x07, 0x64, 0x73, 0x63, 0x20, 0x32, 0x20, 0x30, 0x1d], &ERR_CMD), + // acp 0 Starting PROM Boot process..: the count is hex and nonzero, + // or the PROM sends the same characters again for ever. 0x1c is 28. + ( + &[ + 0x22, 0x61, 0x63, 0x70, 0x20, 0x30, 0x20, 0x53, 0x74, 0x61, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x20, + 0x50, 0x52, 0x4f, 0x4d, 0x20, 0x42, 0x6f, 0x6f, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x2e, 0x2e, 0x61, + ], + &[0x05, 0x6f, 0x6b, 0x20, 0x31, 0x63, 0x6d], + ), + // acp 0 x: ok 1 + (&[0x07, 0x61, 0x63, 0x70, 0x20, 0x30, 0x20, 0x78, 0xdd], &[0x04, 0x6f, 0x6b, 0x20, 0x31, 0xd1]), + ]; + let rig = Rig::new(); + for (request, reply) in exchanges { + assert_eq!(&rig.transact(request), reply, "request {request:02x?}"); + } +} + +#[test] +fn an_acp_reply_never_counts_more_than_was_sent_nor_zero_for_text() { + let rig = Rig::new(); + // The PROM sends up to 86 characters per exchange. A request that carries + // more (up to the 94-byte frame) is cut to 86 and the count says so, so + // the sender resends only the rest. + for sent in [1usize, 2, 15, 16, 85, 86, 88] { + let reply = rig.reply(&format!("acp 0 {}", "x".repeat(sent))); + let count = usize::from_str_radix(reply.strip_prefix("ok ").unwrap(), 16).unwrap(); + assert!(count >= 1 && count <= sent && count <= 86, "{sent}: {reply}"); + } +} + +#[test] +fn replies_stay_below_the_prom_length_limit() { + // The PROM rejects a reply of 96 bytes or more, or one whose byte count + // is not length plus two. + let rig = Rig::new(); + for text in ["ver", "rsw", "get", "tmp", "tas A", "pwr q", "acp 0 abc", "xyz"] { + let resp = rig.transact(&frame(text.as_bytes())); + assert!(resp.len() < 96 + 2 && resp.len() == resp[0] as usize + 2, "{text}"); + assert_eq!(byte_sum(&resp), 0); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 599be6a30..98cd3f626 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -42,6 +42,7 @@ pub mod cdmc; pub mod daynaport; pub mod ds1x86; pub mod eeprom_93c56; +pub mod elsc; pub mod elsc_nvram; pub mod hal2; pub mod hpc3; From 165135c8653f512a0d0f9a8e34522c4ce997aff2 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 05:09:53 -0700 Subject: [PATCH 23/38] elsc: add the Origin 2000 controller store image An Origin 2000 module starts from a different store than an Origin 200: the configuration byte is 0, with no automatic power-up bit, the module number is the one the machine selects, and the clock-select byte at 0x70b is 0. The marker and the password are the same. Offer it as ElscNvram::origin2000 so the machine supplies the image and never builds its contents. The 0x70b value is an approximation. The controller writes 0x78 there on the full-clock midplane type and 0 on the others, and that this module is the full-clock type is only inferred, so the byte stays 0 as the model has always presented it. --- src/dev/elsc_nvram.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/dev/elsc_nvram.rs b/src/dev/elsc_nvram.rs index 44fdb1bc5..87f04b773 100644 --- a/src/dev/elsc_nvram.rs +++ b/src/dev/elsc_nvram.rs @@ -71,6 +71,21 @@ pub fn origin200_image() -> [u8; BYTES] { fresh_image(CONFIG_AUTO_POWER, 0, CLOCK_FULL) } +/// The controller store an Origin 2000 module starts from. The controller +/// writes this after its first power-up: the marker, the password `none`, +/// configuration byte 0 (no automatic power-up bit), the module number the +/// machine selects and clock-select byte 0, which an Origin 2000 PROM reads as +/// the domain. Every other byte is zero. +/// +/// Byte `0x70b` is an approximation. A controller writes its clock-select +/// default there, `0x78` on the full-clock midplane type and 0 on the others, +/// and that this module is the full-clock type is only inferred, so it stays 0. +/// The Origin 200 image differs in the configuration byte (`0x02`) and this +/// byte (`0x78`). +pub fn origin2000_image(module: u8) -> [u8; BYTES] { + fresh_image(0, module, 0) +} + struct State { mem: [u8; BYTES], /// Block the current transfer addressed. @@ -165,6 +180,16 @@ impl ElscNvram { } } + /// The Origin 2000 module's store for module number `module`: see + /// [`origin2000_image`]. In memory when `path` is `None`, otherwise + /// backed by that file as [`Self::open`] does, its contents winning. + pub fn origin2000(module: u8, path: Option<&Path>) -> io::Result { + match path { + None => Ok(Self::from_image(origin2000_image(module), None)), + Some(p) => Self::open(p, 0, module, 0), + } + } + /// A store with no backing file, holding a fresh image. pub fn new(config: u8, module: u8, clock: u8) -> Self { Self::from_image(fresh_image(config, module, clock), None) @@ -354,6 +379,17 @@ mod tests { assert_eq!(ElscNvram::origin200(None).unwrap().contents(), want); } + #[test] + fn the_origin_2000_image_has_no_auto_power_bit_and_the_selected_module() { + let image = origin2000_image(3); + let mut want = [0u8; BYTES]; + want[0x700] = 0x37; + want[0x701..0x705].copy_from_slice(b"none"); + want[0x708] = 3; + assert_eq!(image, want); + assert_eq!(ElscNvram::origin2000(3, None).unwrap().contents(), want); + } + #[test] fn blocks_are_addressed_by_the_i2c_address() { let mut rig = fresh(); From 551f85bad26028094166dc3a4e7a9097a6968e72 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 05:09:53 -0700 Subject: [PATCH 24/38] pcf8584: let several adapters share one module bus An Origin 2000 module puts up to four adapters in front of one controller and one NVRAM. The NVRAM keeps a single address counter, so two nodes' transfers must not interleave. Add a BusOwner that the adapters share: a first START that finds another adapter holding the bus waits and runs after the holder's STOP, a STOP or clearing ESO withdraws it, and the status BB bit reads busy while another adapter holds the bus. The holder's STOP hands the bus to the lowest waiter, so a holder that STARTs again cannot overtake it. What a START on a busy bus does is unspecified, so waiting is a model choice. The clocked shared wire, with bitwise arbitration loss and clock stretching, is not modelled. --- src/dev/pcf8584.rs | 177 +++++++++++++++++++++++++++++++++++++-- src/dev/pcf8584/tests.rs | 97 +++++++++++++++++++++ 2 files changed, 265 insertions(+), 9 deletions(-) diff --git a/src/dev/pcf8584.rs b/src/dev/pcf8584.rs index ee7163073..64d5dbb63 100644 --- a/src/dev/pcf8584.rs +++ b/src/dev/pcf8584.rs @@ -30,6 +30,7 @@ use crate::irq::IrqLine; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK}; use parking_lot::Mutex; use std::sync::atomic::Ordering; +use std::sync::Arc; /// The two addresses the A0 pin selects, spaced as the Hub wires them. pub const A0_STRIDE: u64 = 8; @@ -57,6 +58,86 @@ pub mod sts { pub const BB: u8 = 0x01; } +/// Which of several adapters on one module bus has it open. Adapters share +/// targets and so a transfer by one must not interleave with another's: a +/// START that finds the bus held waits, and the holder's STOP hands the bus +/// to the waiting adapter with the lowest number, so a holder that STARTs +/// again cannot overtake it. +/// +/// The state is a few bits under a leaf mutex rather than atomics: nothing +/// else is published through it, but owner and waiters change together and +/// the handover at a STOP has to be one step. It is only touched by I2C +/// register accesses, which are rare. It is a leaf lock: an adapter takes it +/// under its own, and the module scheduler calls `busy()` under the module +/// state lock, and nothing is called while it is held. +pub struct BusOwner(Mutex); + +struct Held { + owner: Option, + /// One bit per adapter whose first START is waiting. + waiting: u32, +} + +impl BusOwner { + pub fn new() -> Arc { + Arc::new(Self(Mutex::new(Held { owner: None, waiting: 0 }))) + } + + /// Take the bus, or confirm it is already `id`'s (a handover). A free bus + /// goes only to the lowest waiter when there are any. + fn try_acquire(&self, id: u32) -> bool { + let mut held = self.0.lock(); + match held.owner { + Some(owner) => owner == id, + None if held.waiting == 0 || held.waiting.trailing_zeros() == id => { + held.owner = Some(id); + held.waiting &= !(1 << id); + true + } + None => false, + } + } + + /// `id`'s START found the bus held and waits for it. + fn wait(&self, id: u32) { + self.0.lock().waiting |= 1 << id; + } + + /// `id`'s waiting START is withdrawn. If the bus had already been handed + /// to it, it is passed on. + fn withdraw(&self, id: u32) { + self.0.lock().waiting &= !(1 << id); + self.release(id); + } + + /// `id` lets go of the bus; the lowest waiter, if any, has it at once. + fn release(&self, id: u32) { + let mut held = self.0.lock(); + if held.owner != Some(id) { + return; + } + held.owner = None; + if held.waiting != 0 { + let next = held.waiting.trailing_zeros(); + held.waiting &= !(1 << next); + held.owner = Some(next); + } + } + + fn held_by_other(&self, id: u32) -> bool { + self.0.lock().owner.is_some_and(|owner| owner != id) + } + + fn owned_by(&self, id: u32) -> bool { + self.0.lock().owner == Some(id) + } + + /// Some adapter has the bus open. + pub fn busy(&self) -> bool { + self.0.lock().owner.is_some() + } +} + enum Reg { OwnAddress, Clock, @@ -94,6 +175,11 @@ struct State { rx_directed: bool, /// Interrupt output level last reported to the line. irq_level: bool, + /// The bus this adapter shares with others, and its place among them. + shared: Option<(Arc, u32)>, + /// A first START that found the bus held by another adapter, waiting for + /// it to be released. + waiting: Option, } impl State { @@ -116,6 +202,8 @@ impl State { rx_current: false, rx_directed: false, irq_level: false, + shared: None, + waiting: None, } } @@ -230,7 +318,46 @@ impl State { } } + fn acquire(&self) -> bool { + self.shared.as_ref().is_none_or(|(owner, id)| owner.try_acquire(*id)) + } + + fn release(&self) { + if let Some((owner, id)) = &self.shared { + owner.release(*id); + } + } + + fn owns(&self) -> bool { + self.shared.as_ref().is_some_and(|(owner, id)| owner.owned_by(*id)) + } + + fn held_by_other(&self) -> bool { + self.shared.as_ref().is_some_and(|(owner, id)| owner.held_by_other(*id)) + } + + /// A waiting START is withdrawn; a bus already handed to it moves on. + fn withdraw(&mut self) { + if self.waiting.take().is_some() { + if let Some((owner, id)) = &self.shared { + owner.withdraw(*id); + } + } + } + + /// A START that waited for the bus runs once it is handed over. + fn resume_waiting(&mut self) { + if let Some(addr) = self.waiting { + if self.acquire() { + self.waiting = None; + self.start(addr); + } + } + } + fn stop(&mut self) { + self.withdraw(); + self.release(); if self.transfer { self.bus_end(); } @@ -310,7 +437,12 @@ impl State { } fn write_control(&mut self, val: u8) { + let prev = self.control; self.control = val; + if prev & ctl::ESO != 0 && val & ctl::ESO == 0 { + // Clearing ESO withdraws a START that is waiting. + self.withdraw(); + } if val & ctl::PIN != 0 { // PIN from the host is the idle request. It is also written alone @@ -332,7 +464,18 @@ impl State { }; // First start: the address is already staged in the register. self.status |= sts::PIN; - self.start(addr); + self.start_address_staged = false; + if self.acquire() { + self.start(addr); + } else { + // Another adapter has the bus: the START waits for it. + // The specification does not say what a START on a busy + // bus does, so this is a model choice. + self.waiting = Some(addr); + if let Some((owner, id)) = &self.shared { + owner.wait(*id); + } + } } return; } @@ -349,7 +492,8 @@ impl State { // Write-only in the documentation with the serial interface off; // real parts answer with the control bits plus the PIN state. if self.control & ctl::ESO != 0 { - return self.status; + // The bus reads busy while another adapter has it open. + return if self.held_by_other() { self.status & !sts::BB } else { self.status }; } return (self.control & !sts::PIN) | (self.status & sts::PIN); } @@ -381,9 +525,11 @@ impl State { if self.transfer && self.bus_busy() { self.bus_end(); } + self.withdraw(); + self.release(); // The line level survives so the access that follows reports a drop. - let (bus, irq_level) = (std::mem::take(&mut self.bus), self.irq_level); - *self = Self { bus, irq_level, ..Self::new() }; + let (bus, irq_level, shared) = (std::mem::take(&mut self.bus), self.irq_level, self.shared.take()); + *self = Self { bus, irq_level, shared, ..Self::new() }; } } @@ -406,6 +552,15 @@ impl Pcf8584 { } } + /// An adapter among several on one module bus, `id` its place. The + /// adapters share targets, so `owner` keeps one adapter's transfer from + /// interleaving with another's. + pub fn shared(bus: I2cBus, busy: BusBusy, owner: Arc, id: u32) -> Self { + let p = Self::new(bus, busy); + p.state.lock().shared = Some((owner, id)); + p + } + /// Where the chip's interrupt output leads. The Origin 200 Hub does not /// connect it; software polls. pub fn set_irq(&self, irq: IrqLine) { @@ -419,6 +574,7 @@ impl Pcf8584 { let (value, level) = { let mut st = self.state.lock(); Self::service(&mut st); + st.resume_waiting(); let value = f(&mut st); Self::service(&mut st); self.busy.store(st.bus_busy(), Ordering::Relaxed); @@ -441,13 +597,16 @@ impl Pcf8584 { let service: Service = st.bus.targets()[i].service(); if service.release_bus && st.bus_busy() { st.bus_end(); + st.release(); } if let Some(byte) = service.grant { - // An address-only transaction nobody acknowledges; it cannot - // open while a transfer holds the bus. - if !st.bus_busy() { - st.bus_start(byte >> 1, byte & 1 != 0); - st.bus_end(); + // An address-only transaction nobody acknowledges. It is seen + // passively, as the adapter's slave half sees any address phase + // at the STOP that ends it: it takes neither the bus nor + // notice of a START waiting, and an adapter that has a + // transfer of its own open is not listening. + if !st.bus_busy() && (0xe0..=0xef).contains(&byte) { + st.observe_address(byte); } } } diff --git a/src/dev/pcf8584/tests.rs b/src/dev/pcf8584/tests.rs index 4c207744b..7e3ee4bf0 100644 --- a/src/dev/pcf8584/tests.rs +++ b/src/dev/pcf8584/tests.rs @@ -421,3 +421,100 @@ fn every_access_width_is_one_register_access() { assert!(!p.read32(MMIO_SIZE).is_ok()); assert_eq!(p.write8(MMIO_SIZE, 0), BUS_ERR); } + +#[test] +fn a_start_on_a_bus_another_adapter_holds_waits_and_runs_after_its_stop() { + let echo = Echo::new(0x10); + let owner = BusOwner::new(); + let make = |id: u32| { + let mut bus = I2cBus::new(); + bus.add(echo.clone()); + Pcf8584::shared(bus, BusBusy::default(), owner.clone(), id) + }; + let (a, b) = (make(0), make(1)); + for p in [&a, &b] { + p.write_register(CTRL, ctl::PIN | ctl::ESO); + } + a.write_register(DATA, 0x10 << 1); + a.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert!(owner.busy()); + // B sees the bus busy, and its START waits. + assert_eq!(status(&b) & sts::BB, 0); + b.write_register(DATA, 0x10 << 1); + b.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_eq!(status(&b) & sts::PIN, sts::PIN); + a.write_register(DATA, 0x11); + a.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + // After A's STOP, B's START runs and completes. + assert_eq!(status(&b) & (sts::PIN | sts::LRB), 0); + b.write_register(DATA, 0x22); + b.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert!(!owner.busy()); + assert_eq!(echo.state.lock().0, [0x22]); +} + +/// A holder that STARTs again straight after its STOP must not overtake an +/// adapter that was already waiting: the STOP hands the bus to the waiter. +#[test] +fn a_waiting_start_is_served_before_the_holders_next_start() { + let echo = Echo::new(0x10); + let owner = BusOwner::new(); + let make = |id: u32| { + let mut bus = I2cBus::new(); + bus.add(echo.clone()); + Pcf8584::shared(bus, BusBusy::default(), owner.clone(), id) + }; + let (a, b) = (make(0), make(1)); + for p in [&a, &b] { + p.write_register(CTRL, ctl::PIN | ctl::ESO); + } + let start = |p: &Pcf8584| { + p.write_register(DATA, 0x10 << 1); + p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + }; + start(&a); + start(&b); + assert_eq!(status(&b) & sts::PIN, sts::PIN); + a.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + // A, without having looked at the bus, STARTs again at once. + start(&a); + // The bus is B's: A waits, B's START has run. + assert_eq!(status(&a) & sts::PIN, sts::PIN); + assert_eq!(status(&a) & sts::BB, 0); + assert_eq!(status(&b) & (sts::PIN | sts::LRB), 0); + b.write_register(DATA, 0xb1); + b.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + // B's STOP hands it to A in turn. + assert_eq!(status(&a) & (sts::PIN | sts::LRB), 0); + a.write_register(DATA, 0xa1); + a.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert!(!owner.busy()); + assert_eq!(echo.state.lock().0, [0xa1]); +} + +#[test] +fn the_lowest_waiter_is_served_first_and_a_withdrawn_one_passes_the_bus_on() { + let echo = Echo::new(0x10); + let owner = BusOwner::new(); + let make = |id: u32| { + let mut bus = I2cBus::new(); + bus.add(echo.clone()); + Pcf8584::shared(bus, BusBusy::default(), owner.clone(), id) + }; + let adapters: Vec<_> = (0..3).map(make).collect(); + for p in &adapters { + p.write_register(CTRL, ctl::PIN | ctl::ESO); + p.write_register(DATA, 0x10 << 1); + } + let go = |p: &Pcf8584| p.write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + go(&adapters[0]); + go(&adapters[2]); + go(&adapters[1]); + // Adapter 1 gives up while waiting; the bus goes 0 -> 2. + adapters[1].write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + adapters[0].write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert_eq!(status(&adapters[2]) & (sts::PIN | sts::LRB), 0); + assert_eq!(status(&adapters[1]) & sts::BB, 0); + adapters[2].write_register(CTRL, ctl::PIN | ctl::ESO | ctl::STO); + assert!(!owner.busy()); +} From 22a4359ffa82a06e6d2ee317607699d7970b633d Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 05:09:53 -0700 Subject: [PATCH 25/38] elsc: add the Origin 2000 build of the module controller The Origin 2000 and Onyx2 controller is the same program built a second way. It answers dsp, dsc, key and vlm, which the Origin 200 build refuses with err cmd, and not aut. Its version field is VER 3.2, it holds the module in reset 4.4 s at power-up, it has an eight-character display that status messages replace, and its role bit in rsw is not the master flag. What a module owns, not the controller, is reached through a ModuleOps trait: whether the module is powered, whether a reset or power action can happen, the module's NMI, the serial turn on the bus and the midplane one-wire line. A command the module cannot carry out is answered err cmd, because a caller takes ok as proof. Calls that reach back into the controller are queued and delivered with its lock released. The status message texts and the supply margins' electrical effect are unknown and not invented. The Origin 200 master/slave forwarding stays out of both builds. --- src/dev/elsc.rs | 626 ++++++++++++++++++++++++++++++++++++------ src/dev/elsc/tests.rs | 60 +++- 2 files changed, 587 insertions(+), 99 deletions(-) diff --git a/src/dev/elsc.rs b/src/dev/elsc.rs index 6d7c0e1c0..855532ba4 100644 --- a/src/dev/elsc.rs +++ b/src/dev/elsc.rs @@ -1,6 +1,14 @@ -//! Module system controller (MSC, the ELSC) of an Origin 200 module: a -//! checksummed ASCII command and reply protocol over I2C, the arbitration -//! grants the processors wait for, and the AUX serial port. +//! Module system controller (MSC, the ELSC) of an Origin 200 or an Origin +//! 2000 module: a checksummed ASCII command and reply protocol over I2C, the +//! arbitration grants the processors wait for, and the AUX serial port. +//! +//! The two builds are one program built two ways. The Origin 200 build +//! (`V 3.1S`, 4 s power-up hold) publishes its own five-position grant ring +//! and has `aut`. The Origin 2000 build (`VER 3.2`, 4.4 s hold) has an +//! eight-character display that status messages replace, and `dsp`, `dsc`, +//! `key`, `vlm` and (on the serial line) `fre`; the commands a build lacks +//! answer `err cmd`. An Origin 2000 controller leaves grants, power and +//! reset to the module that owns it through [`ModuleOps`]. //! //! The controller is an I2C target at seven-bit address 0x08 on the Hub's //! bus, next to its [`ElscNvram`]. The module's power, reset and NMI are @@ -12,12 +20,12 @@ //! text, the grant schedule, the queue depths and the reply text below are //! the model's own choices, approximations unless a comment says otherwise: //! -//! * the version field `V 3.1S` and its padding; +//! * the version fields `V 3.1S` and `VER 3.2` and their padding; //! * the one-millisecond grant tick, the five ring positions (processor codes //! 0 to 3, then a fifth published as code 4's byte), and that a busy tick //! publishes nothing and does not advance; -//! * the 4 s power-up reset hold, the 1 s waits of `rst` and `nmi`, the -//! 100 ms NMI pulse, the 1 s life of a controller message, the 5 s warning +//! * the 4 s (Origin 200) and 4.4 s (Origin 2000) power-up reset hold, the +//! 1 s waits of `rst` and `nmi`, the 100 ms NMI pulse, the 1 s life of a controller message, the 5 s warning //! and the 5 s off interval of the heartbeat watchdog; //! * the 256-byte console input queue that overwrites its oldest byte; //! * that cold-power latch `pwr q` clears on read; @@ -33,10 +41,10 @@ //! //! Migration and snapshot state are not saved yet. //! -//! Not modelled: the Origin 2000 build, the second module's controller and -//! the master's forwarding to it, sensors, and the Origin 2000 four-adapter -//! bus (a module-management device owns that and would drive the same -//! [`I2cTarget`] and event interfaces). +//! Not modelled: the second Origin 200 module's controller and the master's +//! forwarding to it, sensors, the electrical effect of the supply margins, the +//! status messages' display texts (unknown), and the Origin 2000 module's +//! shared bus, which the module-management device owns. //! //! Ported from hw/misc/sgi-elsc.c in jamesbraid/qemu-sgi-origami. @@ -64,7 +72,10 @@ pub const RX_QUEUE_SIZE: usize = 256; pub const DELAY_MAX: u32 = 0x258; pub const PROCESSORS: usize = 8; -const VERSION: &str = "V 3.1S "; +const VERSION_ORIGIN200: &str = "V 3.1S "; +const VERSION_ORIGIN2000: &str = "VER 3.2 "; +const DISPLAY_CHARS: usize = 8; +const SUPPLIES: usize = 3; const SEE_ALL: u8 = 0xff; const CTRL_T: u8 = 0x14; const TAS_FREE: u8 = b'0'; @@ -72,7 +83,8 @@ const CYCLE_DEFAULT_SECONDS: u32 = 5; const HBT_CYCLE_SECONDS: u64 = 5; const HBT_DEFAULT_DELAY: u32 = 30; const HBT_WARNING: u64 = 5; -const POWER_UP_HOLD_MS: u64 = 4000; +const POWER_UP_HOLD_MS_ORIGIN200: u64 = 4000; +const POWER_UP_HOLD_MS_ORIGIN2000: u64 = 4400; const MESSAGE_LIFETIME_MS: u64 = 1000; const RESET_WAIT_MS: u64 = 1000; const NMI_WAIT_MS: u64 = 1000; @@ -96,8 +108,60 @@ pub enum ElscEvent { Nmi(bool), } +/// Which build of the controller firmware this is: one program built two +/// ways, differing in the commands it answers, its version text and its +/// display. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Build { + Origin200, + Origin2000, +} + +/// What an Origin 2000 module owns that is module-scoped rather than +/// node-scoped: what a reset or power-off actually reaches, when the +/// controller may take the module's shared bus, which processor the +/// module's one alternate console belongs to, and the midplane one-wire net. +/// +/// The controller calls these with its own lock held, so an implementation +/// must answer from state of its own and never call back into the +/// controller. A request that does need the controller (`execute`) is queued +/// and delivered after the controller's lock is released. +pub trait ModuleOps: Send + Sync { + /// Whether the module is powered. + fn powered(&self) -> bool; + /// Whether the module can carry the action out. A command it refuses is + /// answered `err cmd`, since a caller takes `ok` as proof. + fn can_execute(&self, action: Action) -> bool; + /// Carry the action out. Only called after `can_execute` cleared it. + fn execute(&self, action: Action); + /// A reset or NMI pulse is running on the module's outputs. + fn pulsing(&self) -> bool; + /// Pulse the module's NMI output; false when it has no NMI path or a + /// pulse is already running. + fn nmi(&self) -> bool; + /// This controller is being addressed as a target, or has finished. + fn slave_active(&self, active: bool); + /// The processor queued console input is announced to. + fn console_owner(&self, processor: usize); + /// Queued console input is or is not waiting for an announcement. + fn console_input(&self, pending: bool); + /// A serial command asks for its turn on the module's bus: true once + /// granted, with grant publication stopped. + fn serial_turn(&self) -> bool; + /// The serial command is done with the bus. + fn serial_done(&self); + /// One bit slot on the midplane one-wire line. + fn onewire_slot(&self, pulse: u32, sample: u32) -> bool; + /// A controller message for the Hubs: announce it to every processor code. + fn post_message(&self); + /// Whether the message is still announced to this processor; if so, it + /// stops being announced to it. + fn take_message(&self, processor: usize) -> bool; +} + #[derive(Clone, Copy, Debug)] pub struct ElscConfig { + pub build: Build, /// Front-panel DIP switches as the `dip-switches` property gives them /// (inverted when read back by `rsw`). pub dip_switches: u8, @@ -112,12 +176,13 @@ pub struct ElscConfig { impl Default for ElscConfig { fn default() -> Self { - Self { dip_switches: 0, master: true, start_delay_ms: 0 } + Self { dip_switches: 0, master: true, build: Build::Origin200, start_delay_ms: 0 } } } +/// A destructive action a command can ask for. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Action { +pub enum Action { None, Reset, Cycle, @@ -137,6 +202,33 @@ impl Action { } } +/// The status messages an Origin 2000 module controller writes to its +/// display. Their eight-character texts are not known, so none is invented. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Status { + None, + PdRemote, + PowerOff, + PowerUp, + Reset, + Nmi, + Heartbeat, +} + +impl Status { + fn name(self) -> &'static str { + match self { + Status::None => "", + Status::PdRemote => "remote power-down", + Status::PowerOff => "power off", + Status::PowerUp => "power up", + Status::Reset => "reset", + Status::Nmi => "NMI", + Status::Heartbeat => "heartbeat timeout", + } + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Message { None, @@ -186,6 +278,7 @@ const TIMERS: usize = 9; // Commands whose missing effect is logged once. const UNIMP_FSC: u8 = 1 << 0; const UNIMP_TST: u8 = 1 << 1; +const UNIMP_FRE: u8 = 1 << 2; const UNIMP_NIC: u8 = 1 << 3; fn checksum(buf: &[u8]) -> u8 { @@ -283,47 +376,65 @@ fn processor_name(p: u8) -> [u8; 2] { type Handler = fn(&mut State, &mut Req); +/// Builds that have a command in their table. +const B200: u8 = 1; +const B2000: u8 = 2; +const BOTH: u8 = B200 | B2000; + struct Command { name: &'static [u8; 3], + builds: u8, handler: Handler, on_i2c: bool, on_serial: bool, } -const fn cmd(name: &'static [u8; 3], handler: Handler, on_i2c: bool, on_serial: bool) -> Command { - Command { name, handler, on_i2c, on_serial } +const fn cmd(name: &'static [u8; 3], builds: u8, handler: Handler, on_i2c: bool, on_serial: bool) -> Command { + Command { name, builds, handler, on_i2c, on_serial } } -/// What the Origin 200 build answers, and on which channel. A mnemonic it -/// lacks answers `err cmd` with no other effect, and so does a command on a -/// channel that does not take it: `acp` and `get` belong to the Hubs, and the -/// last six to the operator. +impl Command { + fn in_build(&self, build: Build) -> bool { + self.builds & if build == Build::Origin200 { B200 } else { B2000 } != 0 + } +} + +/// What each build answers, and on which channel. A mnemonic a build lacks +/// answers `err cmd` with no other effect (`aut` is the Origin 200's alone; +/// `dsc`, `dsp`, `key`, `vlm` and `fre` the Origin 2000's), and so does a +/// command on a channel that does not take it: `acp` and `get` belong to the +/// Hubs, and the serial-only ones to the operator. static COMMANDS: &[Command] = &[ - cmd(b"acp", State::handle_acp, true, false), - cmd(b"aut", State::handle_aut, true, true), - cmd(b"clr", State::handle_clr, true, true), - cmd(b"fan", State::handle_fan, true, true), - cmd(b"fsc", State::handle_fsc, true, true), - cmd(b"get", State::handle_get, true, false), - cmd(b"hbt", State::handle_hbt, true, true), - cmd(b"nic", State::handle_nic, true, true), - cmd(b"nmi", State::handle_nmi, true, true), - cmd(b"pwr", State::handle_pwr, true, true), - cmd(b"rst", State::handle_rst, true, true), - cmd(b"rsw", State::handle_rsw, true, true), - cmd(b"see", State::handle_see, true, true), - cmd(b"sel", State::handle_sel, true, true), - cmd(b"sla", State::handle_sla, true, true), - cmd(b"tas", State::handle_tas, true, true), - cmd(b"tmp", State::handle_tmp, true, true), - cmd(b"tst", State::handle_tst, true, true), - cmd(b"ver", State::handle_ver, true, true), - cmd(b"cfg", State::handle_cfg, false, true), - cmd(b"dbg", State::handle_dbg, false, true), - cmd(b"ech", State::handle_ech, false, true), - cmd(b"mod", State::handle_mod, false, true), - cmd(b"nvm", State::handle_nvm, false, true), - cmd(b"pas", State::handle_pas, false, true), + cmd(b"acp", BOTH, State::handle_acp, true, false), + cmd(b"aut", B200, State::handle_aut, true, true), + cmd(b"clr", BOTH, State::handle_clr, true, true), + cmd(b"dsc", B2000, State::handle_dsc, true, true), + cmd(b"dsp", B2000, State::handle_dsp, true, true), + cmd(b"fan", BOTH, State::handle_fan, true, true), + cmd(b"fsc", BOTH, State::handle_fsc, true, true), + cmd(b"get", BOTH, State::handle_get, true, false), + cmd(b"hbt", BOTH, State::handle_hbt, true, true), + cmd(b"key", B2000, State::handle_key, true, true), + cmd(b"nic", BOTH, State::handle_nic, true, true), + cmd(b"nmi", BOTH, State::handle_nmi, true, true), + cmd(b"pwr", BOTH, State::handle_pwr, true, true), + cmd(b"rst", BOTH, State::handle_rst, true, true), + cmd(b"rsw", BOTH, State::handle_rsw, true, true), + cmd(b"see", BOTH, State::handle_see, true, true), + cmd(b"sel", BOTH, State::handle_sel, true, true), + cmd(b"sla", BOTH, State::handle_sla, true, true), + cmd(b"tas", BOTH, State::handle_tas, true, true), + cmd(b"tmp", BOTH, State::handle_tmp, true, true), + cmd(b"tst", BOTH, State::handle_tst, true, true), + cmd(b"ver", BOTH, State::handle_ver, true, true), + cmd(b"vlm", B2000, State::handle_vlm, true, true), + cmd(b"cfg", BOTH, State::handle_cfg, false, true), + cmd(b"dbg", BOTH, State::handle_dbg, false, true), + cmd(b"ech", BOTH, State::handle_ech, false, true), + cmd(b"fre", B2000, State::handle_fre, false, true), + cmd(b"mod", BOTH, State::handle_mod, false, true), + cmd(b"nvm", BOTH, State::handle_nvm, false, true), + cmd(b"pas", BOTH, State::handle_pas, false, true), ]; struct State { @@ -401,6 +512,18 @@ struct State { see_processor: u8, console_sender: i32, + /// What an Origin 2000 module owns, if one is attached. + module: Option>, + /// Actions for the module to carry out, delivered once the lock is + /// released because carrying them out reaches back into the controller. + outbox: Vec, + /// The Origin 2000 build's front-panel display and the status message + /// that replaced what the host wrote there. + display: [u8; DISPLAY_CHARS], + status: Status, + /// Each supply's margin (`vlm`): `n`, `h` or `l`. + margins: [u8; SUPPLIES], + /// Bytes for the AUX port, sent once the lock is released. out: Vec, events: Vec, @@ -461,6 +584,11 @@ impl State { sel_processor: 0, see_processor: SEE_ALL, console_sender: -1, + module: None, + outbox: Vec::new(), + display: [b' '; DISPLAY_CHARS], + status: Status::None, + margins: [b'n'; SUPPLIES], out: Vec::new(), events: Vec::new(), release_bus: false, @@ -568,15 +696,63 @@ impl State { // ---- power ---------------------------------------------------------- + /// Whether this controller publishes its own grant ring: an Origin 200 + /// master does. An Origin 2000 module's scheduler does it for the module. + fn rings(&self) -> bool { + !self.is2000() && self.cfg.master + } + + fn is2000(&self) -> bool { + self.cfg.build == Build::Origin2000 + } + + fn version(&self) -> &'static str { + if self.is2000() { + VERSION_ORIGIN2000 + } else { + VERSION_ORIGIN200 + } + } + + /// Record an Origin 2000 display status message. + fn set_status(&mut self, status: Status) { + if self.is2000() { + self.status = status; + } + } + fn powered(&self) -> bool { - !self.module_off + match &self.module { + Some(m) => m.powered(), + None => !self.module_off, + } + } + + fn can_execute(&self, action: Action) -> bool { + self.module.as_ref().is_none_or(|m| m.can_execute(action)) + } + + fn pulsing(&self) -> bool { + self.module.as_ref().is_some_and(|m| m.pulsing()) + } + + /// Tell a module owner where queued console input is announced now. + fn console_update(&self) { + let Some(m) = &self.module else { + return; + }; + let target = self.console_target(); + if target >= 0 { + m.console_owner(target as usize); + } + m.console_input(target >= 0 && !self.rx_queue.is_empty()); } /// A power-up that was to come may have just been cancelled. On an /// unpowered module say again that the power is off, so the machine asks /// whether anything is left to power a module up. fn recheck_shutdown(&mut self) { - if !self.powered() { + if !self.powered() && self.module.is_none() { self.events.push(ElscEvent::Power(false)); } } @@ -606,6 +782,10 @@ impl State { self.resp_owner = -1; self.rx_queue.clear(); self.slave_addressed = false; + if let Some(m) = &self.module { + m.slave_active(false); + } + self.console_update(); } /// Count down to power-up: the off interval of a power cycle, measured @@ -621,7 +801,8 @@ impl State { self.power_sequence = true; self.tas_lock = TAS_FREE; self.reinitialize(); - self.arm_ms(T_POWER_UP, POWER_UP_HOLD_MS); + let hold = if self.is2000() { POWER_UP_HOLD_MS_ORIGIN2000 } else { POWER_UP_HOLD_MS_ORIGIN200 }; + self.arm_ms(T_POWER_UP, hold); } /// Remove module power. The controller keeps running on standby power. @@ -630,15 +811,28 @@ impl State { if !self.powered() { return; } + self.set_status(Status::PowerOff); + if self.module.is_some() { + // The module's power output reaches the machine. + self.outbox.push(Action::Shutdown); + return; + } self.module_off = true; self.reinitialize(); self.release_bus = true; self.events.push(ElscEvent::Power(false)); } - fn power_up_complete(&mut self) { + fn power_up_complete(&mut self, announce: bool) { self.power_sequence = false; self.cold_power_on = true; + if announce { + self.set_status(Status::PowerUp); + } + if self.module.is_some() { + self.outbox.push(Action::PowerUp); + return; + } self.module_off = false; self.events.push(ElscEvent::Power(true)); } @@ -648,7 +842,7 @@ impl State { self.power_sequence_start(); return; } - self.power_up_complete(); + self.power_up_complete(true); } /// Power goes now and comes back after the interval and the power-up @@ -665,7 +859,9 @@ impl State { if self.powered() { return; } - let auto = self + // An Origin 2000 controller powers its module up unconditionally; an + // Origin 200 master does when its NVRAM says automatic power-on. + let auto = self.is2000() || self .nvram .access(elsc_nvram::FIRST_ADDR + 7, 7, None) .is_some_and(|c| c & elsc_nvram::CONFIG_AUTO_POWER != 0); @@ -700,6 +896,16 @@ impl State { self.reinitialize(); return; } + if let Some(m) = &self.module { + // The module drives the pulse itself and frees the lock as it + // starts. + if !m.nmi() { + dlog!(LogModule::Bedrock, "elsc: the module could not pulse its NMI output"); + return; + } + self.tas_lock = TAS_FREE; + return; + } if self.nmi_pulse_active { dlog!(LogModule::Bedrock, "elsc: NMI requested during an NMI pulse"); return; @@ -711,6 +917,14 @@ impl State { fn reset_wait_expired(&mut self) { self.tas_lock = TAS_FREE; + if self.module.is_some() { + if !self.can_execute(Action::Reset) { + dlog!(LogModule::Bedrock, "elsc: the module refused the reset an acknowledged \"rst\" asked for"); + return; + } + self.outbox.push(Action::Reset); + return; + } self.release_bus = true; self.events.push(ElscEvent::Reset); } @@ -733,6 +947,7 @@ impl State { fn hbt_expired(&mut self) { match self.hbt_stage { Heartbeat::Waiting => { + self.set_status(Status::Heartbeat); self.hbt_stage = Heartbeat::Warned; self.hbt_arm(HBT_WARNING); } @@ -744,8 +959,17 @@ impl State { } } Heartbeat::Cycling => { - self.hbt_stage = Heartbeat::Idle; - self.power_cycle(HBT_CYCLE_SECONDS as u32); + if self.can_execute(Action::Cycle) { + self.hbt_stage = Heartbeat::Idle; + self.power_cycle(HBT_CYCLE_SECONDS as u32); + } else if self.pulsing() { + // The watchdog's own NMI pulse, or a reset, is still + // running; the cycle follows once it has ended. + self.arm_ms(T_HBT, NMI_PULSE_MS); + } else { + dlog!(LogModule::Bedrock, "elsc: the heartbeat watchdog's power cycle cannot run on this module"); + self.hbt_stage = Heartbeat::Idle; + } } Heartbeat::Idle => {} } @@ -759,10 +983,16 @@ impl State { /// its serial line. fn post_message(&mut self, message: Message) { self.message = message; - self.serial_puts("\x14\r\nMSC: "); - self.serial_puts(message.text()); - self.serial_puts("\x14\r"); + if !self.is2000() { + self.serial_puts("\x14\r\nMSC: "); + self.serial_puts(message.text()); + self.serial_puts("\x14\r"); + } self.arm_ms(T_MESSAGE, MESSAGE_LIFETIME_MS); + if let Some(m) = &self.module { + m.post_message(); + return; + } self.message_mask = 0xff; } @@ -771,6 +1001,9 @@ impl State { if self.message == Message::None || !(0..PROCESSORS as i32).contains(&processor) { return false; } + if let Some(m) = &self.module { + return m.take_message(processor as usize); + } let bit = 1u8 << processor; if self.message_mask & bit == 0 { return false; @@ -799,13 +1032,18 @@ impl State { self.sel_processor = processor; } self.rx_queue.clear(); + self.console_update(); } fn queue_input(&mut self, c: u8) { + let was_pending = !self.rx_queue.is_empty(); if self.rx_queue.len() == RX_QUEUE_SIZE { self.rx_queue.pop_front(); } self.rx_queue.push_back(c); + if !was_pending { + self.console_update(); + } } // ---- grants --------------------------------------------------------- @@ -934,6 +1172,7 @@ impl State { // The reset comes a second later; another "rst" inside that // second starts the wait again. self.post_message(Message::Rst); + self.set_status(Status::Reset); self.arm_ms(T_RST, RESET_WAIT_MS); } Action::Shutdown => self.remove_power(), @@ -982,6 +1221,7 @@ impl State { fn record_console_sender(&mut self) { if self.rx.len() >= 7 && self.rx[0] >= 5 && &self.rx[1..5] == b"acp " { self.console_sender = self.rx[5] as i32 - b'0' as i32; + self.console_update(); } } @@ -998,7 +1238,7 @@ impl State { } let pos = if line[3] == b'\r' { 3 } else { 4 }; let mut req = Req { text: &line, pos }; - match COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_i2c) { + match COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_i2c && c.in_build(self.cfg.build)) { Some(c) => (c.handler)(self, &mut req), None => self.respond_str("err cmd"), } @@ -1007,7 +1247,7 @@ impl State { // ---- commands ------------------------------------------------------- fn handle_ver(&mut self, _r: &mut Req) { - self.respond_str(&format!("ok {VERSION}")); + self.respond_str(&format!("ok {}", self.version())); } /// The physical switches, active-low, as hex with no leading zero. The @@ -1016,7 +1256,9 @@ impl State { /// follows it whatever the configured switches say. fn handle_rsw(&mut self, _r: &mut Req) { let mut raw = 0xff ^ self.cfg.dip_switches; - raw = if self.cfg.master { raw | 0x04 } else { raw & !0x04 }; + if !self.is2000() { + raw = if self.cfg.master { raw | 0x04 } else { raw & !0x04 }; + } self.respond_str(&format!("ok {raw:x}")); } @@ -1036,8 +1278,12 @@ impl State { Select::None => b'/', Select::Auto => b'.', }; + let before = self.rx_queue.len(); buf.extend(self.rx_queue.drain(..n)); self.respond(&buf); + if before != 0 && self.rx_queue.is_empty() { + self.console_update(); + } } /// Console output. A bare `ok` parses as zero accepted and livelocks the @@ -1135,6 +1381,10 @@ impl State { self.respond_str("err arg"); return; } + if !self.can_execute(Action::Shutdown) { + self.respond_str("err cmd"); + return; + } self.respond_ok(); let immediate = match delay { Some(s) => s == 0, @@ -1145,6 +1395,7 @@ impl State { // controller-initiated shutdown on "pd remote". self.arm_secs(T_PWR_DOWN, delay.unwrap_or(3) as u64); self.post_message(Message::PdRemote); + self.set_status(Status::PdRemote); return; } // A pending countdown keeps running: the firmware removes power @@ -1164,7 +1415,7 @@ impl State { // destructive commands it waits until the "ok" has been read. // A powered module ignores the request. self.respond_ok(); - if !self.powered() { + if !self.powered() && self.can_execute(Action::PowerUp) { self.arm_action(Action::PowerUp, "pwr u"); } } @@ -1187,6 +1438,10 @@ impl State { self.respond_str("err arg"); return; } + if !self.can_execute(Action::Cycle) { + self.respond_str("err cmd"); + return; + } self.respond_ok(); self.cycle_seconds = interval.unwrap_or(CYCLE_DEFAULT_SECONDS); self.arm_action(Action::Cycle, "pwr c"); @@ -1256,15 +1511,22 @@ impl State { /// values from the hex reader, any of them missing a bad argument. No /// device is modelled on the line, which idles high. fn handle_nic(&mut self, r: &mut Req) { - if r.read_hex().is_none() || r.read_hex().is_none() || r.read_hex().is_none() { + let (Some(pulse), Some(sample), Some(_delay)) = (r.read_hex(), r.read_hex(), r.read_hex()) else { self.respond_str("err arg"); return; - } - self.log_unimp_once( - UNIMP_NIC, - "\"nic\" with no one-wire device on this module; the line idles high, an approximation", - ); - self.respond_str("ok 1"); + }; + let pulse_sample = (pulse, sample); + let level = match (&self.module, pulse_sample) { + (Some(m), (pulse, sample)) => m.onewire_slot(pulse, sample), + _ => { + self.log_unimp_once( + UNIMP_NIC, + "\"nic\" with no one-wire device on this module; the line idles high, an approximation", + ); + true + } + }; + self.respond_str(if level { "ok 1" } else { "ok 0" }); } /// Module reset: panic reboot and fatal-error recovery only. A caller @@ -1272,7 +1534,7 @@ impl State { /// own hardware reset, so the reset must fire, once the guest has read /// `ok`. fn handle_rst(&mut self, _r: &mut Req) { - if !self.powered() { + if !self.powered() || !self.can_execute(Action::Reset) { self.respond_str("err cmd"); return; } @@ -1322,6 +1584,10 @@ impl State { self.fan_high = false; self.serial_echo = true; self.supervisor = false; + if self.is2000() { + self.display = [b' '; DISPLAY_CHARS]; + self.status = Status::None; + } self.recheck_shutdown(); self.respond_ok(); } @@ -1414,6 +1680,10 @@ impl State { /// a master starts on `o`, 0x04, 0x05 and 0x06 need the /// controller-to-controller link, which the model does not have. fn handle_sla(&mut self, r: &mut Req) { + if self.is2000() { + self.respond_ok(); + return; + } if let Some(c) = r.read_char() { if !self.cfg.master { self.slave_report = c; @@ -1442,11 +1712,100 @@ impl State { return; } self.arm_ms(T_NMI, NMI_WAIT_MS); + self.set_status(Status::Nmi); self.respond_ok(); } // ---- serial-only commands ------------------------------------------ + /// `dsp `: the Origin 2000 build's eight-character front-panel + /// message. The text starts right after the space that ends the mnemonic, + /// so spaces in it count. Up to eight characters overwrite the display + /// from its first position and leave the rest alone. The text stops at the + /// first unprintable character, the carriage return included. An empty + /// message is a bad argument. + fn handle_dsp(&mut self, r: &mut Req) { + let n = r.text[r.pos.min(r.text.len())..] + .iter() + .take(DISPLAY_CHARS) + .take_while(|c| (0x20..0x7f).contains(*c)) + .count(); + if n == 0 { + self.respond_str("err arg"); + return; + } + let text = r.text[r.pos..r.pos + n].to_vec(); + self.display[..n].copy_from_slice(&text); + // With a multimodule controller declared, host display text is copied + // to it over the serial line, with severity digit 4. + if self.fsc_present { + self.serial_puts("\x14dsp "); + self.serial_write(&text); + self.serial_puts("4\r\n"); + } + self.status = Status::None; + self.respond_ok(); + } + + /// `dsc `: one display character. Both arguments come from the + /// character reader, so the position is the digit 1 to 8, for display + /// positions 0 to 7, and the character is printable and never a space. + fn handle_dsc(&mut self, r: &mut Req) { + let (Some(pos), Some(ch)) = (r.read_char(), r.read_char()) else { + self.respond_str("err arg"); + return; + }; + if !(b'1'..=b'8').contains(&pos) || !(0x20..0x7f).contains(&ch) { + self.respond_str("err arg"); + return; + } + self.display[(pos - b'1') as usize] = ch; + self.status = Status::None; + self.respond_ok(); + } + + /// `key`: the key-switch position. The model's key is always on. + fn handle_key(&mut self, _r: &mut Req) { + self.respond_str("ok on"); + } + + /// `vlm [h|l|n]`: query or set a supply's margin. The supply is + /// `3`, `5` or `v`; a second character sets high, low or normal. The + /// margins are stored and answered, and nothing electrical follows. + fn handle_vlm(&mut self, r: &mut Req) { + const SUPPLY_NAMES: &[u8] = b"35v"; + let Some(supply) = r.read_char().and_then(|c| SUPPLY_NAMES.iter().position(|s| *s == c)) else { + self.respond_str("err arg"); + return; + }; + let Some(m) = r.read_char() else { + self.respond_str(&format!("ok {}", self.margins[supply] as char)); + return; + }; + if !matches!(m, b'h' | b'l' | b'n') { + self.respond_str("err arg"); + return; + } + self.margins[supply] = m; + self.respond_ok(); + } + + /// `fre []`, serial line: the clock-select byte. Writing it also + /// reprograms the midplane clock chip, which the model does not have. + fn handle_fre(&mut self, r: &mut Req) { + match r.read_hex() { + None => self.reply_nvram_byte(11), + Some(v) => { + self.nvram_byte(11, Some(v as u8)); + self.log_unimp_once( + UNIMP_FRE, + "\"fre\" write is stored only; the midplane clock chip is not modelled", + ); + self.respond_ok(); + } + } + } + /// `ech`: set echo, `1` on, `0` off; with no argument it toggles. fn handle_ech(&mut self, r: &mut Req) { match r.read_char() { @@ -1591,7 +1950,8 @@ impl State { b"clr" | b"hbt" | b"nmi" | b"rst" => true, b"pwr" => probe.read_char().is_some_and(|c| matches!(c, b'u' | b'd' | b'c')), b"fan" | b"aut" => probe.read_char().is_some(), - b"mod" | b"dbg" => probe.read_hex().is_some(), + b"mod" | b"dbg" | b"fre" => probe.read_hex().is_some(), + b"vlm" => probe.read_char().is_some() && probe.read_char().is_some(), b"nvm" => probe.read_hex().is_some() && probe.read_hex().is_some(), _ => false, } @@ -1612,7 +1972,7 @@ impl State { } let pos = if line[3] == b'\r' { 3 } else { 4 }; let mut req = Req { text: line, pos }; - let Some(c) = COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_serial) else { + let Some(c) = COMMANDS.iter().find(|c| &line[..3] == c.name && c.on_serial && c.in_build(self.cfg.build)) else { self.respond_str("err cmd"); return; }; @@ -1661,21 +2021,38 @@ impl State { /// A completed line runs once the bus is free, this controller's own I2C /// target is idle and no Hub message is pending; an I2C command arriving /// at the same moment goes first. + /// + /// An Origin 2000 module's scheduler decides that turn instead, and stops + /// publishing grants once it has given it. fn serial_poll(&mut self) { - if self.serial_pending - && self.message_mask == 0 - && !self.slave_addressed - && !self.bus_busy.load(Ordering::Relaxed) - { - self.serial_run(); + if !self.serial_pending { + return; + } + let turn = match &self.module { + Some(m) => !self.slave_addressed && m.serial_turn(), + None => { + self.message_mask == 0 && !self.slave_addressed && !self.bus_busy.load(Ordering::Relaxed) + } + }; + if !turn { + return; + } + self.serial_run(); + if let Some(m) = &self.module { + m.serial_done(); } } + /// An Origin 200 slave has a serial line that takes no command. + fn serial_is_slave(&self) -> bool { + !self.is2000() && !self.cfg.master + } + /// Ctrl+T in pass-through: the prompt, or `Slave!` from a slave. fn serial_enter_command(&mut self) { if self.serial_echo { self.serial_puts("\r\nMSC> "); - if !self.cfg.master { + if self.serial_is_slave() { self.serial_puts("Slave!\r\n"); return; } @@ -1713,7 +2090,7 @@ impl State { } } self.serial_command = false; - if !self.cfg.master { + if self.serial_is_slave() { // A slave never runs serial commands. self.serial_line.clear(); return; @@ -1755,13 +2132,26 @@ impl State { self.module_off = false; self.power_sequence = false; if self.cfg.start_delay_ms != 0 { - // The module starts off. A master counts the delay; a slave - // waits for its master. - self.module_off = true; - if self.cfg.master { + // The module starts off. A controller that powers it up on its + // own counts the delay; an Origin 200 slave waits for its master. + // An Origin 2000 module owns its power state, so only the + // countdown is the controller's. + self.module_off = !self.is2000(); + if self.is2000() || self.cfg.master { self.arm_ms(T_START, self.cfg.start_delay_ms as u64); } } + self.status = Status::None; + self.margins = [b'n'; SUPPLIES]; + // An Origin 2000 controller starts with its version on the display; + // the Origin 200 build has none, and a blank is a model choice. + self.display = if self.is2000() { + let mut d = [b' '; DISPLAY_CHARS]; + d.copy_from_slice(VERSION_ORIGIN2000.as_bytes()); + d + } else { + [b' '; DISPLAY_CHARS] + }; self.cycle_seconds = CYCLE_DEFAULT_SECONDS; self.cancel(T_RST); self.cancel(T_NMI); @@ -1785,11 +2175,11 @@ impl State { // The start announces the controller on its serial line. self.serial_puts("\x14MSC "); - self.serial_puts(VERSION); + self.serial_puts(self.version()); self.serial_puts("\r\n"); // The rotation starts with code 0, and a later start leaves a running // rotation on its schedule. - if self.cfg.master && !self.pending(T_ARB) { + if self.rings() && !self.pending(T_ARB) { if !self.arb_blocked() { self.arb_publish(); } @@ -1815,6 +2205,10 @@ impl State { self.message = Message::None; self.message_mask = 0; self.tas_lock = TAS_FREE; + if let Some(m) = &self.module { + m.slave_active(false); + } + self.console_update(); } fn describe(&self) -> String { @@ -1850,6 +2244,21 @@ impl State { } let _ = writeln!(s, "{}", if self.rx_queue.len() > 64 { "\"..." } else { "\"" }); } + if self.is2000() { + let shown: String = self + .display + .iter() + .map(|c| if (0x20..0x7f).contains(c) { (*c as char).to_string() } else { format!("\\x{c:02x}") }) + .collect(); + let _ = writeln!(s, " display: \"{shown}\""); + if self.status != Status::None { + let _ = writeln!( + s, + " status message: {}, shown in place of the text above (its own text is not known)", + self.status.name() + ); + } + } if self.message != Message::None { let _ = writeln!(s, " controller message: \"{}\"", self.message.text()); } @@ -1879,7 +2288,9 @@ impl State { } else { let _ = writeln!(s, "; output: processor {}", self.see_processor); } - let _ = writeln!(s, " arbitration rotation: next position {} of {}", self.arb_code, ARB_POSITIONS); + if self.rings() { + let _ = writeln!(s, " arbitration rotation: next position {} of {}", self.arb_code, ARB_POSITIONS); + } let _ = writeln!( s, " fan high: {}, fsc: {}, short timeouts: {}, slave report: {:#04x}", @@ -1912,6 +2323,32 @@ impl Elsc { } } + /// Attach the module that owns this controller's power, reset and bus + /// scheduling. Wiring: call it before [`Resettable::power_on`]. + pub fn set_module_ops(&self, module: Arc) { + self.state.lock().module = Some(module); + } + + /// Clear the protocol queues without resetting the controller. A module + /// owner's recovery, reset and power paths reinitialise this much and no + /// more: the framing state goes and the rest stays. + pub fn reinitialize(&self) { + self.run(|st| st.reinitialize()); + } + + /// Whether the controller powers its module up after a delay of its own + /// rather than at once when it starts. + pub fn start_delayed(&self) -> bool { + self.state.lock().cfg.start_delay_ms != 0 + } + + /// The controller's start-up power-up with none of its wait, which an + /// Origin 2000 module-management device calls from its reset so the + /// module is powered before any processor runs. + pub fn power_up_at_start(&self) { + self.run(|st| st.power_up_complete(false)); + } + /// Where the AUX connector's transmit bytes go. pub fn set_serial_backend(&self, backend: Arc) { *self.backend.lock() = backend; @@ -1920,12 +2357,19 @@ impl Elsc { /// Run `f` with the timers brought up to the present, then send what it /// wrote to the AUX port with the lock released. fn run(&self, f: impl FnOnce(&mut State) -> R) -> R { - let (value, out) = { + let (value, out, actions, module) = { let mut st = self.state.lock(); st.advance((self.clock)()); let value = f(&mut st); - (value, std::mem::take(&mut st.out)) + (value, std::mem::take(&mut st.out), std::mem::take(&mut st.outbox), st.module.clone()) }; + // What the module does reaches back into the controller, so it runs + // with the lock released. + if let Some(m) = module { + for action in actions { + m.execute(action); + } + } if !out.is_empty() { let backend = self.backend.lock().clone(); for byte in out { @@ -2009,6 +2453,9 @@ impl I2cTarget for Elsc { fn start(&self, _addr7: u8, recv: bool) -> bool { self.run(|st| { st.slave_addressed = true; + if let Some(m) = &st.module { + m.slave_active(true); + } if recv { // Direction switch: the request just written is complete. st.request_complete(); @@ -2056,6 +2503,9 @@ impl I2cTarget for Elsc { fn finish(&self) { self.run(|st| { st.slave_addressed = false; + if let Some(m) = &st.module { + m.slave_active(false); + } st.rx.clear(); // An unconsumed response stays pending for its requester until // read to its end or superseded; a committed action fires here @@ -2084,4 +2534,4 @@ impl I2cTarget for Elsc { } #[cfg(test)] -mod tests; +pub(crate) mod tests; diff --git a/src/dev/elsc/tests.rs b/src/dev/elsc/tests.rs index 59c9632ea..4926ad589 100644 --- a/src/dev/elsc/tests.rs +++ b/src/dev/elsc/tests.rs @@ -28,7 +28,7 @@ const BANNER: &str = "\x14MSC V 3.1S \r\n"; /// The AUX connector's far end. #[derive(Default)] -struct Aux(Mutex>); +pub(crate) struct Aux(Mutex>); impl SerialBackend for Aux { fn send_byte(&self, byte: u8) { @@ -41,33 +41,33 @@ impl SerialBackend for Aux { } impl Aux { - fn take(&self) -> String { + pub(crate) fn take(&self) -> String { String::from_utf8_lossy(&std::mem::take(&mut *self.0.lock())).into_owned() } } /// Virtual time, set by hand. #[derive(Clone, Default)] -struct TestClock(Arc); +pub(crate) struct TestClock(Arc); impl TestClock { - fn now_ns(&self) -> u64 { + pub(crate) fn now_ns(&self) -> u64 { self.0.load(Ordering::SeqCst) } - fn advance_ns(&self, ns: u64) { + pub(crate) fn advance_ns(&self, ns: u64) { self.0.fetch_add(ns, Ordering::SeqCst); } - fn advance_ms(&self, ms: u64) { + pub(crate) fn advance_ms(&self, ms: u64) { self.advance_ns(ms * 1_000_000); } - fn advance_secs(&self, s: u64) { + pub(crate) fn advance_secs(&self, s: u64) { self.advance_ms(s * 1000); } - fn clock(&self) -> Clock { + pub(crate) fn clock(&self) -> Clock { let now = self.0.clone(); Arc::new(move || now.load(Ordering::SeqCst)) } @@ -75,7 +75,7 @@ impl TestClock { /// The sum of a frame's bytes modulo 256, written out here rather than shared /// with the controller: a valid frame sums to zero. -fn byte_sum(buf: &[u8]) -> u8 { +pub(crate) fn byte_sum(buf: &[u8]) -> u8 { (buf.iter().map(|b| *b as u32).sum::() % 256) as u8 } @@ -88,7 +88,7 @@ struct Rig { aux: Arc, } -fn frame(text: &[u8]) -> Vec { +pub(crate) fn frame(text: &[u8]) -> Vec { let mut req = vec![text.len() as u8]; req.extend_from_slice(text); req.push(((256 - byte_sum(&req) as u32) % 256) as u8); @@ -373,7 +373,7 @@ impl Rig { } /// Text of a framed response. -fn text(resp: &[u8]) -> String { +pub(crate) fn text(resp: &[u8]) -> String { String::from_utf8_lossy(&resp[1..1 + resp[0] as usize]).into_owned() } @@ -1877,3 +1877,41 @@ fn replies_stay_below_the_prom_length_limit() { assert_eq!(byte_sum(&resp), 0); } } + +// ---- the Origin 2000 build, with no module owner --------------------------- + +fn rig_2000() -> Rig { + Rig::with(ElscConfig { build: Build::Origin2000, master: false, ..Default::default() }) +} + +#[test] +fn the_origin_2000_build_has_its_own_version_commands_and_display() { + let rig = rig_2000(); + assert_eq!(rig.aux.take(), "\x14MSC VER 3.2 \r\n"); + rig.assert_reply("ver", "ok VER 3.2 "); + rig.assert_reply("key", "ok on"); + rig.assert_reply("vlm 5", "ok n"); + rig.assert_reply("vlm 5h", "ok "); + rig.assert_reply("vlm 5", "ok h"); + rig.assert_reply("dsp hello", "ok "); + rig.assert_reply("dsc 1 J", "ok "); + assert!(rig.elsc.describe().contains("display: \"Jello.2 \""), "{}", rig.elsc.describe()); + // The Origin 200 build's own commands are not in this table. + for absent in ["aut", "mod", "pas none"] { + rig.assert_reply(absent, "err cmd"); + } + // The role bit belongs to the Origin 200 build. + rig.assert_reply("rsw", "ok ff"); + rig.assert_reply("sla o", "ok "); +} + +#[test] +fn the_origin_2000_build_holds_reset_4_4_seconds_when_it_powers_up() { + let rig = rig_2000(); + rig.assert_reply("pwr c 5", "ok "); + assert_eq!(power_events(&rig.events()), [false]); + rig.clock.advance_ms(9_399); + rig.assert_reply("pwr", "ok d"); + rig.step_ms(1); + rig.assert_reply("pwr", "ok u"); +} From 92c9df5b6430ad6e4d99891056866498f22c51b1 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 05:09:53 -0700 Subject: [PATCH 26/38] o2000_module: add Origin 2000 module management A deskside module has up to four node adapters in front of one controller, one NVRAM and the UST control port. Build that: four Pcf8584 adapters sharing a bus, the controller in its Origin 2000 build, the NVRAM, the UST port at 0x20, the midplane one-wire net the controller's nic line drives, and the scheduler. The scheduler publishes one grant per millisecond, eight processor codes, to every adapter; hands the bus to a controller or serial turn ahead of a grant; counts a bus that stays busy for five seconds and reinitialises the controller; announces a controller message in every code for a second; drives 100 ms reset and NMI pulses; and sequences module power. The machine drains reset, NMI and power changes as events. Timings, the recovery count, the UST port and the waiting START are model choices, named in the module doc. The clocked wire, with bitwise arbitration, mid-byte STOPs and clock stretching, is not modelled. --- src/dev/mod.rs | 1 + src/dev/o2000_module.rs | 775 +++++++++++++++++++++++++++++++++ src/dev/o2000_module/tests.rs | 793 ++++++++++++++++++++++++++++++++++ 3 files changed, 1569 insertions(+) create mode 100644 src/dev/o2000_module.rs create mode 100644 src/dev/o2000_module/tests.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 98cd3f626..c5dbb2cfc 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -51,6 +51,7 @@ pub mod ioc; pub mod mc; pub mod mc_vdma; pub mod mem; +pub mod o2000_module; pub mod pcf8584; pub mod pit8254; pub mod prom; diff --git a/src/dev/o2000_module.rs b/src/dev/o2000_module.rs new file mode 100644 index 000000000..b751bf009 --- /dev/null +++ b/src/dev/o2000_module.rs @@ -0,0 +1,775 @@ +//! Module management for an Origin 2000 or Onyx2 module: up to four node +//! adapters in front of one module controller, one configuration NVRAM and +//! the UST control port, with the module's grant scheduler, its power, reset +//! and NMI outputs and its midplane one-wire net. +//! +//! Each node's Hub reaches the module's management bus through its own +//! [`Pcf8584`]. The controller and the NVRAM are targets on every adapter's +//! bus. The scheduler publishes the controller's grants (one per +//! millisecond, eight processor codes) so that every adapter sees them, and +//! takes the module's turn on the bus for the controller's own requests. +//! +//! What is not modelled: the shared clocked wire. Each adapter completes a +//! byte inside the register access that starts it, so two nodes cannot +//! contend bit by bit, lose arbitration or stretch each other's clock. One +//! adapter holds the bus from its START to its STOP and another's START waits +//! for it (a model choice; the specification leaves a START on a busy bus +//! undefined). The scheduler treats the bus as busy while any adapter holds +//! it, which keeps grants and the controller's own turns from landing in the +//! middle of a transfer. A STOP written mid-byte, a node's STOP after an +//! acknowledged read and a peer's START overtaking a grant have no +//! counterpart here. +//! +//! A grant is seen by every adapter that has ESO set and no transfer of its +//! own open, whoever holds the bus and whether or not a START waits, and an +//! adapter is offered each grant once. The controller puts the grant on an +//! idle wire and every adapter's slave half observes it passively at the +//! STOP; nothing shows what the PROM or the kernel sees if an adapter +//! misses one, so the documented model stands. +//! +//! Model choices, not measured behaviour: the one-millisecond service tick; +//! the 5000-tick (five second) recovery count after which a bus that stays +//! busy has the controller reinitialised; the one-second life of a pending +//! controller message; the 100 ms reset and NMI pulses; that a destructive +//! command is refused for the length of a pulse; that automatic console +//! selection starts at code 0 and treats a CPU index as a processor code; +//! that the UST control port at I2C `0x20` is a one-byte port (as a PCF8574 +//! expander is) reading all ones from power-on and then the last byte +//! written, with a general call accepted. +//! +//! Migration and snapshot state are not saved yet. +//! +//! Ported from hw/misc/sgi-sn0-module-management.c in +//! jamesbraid/qemu-sgi-origami. + +use crate::dev::elsc::{Action, Build, Elsc, ElscConfig, ModuleOps}; +use crate::dev::heart::Clock; +use crate::dev::elsc_nvram::ElscNvram; +use crate::dev::i2c::{BusBusy, I2cBus, I2cTarget, Service}; +use crate::dev::pcf8584::{BusOwner, Pcf8584}; +use crate::dev::w1::{self, crc::crc16, sgi::BoardRecord}; +use crate::devlog::LogModule; +use crate::dlog; +use crate::traits::Resettable; +use parking_lot::Mutex; +use std::sync::{Arc, Weak}; + +/// One adapter per node board on a four-node module. +pub const ADAPTERS: usize = 4; +pub const MODULE_SERIAL_LEN: usize = 10; +/// The module's UST control port, the address the stock PROM uses. +pub const UST_CONTROL_ADDR: u8 = 0x20; + +const PROCESSOR_CODES: usize = 8; +const GRANT_BASE: u8 = 0xe0; +const GRANT_MESSAGE_BIT: u8 = 0x08; +const SERVICE_PERIOD_NS: u64 = 1_000_000; +const MESSAGE_LIFETIME_NS: u64 = 1_000_000_000; +const PULSE_NS: u64 = 100_000_000; +const RECOVERY_TICKS: u32 = 5000; + +/// What the module does to the rest of the machine, in the order it +/// happened. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModuleEvent { + /// The module's reset line, high for the 100 ms of a pulse. + Reset(bool), + /// The module's NMI line, high for the 100 ms of a pulse. + Nmi(bool), + /// The module's power line. + Power(bool), +} + +#[derive(Clone, Copy, Debug)] +pub struct ModuleConfig { + /// The module number the NVRAM's module byte starts with. + pub module_number: u8, + /// Which destructive actions this installation can carry out. A command + /// whose action is not declared is refused (`err cmd`), not answered, + /// since a caller takes `ok` as proof it happened. + pub can_reset: bool, + pub can_nmi: bool, + pub can_power: bool, + /// The front-panel DIP switches, as `rsw` reports them inverted. + pub dip_switches: u8, + /// Milliseconds after the cabinet starts that the controller powers the + /// module up; zero powers it up inside the reset, before any processor + /// runs. + pub start_delay_ms: u32, +} + +impl Default for ModuleConfig { + fn default() -> Self { + Self { + module_number: 1, + can_reset: false, + can_nmi: false, + can_power: false, + dip_switches: 0, + start_delay_ms: 0, + } + } +} + +/// The module's scheduler state. +struct State { + cfg: ModuleConfig, + /// The service tick's next instant, and the other deadlines. + service_deadline: u64, + message_deadline: Option, + pulse_deadline: Option, + now: u64, + recovery_ticks: u32, + processor_code: usize, + pending_messages: u8, + console_processor: usize, + console_input_pending: bool, + controller_request: bool, + controller_granted: bool, + serial_request: bool, + serial_granted: bool, + slave_active: bool, + publication_enabled: bool, + power_on: bool, + pulse_active: bool, + pulse_nmi: bool, + /// The grant last published and a count of publications; each adapter + /// keeps the count it last took. + grant: u8, + grant_seq: u64, + taken: [u64; ADAPTERS], + /// The controller's queues need reinitialising, once the scheduler's lock + /// is released: the controller takes its own lock for that. + reinit: bool, + events: Vec, + /// The level each output line was last driven to; a line is only + /// reported when it changes. + reset_line: Option, + nmi_line: Option, + power_line: Option, +} + +impl State { + fn new(cfg: ModuleConfig) -> Self { + let mut s = Self { + cfg, + service_deadline: SERVICE_PERIOD_NS, + message_deadline: None, + pulse_deadline: None, + now: 0, + recovery_ticks: RECOVERY_TICKS, + processor_code: PROCESSOR_CODES - 1, + pending_messages: 0, + console_processor: 0, + console_input_pending: false, + controller_request: false, + controller_granted: false, + serial_request: false, + serial_granted: false, + slave_active: false, + publication_enabled: true, + power_on: true, + pulse_active: false, + pulse_nmi: false, + grant: 0, + grant_seq: 0, + taken: [0; ADAPTERS], + reinit: false, + events: Vec::new(), + reset_line: None, + nmi_line: None, + power_line: None, + }; + s.reset_state(); + s + } + + fn reset_state(&mut self) { + self.processor_code = PROCESSOR_CODES - 1; + self.pending_messages = 0; + self.controller_request = false; + self.controller_granted = false; + self.serial_request = false; + self.serial_granted = false; + self.slave_active = false; + self.console_input_pending = false; + self.console_processor = 0; + self.recovery_ticks = RECOVERY_TICKS; + self.message_deadline = None; + } + + /// The controller's own reinitialisation: the scheduler's protocol state + /// goes back to reset and the controller's queues follow. The publication + /// enable latch is left alone. It reaches no further: a module reset + /// reaches the nodes through the reset line the machine wires. + fn reinitialize(&mut self) { + self.reset_state(); + self.reinit = true; + } + + fn recovery_tick(&mut self) { + if self.recovery_ticks > 0 { + self.recovery_ticks -= 1; + if self.recovery_ticks == 0 { + self.reinitialize(); + } + } + } + + fn drive_power(&mut self) { + if self.cfg.can_power && self.power_line != Some(self.power_on) { + self.power_line = Some(self.power_on); + self.events.push(ModuleEvent::Power(self.power_on)); + } + } + + fn drive_pulse(&mut self, reset: bool, nmi: bool) { + if self.cfg.can_reset && self.reset_line != Some(reset) { + self.reset_line = Some(reset); + self.events.push(ModuleEvent::Reset(reset)); + } + if self.cfg.can_nmi && self.nmi_line != Some(nmi) { + self.nmi_line = Some(nmi); + self.events.push(ModuleEvent::Nmi(nmi)); + } + } + + fn message_for(&self, code: usize) -> bool { + self.pending_messages & (1 << code) != 0 || (self.console_input_pending && self.console_processor == code) + } + + /// Whether a tick finds the bus held: a controller turn, the controller + /// addressed as a target, or any adapter with a transfer open. + fn blocked(&self, busy: bool) -> bool { + self.controller_granted || self.slave_active || busy + } + + fn publish(&mut self) { + self.processor_code = (self.processor_code + 1) % PROCESSOR_CODES; + self.grant = GRANT_BASE + | self.processor_code as u8 + | if self.message_for(self.processor_code) { GRANT_MESSAGE_BIT } else { 0 }; + self.grant_seq += 1; + // A completed attempt reloads the recovery counter. + self.recovery_ticks = RECOVERY_TICKS; + } + + /// One service tick. + fn tick(&mut self, busy: bool) { + // A serial command owning the protocol path leaves the recovery + // counter alone. + if self.serial_granted { + return; + } + if !self.publication_enabled { + // An unpowered module publishes no grants, but the controller + // keeps running and takes serial commands once its bus is free. + if self.serial_request && self.pending_messages == 0 && !self.slave_active && !busy { + self.serial_request = false; + self.serial_granted = true; + } + return; + } + if self.blocked(busy) { + self.recovery_tick(); + return; + } + // A pending controller interrupt suppresses both handoffs, and a + // handoff that happens skips publication without advancing the code. + if self.pending_messages == 0 && self.controller_request { + self.controller_request = false; + self.controller_granted = true; + return; + } + if self.pending_messages == 0 && self.serial_request { + self.serial_request = false; + self.serial_granted = true; + return; + } + self.publish(); + } + + fn pulse_complete(&mut self) { + self.drive_pulse(false, false); + self.pulse_active = false; + self.pulse_nmi = false; + self.pulse_deadline = None; + self.reinitialize(); + if self.power_on { + self.publication_enabled = true; + } + } + + /// Bring every deadline up to `now`, in order: a message expiry and a + /// pulse before the service tick at the same instant. Ticks that cannot + /// change anything are skipped in one step, up to the next other + /// deadline. + fn advance(&mut self, now: u64, busy: bool) { + if now < self.now { + return; + } + let next = [Some(self.service_deadline), self.message_deadline, self.pulse_deadline].into_iter().flatten().min(); + if next.is_none_or(|d| d > now) { + // Nothing is due: the common case of a query between ticks. + self.now = now; + return; + } + loop { + let other = [self.message_deadline, self.pulse_deadline].into_iter().flatten().min(); + let due_other = other.filter(|d| *d <= now); + if let Some(d) = due_other.filter(|d| *d <= self.service_deadline) { + if self.message_deadline == Some(d) { + self.pending_messages = 0; + self.message_deadline = None; + } else { + self.pulse_complete(); + } + continue; + } + if self.service_deadline > now { + break; + } + let horizon = other.map_or(now, |d| d.min(now)); + let remaining = (horizon.saturating_sub(self.service_deadline)) / SERVICE_PERIOD_NS; + self.tick(busy); + let mut skip = 0; + if remaining > 0 { + if self.serial_granted || !self.publication_enabled { + skip = remaining; + } else if self.blocked(busy) { + skip = remaining.min(self.recovery_ticks.saturating_sub(1) as u64); + self.recovery_ticks -= skip as u32; + } else if self.pending_messages == 0 && !self.controller_request && !self.serial_request { + // Publishing: a whole revolution leaves the code where + // it was. + skip = remaining / PROCESSOR_CODES as u64 * PROCESSOR_CODES as u64; + } + } + self.service_deadline += (1 + skip) * SERVICE_PERIOD_NS; + } + self.now = now; + } + + fn set_message(&mut self, processor: usize, pending: bool) { + if processor >= PROCESSOR_CODES { + dlog!(LogModule::Bedrock, "o2000 module: message for processor {processor}, outside the eight published codes"); + return; + } + if pending { + self.pending_messages |= 1 << processor; + self.message_deadline = Some(self.now + MESSAGE_LIFETIME_NS); + } else { + self.pending_messages &= !(1 << processor); + if self.pending_messages == 0 { + self.message_deadline = None; + } + } + } + + fn start_pulse(&mut self, nmi: bool) -> bool { + if !(if nmi { self.cfg.can_nmi } else { self.cfg.can_reset }) { + dlog!(LogModule::Bedrock, "o2000 module: {} requested on a module that declares no such path", if nmi { "NMI" } else { "reset" }); + return false; + } + if self.pulse_active { + dlog!(LogModule::Bedrock, "o2000 module: pulse requested during a {} pulse", if self.pulse_nmi { "NMI" } else { "reset" }); + return false; + } + self.pulse_active = true; + self.pulse_nmi = nmi; + self.drive_pulse(!nmi, nmi); + self.pulse_deadline = Some(self.now + PULSE_NS); + true + } + + fn set_power(&mut self, on: bool) -> bool { + if !self.cfg.can_power { + dlog!(LogModule::Bedrock, "o2000 module: power {} requested on a module that declares no power path", if on { "up" } else { "down" }); + return false; + } + if !on { + // Publication stops before module reset and power sequencing. + self.publication_enabled = false; + self.power_on = false; + self.reinitialize(); + self.drive_power(); + return true; + } + self.power_on = true; + self.drive_power(); + // The interface is initialised once reset is released, then + // publication is enabled. + self.reinitialize(); + self.publication_enabled = true; + true + } + + fn can_execute(&self, action: Action) -> bool { + let declared = match action { + Action::Reset => self.cfg.can_reset, + Action::Cycle | Action::Shutdown | Action::PowerUp => self.cfg.can_power, + Action::None => return false, + }; + if !declared { + dlog!(LogModule::Bedrock, "o2000 module: destructive command refused, this module declares no {} path", if action == Action::Reset { "reset" } else { "power" }); + return false; + } + if self.pulse_active { + // No source settles that overlap, so every destructive action is + // refused for the duration of a pulse. A model choice. + dlog!(LogModule::Bedrock, "o2000 module: destructive command during a {} pulse", if self.pulse_nmi { "NMI" } else { "reset" }); + return false; + } + true + } +} + +struct Shared { + state: Mutex, + clock: Clock, + /// Which adapter has the shared bus open. + owner: Arc, + elsc: Mutex>, + onewire: Arc>, +} + +impl Shared { + fn any_busy(&self) -> bool { + self.owner.busy() + } + + /// Bring the scheduler to the present. Anything it owes the controller + /// is left for [`Self::run`], which holds no controller lock. + fn with(&self, f: impl FnOnce(&mut State) -> R) -> R { + let mut st = self.state.lock(); + let busy = self.any_busy(); + st.advance((self.clock)(), busy); + f(&mut st) + } + + /// As [`Self::with`], then reinitialise the controller if the scheduler + /// asked: it takes its own lock, so this is never called from inside the + /// controller. + fn run(&self, f: impl FnOnce(&mut State) -> R) -> R { + let (value, reinit) = { + let mut st = self.state.lock(); + let busy = self.any_busy(); + st.advance((self.clock)(), busy); + let value = f(&mut st); + (value, std::mem::take(&mut st.reinit)) + }; + if reinit { + if let Some(elsc) = self.elsc.lock().upgrade() { + elsc.reinitialize(); + } + } + value + } +} + +/// What the controller calls: answers from the scheduler's own state, never +/// calling back into the controller. +struct Link(Arc); + +impl ModuleOps for Link { + fn powered(&self) -> bool { + self.0.with(|s| s.power_on) + } + + fn can_execute(&self, action: Action) -> bool { + self.0.with(|s| s.can_execute(action)) + } + + fn execute(&self, action: Action) { + // Delivered with the controller's lock released, so the controller + // may be reinitialised on the spot. + let done = self.0.run(|s| match action { + Action::Reset => s.start_pulse(false), + Action::Shutdown => s.set_power(false), + Action::PowerUp => s.set_power(true), + // The controller carries a cycle out as a power-off and, after + // its own off interval, a power-up: a cycle that took no time + // here would put power back before the interval began. + Action::Cycle | Action::None => unreachable!("the controller never hands over {action:?}"), + }); + if !done { + dlog!(LogModule::Bedrock, "o2000 module: an acknowledged action did not happen"); + } + } + + fn pulsing(&self) -> bool { + self.0.with(|s| s.pulse_active) + } + + fn nmi(&self) -> bool { + self.0.with(|s| s.start_pulse(true)) + } + + fn slave_active(&self, active: bool) { + self.0.with(|s| s.slave_active = active); + } + + fn console_owner(&self, processor: usize) { + self.0.with(|s| { + if processor >= PROCESSOR_CODES { + dlog!(LogModule::Bedrock, "o2000 module: console output from processor {processor}, outside the eight published codes"); + } else { + s.console_processor = processor; + } + }); + } + + fn console_input(&self, pending: bool) { + self.0.with(|s| s.console_input_pending = pending); + } + + fn serial_turn(&self) -> bool { + self.0.with(|s| { + if s.serial_granted { + return true; + } + s.serial_request = true; + false + }) + } + + fn serial_done(&self) { + self.0.with(|s| s.serial_granted = false); + } + + fn onewire_slot(&self, pulse: u32, sample: u32) -> bool { + w1::slot::slot(&mut self.0.onewire.lock(), pulse, sample) + } + + fn post_message(&self) { + self.0.with(|s| { + for code in 0..PROCESSOR_CODES { + s.set_message(code, true); + } + }); + } + + fn take_message(&self, processor: usize) -> bool { + self.0.with(|s| { + if processor >= PROCESSOR_CODES || s.pending_messages & (1 << processor) == 0 { + return false; + } + s.set_message(processor, false); + true + }) + } +} + +/// One adapter's view of the scheduler: a target on that adapter's bus that +/// claims no address and hands the adapter each grant as it is published. +struct Port { + shared: Arc, + index: usize, +} + +impl I2cTarget for Port { + fn claims(&self, _addr7: u8) -> bool { + false + } + + fn answers_general_call(&self) -> bool { + false + } + + fn send(&self, _byte: u8) -> bool { + false + } + + fn recv(&self) -> u8 { + 0xff + } + + fn service(&self) -> Service { + let grant = self.shared.run(|s| { + if s.taken[self.index] == s.grant_seq { + return None; + } + s.taken[self.index] = s.grant_seq; + Some(s.grant) + }); + Service { grant, release_bus: false } + } +} + +/// The UST control port: one byte, all ones from power-on, then the last byte +/// written. +struct UstControl(Mutex); + +impl I2cTarget for UstControl { + fn claims(&self, addr7: u8) -> bool { + addr7 == UST_CONTROL_ADDR + } + + fn send(&self, byte: u8) -> bool { + *self.0.lock() = byte; + true + } + + fn recv(&self) -> u8 { + *self.0.lock() + } +} + +/// A module's two one-wire identity pages from its ten-character serial: +/// page 0 is the format byte `0x01` and the serial, page 1 the fixed name +/// `MODULEID` at offset sixteen, every other byte `0xff`, both closed by a +/// stored inverted CRC-16. `None` for a serial that does not fit the record. +pub fn identity_pages(serial: &str) -> Option<[[u8; 32]; 2]> { + let bytes = serial.as_bytes(); + if bytes.len() != MODULE_SERIAL_LEN || !bytes.iter().all(|b| (0x20..0x7f).contains(b) && !matches!(b, b':' | b';')) { + return None; + } + let mut pages = [[0xffu8; 32]; 2]; + pages[0][0] = 0x01; + pages[0][1..1 + MODULE_SERIAL_LEN].copy_from_slice(bytes); + pages[1][16..24].copy_from_slice(b"MODULEID"); + for page in &mut pages { + let crc = !crc16(0, &page[..30]); + page[30..].copy_from_slice(&crc.to_le_bytes()); + } + Some(pages) +} + +pub struct Origin2000Module { + shared: Arc, + elsc: Arc, + nvram: Arc, + adapters: [Arc; ADAPTERS], +} + +impl Origin2000Module { + /// Build a module: the controller (Origin 2000 build), `nvram` (see + /// [`ElscNvram::origin2000`]), the UST control port and four adapters. + /// The machine wires the adapters into the Hubs and drains + /// [`Self::take_events`] into the module's reset, NMI and power lines. + pub fn new(cfg: ModuleConfig, nvram: Arc, clock: Clock) -> Arc { + let owner = BusOwner::new(); + let shared = Arc::new(Shared { + state: Mutex::new(State::new(cfg)), + clock: clock.clone(), + owner: owner.clone(), + elsc: Mutex::new(Weak::new()), + onewire: Arc::new(Mutex::new(w1::Bus::new())), + }); + let elsc_cfg = ElscConfig { + build: Build::Origin2000, + dip_switches: cfg.dip_switches, + master: true, + start_delay_ms: cfg.start_delay_ms, + }; + let elsc = Arc::new(Elsc::new(elsc_cfg, nvram.clone(), clock, BusBusy::default())); + elsc.set_module_ops(Arc::new(Link(shared.clone()))); + *shared.elsc.lock() = Arc::downgrade(&elsc); + let ust: Arc = Arc::new(UstControl(Mutex::new(0xff))); + let adapters = std::array::from_fn(|index| { + let mut bus = I2cBus::new(); + bus.add(elsc.clone()); + bus.add(nvram.clone()); + bus.add(ust.clone()); + bus.add(Arc::new(Port { shared: shared.clone(), index })); + Arc::new(Pcf8584::shared(bus, BusBusy::default(), owner.clone(), index as u32)) + }); + Arc::new(Self { shared, elsc, nvram, adapters }) + } + + /// Node `index`'s adapter, to map in its Hub. + pub fn adapter(&self, index: usize) -> Arc { + self.adapters[index].clone() + } + + pub fn elsc(&self) -> &Arc { + &self.elsc + } + + pub fn nvram(&self) -> &Arc { + &self.nvram + } + + /// The reset, NMI and power lines' changes, in the order they happened, + /// with time brought up to the present first. + pub fn take_events(&self) -> Vec { + self.elsc.poll(); + self.shared.run(|s| std::mem::take(&mut s.events)) + } + + /// Let time pass for the module and its controller when no guest is + /// touching the bus. + pub fn poll(&self) { + self.elsc.poll(); + self.shared.run(|_| ()); + } + + pub fn powered(&self) -> bool { + self.shared.run(|s| s.power_on) + } + + /// Fit the module's MODULEID part (ROM serial `identity_serial`, pages + /// from `module_serial`) and, if `midplane` is given, the midplane part + /// holding that board record, on the net the controller's `nic` line + /// drives. + pub fn attach_parts( + &self, + module_serial: &str, + identity_serial: u64, + midplane: Option<(&BoardRecord, u64)>, + ) -> Result<(), String> { + let pages = identity_pages(module_serial) + .ok_or_else(|| format!("module serial '{module_serial}' is not ten printable non-separator characters"))?; + let image: Vec = pages.iter().flatten().copied().collect(); + let mut net = self.shared.onewire.lock(); + net.add(Box::new(w1::Ds2502::new(0x09, identity_serial, &image))); + if let Some((record, serial)) = midplane { + record.check()?; + net.add(Box::new(w1::sgi::board_record_part(record, serial))); + } + Ok(()) + } + + /// The midplane net, to wire the second node's Hub line to it. + pub fn midplane_onewire(&self) -> Arc> { + self.shared.onewire.clone() + } +} + +impl Resettable for Origin2000Module { + /// The cabinet's own start, the only reset that reaches the module + /// management: a module's reset or power cycle leaves it alone. The + /// module starts off. With no start delay the controller powers it up + /// here, in the reset, so power is on before any processor runs; with a + /// delay it counts that delay and runs the whole power-up sequence. A + /// module with no power path starts powered, as it has no controller to + /// power it. + fn power_on(&self) { + for adapter in &self.adapters { + adapter.power_on(); + } + self.nvram.power_on(); + self.elsc.power_on(); + let direct = self.shared.run(|s| { + s.pulse_active = false; + s.pulse_nmi = false; + s.pulse_deadline = None; + s.drive_pulse(false, false); + s.reset_state(); + s.power_on = !s.cfg.can_power; + s.publication_enabled = s.power_on; + s.service_deadline = s.now + SERVICE_PERIOD_NS; + let up_now = s.cfg.can_power && s.cfg.start_delay_ms == 0; + if !up_now { + s.drive_power(); + } + up_now + }); + if direct { + self.elsc.power_up_at_start(); + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/o2000_module/tests.rs b/src/dev/o2000_module/tests.rs new file mode 100644 index 000000000..d432880c9 --- /dev/null +++ b/src/dev/o2000_module/tests.rs @@ -0,0 +1,793 @@ +//! Tests of an Origin 2000 module through each node's adapter, on a hand-set +//! clock. Replies are checked against literal text. + +use super::*; +use crate::dev::elsc::tests::{byte_sum, frame, text, Aux, TestClock}; +use crate::dev::pcf8584::{ctl, sts}; + +const DATA: u64 = 0; +const CTRL: u64 = 8; +const MSC: u8 = 0x08; +const INVALID: u8 = 0x7f; +const SECOND: u64 = 1000; +const GRANT: u8 = 0xe0; + +struct Rig { + clock: TestClock, + module: Arc, + aux: Arc, +} + +impl Rig { + fn new() -> Self { + Self::with(ModuleConfig { can_reset: true, can_nmi: true, can_power: true, ..Default::default() }) + } + + fn with(cfg: ModuleConfig) -> Self { + let clock = TestClock::default(); + let nvram = Arc::new(ElscNvram::origin2000(cfg.module_number, None).unwrap()); + let module = Origin2000Module::new(cfg, nvram, clock.clock()); + let aux = Arc::new(Aux::default()); + module.elsc().set_serial_backend(aux.clone()); + module.power_on(); + let rig = Self { clock, module, aux }; + rig.init_adapters(); + rig + } + + /// What the PROM does to each node's adapter after a reset. + fn init_adapters(&self) { + for node in 0..ADAPTERS { + self.w(node, CTRL, ctl::PIN | ctl::ES1); + self.w(node, DATA, 0x1c); + self.w(node, CTRL, ctl::PIN); + self.w(node, DATA, 0x09 + node as u8); + self.w(node, CTRL, ctl::PIN | ctl::ESO); + } + } + + fn w(&self, node: usize, off: u64, v: u8) { + self.module.adapter(node).write_register(off, v); + } + + fn r(&self, node: usize, off: u64) -> u8 { + self.module.adapter(node).read_register(off) + } + + fn step_ms(&self, ms: u64) { + self.clock.advance_ms(ms); + } + + fn events(&self) -> Vec { + self.module.take_events() + } + + fn info(&self) -> String { + self.module.elsc().describe() + } + + // ---- transfers ------------------------------------------------------- + + fn start(&self, node: usize, addr: u8, recv: bool) -> bool { + self.w(node, CTRL, ctl::PIN | ctl::ESO); + self.w(node, DATA, (addr << 1) | recv as u8); + self.w(node, CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + self.r(node, CTRL) & sts::LRB == 0 + } + + fn restart(&self, node: usize, addr: u8, recv: bool) -> bool { + self.w(node, CTRL, ctl::ESO | ctl::STA | ctl::ACK); + self.w(node, DATA, (addr << 1) | recv as u8); + self.r(node, CTRL) & sts::LRB == 0 + } + + fn stop(&self, node: usize) { + self.w(node, CTRL, ctl::PIN | ctl::ESO | ctl::STO); + } + + /// One controller command from `node`: the combined transaction and the + /// closing start to the invalid address, then a stop. + fn msc(&self, node: usize, request: &[u8]) -> Vec { + assert!(self.start(node, MSC, false)); + for b in request { + self.w(node, DATA, *b); + } + assert!(self.restart(node, MSC, true)); + let _ = self.r(node, DATA); + let mut resp = vec![self.r(node, DATA)]; + let total = resp[0] as usize + 2; + for i in 1..total { + if i == total - 1 { + self.w(node, CTRL, ctl::ESO); + } + resp.push(self.r(node, DATA)); + } + assert!(!self.restart(node, INVALID, false)); + self.stop(node); + assert_eq!(byte_sum(&resp), 0); + resp + } + + fn cmd(&self, node: usize, line: &str) -> String { + text(&self.msc(node, &frame(line.as_bytes()))) + } + + #[track_caller] + fn assert_cmd(&self, node: usize, line: &str, want: &str) { + assert_eq!(self.cmd(node, line), want, "node {node}: {line:?}"); + } + + fn nvram_read(&self, node: usize, addr: u16) -> u8 { + let a7 = 0x50 + (addr >> 8) as u8; + assert!(self.start(node, a7, false)); + self.w(node, DATA, addr as u8); + assert!(self.restart(node, a7, true)); + let _ = self.r(node, DATA); + self.w(node, CTRL, ctl::ESO); + let v = self.r(node, DATA); + assert!(!self.restart(node, INVALID, false)); + self.stop(node); + v + } + + fn nvram_write(&self, node: usize, addr: u16, data: &[u8]) { + let a7 = 0x50 + (addr >> 8) as u8; + assert!(self.start(node, a7, false)); + self.w(node, DATA, addr as u8); + for b in data { + self.w(node, DATA, *b); + assert_eq!(self.r(node, CTRL) & sts::LRB, 0); + } + self.stop(node); + } + + fn ust_read(&self, node: usize) -> u8 { + assert!(self.start(node, UST_CONTROL_ADDR, true)); + let _ = self.r(node, DATA); + self.w(node, CTRL, ctl::ESO); + let v = self.r(node, DATA); + self.stop(node); + v + } + + fn ust_write(&self, node: usize, v: u8) { + assert!(self.start(node, UST_CONTROL_ADDR, false)); + self.w(node, DATA, v); + self.stop(node); + } + + /// Step until node 0's data register shows the grant `want`. + #[track_caller] + fn wait_for_grant(&self, want: u8) { + for _ in 0..200 { + if self.r(0, DATA) == want { + return; + } + self.clock.advance_ns(100_000); + } + panic!("grant {want:#04x} never published"); + } + + // ---- one-wire -------------------------------------------------------- + + fn slot(&self, node: usize, pulse: u32, sample: u32, delay: u32) -> bool { + let resp = self.msc(node, &frame(format!("nic {pulse:x} {sample:x} {delay:x}").as_bytes())); + assert_eq!(&resp[1..4], b"ok "); + resp[4] == b'1' + } + + fn ow_reset(&self, node: usize) -> bool { + !self.slot(node, 520, 65, 500) + } + + fn ow_write_byte(&self, node: usize, v: u8) { + for bit in 0..8 { + if v & (1 << bit) != 0 { + self.slot(node, 6, 110, 0); + } else { + self.slot(node, 80, 30, 0); + } + } + } + + fn ow_read_byte(&self, node: usize) -> u8 { + (0..8).fold(0, |v, bit| v | (self.slot(node, 6, 13, 100) as u8) << bit) + } + + fn ow_start_read(&self, node: usize, rom: &[u8; 8], command: u8, address: u16) { + assert!(self.ow_reset(node)); + self.ow_write_byte(node, 0x55); + for b in rom { + self.ow_write_byte(node, *b); + } + self.ow_write_byte(node, command); + self.ow_write_byte(node, address as u8); + self.ow_write_byte(node, (address >> 8) as u8); + } +} + +fn power_events(events: &[ModuleEvent]) -> Vec { + events + .iter() + .filter_map(|e| if let ModuleEvent::Power(on) = e { Some(*on) } else { None }) + .collect() +} + +// ---- adapters, controller, store -------------------------------------------- + +#[test] +fn the_windows_are_separate_adapters() { + let rig = Rig::new(); + for node in 0..ADAPTERS { + rig.w(node, CTRL, ctl::PIN); + assert_eq!(rig.r(node, DATA), 0x09 + node as u8); + rig.w(node, CTRL, ctl::PIN | ctl::ESO); + } +} + +/// Four per-node controllers would answer `ver` just as happily, so the +/// discriminator is the test-and-set cell: taken from one node, every other +/// node finds it held. +#[test] +fn one_controller_answers_every_node() { + let rig = Rig::new(); + for node in 0..ADAPTERS { + assert_eq!(rig.cmd(node, "ver"), "ok VER 3.2 "); + } + rig.assert_cmd(0, "tas A", "ok 0"); + for node in 1..ADAPTERS { + rig.assert_cmd(node, "tas B", "ok A"); + } +} + +#[test] +fn the_nvram_is_shared_by_every_node() { + let rig = Rig::new(); + for (i, offset) in [0x000u16, 0x123, 0x400].into_iter().enumerate() { + let value = 0xa0 + i as u8; + rig.nvram_write(i, offset, &[value]); + assert_eq!(rig.nvram_read(ADAPTERS - 1, offset), value); + } +} + +#[test] +fn a_new_store_holds_the_marker_the_password_and_module_one() { + const BLOCK: [u8; 16] = [0x37, b'n', b'o', b'n', b'e', 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]; + let rig = Rig::new(); + for (i, want) in BLOCK.iter().enumerate() { + assert_eq!(rig.nvram_read(i % ADAPTERS, 0x700 + i as u16), *want, "byte {i}"); + } + assert_eq!(rig.nvram_read(0, 0), 0); +} + +#[test] +fn the_module_number_comes_from_the_configuration() { + let rig = Rig::with(ModuleConfig { module_number: 6, ..Default::default() }); + assert_eq!(rig.nvram_read(2, 0x708), 6); +} + +#[test] +fn a_twelve_byte_block_write_lands_in_one_transaction() { + const BLOCK: [u8; 12] = [0x37, b'w', b'x', b'y', b'z', 0x15, 0x16, 0x01, 0x02, 0x03, 0x04, 0x05]; + let rig = Rig::new(); + rig.nvram_write(0, 0x700, &BLOCK); + for (i, want) in BLOCK.iter().enumerate() { + assert_eq!(rig.nvram_read(3, 0x700 + i as u16), *want); + } + assert_eq!(rig.nvram_read(3, 0x70c), 0); +} + +#[test] +fn the_ust_control_port_reads_all_ones_then_the_last_byte_written() { + let rig = Rig::new(); + assert_eq!(rig.ust_read(1), 0xff); + rig.ust_write(1, 0x3f); + assert_eq!(rig.ust_read(1), 0x3f); + assert_eq!(rig.ust_read(0), 0x3f); +} + +#[test] +fn an_open_transfer_leaves_every_other_node_seeing_a_busy_bus() { + let rig = Rig::new(); + assert_ne!(rig.r(1, CTRL) & sts::BB, 0); + assert!(rig.start(0, 0x50, false)); + for node in 1..ADAPTERS { + assert_eq!(rig.r(node, CTRL) & sts::BB, 0, "node {node}"); + } + rig.stop(0); + for node in 1..ADAPTERS { + assert_ne!(rig.r(node, CTRL) & sts::BB, 0, "node {node}"); + } +} + +/// One adapter's open transfer must survive another adapter addressing the +/// same part. The part keeps a single address counter, so a second adapter +/// reaching it would move the counter the first is about to read from. +#[test] +fn a_second_adapter_waits_for_the_first_and_does_not_move_its_counter() { + let rig = Rig::new(); + // Node 0 selects byte 0x701 of the controller's block... + assert!(rig.start(0, 0x57, false)); + rig.w(0, DATA, 0x01); + // ...node 1 tries to read from the same block meanwhile: its START waits + // (the bus reads busy, PIN stays inactive)... + rig.w(1, CTRL, ctl::PIN | ctl::ESO); + rig.w(1, DATA, 0x57 << 1); + rig.w(1, CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + assert_ne!(rig.r(1, CTRL) & sts::PIN, 0); + // ...and node 0 still reads its own byte. + assert!(rig.restart(0, 0x57, true)); + let _ = rig.r(0, DATA); + rig.w(0, CTRL, ctl::ESO); + assert_eq!(rig.r(0, DATA), b'n'); + assert!(!rig.restart(0, INVALID, false)); + rig.stop(0); + // Once node 0 stopped, node 1's START runs. + assert_eq!(rig.r(1, CTRL) & sts::PIN, 0); + assert_eq!(rig.r(1, CTRL) & sts::LRB, 0); + rig.stop(1); +} + +#[test] +fn a_waiting_start_is_withdrawn_by_a_stop_or_by_clearing_eso() { + let rig = Rig::new(); + assert!(rig.start(0, 0x57, false)); + for withdraw in [ctl::PIN | ctl::ESO | ctl::STO, ctl::PIN] { + rig.w(1, CTRL, ctl::PIN | ctl::ESO); + rig.w(1, DATA, 0x57 << 1); + rig.w(1, CTRL, ctl::PIN | ctl::ESO | ctl::STA | ctl::ACK); + rig.w(1, CTRL, withdraw); + } + rig.stop(0); + // Nothing started: node 1 is idle and the bus is free. + rig.w(1, CTRL, ctl::PIN | ctl::ESO); + assert_eq!(rig.r(1, CTRL), sts::PIN | sts::BB); +} + +// ---- grants ------------------------------------------------------------------ + +#[test] +fn every_adapter_sees_the_eight_code_rotation() { + let rig = Rig::new(); + for n in 1..=24u64 { + rig.step_ms(1); + let want = GRANT | ((n - 1) % 8) as u8; + for node in 0..ADAPTERS { + assert_eq!(rig.r(node, DATA), want, "tick {n} node {node}"); + } + } +} + +#[test] +fn a_command_in_a_processors_turn_leaves_the_rotation_running() { + let rig = Rig::new(); + for processor in 0..8u8 { + rig.wait_for_grant(GRANT | processor); + let node = (processor / 2) as usize; + assert_eq!(rig.cmd(node, "ver"), "ok VER 3.2 "); + assert_eq!(rig.r(node, CTRL) & (sts::BER | sts::LAB), 0); + } + // A reader that starts nothing does not hold the ring either. + rig.wait_for_grant(GRANT); + rig.clock.advance_ns(1_500_000); + for node in 0..ADAPTERS { + assert_ne!(rig.r(node, CTRL) & sts::BB, 0); + assert_eq!(rig.r(node, DATA), GRANT | 1); + } +} + +#[test] +fn a_long_idle_gap_keeps_the_ring_on_its_grid() { + let rig = Rig::new(); + rig.step_ms(1); + assert_eq!(rig.r(0, DATA), GRANT); + // 80 000 ticks later is a whole number of revolutions. + rig.step_ms(80_000); + assert_eq!(rig.r(0, DATA), GRANT); + rig.step_ms(1); + assert_eq!(rig.r(0, DATA), GRANT | 1); +} + +#[test] +fn a_bus_held_for_five_seconds_has_the_controller_reinitialised() { + let rig = Rig::new(); + // Leave a transfer open with the controller selected and a response + // pending. + assert!(rig.start(0, MSC, false)); + for b in frame(b"ver") { + rig.w(0, DATA, b); + } + assert!(rig.restart(0, MSC, true)); + // Each tick finds the bus busy; the controller is addressed, so these + // are not published. 5 s of them reinitialise its protocol queues. + rig.step_ms(4_000); + assert!(rig.info().contains("pending response")); + assert!(!rig.info().contains("pending response: none")); + rig.step_ms(1_100); + rig.r(1, CTRL); + assert!(rig.info().contains("pending response: none")); +} + +// ---- the Origin 2000 build ------------------------------------------------- + +#[test] +fn the_origin_2000_display_takes_dsc_and_dsp() { + let rig = Rig::new(); + assert!(rig.info().contains("display: \"VER 3.2 \"")); + rig.assert_cmd(0, "dsc 1 A", "ok "); + rig.assert_cmd(0, "dsc 8 Z", "ok "); + assert!(rig.info().contains("display: \"AER 3.2Z\"")); + for refused in ["dsc 0 X", "dsc 9 X", "dsc 4 ", "dsc", "dsp"] { + rig.assert_cmd(0, refused, "err arg"); + } + rig.assert_cmd(0, "dsp testing", "ok "); + assert!(rig.info().contains("display: \"testingZ\"")); + rig.assert_cmd(0, "dsp ninechars", "ok "); + assert!(rig.info().contains("display: \"ninechar\"")); + // Spaces before each argument are skipped. + rig.assert_cmd(0, "dsc 2 Q", "ok "); + assert!(rig.info().contains("display: \"nQnechar\"")); + // The message's own leading spaces are characters, and it stops at the + // first unprintable one, leaving the rest of the display. + rig.assert_cmd(0, "dsp ab\x01zz", "ok "); + assert!(rig.info().contains("display: \" abechar\"")); + rig.assert_cmd(0, "ver", "ok VER 3.2 "); +} + +#[test] +fn the_origin_2000_build_has_key_and_vlm_and_no_aut() { + let rig = Rig::new(); + rig.assert_cmd(0, "key", "ok on"); + rig.assert_cmd(1, "vlm 5", "ok n"); + rig.assert_cmd(1, "vlm 5h", "ok "); + rig.assert_cmd(2, "vlm 5", "ok h"); + rig.assert_cmd(0, "vlm 3 l", "ok "); + rig.assert_cmd(0, "vlm 3", "ok l"); + rig.assert_cmd(0, "vlm v", "ok n"); + for bad in ["vlm", "vlm x", "vlm 5 q"] { + rig.assert_cmd(0, bad, "err arg"); + } + for absent in ["aut", "mod", "pas none"] { + rig.assert_cmd(0, absent, "err cmd"); + } + rig.assert_cmd(0, "sla o", "ok "); + rig.assert_cmd(0, "tmp", "ok n"); + rig.assert_cmd(0, "dsp abc", "ok "); + rig.assert_cmd(0, "clr", "ok "); + assert!(rig.info().contains("display: \" \"")); + rig.assert_cmd(3, "vlm 5", "ok h"); +} + +#[test] +fn a_status_message_replaces_the_hosts_display_text_until_the_host_writes() { + let rig = Rig::new(); + let info = rig.info(); + assert!(info.contains("display: \"VER 3.2 \"")); + assert!(!info.contains("status message")); + rig.assert_cmd(0, "nmi", "ok "); + assert!(rig.info().contains("status message: NMI")); + rig.assert_cmd(0, "dsp hi", "ok "); + assert!(!rig.info().contains("status message")); +} + +#[test] +fn the_dip_switches_reach_the_controller_with_no_role_bit() { + let rig = Rig::with(ModuleConfig { dip_switches: 0x10, ..Default::default() }); + rig.assert_cmd(2, "rsw", "ok ef"); + let rig = Rig::with(ModuleConfig { dip_switches: 0x04, ..Default::default() }); + rig.assert_cmd(2, "rsw", "ok fb"); +} + +// ---- literal frames ------------------------------------------------------------ + +#[test] +fn the_origin_2000_build_answers_the_prom_exchanges_with_these_frames() { + const OK: [u8; 5] = [0x03, 0x6f, 0x6b, 0x20, 0x03]; + let rig = Rig::new(); + let exchanges: &[(&[u8], &[u8])] = &[ + // dsp and eight blanks; dsc 1 e: the display takes them + (&[0x0b, 0x64, 0x73, 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xae], &OK), + (&[0x07, 0x64, 0x73, 0x63, 0x20, 0x31, 0x20, 0x65, 0xe9], &OK), + // rsw: ok ff + (&[0x03, 0x72, 0x73, 0x77, 0xa1], &[0x05, 0x6f, 0x6b, 0x20, 0x66, 0x66, 0x35]), + // pwr q: ok 1 + (&[0x05, 0x70, 0x77, 0x72, 0x20, 0x71, 0x11], &[0x04, 0x6f, 0x6b, 0x20, 0x31, 0xd1]), + // ver: ok VER 3.2 + ( + &[0x03, 0x76, 0x65, 0x72, 0xb0], + &[0x0b, 0x6f, 0x6b, 0x20, 0x56, 0x45, 0x52, 0x20, 0x33, 0x2e, 0x32, 0x20, 0x3b], + ), + // dsc 0 e and dsc 9 e: position out of range, err arg + ( + &[0x07, 0x64, 0x73, 0x63, 0x20, 0x30, 0x20, 0x65, 0xea], + &[0x07, 0x65, 0x72, 0x72, 0x20, 0x61, 0x72, 0x67, 0x56], + ), + ( + &[0x07, 0x64, 0x73, 0x63, 0x20, 0x39, 0x20, 0x65, 0xe1], + &[0x07, 0x65, 0x72, 0x72, 0x20, 0x61, 0x72, 0x67, 0x56], + ), + ]; + for (request, reply) in exchanges { + assert_eq!(&rig.msc(0, request), reply, "request {request:02x?}"); + } +} + +// ---- messages ---------------------------------------------------------------- + +#[test] +fn a_controller_message_is_announced_in_every_code_and_fetched_once() { + let rig = Rig::new(); + rig.assert_cmd(0, "pwr d 8", "ok "); + rig.wait_for_grant(GRANT | 0x08 | 3); + rig.assert_cmd(0, "get", "pd remote"); + rig.assert_cmd(0, "get", "acp . "); + // Code 5 was not fetched: it still sees the message. + rig.wait_for_grant(GRANT | 0x08 | 5); + // Code 0 fetched its message: it no longer does. + rig.wait_for_grant(GRANT); + rig.assert_cmd(0, "clr", "ok "); +} + +#[test] +fn a_controller_message_lasts_a_second_there_and_in_the_controller() { + let rig = Rig::new(); + rig.assert_cmd(0, "pwr d 30", "ok "); + assert!(rig.info().contains("controller message: \"pd remote\"")); + rig.step_ms(900); + assert!(rig.info().contains("controller message: \"pd remote\"")); + rig.step_ms(100); + assert!(!rig.info().contains("controller message")); + rig.wait_for_grant(GRANT | 6); +} + +#[test] +fn queued_console_input_is_announced_to_the_selected_processor() { + let rig = Rig::new(); + rig.assert_cmd(0, "sel 2a", "ok "); + for b in b"hi" { + rig.module.elsc().serial_input(*b); + } + // 2a is processor code 2. + rig.wait_for_grant(GRANT | 0x08 | 2); + rig.wait_for_grant(GRANT | 3); +} + +// ---- power, reset, NMI ------------------------------------------------------- + +#[test] +fn nmi_pulses_the_modules_output_after_a_second() { + let rig = Rig::new(); + rig.events(); + rig.assert_cmd(2, "nmi", "ok "); + rig.step_ms(990); + assert!(rig.events().is_empty()); + rig.step_ms(20); + assert_eq!(rig.events(), [ModuleEvent::Nmi(true)]); + rig.step_ms(100); + assert_eq!(rig.events(), [ModuleEvent::Nmi(false)]); + assert_eq!(rig.cmd(0, "ver"), "ok VER 3.2 "); +} + +#[test] +fn rst_pulses_the_modules_reset_output() { + let rig = Rig::new(); + rig.events(); + rig.assert_cmd(1, "rst", "ok "); + rig.step_ms(1000); + assert_eq!(rig.events(), [ModuleEvent::Reset(true)]); + // The pulse is 100 ms, and a destructive command is refused meanwhile. + rig.assert_cmd(0, "pwr d 0", "err cmd"); + rig.step_ms(100); + assert_eq!(rig.events(), [ModuleEvent::Reset(false)]); + assert_eq!(rig.cmd(0, "ver"), "ok VER 3.2 "); +} + +#[test] +fn power_down_reaches_the_modules_power_line_and_stops_the_grants() { + let rig = Rig::new(); + rig.events(); + rig.assert_cmd(0, "pwr d 0", "ok "); + assert_eq!(power_events(&rig.events()), [false]); + assert!(!rig.module.powered()); + rig.assert_cmd(0, "pwr", "ok d"); + let data = rig.r(0, DATA); + rig.step_ms(20); + assert_eq!(rig.r(0, DATA), data); +} + +#[test] +fn a_power_cycle_holds_the_module_for_the_interval_and_the_4_4_second_hold() { + let rig = Rig::new(); + rig.events(); + rig.assert_cmd(0, "pwr c 5", "ok "); + rig.step_ms(1); + rig.assert_cmd(1, "pwr", "ok d"); + assert_eq!(power_events(&rig.events()), [false]); + rig.assert_cmd(2, "rst", "err cmd"); + rig.assert_cmd(3, "clr", "ok "); + + // 5 s off, then 4.4 s with reset held: back at 9.4 s. + rig.step_ms(9_300 - 1); + rig.assert_cmd(0, "pwr", "ok d"); + rig.step_ms(100); + assert_eq!(power_events(&rig.events()), [true]); + rig.init_adapters(); + rig.assert_cmd(0, "pwr", "ok u"); + rig.assert_cmd(0, "pwr q", "ok 1"); +} + +#[test] +fn the_controller_powers_the_module_up_inside_the_reset() { + let rig = Rig::new(); + let info = rig.info(); + assert!(info.contains("module power: on")); + assert!(info.contains("display: \"VER 3.2 \"")); + assert!(!info.contains("status message")); + assert!(!info.contains("start-up power-up pending")); + assert!(rig.module.powered()); + assert_eq!(power_events(&rig.events()), [true]); + rig.assert_cmd(0, "pwr", "ok u"); + rig.assert_cmd(0, "pwr q", "ok 1"); +} + +#[test] +fn with_a_start_delay_the_module_starts_off_and_the_controller_powers_it_up() { + let rig = Rig::with(ModuleConfig { + can_power: true, + can_reset: true, + can_nmi: true, + start_delay_ms: 6000, + ..Default::default() + }); + assert!(!rig.module.powered()); + rig.assert_cmd(0, "pwr", "ok d"); + assert!(rig.module.elsc().power_return_pending()); + rig.assert_cmd(0, "clr", "ok "); + rig.step_ms(7 * SECOND); + assert!(rig.info().contains("powering up")); + rig.step_ms(5 * SECOND); + rig.module.poll(); + rig.init_adapters(); + assert!(rig.module.powered()); + rig.assert_cmd(0, "pwr", "ok u"); + rig.assert_cmd(0, "pwr q", "ok 1"); +} + +#[test] +fn an_ac_cycle_powers_the_module_up_again() { + let rig = Rig::new(); + rig.assert_cmd(0, "pwr c 258", "ok "); + rig.step_ms(1000); + rig.assert_cmd(1, "pwr", "ok d"); + rig.module.power_on(); + rig.init_adapters(); + assert!(rig.module.powered()); + rig.assert_cmd(0, "pwr", "ok u"); + // No power-up is left to come from the cycle the reset ended. + rig.step_ms(300 * SECOND); + rig.assert_cmd(0, "pwr", "ok u"); +} + +#[test] +fn a_module_with_no_power_path_refuses_destructive_commands_and_stays_powered() { + let rig = Rig::with(ModuleConfig::default()); + for refused in ["pwr d 0", "pwr c", "rst"] { + rig.assert_cmd(0, refused, "err cmd"); + } + rig.assert_cmd(0, "pwr", "ok u"); + assert!(rig.module.powered()); +} + +#[test] +fn the_operators_serial_line_runs_when_the_scheduler_gives_it_a_turn() { + let rig = Rig::new(); + rig.aux.take(); + for b in b"\x14ver\r" { + rig.module.elsc().serial_input(*b); + } + // The line waits for the scheduler's turn at the next tick. + assert_eq!(rig.aux.take(), "\r\nMSC> ver\r\n"); + rig.step_ms(1); + rig.module.poll(); + assert_eq!(rig.aux.take(), "\x14ok VER 3.2 \r\n"); +} + +#[test] +fn an_unpowered_module_still_takes_serial_commands() { + let rig = Rig::new(); + rig.assert_cmd(0, "pwr c 258", "ok "); + rig.step_ms(1000); + rig.aux.take(); + let serial = |line: &str| { + for b in format!("\x14{line}\r").as_bytes() { + rig.module.elsc().serial_input(*b); + } + rig.step_ms(2); + rig.module.poll(); + rig.aux.take() + }; + assert_eq!(serial("pas none"), "\r\nMSC> pas none\r\n\x14ok\r\n"); + assert_eq!(serial("pwr"), "\r\nMSC> pwr\r\n\x14ok d\r\n"); + assert_eq!(serial("pwr u"), "\r\nMSC> pwr u\r\n\x14ok\r\n"); + rig.step_ms(5 * SECOND); + rig.module.poll(); + assert!(rig.module.powered()); +} + +#[test] +fn the_start_announces_the_origin_2000_version_and_fsc_mirrors_dsp() { + let rig = Rig::new(); + assert_eq!(rig.aux.take(), "\x14MSC VER 3.2 \r\n"); + rig.assert_cmd(0, "dsp quiet", "ok "); + rig.assert_cmd(0, "fsc 1", "ok "); + rig.assert_cmd(1, "dsp hi", "ok "); + assert_eq!(rig.aux.take(), "\x14dsp hi4\r\n"); +} + +// ---- one-wire -------------------------------------------------------------------- + +// The ROM serial bytes, 00 'Y' 'U' 'M' 'E' 'Q', are a synthetic marker +// for the MODULEID part, not a real serial. The pages follow from the module +// serial "K000000001". +const MODULE_ROM: [u8; 8] = [0x09, 0x00, 0x59, 0x55, 0x4d, 0x45, 0x51, 0x33]; +const MODULE_PAGES: [u8; 64] = [ + 0x01, 0x4b, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0xa2, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, + 0x49, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4b, 0x59, +]; + +#[test] +fn the_identity_pages_are_the_format_byte_the_serial_and_the_moduleid_name() { + let pages = identity_pages("K000000001").unwrap(); + assert_eq!(pages.iter().flatten().copied().collect::>(), MODULE_PAGES); + for bad in ["SHORT", "K0000000012", "K000000:01", "K000000;01", "K000000\x0101"] { + assert!(identity_pages(bad).is_none(), "{bad:?}"); + } +} + +#[test] +fn the_moduleid_part_answers_through_the_nic_line_of_every_node() { + let rig = Rig::new(); + rig.module.attach_parts("K000000001", 0x5145_4d55_5900, None).unwrap(); + // Read Memory from the start: the command echo CRC 0x8d, then the pages. + for node in 0..ADAPTERS { + rig.ow_start_read(node, &MODULE_ROM, 0xf0, 0); + assert_eq!(rig.ow_read_byte(node), 0x8d, "node {node}"); + assert_eq!(rig.ow_read_byte(node), MODULE_PAGES[0], "node {node}"); + } + rig.ow_start_read(1, &MODULE_ROM, 0xf0, 0); + assert_eq!(rig.ow_read_byte(1), 0x8d); + let pages: Vec = (0..64).map(|_| rig.ow_read_byte(1)).collect(); + assert_eq!(pages, MODULE_PAGES); +} + +#[test] +fn a_line_with_no_part_reads_no_presence_and_a_bad_serial_attaches_nothing() { + let rig = Rig::new(); + assert!(!rig.ow_reset(0)); + assert!(rig.module.attach_parts("short", 1, None).is_err()); + assert!(!rig.ow_reset(0)); +} + +#[test] +fn the_net_a_hub_line_can_share_holds_the_attached_part() { + let rig = Rig::new(); + rig.module.attach_parts("K000000001", 0x5145_4d55_5900, None).unwrap(); + assert_eq!(rig.module.midplane_onewire().lock().roms(), vec![MODULE_ROM]); +} + +/// Every adapter sees a grant passively, whoever holds the bus: a node +/// reading while a peer has a transfer open still finds the grant. +#[test] +fn an_adapter_sees_a_grant_while_a_peer_holds_the_bus() { + let rig = Rig::new(); + rig.step_ms(1); + // Node 0 takes grant 0xe0 and opens a transfer. + assert_eq!(rig.r(0, DATA), GRANT); + assert!(rig.start(0, 0x50, false)); + // Node 2, which never looked, finds the same grant. + assert_eq!(rig.r(2, DATA), GRANT); + rig.stop(0); + assert_eq!(rig.r(3, DATA), GRANT); +} From 094365e2bd7d0b328fc84ab97ff0d6314dbbc572 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:35:32 -0700 Subject: [PATCH 27/38] node_flash: add the SN0 node boot flash The IP27 PROM runs from a byte-wide 1 MiB AMD-command part behind the Hub's flash controller. The existing Bedrock flash is two 16-bit 8 MiB arrays and cannot stand in for it. The part is seen as the chip itself (the reset view) and through the controller window, which repeats the image and takes commands on 64-bit writes only. Program and erase finish inside the store that starts them. initial_image() builds the first contents: the jump-table template, the PROM payload over it, and the product and processor-module words patched into the configuration block with its byte sum kept. --- src/dev/mod.rs | 1 + src/dev/node_flash.rs | 518 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 src/dev/node_flash.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index c5dbb2cfc..ad774a4cd 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -65,6 +65,7 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod node_flash; pub mod hub; pub mod hub_io; pub mod hub_md; diff --git a/src/dev/node_flash.rs b/src/dev/node_flash.rs new file mode 100644 index 000000000..14df10581 --- /dev/null +++ b/src/dev/node_flash.rs @@ -0,0 +1,518 @@ +//! The boot flash of an SN0 node: one byte-wide 1 MiB AMD-command part in +//! sixteen 64 KiB sectors, behind the Hub's flash controller. +//! +//! The part is seen two ways. [`NodeFlash::chip`] is the chip itself, byte +//! addressed, as the reset view at node offset 0x1fc0_0000 shows it. The Hub's +//! controller window ([`NodeFlash::window`]) decodes the chip onto the 64-bit +//! junk bus: a read repeats the image across the window, and a command is a +//! 64-bit write with chip byte N at bus offset 8N. Both reach the same bytes +//! and the same command state. +//! +//! The part is not identified. The geometry, the manufacturer and device +//! codes (0x01, 0xd5) and the unlock addresses (0x5555, 0x2aaa) are the ones +//! the QEMU model uses. Approximations: +//! +//! * Program and sector erase complete inside the write that starts them, so +//! a status poll finds the finished data and no toggle ever shows. +//! * Programming only clears bits, as on the part. A program of an address in +//! a protected sector is not modelled (there is no protection). +//! * Chip erase and the CFI query set are not implemented; either is logged +//! and ignored. +//! +//! Ported from hw/mips/sgi/sn0.c and hw/mips/sgi/initial-flash.c in +//! jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use crate::dlog; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::io; +use std::path::Path; +use std::sync::Arc; + +use super::sflash::storage::FlashBacking; + +pub const SIZE: usize = 1 << 20; +pub const SECTOR_SIZE: usize = 0x1_0000; +const UNLOCK_1: usize = 0x5555; +const UNLOCK_2: usize = 0x2aaa; +pub const MANUFACTURER: u8 = 0x01; +pub const DEVICE: u8 = 0xd5; + +/// The configuration block at 0x60 and the values a node's product and +/// processor module put in it. +pub const CONFIG_OFFSET: usize = 0x60; +const CONFIG_SIZE: usize = 0x50; +const CONFIG_MAGIC: u64 = 0x6970_3237_636f_6e66; // "ip27conf" +/// The environment log's sector and header. +pub const LOG_SECTOR: usize = 14; +const LOG_MAGIC: u32 = 0x504c_4f47; // "PLOG" + +/// The Hub's two fixed clocks, not configuration. +const HUB_CLOCK_HZ: u64 = 0x055d_4a80; +const HUB_RTC_CLOCK_HZ: u64 = 0x0013_12d0; + +/// What a product and its processor module patch into the configuration block. +#[derive(Clone, Copy, Debug)] +pub struct FlashValues { + pub machine_type: u32, + pub read_cycles: u32, + pub write_cycles: u32, + pub cpu_mode_bits: u32, + pub cpu_clock_hz: u64, +} + +impl FlashValues { + /// The Origin 200 with a 180 MHz R10000 and 1 MiB of secondary cache. + pub const ORIGIN200_R10000_180_1M: Self = Self { + machine_type: 1, + read_cycles: 15, + write_cycles: 4, + cpu_mode_bits: 0x1211_a785, + cpu_clock_hz: 180_000_000, + }; +} + +fn put32(b: &mut [u8], at: usize, v: u32) { + b[at..at + 4].copy_from_slice(&v.to_be_bytes()); +} +fn put64(b: &mut [u8], at: usize, v: u64) { + b[at..at + 8].copy_from_slice(&v.to_be_bytes()); +} +fn byte_sum(b: &[u8]) -> u8 { + b.iter().fold(0u8, |s, x| s.wrapping_add(*x)) +} + +/// The template a flash starts from: entry zero spins, the other nine jump-table +/// entries return, the configuration block is filled in and the environment +/// log has its header. A payload overlays the front of it. +fn template(values: &FlashValues) -> Vec { + let mut f = vec![0xff; SIZE]; + for i in 0..10 { + put32(&mut f, i * 8, if i == 0 { 0x1000_ffff } else { 0x03e0_0008 }); + put32(&mut f, i * 8 + 4, 0); + } + let c = &mut f[CONFIG_OFFSET..CONFIG_OFFSET + CONFIG_SIZE]; + put32(c, 0x00, 0xb); + put32(c, 0x04, values.cpu_mode_bits); + put64(c, 0x08, CONFIG_MAGIC); + put64(c, 0x10, values.cpu_clock_hz); + put64(c, 0x18, HUB_CLOCK_HZ); + put64(c, 0x20, HUB_RTC_CLOCK_HZ); + put32(c, 0x28, 1); + put32(c, 0x2c, values.read_cycles); + put32(c, 0x30, values.machine_type); + put32(c, 0x34, 0); + put32(c, 0x38, 0); + put32(c, 0x3c, values.write_cycles); + put32(c, 0x40, 6); + put32(c, 0x44, 156); + put32(c, 0x48, 0); + let log = LOG_SECTOR * SECTOR_SIZE; + put32(&mut f, log + 0x10, LOG_MAGIC); + put32(&mut f, log + 0x14, 1); + put32(&mut f, log + 0x18, 1); + f +} + +/// Patch the product and module values into the configuration block, keeping +/// the block's byte sum: the checksum adjustment word absorbs the change. A +/// block without the magic is left alone. +fn configure(f: &mut [u8], values: &FlashValues) { + let c = &mut f[CONFIG_OFFSET..CONFIG_OFFSET + CONFIG_SIZE]; + if u64::from_be_bytes(c[0x08..0x10].try_into().unwrap()) != CONFIG_MAGIC { + return; + } + let old = byte_sum(c); + put32(c, 0x34, 0); + put32(c, 0x04, values.cpu_mode_bits); + put64(c, 0x10, values.cpu_clock_hz); + put32(c, 0x2c, values.read_cycles); + put32(c, 0x30, values.machine_type); + put32(c, 0x3c, values.write_cycles); + let adjust = old.wrapping_sub(byte_sum(c)); + put32(c, 0x34, u32::from(adjust)); +} + +/// A fresh flash image: the template, the PROM payload over its front, and the +/// product and module values patched into whichever configuration block is +/// there. +pub fn initial_image(payload: &[u8], values: &FlashValues) -> Result, String> { + if payload.len() > SIZE { + return Err(format!("the PROM payload is {} bytes; the node flash holds {SIZE}", payload.len())); + } + let mut f = template(values); + f[..payload.len()].copy_from_slice(payload); + configure(&mut f, values); + Ok(f) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Mode { + Array, + Unlock1, + Unlock2, + Program, + Erase1, + Erase2, + Erase3, + Identify, +} + +struct State { + data: Vec, + mode: Mode, + backing: Option, +} + +impl State { + fn read(&self, off: usize) -> u8 { + match self.mode { + Mode::Identify => match off & 0xff { + 0 => MANUFACTURER, + 1 => DEVICE, + 2 => 0, // sector not protected + _ => 0xff, + }, + _ => self.data[off], + } + } + + fn persist(&mut self, off: usize, len: usize) { + if let Some(b) = &mut self.backing { + if let Err(e) = b.write_range(off, &self.data[off..off + len]) { + log::error!("node-flash: backing write at {off:#x} failed: {e}"); + } + } + } + + fn write(&mut self, off: usize, v: u8) { + let reset = v == 0xf0; + match self.mode { + _ if reset => self.mode = Mode::Array, + Mode::Array | Mode::Identify => { + self.mode = if off == UNLOCK_1 && v == 0xaa { + Mode::Unlock1 + } else { + dlog!(LogModule::Hub, "node flash: stray write {v:#x} at {off:#x} in array mode"); + Mode::Array + }; + } + Mode::Unlock1 => { + self.mode = if off == UNLOCK_2 && v == 0x55 { Mode::Unlock2 } else { Mode::Array }; + } + Mode::Unlock2 => { + self.mode = match (off, v) { + (UNLOCK_1, 0xa0) => Mode::Program, + (UNLOCK_1, 0x80) => Mode::Erase1, + (UNLOCK_1, 0x90) => Mode::Identify, + _ => { + dlog!(LogModule::Hub, "node flash: unsupported command {v:#x} at {off:#x}"); + Mode::Array + } + }; + } + Mode::Program => { + let old = self.data[off]; + self.data[off] = old & v; + self.persist(off, 1); + self.mode = Mode::Array; + } + Mode::Erase1 => { + self.mode = if off == UNLOCK_1 && v == 0xaa { Mode::Erase2 } else { Mode::Array }; + } + Mode::Erase2 => { + self.mode = if off == UNLOCK_2 && v == 0x55 { Mode::Erase3 } else { Mode::Array }; + } + Mode::Erase3 => { + if v == 0x30 { + let start = off & !(SECTOR_SIZE - 1); + self.data[start..start + SECTOR_SIZE].fill(0xff); + self.persist(start, SECTOR_SIZE); + } else { + dlog!(LogModule::Hub, "node flash: unsupported erase {v:#x} at {off:#x}"); + } + self.mode = Mode::Array; + } + } + } +} + +pub struct NodeFlash { + st: Mutex, +} + +impl NodeFlash { + pub fn new(image: Vec) -> Result, String> { + if image.len() != SIZE { + return Err(format!("the node flash is {} bytes, not {SIZE}", image.len())); + } + Ok(Arc::new(Self { + st: Mutex::new(State { data: image, mode: Mode::Array, backing: None }), + })) + } + + /// A flash backed by `path`: an existing file of exactly 1 MiB replaces + /// `image` (completed programs and erases are written through), an empty + /// or missing one is seeded from it. + pub fn with_file(image: Vec, path: impl AsRef) -> io::Result> { + let mut data = image; + let backing = FlashBacking::open(path, &mut data)?; + Ok(Arc::new(Self { + st: Mutex::new(State { data, mode: Mode::Array, backing: Some(backing) }), + })) + } + + pub fn contents(&self) -> Vec { + self.st.lock().data.clone() + } + + /// The chip, byte addressed from its first byte. + pub fn chip(self: &Arc) -> Arc { + Arc::new(Chip(self.clone())) + } + + /// The Hub's controller window over the chip. + pub fn window(self: &Arc) -> Arc { + Arc::new(Window(self.clone())) + } +} + +impl Resettable for NodeFlash { + /// The chip returns to array reads; its contents survive. + fn power_on(&self) { + self.st.lock().mode = Mode::Array; + } +} + +struct Chip(Arc); + +impl Chip { + fn bytes(&self, a: u64, n: usize) -> Option { + let a = usize::try_from(a).ok().filter(|a| a.checked_add(n).is_some_and(|e| e <= SIZE))?; + let st = self.0.st.lock(); + Some((0..n).fold(0u64, |v, i| (v << 8) | u64::from(st.read(a + i)))) + } +} + +macro_rules! chip_read { + ($r:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + self.bytes(a, std::mem::size_of::<$t>()).map_or_else($out::err, |v| $out::ok(v as $t)) + } + }; +} +impl BusDevice for Chip { + chip_read!(read8, u8, BusRead8); + chip_read!(read16, u16, BusRead16); + chip_read!(read32, u32, BusRead32); + chip_read!(read64, u64, BusRead64); + fn write8(&self, a: u64, v: u8) -> u32 { + match usize::try_from(a) { + Ok(a) if a < SIZE => { + self.0.st.lock().write(a, v); + BUS_OK + } + _ => BUS_ERR, + } + } +} + +/// The controller window: reads repeat the image, only 64-bit writes reach +/// the command port. +struct Window(Arc); + +macro_rules! window_read { + ($r:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + let st = self.0.st.lock(); + let n = std::mem::size_of::<$t>(); + let v = (0..n).fold(0u64, |v, i| (v << 8) | u64::from(st.read((a as usize + i) % SIZE))); + $out::ok(v as $t) + } + }; +} +impl BusDevice for Window { + window_read!(read8, u8, BusRead8); + window_read!(read16, u16, BusRead16); + window_read!(read32, u32, BusRead32); + window_read!(read64, u64, BusRead64); + fn write64(&self, a: u64, v: u64) -> u32 { + self.0.st.lock().write((a as usize / 8) % SIZE, v as u8); + BUS_OK + } + fn write8(&self, a: u64, v: u8) -> u32 { + dlog!(LogModule::Hub, "node flash: ignored {v:#x} byte write at window offset {a:#x}"); + BUS_OK + } + fn write16(&self, a: u64, v: u16) -> u32 { + dlog!(LogModule::Hub, "node flash: ignored {v:#x} halfword write at window offset {a:#x}"); + BUS_OK + } + fn write32(&self, a: u64, v: u32) -> u32 { + dlog!(LogModule::Hub, "node flash: ignored {v:#x} word write at window offset {a:#x}"); + BUS_OK + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const V: FlashValues = FlashValues { + machine_type: 1, + read_cycles: 15, + write_cycles: 4, + cpu_mode_bits: 0x1211_a785, + cpu_clock_hz: 180_000_000, + }; + + fn payload() -> Vec { + // A payload whose configuration block is the stock shape: magic in + // place, product fields as a stock image holds them. + let mut p = vec![0u8; 0x1000]; + let c = &mut p[0x60..0xb0]; + put32(c, 0x04, 0x1211_a785); + put64(c, 0x08, 0x6970_3237_636f_6e66u64); + put64(c, 0x10, 180_000_000); + put32(c, 0x2c, 8); + put32(c, 0x30, 0); + put32(c, 0x3c, 1); + p + } + + #[test] + fn the_four_product_words_are_patched_and_the_block_sum_kept() { + let p = payload(); + let before = byte_sum(&p[0x60..0xb0]); + let f = initial_image(&p, &V).unwrap(); + let c = &f[0x60..0xb0]; + assert_eq!(u32::from_be_bytes(c[0x2c..0x30].try_into().unwrap()), 15); + assert_eq!(u32::from_be_bytes(c[0x30..0x34].try_into().unwrap()), 1); + assert_eq!(u32::from_be_bytes(c[0x3c..0x40].try_into().unwrap()), 4); + assert_ne!(c[0x34..0x38], [0; 4], "the adjustment word absorbs the change"); + assert_eq!(byte_sum(c), before); + } + + /// A stock block (the shipped PROM's values) patched for the default + /// module moves exactly the cycle counts and machine type, and the + /// adjustment word that keeps the block's sum is 0xf5. + #[test] + fn the_adjustment_byte_for_the_stock_block_is_0xf5() { + let mut p = vec![0u8; 0x1000]; + let c = &mut p[0x60..0xb0]; + put32(c, 0x00, 0xb); + put32(c, 0x04, 0x1211_a785); + put64(c, 0x08, 0x6970_3237_636f_6e66); + put64(c, 0x10, 180_000_000); + put64(c, 0x18, 90_000_000); + put64(c, 0x20, 1_250_000); + put32(c, 0x28, 1); + put32(c, 0x2c, 8); + put32(c, 0x3c, 1); + put32(c, 0x40, 6); + put32(c, 0x44, 156); + let f = initial_image(&p, &V).unwrap(); + assert_eq!(&f[0x94..0x98], &0xf5u32.to_be_bytes()); + assert_eq!(&f[0x8c..0x90], &15u32.to_be_bytes()); + assert_eq!(&f[0x90..0x94], &1u32.to_be_bytes()); + assert_eq!(&f[0x9c..0xa0], &4u32.to_be_bytes()); + } + + #[test] + fn a_payload_without_the_magic_keeps_its_block() { + let mut p = vec![0u8; 0x1000]; + p[0x60 + 0x30 + 3] = 7; + let f = initial_image(&p, &V).unwrap(); + assert_eq!(f[0x60 + 0x33], 7); + } + + #[test] + fn the_template_fills_what_the_payload_leaves() { + let f = initial_image(&[], &V).unwrap(); + assert_eq!(&f[0..8], &[0x10, 0, 0xff, 0xff, 0, 0, 0, 0], "entry zero spins"); + assert_eq!(&f[8..12], &0x03e0_0008u32.to_be_bytes()); + let log = 14 * 0x1_0000; + assert_eq!(&f[log + 0x10..log + 0x14], &0x504c_4f47u32.to_be_bytes()); + assert_eq!(&f[log + 0x14..log + 0x18], &1u32.to_be_bytes()); + assert_eq!(&f[log + 0x18..log + 0x1c], &1u32.to_be_bytes()); + assert_eq!(&f[0x60 + 0x44..0x60 + 0x48], &156u32.to_be_bytes(), "the firmware revision the banner shows"); + assert!(f[log + 0x1c..log + 0x1_0000].iter().all(|b| *b == 0xff)); + assert_eq!(f[0x10_0000 - 1], 0xff); + } + + #[test] + fn a_payload_larger_than_the_part_is_refused() { + assert!(initial_image(&vec![0; 0x10_0001], &V).is_err()); + } + + fn command(f: &Arc, off: usize, v: u8) { + assert_eq!(f.window().write64(off as u64 * 8, u64::from(v)), BUS_OK); + } + + #[test] + fn program_clears_bits_and_erase_restores_a_sector() { + let f = NodeFlash::new(vec![0xff; 0x10_0000]).unwrap(); + let chip = f.chip(); + for (off, v) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0xa0), (0x1234, 0x5a)] { + command(&f, off, v); + } + assert_eq!(chip.read8(0x1234).data, 0x5a); + // Programming cannot set a bit. + for (off, v) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0xa0), (0x1234, 0xa5)] { + command(&f, off, v); + } + assert_eq!(chip.read8(0x1234).data, 0x00); + for (off, v) in [ + (0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0x80), + (0x5555, 0xaa), (0x2aaa, 0x55), (0x1000, 0x30), + ] { + command(&f, off, v); + } + assert_eq!(chip.read8(0x1234).data, 0xff); + } + + #[test] + fn identify_mode_reads_the_codes_until_reset() { + let f = NodeFlash::new(vec![0x11; 0x10_0000]).unwrap(); + for (off, v) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0x90)] { + command(&f, off, v); + } + assert_eq!(f.chip().read8(0).data, 0x01); + assert_eq!(f.chip().read8(1).data, 0xd5); + command(&f, 0, 0xf0); + assert_eq!(f.chip().read8(0).data, 0x11); + } + + #[test] + fn the_window_repeats_the_image_and_takes_commands_on_64_bit_writes_only() { + let mut image = vec![0xff; 0x10_0000]; + image[5] = 0x42; + let f = NodeFlash::new(image).unwrap(); + let w = f.window(); + assert_eq!(w.read8(5).data, 0x42); + assert_eq!(w.read8(5 + 0x10_0000u64 * 3).data, 0x42, "repeats across the window"); + w.write32(0x5555 as u64 * 8, 0xaa); + w.write8(0x5555 as u64 * 8, 0xaa); + for (off, v) in [(0x2aaa, 0x55), (0x5555, 0xa0), (5, 0)] { + command(&f, off, v); + } + assert_eq!(w.read8(5).data, 0x42, "the narrow writes started no command"); + } + + #[test] + fn a_backing_file_keeps_programmed_bytes() { + let dir = std::env::temp_dir().join(format!("iris-node-flash-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("flash.raw"); + let _ = std::fs::remove_file(&path); + let f = NodeFlash::with_file(vec![0xff; 0x10_0000], &path).unwrap(); + for (off, v) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0xa0), (9, 0x12)] { + command(&f, off, v); + } + drop(f); + let again = NodeFlash::with_file(vec![0xff; 0x10_0000], &path).unwrap(); + assert_eq!(again.chip().read8(9).data, 0x12); + std::fs::remove_dir_all(&dir).unwrap(); + } +} From f63fa91d6e79a087c242227c9374302618f56f8a Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 01:53:23 -0700 Subject: [PATCH 28/38] ip27: add the node memory and the processor's uncached views An SN0 node's RAM sits at fixed bank strides, its boot flash at the reset address, and the processor reaches four uncached spaces selected by address bits 58:57. This adds the node's memory on a decode table (with the ppmem window the cache and JIT read), the Hub-special space with its memory and flash windows and the ECC and directory back doors, and the bus that indexes the four spaces with no per-access decision. --- src/ip27.rs | 303 ++++++++++++++++++++++++++++++++++++++++++++++ src/ip27/tests.rs | 88 ++++++++++++++ src/lib.rs | 3 + 3 files changed, 394 insertions(+) create mode 100644 src/ip27.rs create mode 100644 src/ip27/tests.rs diff --git a/src/ip27.rs b/src/ip27.rs new file mode 100644 index 000000000..cda9beb5e --- /dev/null +++ b/src/ip27.rs @@ -0,0 +1,303 @@ +//! The SN0 processor bus: a node's memory, its uncached views and the Hub +//! special windows. +//! +//! The processor drives two attribute bits beside an uncached address (XKPHYS +//! bits 58:57) that pick one of four spaces: 0 Hub special, 1 I/O, 2 memory +//! special and 3 uncached memory. [`Ip27Bus`] keeps one device per space and +//! indexes them with those bits, so an access takes no decision beyond the +//! index. A plain address, as the compatibility segments produce, carries no +//! attribute and lands in view 0, the Hub-special space, as it does on the +//! Fuel. That is an approximation: on the machine a compatibility uncached +//! reference is attribute 3, memory. The two agree wherever this model +//! answers, because the Hub-special space repeats node memory in its first +//! 256 MiB and the boot flash in the next, which is where the reset vector +//! and the PROM's uncached references land. +//! +//! Cached references use the node's [`Physical`] directly: RAM at its +//! node-relative offsets, with the ppmem window the cache and the JIT read. +//! +//! # What is approximated here +//! +//! - Cached accesses ignore the node number in address bits 39:32 and use +//! node-relative offsets, so they are right for a node numbered zero, which +//! is the one-node Origin 200. +//! - The low 128 KiB of the Hub-special memory view is not private to each +//! processor of the pair. +//! - A bank no larger than 128 MiB is one run at its 512 MiB stride; a larger +//! one is four runs at 128 MiB intervals. +//! - Memory that is not fitted reads zero and drops writes, logged under +//! `phys hole on`. +//! - The ECC and directory back doors keep only the low two bytes of each +//! eight-byte entry. An unfitted bank's back door answers a fixed pattern +//! to reads and drops writes. +//! +//! Ported from hw/mips/sgi/sn0.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::node_flash::NodeFlash; +use crate::ip35::SparseWindowMap; +use crate::physical::{Physical, RamBank}; +use crate::ppmem::devmap::SLOT_SHIFT; +use crate::ppmem::MappedMemory; +use crate::traits::*; +use parking_lot::Mutex; +use std::sync::Arc; + +pub(crate) const PA_MASK: u64 = (1u64 << 40) - 1; +/// XKPHYS bits 58:57 ride along with the physical address into the bus. +pub(crate) const CPU_PHYS_MASK: u64 = PA_MASK | (3u64 << 57); + +/// The boot flash's reset view, and the node memory a bank stride spans. +pub const RESET_VIEW: u64 = 0x1fc0_0000; +pub const BANK_STRIDE: u64 = 512 << 20; +pub const SLOT_STRIDE: u64 = 128 << 20; +/// Banks the Origin 200's board wires. +pub const BANKS: usize = 4; + +const MIB: u64 = 1 << 20; + +/// Where bank `bank` of `mb` MiB lies: `(node offset, length)` runs, in the +/// order they take the bank's storage. +pub fn bank_runs(bank: usize, mb: u32) -> Vec<(u64, u64)> { + let size = u64::from(mb) * MIB; + if size == 0 { + return Vec::new(); + } + let base = bank as u64 * BANK_STRIDE; + if size <= SLOT_STRIDE { + vec![(base, size)] + } else { + let run = size / 4; + (0..4).map(|i| (base + i * SLOT_STRIDE, run)).collect() + } +} + +/// The node's memory: the banks on a decode table, placed at their stride. +pub struct NodeMemory { + phys: Arc, + banks_mb: [u32; BANKS], +} + +impl NodeMemory { + pub fn new(banks_mb: [u32; BANKS]) -> Result { + let mut banks = Vec::new(); + for (i, &mb) in banks_mb.iter().enumerate() { + if mb != 0 { + banks.push(RamBank::try_new(mb as usize).map_err(|e| format!("bank {i}: {e}"))?); + } + } + let phys = Physical::new(banks); + phys.set_default(Arc::new(Absent), "unfitted memory"); + let mut index = 0; + for (bank, &mb) in banks_mb.iter().enumerate() { + if mb == 0 { + continue; + } + let mut src = 0u64; + for (at, len) in bank_runs(bank, mb) { + phys.fill_slots(at >> SLOT_SHIFT, len >> SLOT_SHIFT, phys.bank_view(index, at.wrapping_sub(src))); + if let Some(sp) = phys.ppmem_space() { + if at + len <= sp.window_size() { + sp.map_range(index, src, at, len).map_err(|e| format!("bank {bank}: window map: {e}"))?; + } + } + src += len; + } + index += 1; + } + Ok(Self { phys, banks_mb }) + } + + pub fn physical(&self) -> &Arc { + &self.phys + } + + pub fn banks_mb(&self) -> [u32; BANKS] { + self.banks_mb + } + + /// Place the boot flash's reset view in node memory. + pub fn map_reset_view(&self, flash: &Arc) { + crate::ip35::map_device(&self.phys, RESET_VIEW, crate::dev::node_flash::SIZE as u64, flash.chip(), "boot flash"); + } + + /// The ranges the CPU model reports as memory. + pub fn memory_map(&self) -> crate::memory_map::MemoryMap { + use crate::memory_map::{MemKind, MemRange, MemoryMap}; + const NAMES: [&str; 16] = [ + "bank 0 run 0", "bank 0 run 1", "bank 0 run 2", "bank 0 run 3", + "bank 1 run 0", "bank 1 run 1", "bank 1 run 2", "bank 1 run 3", + "bank 2 run 0", "bank 2 run 1", "bank 2 run 2", "bank 2 run 3", + "bank 3 run 0", "bank 3 run 1", "bank 3 run 2", "bank 3 run 3", + ]; + let mut ranges = Vec::new(); + for (bank, &mb) in self.banks_mb.iter().enumerate() { + for (run, (at, len)) in bank_runs(bank, mb).into_iter().enumerate() { + ranges.push(MemRange::new(NAMES[bank * 4 + run], MemKind::Ram, at, at + len)); + } + } + ranges.push(MemRange::new( + "boot flash", + MemKind::Rom, + RESET_VIEW, + RESET_VIEW + crate::dev::node_flash::SIZE as u64, + )); + MemoryMap::new(ranges) + } + + /// The back door onto the ECC bytes or the directory entries. + pub fn back_door(&self) -> Arc { + Backdoor::new(self.banks_mb) + } +} + +/// Memory that is not fitted: reads zero, drops writes. `phys hole on` logs it. +struct Absent; +impl BusDevice for Absent { + fn read8(&self, _: u64) -> BusRead8 { BusRead8::ok(0) } + fn read16(&self, _: u64) -> BusRead16 { BusRead16::ok(0) } + fn read32(&self, _: u64) -> BusRead32 { BusRead32::ok(0) } + fn read64(&self, _: u64) -> BusRead64 { BusRead64::ok(0) } + fn write8(&self, _: u64, _: u8) -> u32 { BUS_OK } + fn write16(&self, _: u64, _: u16) -> u32 { BUS_OK } + fn write32(&self, _: u64, _: u32) -> u32 { BUS_OK } + fn write64(&self, _: u64, _: u64) -> u32 { BUS_OK } +} + +const BACKDOOR_ENTRY: usize = 8; +const BACKDOOR_LIVE: usize = 2; +const BACKDOOR_BANK_STRIDE: u64 = 128 * MIB; +/// What an unfitted bank's back door reads: firmware's presence probe calls a +/// bank absent when neither a pattern nor zero survives a write. +const BACKDOOR_ABSENT: u64 = 0xa5a5_a5a5_a5a5_a5a5; + +/// One back door window (1 GiB, eight banks of 128 MiB): per fitted bank, a +/// quarter of the bank's size of storage folded across the bank's span. +pub struct Backdoor { + spans: [u64; 8], + offsets: [u64; 8], + data: Mutex>, +} + +impl Backdoor { + fn new(banks_mb: [u32; BANKS]) -> Arc { + let mut spans = [0u64; 8]; + let mut offsets = [0u64; 8]; + let mut total = 0; + for (i, &mb) in banks_mb.iter().enumerate() { + spans[i] = u64::from(mb) * MIB / 4; + offsets[i] = total; + total += spans[i]; + } + Arc::new(Self { spans, offsets, data: Mutex::new(vec![0; total as usize]) }) + } + + /// The storage index of byte `a`, or None for a bank that is not fitted. + fn index(&self, a: u64) -> Option { + let bank = (a / BACKDOOR_BANK_STRIDE) as usize; + let span = *self.spans.get(bank)?; + (span != 0).then(|| (self.offsets[bank] + (a % BACKDOOR_BANK_STRIDE) % span) as usize) + } + + fn read(&self, a: u64, n: usize) -> u64 { + let Some(at) = self.index(a) else { + return BACKDOOR_ABSENT & (u64::MAX >> (64 - 8 * n as u32)); + }; + let data = self.data.lock(); + (0..n).fold(0u64, |v, i| (v << 8) | u64::from(data[at + i])) + } + + fn write(&self, a: u64, n: usize, v: u64) { + let Some(at) = self.index(a) else { return }; + let mut data = self.data.lock(); + for i in 0..n { + // Only the low two bytes of an eight-byte entry are stored. + if (a as usize + i) % BACKDOOR_ENTRY >= BACKDOOR_ENTRY - BACKDOOR_LIVE { + data[at + i] = (v >> (8 * (n - 1 - i))) as u8; + } + } + } +} + +macro_rules! backdoor_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + $out::ok(self.read(a, std::mem::size_of::<$t>()) as $t) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.write(a, std::mem::size_of::<$t>(), u64::from(v)); + BUS_OK + } + }; +} +impl BusDevice for Backdoor { + backdoor_access!(read8, write8, u8, BusRead8); + backdoor_access!(read16, write16, u16, BusRead16); + backdoor_access!(read32, write32, u32, BusRead32); + backdoor_access!(read64, write64, u64, BusRead64); +} + +/// The Hub-special space of a node: its own memory and boot flash and the +/// named node's, and the two back doors. +pub fn hub_special(memory: Arc, flash: &Arc, ecc: Arc, directory: Arc) -> Arc { + const WINDOW: u64 = 256 * MIB; + const BACK_DOOR: u64 = 1 << 30; + let mut map = SparseWindowMap::default(); + for base in [0, 0x2000_0000] { + map.map(base, WINDOW, memory.clone()); + } + for base in [0x1000_0000, 0x3000_0000] { + map.map(base, WINDOW, flash.window()); + } + map.map(0x8000_0000, BACK_DOOR, ecc); + map.map(0xc000_0000, BACK_DOOR, directory); + Arc::new(map) +} + +/// The processor's uncached bus: one device per attribute, indexed by +/// address bits 58:57. See the module notes for plain addresses. +pub struct Ip27Bus { + views: [Arc; 4], +} + +impl Ip27Bus { + /// `special`, `io`, `fetchop` and `memory` are the devices attributes 0 + /// to 3 select. + pub fn new(special: Arc, io: Arc, fetchop: Arc, memory: Arc) -> Self { + Self { views: [special, io, fetchop, memory] } + } + + #[inline(always)] + fn target(&self, a: u64) -> (&dyn BusDevice, u64) { + (&*self.views[((a >> 57) & 3) as usize], a & PA_MASK) + } +} + +macro_rules! bus_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + let (v, pa) = self.target(a); + v.$r(pa) + } + fn $w(&self, a: u64, d: $t) -> u32 { + let (v, pa) = self.target(a); + v.$w(pa, d) + } + }; +} +impl BusDevice for Ip27Bus { + fn write64_masked(&self, a: u64, val: u64, mask: u64) -> u32 { + let (v, pa) = self.target(a); + v.write64_masked(pa, val, mask) + } + fn write_narrow_from(&self, a: u64, size: usize, gpr: u64) -> u32 { + let (v, pa) = self.target(a); + v.write_narrow_from(pa, size, gpr) + } + bus_access!(read8, write8, u8, BusRead8); + bus_access!(read16, write16, u16, BusRead16); + bus_access!(read32, write32, u32, BusRead32); + bus_access!(read64, write64, u64, BusRead64); +} + +#[cfg(test)] +mod tests; diff --git a/src/ip27/tests.rs b/src/ip27/tests.rs new file mode 100644 index 000000000..7015539ce --- /dev/null +++ b/src/ip27/tests.rs @@ -0,0 +1,88 @@ +use super::*; +use crate::dev::node_flash::{self, NodeFlash}; + +#[test] +fn a_small_bank_is_one_run_and_a_large_one_four() { + assert_eq!(bank_runs(0, 64), vec![(0, 0x400_0000)]); + assert_eq!(bank_runs(1, 128), vec![(0x2000_0000, 0x800_0000)]); + assert_eq!( + bank_runs(2, 256), + (0..4).map(|i| (0x4000_0000 + i * 0x800_0000, 0x400_0000)).collect::>() + ); + assert!(bank_runs(3, 0).is_empty()); +} + +#[test] +fn fitted_memory_answers_where_the_runs_are_and_nowhere_else() { + let mem = NodeMemory::new([64, 0, 128, 0]).unwrap(); + let phys = mem.physical(); + assert_eq!(phys.write32(0x1000, 0xdead_beef), BUS_OK); + assert_eq!(phys.read32(0x1000).data, 0xdead_beef); + assert_eq!(phys.write32(0x4000_0008, 7), BUS_OK); + assert_eq!(phys.read32(0x4000_0008).data, 7); + // Past the 64 MiB bank and in the empty bank 1: zero, writes dropped. + assert_eq!(phys.write32(0x400_0000, 1), BUS_OK); + assert_eq!(phys.read32(0x400_0000).data, 0); + assert_eq!(phys.read32(0x2000_0000).data, 0); +} + +#[test] +fn the_reset_view_holds_the_flash_where_bank_zero_does_not_reach() { + let mem = NodeMemory::new([64, 0, 0, 0]).unwrap(); + let mut image = vec![0xff; 0x10_0000]; + image[0] = 0x12; + let flash = NodeFlash::new(image).unwrap(); + mem.map_reset_view(&flash); + assert_eq!(mem.physical().read8(0x1fc0_0000).data, 0x12); + let map = mem.memory_map(); + assert!(map.contains(0)); + assert!(map.contains(0x1fc0_0000)); + assert!(!map.contains(0x400_0000)); +} + +#[test] +fn the_bus_picks_its_space_from_the_attribute_bits() { + struct Tag(u8); + impl BusDevice for Tag { + fn read8(&self, a: u64) -> BusRead8 { + BusRead8::ok(self.0 ^ (a as u8)) + } + } + let bus = Ip27Bus::new(Arc::new(Tag(0x10)), Arc::new(Tag(0x20)), Arc::new(Tag(0x30)), Arc::new(Tag(0x40))); + for (attr, tag) in [(0u64, 0x10u8), (1, 0x20), (2, 0x30), (3, 0x40)] { + assert_eq!(bus.read8((attr << 57) | 0x1_0000_0001).data, tag ^ 1, "attribute {attr}"); + } + // A plain compatibility address has no attribute and reaches space 0. + assert_eq!(bus.read8(0x1fc0_0001).data, 0x10 ^ 1); +} + +#[test] +fn a_back_door_stores_two_bytes_of_each_entry_and_folds_a_bank() { + let door = Backdoor::new([64, 0, 0, 0]); + door.write64(0, 0x1122_3344_5566_7788); + assert_eq!(door.read64(0).data, 0x0000_0000_0000_7788); + // 64 MiB of memory keeps 16 MiB of storage, repeated across the stride. + assert_eq!(door.read64(0x100_0000).data, 0x7788); + // A bank that is not fitted reads the pattern and drops writes. + door.write64(0x800_0000, 5); + assert_eq!(door.read64(0x800_0000).data, 0xa5a5_a5a5_a5a5_a5a5); + assert_eq!(door.read8(0x800_0000).data, 0xa5); +} + +#[test] +fn hub_special_repeats_memory_and_the_flash_at_the_named_node_offsets_too() { + let mem = NodeMemory::new([64, 0, 0, 0]).unwrap(); + mem.physical().write32(0x100, 0xcafe_f00d); + let mut image = vec![0xff; 0x10_0000]; + image[3] = 0x33; + let flash = NodeFlash::new(image).unwrap(); + let special = hub_special(mem.physical().clone(), &flash, mem.back_door(), mem.back_door()); + for base in [0u64, 0x2000_0000] { + assert_eq!(special.read32(base + 0x100).data, 0xcafe_f00d); + } + for base in [0x1000_0000u64, 0x3000_0000] { + assert_eq!(special.read8(base + 3).data, 0x33); + assert_eq!(special.read8(base + 3 + 0x10_0000u64).data, 0x33, "the image repeats"); + } + assert_eq!(special.read8(0x4000_0000).status, BUS_ERR, "nothing between the windows"); +} diff --git a/src/lib.rs b/src/lib.rs index 02f2c4645..c0e305362 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,9 @@ pub mod ip32_machine; /// The SGI Octane (IP30): its bus and machine -- see `docs/octane-ip30-address-map.md`. pub mod ip30; pub mod ip30_machine; +/// The SN0 node: its processor bus, memory and uncached views. The Origin 200 and +/// the Origin 2000 and Onyx2 desksides are built from it. +pub mod ip27; /// Fuel has an IP34 board and uses the IP35 software platform and processor views. pub mod ip35; pub mod machine_state; From 7f73fc33772ecb129f04db0a7257bcad0b593e38 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 02:32:49 -0700 Subject: [PATCH 29/38] dp83840: add the DP83840A transceiver model The Origin 200 System Board's Ethernet part is a DP83840A, which the PROM's transceiver test checks register by register: identity, the negotiation and PCS reset values, and the three loopback switches. The existing DP83843 model has another identity and other reset values, so the test failed against it. Out of reset the link is down; the first status read arms a two-second settle, and a control write links at once because there is no cable to negotiate against. --- src/dev/dp83840.rs | 340 +++++++++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 2 files changed, 341 insertions(+) create mode 100644 src/dev/dp83840.rs diff --git a/src/dev/dp83840.rs b/src/dev/dp83840.rs new file mode 100644 index 000000000..db880147c --- /dev/null +++ b/src/dev/dp83840.rs @@ -0,0 +1,340 @@ +//! The DP83840A MII model: the Origin 200 System Board's Ethernet +//! transceiver, reached through the IOC3's management registers. +//! +//! Out of reset the link is down. The first status read arms a settle, after +//! which negotiation reads complete; a write to the control register links at +//! once, since there is no cable to negotiate against. The DP83843 has its own +//! model, [`crate::dev::dp83843`]. +//! +//! # What is approximated here +//! +//! - The synthetic partner offers every mode and the settle is two seconds, +//! inside the part's 1.5 to 2.3 s range, long enough that SGI's diagnostic +//! sweep still finds the link settling. +//! - Registers 0x17, 0x18 and 0x1c are storage with their reset values; the +//! pin-latched bits of 0x18 are unobserved and modelled as normal +//! operation. Every other register reads zero and is logged. +//! +//! Ported from hw/net/dp83840.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +use crate::dev::ioc3_eth::MiiPhy; +use crate::snapshot::{field_bool, field_u16, field_u64, field_u8, StateTable}; + +pub mod reg { + pub const CONTROL: u8 = 0; + pub const STATUS: u8 = 1; + pub const ID1: u8 = 2; + pub const ID2: u8 = 3; + pub const ADVERTISE: u8 = 4; + pub const PARTNER: u8 = 5; + pub const EXPANSION: u8 = 6; + pub const PCS_CONFIG: u8 = 0x17; + pub const LOOPBACK_BYPASS: u8 = 0x18; + pub const ADDRESS_STATUS: u8 = 0x19; + pub const TENBT_CONFIG: u8 = 0x1c; +} + +const ID1: u16 = 0x2000; +const ID2: u16 = 0x5c01; +const CONTROL_RESET: u16 = 0x3100; +const PCS_RESET: u16 = 0x8040; +const TENBT_RESET: u16 = 0x80b9; +const LOOPBACK_RESET: u16 = 0x8020; +/// Both ends offer every mode; what a real machine negotiates is unrecorded. +const ADVERTISE_RESET: u16 = 0x1e1; +const SETTLE: Duration = Duration::from_secs(2); + +const BMCR_RESET: u16 = 1 << 15; +const BMCR_LOOPBACK: u16 = 1 << 14; +const BMCR_AUTONEG: u16 = 1 << 12; +const BMCR_ISOLATE: u16 = 1 << 10; +const BMCR_RESTART: u16 = 1 << 9; +const BMSR_CAPS: u16 = 0x7809; // 100TX/10T both duplexes, autoneg and ext-cap +const BMSR_LINK: u16 = 1 << 2; +const BMSR_NEGOTIATED: u16 = 1 << 5; +const LBREMR_LOOPBACK: u16 = 0x0300 | 0x0800; + +pub struct Dp83840 { + addr: u8, + control: u16, + advertise: u16, + pcs_config: u16, + loopback_bypass: u16, + tenbt_config: u16, + negotiated: bool, + armed: bool, + deadline: Option, + forced: bool, + backend_up: bool, +} + +impl Dp83840 { + pub fn new(addr: u8) -> Self { + let mut p = Self { + addr: addr & 31, + control: 0, + advertise: 0, + pcs_config: 0, + loopback_bypass: 0, + tenbt_config: 0, + negotiated: false, + armed: false, + deadline: None, + forced: false, + backend_up: true, + }; + p.reset(); + p + } + + pub fn reset(&mut self) { + self.control = CONTROL_RESET; + self.advertise = ADVERTISE_RESET; + self.pcs_config = PCS_RESET; + self.loopback_bypass = LOOPBACK_RESET; + self.tenbt_config = TENBT_RESET; + self.negotiated = false; + self.armed = false; + self.deadline = None; + self.forced = false; + } + + fn link(&self) -> bool { + self.backend_up && (self.negotiated || self.forced) + } + + fn status(&self) -> u16 { + let mut v = BMSR_CAPS; + if self.link() { + v |= BMSR_LINK; + if self.negotiated { + v |= BMSR_NEGOTIATED; + } + } + v + } + + fn unimplemented(&self, r: u8, write: Option) { + match write { + Some(v) => crate::dlog!(crate::devlog::LogModule::Ioc, "dp83840: write of {v:#06x} to unimplemented register {r:#x}"), + None => crate::dlog!(crate::devlog::LogModule::Ioc, "dp83840: read of unimplemented register {r:#x}"), + } + } +} + +impl MiiPhy for Dp83840 { + fn address(&self) -> u8 { + self.addr + } + + fn deadline(&self) -> Option { + self.deadline + } + + /// The model does not gate frames on the link. + fn wire_enabled(&self) -> bool { + true + } + + /// Three switches select loopback; the diagnostic uses more than one. + fn in_loopback(&self) -> bool { + self.control & BMCR_LOOPBACK != 0 || self.loopback_bypass & LBREMR_LOOPBACK != 0 + } + + fn sync(&mut self, now: Instant) { + if self.deadline.is_some_and(|d| now >= d) { + self.deadline = None; + self.negotiated = true; + } + } + + fn read(&mut self, register: u8, now: Instant) -> u16 { + self.sync(now); + match register { + reg::CONTROL => self.control, + reg::STATUS => { + // The settle arms on the first status read: armed at reset it + // would elapse before SGI's diagnostic probes, which must find + // the link down. + if !self.armed && !self.negotiated && self.control & BMCR_AUTONEG != 0 { + self.armed = true; + self.deadline = Some(now + SETTLE); + } + self.status() + } + reg::ID1 => ID1, + reg::ID2 => ID2, + reg::ADVERTISE => self.advertise, + reg::PARTNER => { + if self.negotiated { + ADVERTISE_RESET + } else { + 0 + } + } + reg::EXPANSION => u16::from(self.negotiated), + reg::PCS_CONFIG => self.pcs_config, + reg::LOOPBACK_BYPASS => self.loopback_bypass, + reg::TENBT_CONFIG => self.tenbt_config, + reg::ADDRESS_STATUS => u16::from(self.addr), + r => { + // Zero: all ones reads as an absent transceiver. + self.unimplemented(r, None); + 0 + } + } + } + + fn write(&mut self, register: u8, value: u16, now: Instant) { + self.sync(now); + match register { + reg::CONTROL => { + if value & BMCR_RESET != 0 { + self.reset(); + return; + } + // No cable to negotiate against: a configuring write links at + // once, unless it isolates the part. + self.forced = false; + self.deadline = None; + self.armed = true; + self.negotiated = value & BMCR_ISOLATE == 0; + self.control = value & !(BMCR_RESET | BMCR_RESTART); + } + reg::ADVERTISE => self.advertise = value, + reg::PCS_CONFIG => self.pcs_config = value, + reg::LOOPBACK_BYPASS => self.loopback_bypass = value, + reg::TENBT_CONFIG => self.tenbt_config = value, + reg::STATUS | reg::ID1 | reg::ID2 | reg::PARTNER | reg::EXPANSION | reg::ADDRESS_STATUS => { + crate::dlog!(crate::devlog::LogModule::Ioc, "dp83840: write of {value:#06x} to read-only register {register:#x}"); + } + r => self.unimplemented(r, Some(value)), + } + } + + fn set_backend_link(&mut self, up: bool, now: Instant) { + self.sync(now); + self.backend_up = up; + } + + fn power_on(&mut self) { + self.reset(); + } + + fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u8("addr", self.addr) + .u16("control", self.control) + .u16("advertise", self.advertise) + .u16("pcs_config", self.pcs_config) + .u16("loopback_bypass", self.loopback_bypass) + .u16("tenbt_config", self.tenbt_config) + .bool("negotiated", self.negotiated) + .bool("armed", self.armed) + .u64("deadline", self.deadline.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1)) + .bool("forced", self.forced) + .bool("backend_up", self.backend_up) + .build() + } + + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { + let left = field_u64(v, "deadline")?; + *self = Self { + addr: field_u8(v, "addr")?, + control: field_u16(v, "control")?, + advertise: field_u16(v, "advertise")?, + pcs_config: field_u16(v, "pcs_config")?, + loopback_bypass: field_u16(v, "loopback_bypass")?, + tenbt_config: field_u16(v, "tenbt_config")?, + negotiated: field_bool(v, "negotiated")?, + armed: field_bool(v, "armed")?, + deadline: (left != 0).then(|| now + Duration::from_nanos(left - 1)), + forced: field_bool(v, "forced")?, + backend_up: field_bool(v, "backend_up")?, + }; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sgi-ioc3-eth-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + + #[test] + fn the_reset_values_are_the_ones_sgis_diagnostic_compares() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + assert_eq!(p.address(), 31); + assert_eq!(p.read(reg::CONTROL, now), 0x3100); + assert_eq!(p.read(reg::ID1, now), 0x2000); + assert_eq!(p.read(reg::ID2, now) & 0xfffe, 0x5c00); + assert_eq!(p.read(reg::EXPANSION, now), 0); + assert_eq!(p.read(reg::PCS_CONFIG, now), 0x8040); + assert_eq!(p.read(reg::TENBT_CONFIG, now) & 0x3d, 0x39); + assert_eq!(p.read(reg::ADDRESS_STATUS, now), 31); + } + + #[test] + fn the_link_is_down_until_the_settle_after_the_first_status_read() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + assert_eq!(p.read(reg::STATUS, now) & 0x4, 0); + assert_eq!(p.read(reg::STATUS, now + Duration::from_millis(1999)) & 0x4, 0); + let up = p.read(reg::STATUS, now + Duration::from_secs(2)); + assert_eq!(up & (0x24), 0x24); + p.set_backend_link(false, now + Duration::from_secs(3)); + assert_eq!(p.read(reg::STATUS, now + Duration::from_secs(3)) & 0x4, 0); + } + + #[test] + fn a_control_write_links_at_once_and_isolate_does_not() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + p.write(reg::CONTROL, 0x1000, now); + assert_eq!(p.read(reg::STATUS, now) & 0x4, 0x4); + p.write(reg::CONTROL, 0x1400, now); + assert_eq!(p.read(reg::STATUS, now) & 0x4, 0); + p.write(reg::CONTROL, 0x8000, now); + assert_eq!(p.read(reg::CONTROL, now), 0x3100); + } + + #[test] + fn any_of_three_switches_selects_loopback() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + assert!(!p.in_loopback()); + p.write(reg::CONTROL, 0x5000, now); + assert!(p.in_loopback()); + p.write(reg::CONTROL, 0x1000, now); + p.write(reg::LOOPBACK_BYPASS, 0x8020 | 0x0100, now); + assert!(p.in_loopback()); + p.write(reg::LOOPBACK_BYPASS, 0x8020 | 0x0800, now); + assert!(p.in_loopback()); + p.write(reg::LOOPBACK_BYPASS, 0x8020, now); + assert!(!p.in_loopback()); + } + + #[test] + fn read_only_and_unknown_registers_keep_their_value_and_read_zero() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + p.write(reg::ID2, 0, now); + assert_eq!(p.read(reg::ID2, now), 0x5c01); + p.write(0x12, 5, now); + assert_eq!(p.read(0x12, now), 0); + } + + #[test] + fn a_snapshot_keeps_the_settle_it_had_left() { + let now = Instant::now(); + let mut p = Dp83840::new(31); + p.read(reg::STATUS, now); + let saved = p.save_state(now + Duration::from_millis(500)); + let mut q = Dp83840::new(31); + q.load_state(&saved, now).unwrap(); + assert_eq!(q.deadline(), Some(now + Duration::from_millis(1500))); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index ad774a4cd..99eba9d72 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -71,6 +71,7 @@ pub mod hub_io; pub mod hub_md; pub mod hub_ni; pub mod hub_pi; +pub mod dp83840; pub mod dp83843; pub mod ics1890; pub mod timekeeper; From 37f13da820ffffb0d42a24d507548026169471ec Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 02:12:04 -0700 Subject: [PATCH 30/38] ioc3_eth: add the Origin 200 transceiver address The System Board's Ethernet transceiver answers at management address 31, not at the address the Octane and Fuel boards use, so the PROM's register probe read an absent part. Ioc3Eth::new_origin200 puts the DP83840A there. --- src/dev/ioc3_eth.rs | 7 +++++++ src/dev/ioc3_eth/tests.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/dev/ioc3_eth.rs b/src/dev/ioc3_eth.rs index 4b9e9fa63..a319e8912 100644 --- a/src/dev/ioc3_eth.rs +++ b/src/dev/ioc3_eth.rs @@ -161,6 +161,8 @@ const FRAME_MAX: usize = 2048; const NREGS: usize = ((MII_DATA_W + 4 - MAC_CONTROL) / 4) as usize; /// The PHY's management address on the Octane's IOC3. pub const PHY_ADDR: u8 = 1; +/// The Origin 200 System Board's transceiver address. +pub const ORIGIN200_PHY_ADDR: u8 = 31; /// How long the synthetic link partner takes to complete negotiation. pub const NEGOTIATION_DELAY: Duration = Duration::from_millis(100); @@ -764,6 +766,11 @@ impl Ioc3Eth { Self::with_phy(mac, Box::new(crate::dev::dp83843::Dp83843::new(PHY_ADDR)), "ioc3-eth") } + /// The Origin 200 System Board's DP83840A, at management address 31. + pub fn new_origin200(mac: [u8; 6]) -> Arc { + Self::with_phy(mac, Box::new(crate::dev::dp83840::Dp83840::new(ORIGIN200_PHY_ADDR)), "ioc3-eth") + } + fn with_phy(mac: [u8; 6], phy: Box, thread_prefix: &'static str) -> Arc { Arc::new_cyclic(|me| Self { mac, diff --git a/src/dev/ioc3_eth/tests.rs b/src/dev/ioc3_eth/tests.rs index ec5a9d167..8a835fa4d 100644 --- a/src/dev/ioc3_eth/tests.rs +++ b/src/dev/ioc3_eth/tests.rs @@ -178,6 +178,19 @@ fn software_reset_restores_defaults_and_self_clears() { assert_eq!(t.mii(1, 0x13), 0x4001); } +/// The Origin 200 System Board's transceiver is a DP83840A at management +/// address 31: its identity answers there and an address the Octane and Fuel +/// use reads as an absent part. +#[test] +fn the_origin_200_transceiver_answers_at_address_31_only() { + let mut t = rig(); + t.eth = Ioc3Eth::new_origin200(STATION); + assert_eq!(t.mii(31, 2), 0x2000); + assert_eq!(t.mii(31, 3), 0x5c01); + assert_eq!(t.mii(1, 3), 0xffff); + assert_eq!(t.mii(1, 0), 0xffff); +} + #[test] fn wrong_address_writes_do_not_reach_the_phy() { let t = rig(); From 9958b33da828cc3e31d14fea2e1919cbfaeddf05 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 02:16:35 -0700 Subject: [PATCH 31/38] ip27: add the Origin 200 machine One Hub node with an R10000 or R12000, the node flash and memory, the module controller on the Hub's I2C, and the IP29 System Board's I/O: a Bridge straight on the Hub's XIO port with two ISP1040s, an IOC3 and the board's one-wire record. Selected with profile = "origin200_ip29". Ip27Machine is generic over an Ip27Board so another SN0 product can reuse the node; the Origin 200 is the one board. The Hub's CPU enable is set at power-on because the stock PROM never stores it. The module controller's configuration store comes from the controller's own constructor through one function the machine does not look inside. The Bridge reaches the Hub through its inbound face and interrupt target, the smallest adapter it needs until the Bridge learns the Hub as a master widget. --- src/config.rs | 79 +++- src/cpu/mips_cache_shadow.rs | 7 + src/cpu/parts.rs | 6 +- src/ip27_machine.rs | 836 +++++++++++++++++++++++++++++++++++ src/ip27_machine/tests.rs | 239 ++++++++++ src/lib.rs | 1 + src/machine.rs | 8 +- src/workstation.rs | 1 + 8 files changed, 1164 insertions(+), 13 deletions(-) create mode 100644 src/ip27_machine.rs create mode 100644 src/ip27_machine/tests.rs diff --git a/src/config.rs b/src/config.rs index e22bab8c9..78da262be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -614,6 +614,11 @@ pub enum MachineProfile { /// R10000. Configured by the shared keys and the `[octane]` section; see /// `src/ip30_machine.rs` and `docs/octane-ip30-address-map.md`. OctaneIp30, + /// SGI Origin 200: the IP29 System Board on the IP27 software platform, + /// one Hub node with an R10000 or R12000, the Bridge, two ISP1040s and an + /// IOC3. Configured by the shared keys and `[origin200]`; see + /// `src/ip27_machine.rs`. + Origin200Ip29, } impl MachineProfile { @@ -634,6 +639,7 @@ impl MachineProfile { pub fn bank_sizes(self) -> &'static [u32] { match self { Self::OctaneIp30 => &[0, 64, 128, 256, 512, 1024, 2048], + Self::Origin200Ip29 => &[0, 64, 128, 256, 512], Self::O2Ip32 => &[0, 32, 64, 128, 256], Self::Indigo2Ip28 => MC_BANK_SIZES, _ => &[0, 8, 16, 32, 64, 128], @@ -652,6 +658,7 @@ impl MachineProfile { pub fn prom_path(self, prom: &str) -> String { match self { Self::OctaneIp30 => prom_file(prom, crate::ip30_machine::OCTANE_PROM_FILE), + Self::Origin200Ip29 => prom_file(prom, crate::ip27_machine::ORIGIN200_PROM_FILE), Self::O2Ip32 => prom_file(prom, crate::ip32_machine::O2_PROM_FILE), _ => prom.to_string(), } @@ -659,12 +666,13 @@ impl MachineProfile { /// Every profile, in the order `workstation::build` lists them. `ALL` is /// only the ones the GUI offers. - pub const EVERY: [Self; 5] = [ + pub const EVERY: [Self; 6] = [ Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28, Self::O2Ip32, Self::OctaneIp30, + Self::Origin200Ip29, ]; /// The name `[machine] profile` and `--machine` give this profile. @@ -675,6 +683,7 @@ impl MachineProfile { Self::Indigo2Ip28 => "indigo2_ip28", Self::O2Ip32 => "o2_ip32", Self::OctaneIp30 => "octane_ip30", + Self::Origin200Ip29 => "origin200_ip29", } } @@ -729,6 +738,7 @@ impl MachineProfile { Self::Indigo2Ip28 => "SGI Indigo2 IMPACT (IP28)", Self::O2Ip32 => "SGI O2 (IP32)", Self::OctaneIp30 => "SGI Octane (IP30)", + Self::Origin200Ip29 => "SGI Origin 200 (IP29)", } } @@ -738,7 +748,7 @@ impl MachineProfile { /// connector), one on the Indy. pub fn scsi_controllers(self) -> u8 { match self { - Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 => 2, + Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 => 2, _ => 1, } } @@ -750,7 +760,7 @@ impl MachineProfile { /// purpose. pub fn max_scsi_id(self) -> u8 { match self { - Self::OctaneIp30 => 15, + Self::OctaneIp30 | Self::Origin200Ip29 => 15, _ => 7, } } @@ -770,6 +780,7 @@ impl MachineProfile { pub fn cpus(self) -> &'static [&'static Part] { match self { Self::OctaneIp30 => crate::ip30_machine::CPUS, + Self::Origin200Ip29 => crate::ip27_machine::CPUS, Self::O2Ip32 => crate::ip32_machine::CPUS, Self::Indigo2Ip28 => crate::machine::IP28_CPUS, Self::IndyIp24 | Self::Indigo2Ip22 => crate::machine::CPUS, @@ -884,6 +895,7 @@ impl GraphicsBoard { pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, + MachineProfile::Origin200Ip29 => self == Self::None, MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -893,6 +905,7 @@ impl GraphicsBoard { pub fn default_for(profile: MachineProfile) -> Self { match profile { MachineProfile::OctaneIp30 => Self::SolidImpact, + MachineProfile::Origin200Ip29 => Self::None, _ => Self::Newport, } } @@ -1218,6 +1231,25 @@ impl OctaneSection { } } +/// `[origin200]`: the Origin 200's persistent stores. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Origin200Section { + /// Persistent 1 MiB node boot flash; absent means volatile flash. + #[serde(default)] + pub flash: Option, + /// Persistent 2048-byte module controller store; absent means volatile. + #[serde(default)] + pub controller_store: Option, +} + +impl Origin200Section { + /// Left out of a written config when nothing in it was set. + fn is_default(&self) -> bool { + self.flash.is_none() && self.controller_store.is_none() + } +} + /// An entry of the deprecated `o2.disks` list; see [`O2Section::disks`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -1915,6 +1947,9 @@ pub struct MachineConfig { /// The Octane (`[octane]` section), used when `[machine] profile = "octane_ip30"`. #[serde(default, skip_serializing_if = "OctaneSection::is_default")] pub octane: OctaneSection, + /// Persistent flash and controller store for the Origin 200. + #[serde(default, skip_serializing_if = "Origin200Section::is_default")] + pub origin200: Origin200Section, /// Host performance tuning (`[perf]` section). #[serde(default)] @@ -2144,6 +2179,7 @@ impl Default for MachineConfig { jitv2: Jitv2Config::default(), o2: O2Section::default(), octane: OctaneSection::default(), + origin200: Origin200Section::default(), perf: PerfConfig::default(), clock: ClockConfig::default(), rtc_offset: RtcOffset::default(), @@ -2193,6 +2229,7 @@ impl MachineConfig { } // The Octane has a PCI bridge with a free slot. MachineProfile::OctaneIp30 => Ok(()), + MachineProfile::Origin200Ip29 => Err("[virtio_serial] is not wired on the Origin 200".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)" @@ -2390,6 +2427,8 @@ impl MachineConfig { if !board.supports(self.machine.profile) { return Err(if self.machine.profile == MachineProfile::OctaneIp30 { format!("graphics.board \"{name}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)") + } else if self.machine.profile == MachineProfile::Origin200Ip29 { + format!("graphics.board \"{name}\" is not modelled on the Origin 200 (valid: none)") } else { format!("graphics.board \"{name}\" is only valid on the Octane") }); @@ -2431,7 +2470,7 @@ impl MachineConfig { "scsi.{} and scsi.{} both name {}", other, key, scsi_label(controller, id) )); } - if dev.is_daynaport() && self.machine.profile == MachineProfile::O2Ip32 { + if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::Origin200Ip29) { return Err(format!( "scsi.{}: kind = \"daynaport\" is not available on the O2 (its Ethernet is built in)", key )); @@ -2468,6 +2507,28 @@ impl MachineConfig { } } crate::dev::crime_mem::check_kits(&self.o2_kits()).map_err(|e| format!("banks: {e}"))?; + } else if self.machine.profile == MachineProfile::Origin200Ip29 { + // The System Board has four banks of a DIMM pair each, filled from + // bank 0 without a gap. + let banks = &self.banks[..bank_count]; + if banks[0] == 0 { + return Err("banks: an Origin 200 needs memory in bank 0".into()); + } + if let Some(gap) = banks.iter().position(|&b| b == 0) { + if banks[gap..].iter().any(|&b| b != 0) { + return Err(format!("banks: bank {gap} is empty but a later bank is populated; fill the banks from 0")); + } + } + for (i, &sz) in banks.iter().enumerate() { + if !MachineProfile::Origin200Ip29.bank_sizes().contains(&sz) { + return Err(format!("bank{i} size {sz} MB is invalid for an Origin 200 (valid: 64, 128, 256, 512)")); + } + // The stock PROM disables a 256 MB bank whenever the processor + // clock is 180 MHz, which leaves a bank 0 of that size no memory. + if sz == 256 && self.cpu() == &parts::R10000_180 { + return Err(format!("bank{i} size 256 MB: the PROM disables 256 MB banks on the 180 MHz R10000 module (use 128 and 128, or the r12000 module)")); + } + } } 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). @@ -2513,8 +2574,8 @@ impl MachineConfig { } } match (self.network.mode, self.machine.profile) { - (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 => { - return Err("network.mode = \"off\" is only available on the O2 and the Octane for now; the Indy and Indigo2 always have their Ethernet".into()); + (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::Origin200Ip29 => { + return Err("network.mode = \"off\" is only available on the O2, the Octane and the Origin 200 for now; the Indy and Indigo2 always have their Ethernet".into()); } (NetMode::Stream, _) => { let ep = self.network.stream.as_deref() @@ -3578,7 +3639,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)"); + assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, origin200_ip29)"); let list = MachineProfile::list(); assert_eq!(list.lines().count(), MachineProfile::EVERY.len()); assert!(list.starts_with("indy_ip24 SGI Indy (IP24)\n"), "{list}"); @@ -3587,7 +3648,7 @@ mod export_tests { for p in MachineProfile::EVERY { match p { MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 - | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 => {} + | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 => {} } let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"{}\"", p.name())).unwrap(); assert_eq!(cfg.machine.profile, p); @@ -3606,7 +3667,7 @@ mod export_tests { // 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] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29] { for cpus in 0..=4u8 { let old_ok = if p == MachineProfile::OctaneIp30 { (1..=2).contains(&cpus) } else { cpus == 1 }; assert_eq!(p.accepts_cpus(cpus).is_ok(), old_ok, "{p:?} with {cpus}"); diff --git a/src/cpu/mips_cache_shadow.rs b/src/cpu/mips_cache_shadow.rs index c50605acc..7f056a863 100644 --- a/src/cpu/mips_cache_shadow.rs +++ b/src/cpu/mips_cache_shadow.rs @@ -781,6 +781,13 @@ pub type R10000ShadowCache = pub type R12000ShadowCache = ShadowCache<32768, 64, 32768, 32, 2097152, 128, true, 0x0000_0e35, 0x0000_0900, 64, { model::R12000 }>; +/// R12000 as the Origin 200's 270 MHz module has it: the Octane part with the +/// module's 4 MB secondary cache (its Config word says SS = 3). The stock IP27 +/// PROM's secondary-cache tag test fails on the Octane type's 2 MB. The 128-byte +/// lines are the Octane part's, not read from the module. +pub type R12000Module4MShadowCache = + ShadowCache<32768, 64, 32768, 32, 4194304, 128, true, 0x0000_0e35, 0x0000_0900, 64, { model::R12000 }>; + /// 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 diff --git a/src/cpu/parts.rs b/src/cpu/parts.rs index 598b923a8..ceda3972c 100644 --- a/src/cpu/parts.rs +++ b/src/cpu/parts.rs @@ -44,6 +44,10 @@ pub const R5000_180: Part = Part { family: Family::R5000, mhz: 180, rev: (2, 1), pub const R5000_180_EARLY: Part = Part { rev: (1, 0), ..R5000_180 }; pub const R10000_195: Part = Part { family: Family::R10000, mhz: 195, rev: (2, 7), l2_kb: 1024 }; pub const R10000_195_EARLY: Part = Part { rev: (2, 5), ..R10000_195 }; +/// The Origin 200's processor modules: the 180 MHz R10000 (rev 2.6, 1 MB +/// secondary cache) and the 270 MHz R12000 (rev 2.3, 4 MB). +pub const R10000_180: Part = Part { mhz: 180, rev: (2, 6), ..R10000_195 }; +pub const R12000_270: Part = Part { mhz: 270, rev: (2, 3), l2_kb: 4096, ..R12000_360 }; pub const R10000_250: Part = Part { mhz: 250, rev: (3, 4), ..R10000_195 }; pub const R12000_360: Part = Part { family: Family::R12000, mhz: 360, rev: (3, 5), l2_kb: 2048 }; pub const R12000_400: Part = Part { mhz: 400, ..R12000_360 }; @@ -53,7 +57,7 @@ pub const R14000_600: Part = Part { family: Family::R14000, mhz: 600, rev: (2, 4 /// 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, - &R12000_360, &R12000_400, &R14000_600, + &R10000_180, &R12000_270, &R12000_360, &R12000_400, &R14000_600, ]; impl Part { diff --git a/src/ip27_machine.rs b/src/ip27_machine.rs new file mode 100644 index 000000000..598b6f9a7 --- /dev/null +++ b/src/ip27_machine.rs @@ -0,0 +1,836 @@ +//! The SN0 machines (IP27 software platform): a Hub node with its memory, +//! boot flash, module controller and processors, and a board that supplies +//! the I/O around it. +//! +//! [`Ip27Machine`] is the machine every SN0 product shares. A product is +//! data, a [`BoardSpec`] of the facts the machine reads (the Hub, the +//! processor module, the memory and the flash values), plus the one thing +//! that differs in behaviour, how its I/O is wired to the node +//! ([`Ip27Board::wire`], which hands back a [`BoardIo`]). The one board here +//! is the Origin 200 (IP29), selected with +//! `[machine] profile = "origin200_ip29"`. One node and one CPU are built; the +//! PROM, the controller store and the timekeeper's NVRAM come from the shared +//! `prom` and `nvram` keys and `[origin200]`. +//! +//! # What is approximated here +//! +//! - One node, one processor, node number zero. Cached accesses ignore the +//! node number (see [`crate::ip27`]). +//! - The Hub's per-slice enable, NMI and cached-alias hooks only log: what a +//! store to those registers does to a processor is not established. +//! - The Bridge reaches the Hub through [`Hub::inbound`] for DMA and +//! [`Hub::interrupt_target`] for interrupts, with the Hub addressed as +//! widget 0xa. Both are the smallest adapter the Bridge needs and are meant +//! to be replaced by the shared host interface. +//! - The module controller's configuration store comes from +//! `ElscNvram::origin200`; the machine never looks inside it. +//! - The Bridge answers at fifteen widget numbers (every one but the Hub's +//! own); which numbers real hardware answers is not established. +//! - The Origin 200 node keeps Hub revision 3 and the IP29 board record +//! 030-1244-001 revision H whatever the processor module. Real 225 and 270 +//! MHz machines report Hub revision 5 and board 030-1389-001. +//! - The node flash and the uncached Hub-special views carry no JIT page +//! generation (`gen_ptr`), so a jitv2 build compiles no code from them; the +//! PROM runs from them on the interpreter's path. +//! +//! Ported from hw/mips/sgi/sn-machine.c and hw/mips/sgi/sn0.c in +//! jamesbraid/qemu-sgi-origami. + +use crate::config::{MachineConfig, MachineProfile}; +use crate::cpu::parts::{self, Part}; +use crate::cpu::mips_cache_v2::MipsCache; +use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; +use crate::cpu::mips_tlb::MipsTlb; +use crate::dev::bridge::Bridge; +use crate::dev::elsc::{Elsc, ElscConfig, ElscEvent}; +use crate::dev::elsc_nvram::ElscNvram; +use crate::dev::heart::{host_clock, Clock}; +use crate::dev::hub::{Hub, HubConfig, HubHooks, Space}; +use crate::dev::hub_pi; +use crate::dev::i2c::{BusBusy, I2cBus}; +use crate::dev::ioc3::{Ioc3, Ioc3Config}; +use crate::dev::ioc3_eth::Ioc3Eth; +use crate::dev::isp1040::Isp1040; +use crate::dev::node_flash::{self, FlashValues, NodeFlash}; +use crate::dev::pcf8584::Pcf8584; +use crate::dev::prom_image; +use crate::dev::ps2::Ps2Controller; +use crate::dev::serial_host::CiSerialBackend; +use crate::dev::timekeeper::Timekeeper; +use crate::dev::w1; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; +use crate::devlog::{init_devlog, LogModule}; +use crate::gdb_stub::CpuDebug; +use crate::gfx_display::GfxDisplay; +use crate::hptimer::{TimerId, TimerManager, TimerReturn}; +use crate::ip27::{self, Ip27Bus, NodeMemory}; +use crate::irq::InterruptController; +use crate::machine_state::GuestState; +use crate::monitor::{Monitor, DEFAULT_PORT as DEFAULT_MONITOR_PORT}; +use crate::net::{GatewayConfig, Link, NatControl}; +use crate::scsi::ScsiBus; +use crate::traits::{BusDevice, Device, MachineEvent, Resettable, Signal, BUS_ERR}; +use crate::xtalk::{Upstream, XtalkHost}; +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::{channel, Receiver, Sender}; +use std::sync::{atomic::AtomicU64, Arc, Weak}; + +/// The PROM image tried when `prom` is left at the shared default. +pub const ORIGIN200_PROM_FILE: &str = "ip27prom.img"; + +/// How often the module controller is brought up to date when the guest +/// leaves its bus idle. The controller keeps its own time, so a late visit +/// loses nothing. +const CONTROLLER_PERIOD: std::time::Duration = std::time::Duration::from_millis(10); + +/// The Hub's own widget number: a small window there is not the Bridge's. +const WIDGET_HUB: u8 = 1; + +/// A processor module the machine fits. +#[derive(Clone, Copy, Debug)] +pub struct CpuModule { + pub part: &'static Part, + /// The mode word the flash configuration block carries. + pub mode_word: u32, +} + +impl CpuModule { + /// The pipeline clock in hertz. + pub fn clock_hz(&self) -> u64 { + u64::from(self.part.mhz) * 1_000_000 + } + + /// The PRId the PROM and IRIX read. + pub fn prid(&self) -> u32 { + self.part.prid() + } + + /// CP0 Count runs at half the pipeline clock. + pub fn count_hz(&self) -> u64 { + self.part.count_hz() + } + + /// Config at cold reset: the mode word's bits 24:0 under the hardwired + /// 32 KiB primary-cache fields. + pub fn config(&self) -> u32 { + 0x6c00_0000 | (self.mode_word & 0x01ff_ffff) + } +} + +/// The processors an Origin 200 takes, the default first: the 180 MHz R10000 +/// and the 270 MHz R12000. +pub const CPUS: &[&Part] = &[&parts::R10000_180, &parts::R12000_270]; + +/// The processor modules an Origin 200 shipped with, by chip: the 180 MHz +/// R10000 (revision 2.6, 1 MiB secondary cache) and the 270 MHz R12000 +/// (revision 2.3, 4 MiB). +/// +/// The mode word is the module's: its Config word says 1 MiB and 64-byte +/// secondary lines for the R10000 and 4 MiB for the R12000. The R10000 module +/// runs on the Octane's R10000 cache type (1 MiB, 128-byte lines, which the +/// PROM accepts); the R12000 module has its own 4 MiB type, since the +/// Octane's 2 MiB fails the PROM's secondary-cache tag test. The line size of +/// both is the Octane part's, not the module's. +pub const MODULES: [CpuModule; 2] = [ + CpuModule { part: &parts::R10000_180, mode_word: 0x1211_a785 }, + CpuModule { part: &parts::R12000_270, mode_word: 0x1213_ab85 }, +]; + +pub fn cpu_module(part: &Part) -> Option<&'static CpuModule> { + MODULES.iter().find(|m| m.part == part) +} + +/// The node every SN0 board plugs into. +pub struct Node { + pub hub: Arc, + pub memory: NodeMemory, + pub flash: Arc, + pub clock: Clock, + pub timers: Arc, +} + +impl Node { + /// The populated processor's enable bit. A Hub comes out of reset with + /// both enables clear, and the stock PROM never sets its own, so the + /// board does what the processor module's strap does on the machine. + fn enable_cpu(&self) { + self.hub.write64(hub_pi::reg::CPU_ENABLE_A, 1); + } +} + +/// The facts about a product the machine reads. +pub struct BoardSpec { + pub hub: HubConfig, + pub module: CpuModule, + pub banks_mb: [u32; ip27::BANKS], + /// What the flash configuration block carries for this product and module. + pub flash: FlashValues, + /// The record on the node's one-wire part. None leaves the part + /// unprogrammed, which IRIX reports as missing page data. + pub node_record: Option, +} + +/// What a node board's one-wire part says about the board. +#[derive(Clone, Copy, Debug)] +pub struct NodeRecord { + pub part: &'static str, + pub revision: &'static str, + pub name: &'static str, +} + +impl NodeRecord { + /// The 180 MHz one-processor module of an Origin 200, as a published sale + /// listing shows it. No inventory gives the 270 MHz R12000 module's + /// record, so that module has none. + pub const PIMM_1XT5: Self = Self { part: "013-1895-001", revision: "C", name: "PIMM_1XT5_1MB" }; + /// The Origin 2000 and Onyx2 node board. + pub const IP27: Self = Self { part: "030-0733-003", revision: "G", name: "IP27" }; +} + +/// What a board's I/O hands the machine once it is wired to the node. +pub struct BoardIo { + /// The devices with threads or timers, in start order. + pub devices: Vec>, + /// Devices the monitor gets commands from. + pub monitor: Vec>, + /// The SCSI controllers; the first takes disc changes. + pub scsi: Vec>, + pub ps2: Arc, + pub timekeeper: Arc, + /// The console, when it serves the CI socket. + pub console: Option>, + /// The `scratch = true` volume. + pub scratch: Option, + pub nat: Option, + /// Put the board's devices back to power-on. + pub reset: Box, + /// The Bridge, for a test to drive. + #[cfg(test)] + pub bridge: Arc, +} + +/// The Ethernet's NAT gateway, as the front ends read it. +pub struct Nat { + pub control: Arc, + pub expected: (Ipv4Addr, Ipv4Addr, Ipv4Addr), + pub frames: Arc, +} + +/// A product. Only wiring differs in behaviour between SN0 boards; everything +/// else about one is the [`BoardSpec`]. +pub trait Ip27Board: 'static { + const NAME: &'static str; + fn spec(cfg: &MachineConfig) -> Result; + /// Wire the board's I/O to the node. + fn wire(node: &Node, cfg: &MachineConfig) -> Result; +} + +pub type Origin200Machine = Ip27Machine; + +pub struct Ip27Machine { + io: BoardIo, + _board: std::marker::PhantomData B>, + node: Node, + elsc: Arc, + controller_events: Sender, + i2c: Arc, + /// The recurring timer that brings the controller up to date. + controller_timer: Option, + cpu: Arc, + monitor: Arc, + monitor_port: u16, + ci: bool, + nvram: String, + events: Option>, + slot: Option, + lifecycle: GuestState, +} + +/// The CPU's three Hub lines are hardware interrupts 0 to 2, Cause IP2 to IP4. +struct CpuIrq(Weak); +impl InterruptController for CpuIrq { + fn set_line(&self, line: u32, level: bool) { + if line < 3 { + if let Some(cpu) = self.0.upgrade() { + cpu.as_device().signal(Signal::Interrupt(line + 2, level)); + } + } + } +} + +/// What the Hub asks of the machine around it. +struct Hooks { + cpu: Weak, + events: Sender, +} +impl HubHooks for Hooks { + fn cpu_enable(&self, slice: usize, enabled: bool) { + crate::dlog!(LogModule::Hub, "Origin 200: PI_CPU_ENABLE {slice} = {enabled} (no effect on the processor)"); + } + fn nmi(&self, slice: usize) { + crate::dlog!(LogModule::Hub, "Origin 200: NMI to slice {slice} (not delivered)"); + } + fn soft_reset(&self, _slice: usize) { + if let Some(cpu) = self.cpu.upgrade() { + cpu.as_device().signal(Signal::Reset(true)); + } + } + fn node_number_changed(&self, number: u16) { + if number != 0 { + crate::dlog!(LogModule::Hub, "Origin 200: node number {number}: cached accesses still use node-relative offsets"); + } + } + fn machine_restart(&self) { + let _ = self.events.send(MachineEvent::HardReset); + } +} + +/// What the module controller's outputs ask of the machine: its power output +/// falling is the module losing power, rising or a reset pulse a restart. +/// The NMI output has no path to the processor yet and is only logged. +fn controller_output(event: ElscEvent, machine: &Sender) { + let _ = match event { + ElscEvent::Power(false) => machine.send(MachineEvent::PowerOff), + ElscEvent::Power(true) | ElscEvent::Reset => machine.send(MachineEvent::HardReset), + ElscEvent::Nmi(level) => { + crate::dlog!(LogModule::Hub, "Origin 200: module controller NMI {level} (not delivered)"); + Ok(()) + } + }; +} + +/// Accesses to a space the model does not carry stay visible in the log. +struct Unmodelled(&'static str); +impl BusDevice for Unmodelled { + fn read32(&self, a: u64) -> crate::traits::BusRead32 { + crate::dlog!(LogModule::Hub, "Origin 200: read of unmodelled {} {a:#x}", self.0); + crate::traits::BusRead32::err() + } + fn read64(&self, a: u64) -> crate::traits::BusRead64 { + crate::dlog!(LogModule::Hub, "Origin 200: read of unmodelled {} {a:#x}", self.0); + crate::traits::BusRead64::err() + } + fn write32(&self, a: u64, v: u32) -> u32 { + crate::dlog!(LogModule::Hub, "Origin 200: write of {v:#x} to unmodelled {} {a:#x}", self.0); + BUS_ERR + } + fn write64(&self, a: u64, v: u64) -> u32 { + crate::dlog!(LogModule::Hub, "Origin 200: write of {v:#x} to unmodelled {} {a:#x}", self.0); + BUS_ERR + } +} + +impl Ip27Machine { + pub fn new(cfg: &MachineConfig) -> Result { + cfg.validate()?; + let spec = B::spec(cfg)?; + let module = spec.module; + let prom = prom_image::load(Path::new(&cfg.machine.profile.prom_path(&cfg.prom)), node_flash::SIZE) + .map_err(|e| format!("{} {e}", B::NAME))?; + let image = node_flash::initial_image(&prom, &spec.flash)?; + let flash = match &cfg.origin200.flash { + Some(path) => NodeFlash::with_file(image, path).map_err(|e| format!("origin200.flash {path}: {e}"))?, + None => NodeFlash::new(image)?, + }; + let clock = host_clock(); + let timers = Arc::new(TimerManager::new()); + let memory = NodeMemory::new(spec.banks_mb)?; + memory.map_reset_view(&flash); + let hub = Hub::new(spec.hub, clock.clone()); + hub.pi().set_timer_manager(timers.clone()); + // The Hub's one-wire master reads the node board's record, whose + // serial is the node's identity: a peer's PROM reads it as the remote + // NIC. + let mut onewire = w1::Bus::new(); + match &spec.node_record { + Some(r) => { + let record = BoardRecord { barcode: "EMUN00", part: r.part, revision: r.revision, name: r.name }; + onewire.add(Box::new(board_record_part(&record, 0x5145_4d55_4e00))); + } + None => onewire.add(Box::new(w1::Ds2505::new(0x0b, 0x5145_4d55_4e00, &[]))), + } + hub.md().set_onewire(onewire); + hub.set_space(Space::Memory, memory.physical().clone()); + hub.set_space( + Space::HubSpecial, + ip27::hub_special(memory.physical().clone(), &flash, memory.back_door(), memory.back_door()), + ); + hub.set_space(Space::Io, hub.io_space()); + hub.set_space(Space::Special, Arc::new(Unmodelled("memory-special space"))); + let node = Node { hub, memory, flash, clock: clock.clone(), timers: timers.clone() }; + node.enable_cpu(); + + // The module controller on the Hub's I2C controller. + let store = ElscNvram::origin200(cfg.origin200.controller_store.as_deref().map(Path::new)).map(Arc::new) + .map_err(|e| format!("origin200.controller_store: {e}"))?; + let busy = BusBusy::default(); + let elsc = Arc::new(Elsc::new(ElscConfig::default(), store.clone(), clock.clone(), busy.clone())); + let mut bus = I2cBus::new(); + bus.add(elsc.clone()); + bus.add(store); + let i2c = Arc::new(Pcf8584::new(bus, busy)); + node.hub.attach_i2c(i2c.clone()); + + let io = B::wire(&node, cfg)?; + + let (tx, rx) = channel(); + let cpu = Self::build_cpu(cfg, &module, &node)?; + node.hub.pi().set_interrupt_controller(Arc::new(CpuIrq(Arc::downgrade(&cpu)))); + node.hub.set_hooks(Arc::new(Hooks { cpu: Arc::downgrade(&cpu), events: tx.clone() })); + node.hub.register_locks(); + + let monitor = Monitor::new(); + monitor.register_device(init_devlog()); + monitor.register_device(Arc::new(crate::cpu::cpu_monitor::CpuMonitor::new(vec![cpu.clone()]))); + monitor.register_device(node.memory.physical().clone()); + monitor.register_device(node.hub.clone()); + for d in &io.monitor { + monitor.register_device(d.clone()); + } + cpu.register_locks(); + + eprintln!( + "iris: {}, {}, {} MB", + B::NAME, + module.part.label(), + node.memory.banks_mb().iter().sum::() + ); + Ok(Self { + io, + _board: std::marker::PhantomData, + node, + elsc, + controller_events: tx, + i2c, + controller_timer: None, + cpu, + monitor: Arc::new(monitor), + monitor_port: cfg.monitor_port.unwrap_or(DEFAULT_MONITOR_PORT), + ci: cfg.ci, + nvram: cfg.nvram.clone(), + events: Some(rx), + slot: None, + lifecycle: GuestState::Idle, + }) + } + + /// The board's Bridge, for tests. + #[cfg(test)] + pub(crate) fn bridge(&self) -> &Arc { + &self.io.bridge + } + + fn build_cpu(cfg: &MachineConfig, module: &CpuModule, node: &Node) -> Result, String> { + let bus = Arc::new(Ip27Bus::new( + node.hub.cpu_special(), + node.hub.processor_io(node.hub.claim(Space::Io)), + node.hub.claim(Space::Special), + node.hub.cpu_memory(), + )); + let phys: Arc = node.memory.physical().clone(); + let memory = Arc::new(node.memory.memory_map()); + let count_hz = cfg.clock.count_hz(module.part).max(1); + // The R12000 module's 4 MB secondary cache has a type of its own: the + // shared table has only the Octane's 2 MB, which the PROM's tag test + // refuses. + let cpu = if module.part.l2_kb == 4096 { + Self::cpu::(module, bus, phys, memory, &node.timers, count_hz) + } else { + crate::with_cpu!(module.part, true, false, C => Self::cpu::(module, bus, phys, memory, &node.timers, count_hz)) + }; + // The shadow cache and the JIT read RAM straight from the ppmem window. + if let Some(sp) = node.memory.physical().ppmem_space() { + use crate::ppmem::MappedMemory; + unsafe { + sp.set_bitmap_sink(cpu.ppmem_bitmap_ptr()); + cpu.set_tcache_window(sp.window_base()); + cpu.set_tcache_shift(crate::ppmem::window_shift(sp.window_size())); + let bitmap = cpu.tcache_bitmap_ptr(); + if !bitmap.is_null() { + sp.set_bitmap_sink2(bitmap); + } + #[cfg(feature = "jitv2")] + cpu.set_tcache_gen_window(sp.gen_window_base()); + } + node.memory.physical().resync_ppmem_bitmap(); + } + #[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)); + // Not under lockstep, as on the other machines: lockstep compiles + // inline on the CPU thread, and a running pool both owns the + // Codegen inline needs and deadlocks the CPU thread's flush. + #[cfg(not(feature = "jitv2_lockstep"))] + { + drop(g); + crate::cpu::jitv2::jitv2::start_compiling(&*cpu, &jit, cfg.jitv2.compile, node.memory.physical().clone() as Arc); + } + } + Ok(cpu) + } + + /// The controller's timers run when a bus access or this timer touches + /// it. A guest that leaves the bus idle (an operator typing at the AUX + /// port, a countdown expiring) still needs them, and its outputs, so a + /// recurring timer brings it up to date. Nothing runs per instruction. + fn start_controller_timer(&mut self) { + self.stop_controller_timer(); + let (elsc, events) = (self.elsc.clone(), self.controller_events.clone()); + let id = self.node.timers.add_recurring( + std::time::Instant::now() + CONTROLLER_PERIOD, + CONTROLLER_PERIOD, + (), + move |_| { + for event in elsc.take_events() { + controller_output(event, &events); + } + TimerReturn::Continue + }, + ); + self.controller_timer = Some(id); + } + + fn stop_controller_timer(&mut self) { + if let Some(id) = self.controller_timer.take() { + self.node.timers.remove(id); + } + } + + fn cpu( + module: &CpuModule, + bus: Arc, + cached: Arc, + memory: Arc, + timers: &Arc, + count_hz: u64, + ) -> Arc + where + C: crate::cpu::mips_cache_v2::CpuModel + MipsCache + From> + 'static, + MipsCpu: CpuDevice, + { + let cfg = MipsCpuConfig::for_model::(memory).with_prid(module.prid()); + let tlb = MipsTlb::new(cfg.tlb_entries); + let mut exec: MipsExecutor = MipsExecutor::new(bus as Arc, tlb, &cfg); + exec.cache = C::from(cached); + exec.core.set_timer_manager(timers.clone()); + exec.core.set_count_hz(count_hz); + exec.core.cp0_config = module.config(); + exec.core.count_write_samples = true; + exec.phys_mask = ip27::CPU_PHYS_MASK; + exec.core.cpu_id = 0; + Arc::new(MipsCpu::new(exec)) + } +} + +impl Workstation for Ip27Machine { + 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) { + self.cpu.clone().as_device().stop(); + self.stop_controller_timer(); + for d in self.io.devices.iter().rev() { + d.stop(); + } + self.node.hub.stop(); + if let Err(e) = self.io.timekeeper.flush_nvram() { + crate::dlog!(LogModule::Hub, "{} NVRAM: {e}", B::NAME); + } + if self.lifecycle != GuestState::Idle { + self.lifecycle = GuestState::Halted; + } + } + fn reset(&mut self) { + self.stop(); + self.cpu.power_on(); + self.node.hub.power_on(); + self.node.enable_cpu(); + self.node.flash.power_on(); + self.i2c.power_on(); + self.elsc.power_on(); + (self.io.reset)(); + self.io.timekeeper.power_on(); + self.restart_peripherals(); + } + fn restart_peripherals(&mut self) { + self.node.hub.start(); + for d in &self.io.devices { + d.start(); + } + self.start_controller_timer(); + } + fn cpu_start(&self) { + self.cpu.clone().as_device().start(); + } + fn cpu_is_running(&self) -> bool { + self.cpu.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("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.io.ps2.clone() + } + fn get_timer_manager(&self) -> Arc { + self.node.timers.clone() + } + fn get_cpu_debug(&self) -> Arc { + self.cpu.clone().debug_adapter() + } + fn fasttick_count(&self) -> Arc { + self.cpu.fasttick_count() + } + fn get_ci_serial(&self) -> Option> { + self.io.console.clone() + } + fn inject_serial_console(&self, bytes: &[u8]) { + if let Some(console) = &self.io.console { + 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(format!("{} NVRAM save uses the configured path", B::NAME)); + } + self.io.timekeeper.flush_nvram()?; + Ok(self.nvram.clone()) + } + fn scratch_path(&self) -> Option<&Path> { + self.io.scratch.as_deref() + } + fn disc_changer(&self) -> Option> { + self.io.scsi.first().map(|c| c.clone() as Arc) + } + fn pending_chd_sync_count(&self) -> usize { + self.io.scsi.iter().map(|c| c.bus().pending_chd_sync_count()).sum() + } + fn flush_disks(&self) -> std::io::Result<()> { + self.io.scsi.iter().try_for_each(|c| c.bus().flush()) + } + fn sync_chd_disks( + &self, + only: Option, + progress: &mut dyn FnMut(usize, usize, f32), + cancel: &dyn Fn() -> bool, + ) -> std::io::Result { + let buses: Vec<&ScsiBus> = self.io.scsi.iter().map(|c| c.bus()).collect(); + ScsiBus::sync_chd_disks(&buses, only, progress, cancel) + } + fn net_frames(&self) -> Option> { + self.io.nat.as_ref().map(|n| n.frames.clone()) + } + fn nat_control(&self) -> Option> { + self.io.nat.as_ref().map(|n| n.control.clone()) + } + fn nat_expected(&self) -> Option<(Ipv4Addr, Ipv4Addr, Ipv4Addr)> { + self.io.nat.as_ref().map(|n| n.expected) + } +} + +impl Drop for Ip27Machine { + fn drop(&mut self) { + if let Some(slot) = self.slot.take() { + slot.lock().0 = MachineRef::null(); + } + self.stop(); + // The compile pool's requests point into the memory freed below. + #[cfg(feature = "jitv2")] + crate::cpu::jitv2::Jitv2::stop_for_drop(&self.cpu.jitv2(), || self.cpu.clone().as_device().stop()); + crate::workstation::flush_on_drop(self); + self.monitor.shutdown(); + // The bus holds the devices, and the Hub and Bridge hold its memory, + // which owns the bus: let go, or none of it is freed. + self.node.memory.physical().release_devices(); + } +} + +/// The Hub as the Bridge addresses it: the master widget the PROM programs +/// for DMA and interrupts, taking an interrupt packet at any address and +/// naming one of 128 vectors. +const HUB_HOST: XtalkHost = XtalkHost { widget: 0xa, int_packet: 0, int_packet_mask: 0, vectors: 128 }; +/// The IP29 System Board's PCI subsystem identification. +const SUBSYS_IP29_SYSBOARD: u16 = 0xc303; +/// The Bridge input the IOC3's SuperIO interrupt is wired to. +const SUPERIO_PIN: u8 = 3; +/// The IP29 System Board's Bridge revision. +const BRIDGE_REVISION: u32 = 4; + +/// The Origin 200 (IP29 System Board): the Hub's XIO port straight to a +/// Bridge with two ISP1040s and an IOC3, no Crossbow. +pub struct Origin200; + +impl Origin200 { + fn ioc3_config(mac: [u8; 6]) -> Ioc3Config { + Ioc3Config { + subsystem_id: SUBSYS_IP29_SYSBOARD, + pci_policy: Default::default(), + reset_pci_on_power_on: false, + mac, + serial: [true, true], + parallel: true, + keyboard_mouse: true, + rtc: false, + gpio_input_mask: 0, + gpio_input_values: 0, + records: Vec::new(), + } + } + + /// The Bridge's one-wire line carries the System Board's record. The + /// barcode and ROM serial are synthetic. + fn board_record() -> w1::Bus { + let mut bus = w1::Bus::new(); + let record = BoardRecord { barcode: "EMUN00", part: "030-1244-001", revision: "H", name: "IP29" }; + bus.add(Box::new(board_record_part(&record, 0x5145_4d55_4200))); + bus + } +} + +impl Ip27Board for Origin200 { + const NAME: &'static str = "Origin 200"; + + fn spec(cfg: &MachineConfig) -> Result { + let module = *cpu_module(cfg.cpu_part()?).ok_or("an Origin 200 has an R10000 or an R12000")?; + Ok(BoardSpec { + // Hub 2.1 on the Origin 200; the node is not in a numbered midplane slot. + hub: HubConfig { cpus: 1, ni_revision: 3, ..Default::default() }, + module, + banks_mb: std::array::from_fn(|i| cfg.banks[i]), + flash: FlashValues { + cpu_mode_bits: module.mode_word, + cpu_clock_hz: module.clock_hz(), + ..FlashValues::ORIGIN200_R10000_180_1M + }, + node_record: (module.part == &parts::R10000_180).then_some(NodeRecord::PIMM_1XT5), + }) + } + + fn wire(node: &Node, cfg: &MachineConfig) -> Result { + let bridge = Bridge::new(Upstream::single(HUB_HOST, node.hub.inbound(), node.hub.interrupt_target())); + bridge.set_revision(BRIDGE_REVISION); + bridge.set_microlan(Self::board_record()); + // The Bridge answers every small window but the Hub's own. + for widget in (0..16).filter(|&w| w != WIDGET_HUB) { + node.hub.io().connect_widget(widget, bridge.clone()); + } + + let heartbeat = Arc::new(AtomicU64::new(0)); + let mac = cfg.network().mac; + let (bus_a, bus_b) = (Isp1040::new_bus(), Isp1040::new_bus()); + let attached = crate::scsi::attach_from_config(cfg, Some("origin200"), &|c| match c { + 0 => Ok(&bus_a), + 1 => Ok(&bus_b), + _ => Err(format!("controller {c} (an Origin 200 has controllers 0 and 1)")), + })?; + if let Some(d) = attached.daynaports.first() { + return Err(format!("{}: kind = \"daynaport\" is not available on the Origin 200", crate::config::scsi_label(d.controller, d.id))); + } + let scsi = vec![Arc::new(Isp1040::new_at(0, bus_a)), Arc::new(Isp1040::new_at(1, bus_b))]; + for s in &scsi { + s.set_heartbeat(heartbeat.clone()); + } + bridge.attach(0, scsi[0].clone()); + bridge.attach(1, scsi[1].clone()); + let ioc3 = Arc::new(Ioc3::new(Self::ioc3_config(mac))); + ioc3.set_heartbeat(heartbeat.clone()); + let eth = Ioc3Eth::new_origin200(mac); + eth.set_heartbeat(heartbeat); + ioc3.attach_eth(eth.clone()); + bridge.attach(2, ioc3.clone()); + ioc3.connect_superio_interrupt(bridge.pin_line(SUPERIO_PIN)); + ioc3.set_timer_manager(node.timers.clone()); + // The Fuel's M48T35 Timekeeper is the same part. + let timekeeper = Arc::new(Timekeeper::fuel_with_nvram_file(Path::new(&cfg.nvram))?); + ioc3.attach_timekeeper(timekeeper.clone()); + + let console = cfg.ci.then(|| Arc::new(CiSerialBackend::new())); + let (port_a, port_b) = crate::dev::serial_host::console_backends(cfg, Self::NAME, console.as_ref()); + ioc3.set_serial_backend(0, port_a); + if let Some(b) = port_b { + ioc3.set_serial_backend(1, b); + } + + let mut nat = None; + let net = GatewayConfig::from_network(&cfg.network()); + if let Some(link) = Link::with_gateway(net.clone())? { + nat = Some(Nat { control: link.nat(), expected: (net.client_ip, net.gateway_ip, net.netmask), frames: link.frames() }); + eth.attach(link); + } + bridge.register_locks(); + ioc3.register_locks(); + eth.register_locks(); + for s in &scsi { + s.register_locks(); + } + + let mut monitor: Vec> = scsi.iter().map(|s| s.clone() as Arc).collect(); + monitor.push(ioc3.ps2()); + let devices: Vec> = vec![ioc3.clone(), eth.clone(), scsi[0].clone(), scsi[1].clone()]; + let ps2 = ioc3.ps2(); + #[cfg(test)] + let test_bridge = bridge.clone(); + let reset_scsi = scsi.clone(); + let reset: Box = Box::new(move || { + bridge.reset(); + bridge.reset_pci(); + ioc3.power_on(); + eth.power_on(); + for s in &reset_scsi { + s.reset(); + } + }); + Ok(BoardIo { + devices, + monitor, + scsi, + ps2, + timekeeper, + console, + scratch: attached.scratch, + nat, + reset, + #[cfg(test)] + bridge: test_bridge, + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip27_machine/tests.rs b/src/ip27_machine/tests.rs new file mode 100644 index 000000000..32ae74c1a --- /dev/null +++ b/src/ip27_machine/tests.rs @@ -0,0 +1,239 @@ +use super::*; +use crate::config::{GraphicsBoard, NetMode}; +use crate::dev::hub_pi; +use crate::traits::BUS_OK; + +struct Scratch(PathBuf); +impl Scratch { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("iris-o200-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + Scratch(dir) + } +} +impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// A PROM stand-in: a recognisable first word and a configuration block in +/// the shape the flash initialiser patches. +fn prom(dir: &Path) -> String { + let mut p = vec![0u8; 0x1000]; + p[..4].copy_from_slice(&0x3c08_a600u32.to_be_bytes()); + p[0x68..0x70].copy_from_slice(&0x6970_3237_636f_6e66u64.to_be_bytes()); + let path = dir.join("prom.bin"); + std::fs::write(&path, p).unwrap(); + path.to_string_lossy().into_owned() +} + +fn config(dir: &Path) -> MachineConfig { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::Origin200Ip29; + cfg.set_cpu(&parts::R10000_180); + cfg.banks = [64, 0, 0, 0, 0, 0, 0, 0]; + cfg.prom = prom(dir); + cfg.nvram = dir.join("nvram.bin").to_string_lossy().into_owned(); + cfg.headless = true; + cfg.network.mode = NetMode::Off; + let held: Vec<_> = (0..3).map(|_| std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap()).collect(); + cfg.monitor_port = Some(held[0].local_addr().unwrap().port()); + cfg.serial_port_a = Some(held[1].local_addr().unwrap().port()); + cfg.serial_port_b = Some(held[2].local_addr().unwrap().port()); + let disk = dir.join("disk.raw"); + std::fs::write(&disk, vec![0u8; 4 << 20]).unwrap(); + cfg.scsi.clear(); + cfg.scsi.insert(1, toml::from_str(&format!("path = {:?}\noverlay = true", disk.to_string_lossy())).unwrap()); + cfg +} + +#[test] +fn the_processor_config_is_the_mode_word_under_the_primary_cache_fields() { + let r10000 = cpu_module(&parts::R10000_180).unwrap(); + assert_eq!(r10000.config(), 0x6c11_a785); + assert_eq!(r10000.count_hz(), 90_000_000); + assert_eq!(cpu_module(&parts::R12000_270).unwrap().config(), 0x6c13_ab85); + assert!(cpu_module(&parts::R14000_600).is_none()); +} + +/// The module's secondary cache size is the Config word's (SS, bits 18:16, as +/// 512 KB << SS), the part table's and the cache type the machine builds. +#[test] +fn a_modules_config_word_part_and_cache_type_say_the_same_secondary_cache() { + for (part, module_kb, model_kb) in [(&parts::R10000_180, 1024, 1024), (&parts::R12000_270, 4096, 4096)] { + let module = cpu_module(part).unwrap(); + assert_eq!(512u32 << ((module.config() >> 16) & 7), module_kb, "{}", part.name()); + assert_eq!(part.l2_kb, module_kb, "{}", part.name()); + let model = if part.l2_kb == 4096 { + ::L2_SIZE as u32 / 1024 + } else { + crate::with_cpu!(part, true, false, C => ::L2_SIZE as u32 / 1024) + }; + assert_eq!(model, model_kb, "{}", part.name()); + } +} + +#[test] +fn the_r12000_module_has_no_record_on_its_one_wire_part() { + let mut cfg = config(&std::env::temp_dir()); + cfg.set_cpu(&parts::R12000_270); + assert!(Origin200::spec(&cfg).unwrap().node_record.is_none()); + cfg.set_cpu(&parts::R10000_180); + assert_eq!(Origin200::spec(&cfg).unwrap().node_record.unwrap().name, "PIMM_1XT5_1MB"); +} + +#[test] +fn the_profile_takes_an_r10000_or_r12000_and_fills_banks_from_zero() { + let dir = Scratch::new("config"); + let mut cfg = config(&dir.0); + assert_eq!(cfg.validate(), Ok(())); + cfg.set_cpu(&parts::R14000_600); + assert!(cfg.validate().unwrap_err().contains("takes [machine] cpu")); + cfg.set_cpu(&parts::R12000_270); + assert_eq!(cfg.validate(), Ok(())); + cfg.banks = [64, 0, 64, 0, 0, 0, 0, 0]; + assert!(cfg.validate().unwrap_err().contains("fill the banks from 0")); + cfg.banks = [32, 0, 0, 0, 0, 0, 0, 0]; + assert!(cfg.validate().unwrap_err().contains("invalid for an Origin 200")); + // The PROM disables 256 MB banks on the 180 MHz module only. + cfg.banks = [256, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(cfg.validate(), Ok(())); + cfg.set_cpu(&parts::R10000_180); + assert!(cfg.validate().unwrap_err().contains("disables 256 MB banks")); + cfg.banks = [128, 128, 0, 0, 0, 0, 0, 0]; + assert_eq!(cfg.validate(), Ok(())); + cfg.banks = [64, 0, 0, 0, 0, 0, 0, 0]; + cfg.graphics.board = Some(GraphicsBoard::V12); + assert!(cfg.validate().unwrap_err().contains("not modelled on the Origin 200")); +} + +#[test] +fn the_profile_is_chosen_by_its_snake_case_name() { + assert_eq!(MachineProfile::from_name("origin200_ip29"), Ok(MachineProfile::Origin200Ip29)); + assert_eq!(MachineProfile::Origin200Ip29.name(), "origin200_ip29"); +} + +/// Building a machine puts more than a megabyte on the stack. +fn big_stack(body: impl FnOnce() + Send + 'static) { + std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join().expect("thread panicked"); +} + +#[test] +fn the_reset_vector_reaches_the_flash_and_the_processor_is_enabled() { + big_stack(|| { + let dir = Scratch::new("reset"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + // Node memory view, the Hub-special views of the flash (own and named), + // all show the PROM's first word at the reset address and the window. + let memory = m.node.hub.cpu_memory(); + assert_eq!(memory.read32(0x1fc0_0000).data, 0x3c08_a600); + let special = m.node.hub.cpu_special(); + assert_eq!(special.read32(0x1000_0000).data, 0x3c08_a600); + assert_eq!(special.read32(0x3000_0000).data, 0x3c08_a600); + // The Hub's own register: PI_CPU_ENABLE_A reads one without the PROM + // having stored it. + let io = m.node.hub.claim(Space::Io); + let enable = crate::dev::hub::IO_REGISTERS_LOCAL + hub_pi::reg::CPU_ENABLE_A; + assert_eq!(io.read64(enable).data, 1); + // And its slice B, which a one-processor module does not fit, is clear. + assert_eq!(io.read64(crate::dev::hub::IO_REGISTERS_LOCAL + hub_pi::reg::CPU_ENABLE_B).data, 0); + }); +} + +#[test] +fn the_bridge_answers_through_the_hub_at_every_widget_but_its_own() { + big_stack(|| { + let dir = Scratch::new("bridge"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + let io = m.node.hub.claim(Space::Io); + let widget = |w: u64| w << 24; + // The Bridge's identification register, in the data half of its slot. + let id = |w: u64| io.read32(widget(w) + 4).data; + assert_ne!(id(WIDGET_HUB.into()), id(8), "widget 1 is the Hub's, not the Bridge's"); + for w in [0, 2, 8, 15] { + assert_eq!(id(w), id(8), "widget {w}"); + } + assert_eq!(io.write32(widget(8) + crate::dev::bridge::reg::INT_ENABLE as u64 + 4, 0), BUS_OK); + }); +} + +#[test] +fn a_reset_enables_the_processor_again() { + big_stack(|| { + let dir = Scratch::new("restart"); + let mut m = Origin200Machine::new(&config(&dir.0)).unwrap(); + m.reset(); + let io = m.node.hub.claim(Space::Io); + assert_eq!(io.read64(crate::dev::hub::IO_REGISTERS_LOCAL + hub_pi::reg::CPU_ENABLE_A).data, 1); + m.stop(); + }); +} + +#[test] +fn the_flash_carries_the_products_machine_type_and_cycle_counts() { + big_stack(|| { + let dir = Scratch::new("flash"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + let word = |at: u64| m.node.hub.cpu_special().read32(0x1000_0000 + at).data; + // Offsets 0x8c, 0x90, 0x94 and 0x9c of the configuration block. + assert_eq!(word(0x8c), 15); + assert_eq!(word(0x90), 1); + assert_eq!(word(0x9c), 4); + // The processor module's mode word and clock. + assert_eq!(word(0x64), 0x1211_a785); + assert_eq!(word(0x74), 180_000_000); + }); +} + +#[test] +fn a_scratch_volume_is_the_machines_scratch_path() { + big_stack(|| { + let dir = Scratch::new("scratch"); + let mut cfg = config(&dir.0); + let disk = dir.0.join("scratch.raw"); + cfg.scsi.clear(); + cfg.scsi.insert(2, toml::from_str(&format!("path = {:?}\nscratch = true\nsize_mb = 4", disk.to_string_lossy())).unwrap()); + let m = Origin200Machine::new(&cfg).unwrap(); + assert_eq!(m.scratch_path(), Some(disk.as_path())); + }); +} + +#[test] +fn the_controller_is_brought_up_to_date_by_a_timer_and_stopped_with_the_machine() { + big_stack(|| { + let dir = Scratch::new("timer"); + let mut m = Origin200Machine::new(&config(&dir.0)).unwrap(); + assert!(m.controller_timer.is_none()); + m.restart_peripherals(); + let first = m.controller_timer; + assert!(first.is_some()); + m.restart_peripherals(); + assert_ne!(m.controller_timer, first, "a restart replaces the timer, never doubling it"); + m.stop(); + assert!(m.controller_timer.is_none()); + }); +} + +/// Every dropped machine frees its Hub, its memory decode table and its timer +/// manager. The process holds a few dozen decode tables, so a leak here ends +/// a test run. +#[test] +fn a_dropped_machine_frees_its_hub_memory_and_timers() { + big_stack(|| { + let dir = Scratch::new("free"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + let hub = Arc::downgrade(&m.node.hub); + let memory = Arc::downgrade(m.node.memory.physical()); + let timers = Arc::downgrade(&m.node.timers); + let cpu = Arc::downgrade(&m.cpu); + let bridge = Arc::downgrade(m.bridge()); + drop(m); + assert_eq!(hub.strong_count(), 0, "the Hub"); + assert_eq!(memory.strong_count(), 0, "the memory decode table"); + assert_eq!(timers.strong_count(), 0, "the timer manager"); + assert_eq!(cpu.strong_count(), 0, "the processor"); + assert_eq!(bridge.strong_count(), 0, "the Bridge"); + }); +} diff --git a/src/lib.rs b/src/lib.rs index c0e305362..8e7887d73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,6 +185,7 @@ pub mod ip30_machine; /// The SN0 node: its processor bus, memory and uncached views. The Origin 200 and /// the Origin 2000 and Onyx2 desksides are built from it. pub mod ip27; +pub mod ip27_machine; /// Fuel has an IP34 board and uses the IP35 software platform and processor views. pub mod ip35; pub mod machine_state; diff --git a/src/machine.rs b/src/machine.rs index 74f73c831..9f776fbf9 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::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29] .into_iter().find(|p| *p as u8 == raw) } @@ -68,6 +68,7 @@ fn emulator_name_thirds(profile: Option) -> &'static [&'static s Some(MachineProfile::Indigo2Ip28) => &["IRIX", "Iris", "IPsomething", "Indigo2", "IP28"], Some(MachineProfile::O2Ip32) => &["IRIX", "Iris", "IPsomething", "IP32"], Some(MachineProfile::OctaneIp30) => &["IRIX", "Iris", "IPsomething", "IP30"], + Some(MachineProfile::Origin200Ip29) => &["IRIX", "Iris", "IPsomething", "IP29"], None => &["IRIX", "Iris", "IPsomething"], } } @@ -2168,11 +2169,12 @@ mod emulator_name_tests { Some(MachineProfile::Indigo2Ip28) => &["Indy", "IP24", "IP22", "IP32", "O2"], Some(MachineProfile::O2Ip32) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "O2"], Some(MachineProfile::OctaneIp30) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP32", "O2"], + Some(MachineProfile::Origin200Ip29) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "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::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::Origin200Ip29)] { 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] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29] { set_emulator_profile(p); assert_eq!(emulator_profile(), Some(p)); } diff --git a/src/workstation.rs b/src/workstation.rs index 0d5537a80..cb73271ac 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -44,6 +44,7 @@ pub fn build(cfg: MachineConfig) -> Result, String> { crate::config::MachineProfile::O2Ip32 => Ok(Box::new(crate::ip32_machine::O2Machine::new(&cfg)?)), // So is the Octane (HEART, Xbow, Bridge). crate::config::MachineProfile::OctaneIp30 => Ok(Box::new(crate::ip30_machine::OctaneMachine::new(&cfg)?)), + crate::config::MachineProfile::Origin200Ip29 => Ok(Box::new(crate::ip27_machine::Origin200Machine::new(&cfg)?)), _ => Ok(Box::new(Machine::new(cfg))), } } From f1486a40f1541c282d53d61ad185a41ae4ffc97b Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 02:45:29 -0700 Subject: [PATCH 32/38] docs: describe the Origin 200 machine Add the sample configuration, a guide with the machine's limits (one node, the Bridge's master widget, the logged Hub stores), and the feature-list and changelog entries. --- CHANGELOG.md | 6 ++++ FEATURES.md | 4 +-- docs/origin200-ip29.md | 63 ++++++++++++++++++++++++++++++++++++++++++ origin200.toml.example | 37 +++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 docs/origin200-ip29.md create mode 100644 origin200.toml.example diff --git a/CHANGELOG.md b/CHANGELOG.md index 435dc3d43..3c5a61857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ 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. +### Origin 200 + +- `machine.profile = "origin200_ip29"` runs the stock IP27 PROM on one Hub + node with an R10000 or R12000 and boots IRIX 6.5.30 to a serial login. + See [the guide](docs/origin200-ip29.md). + ### Boot test before publishing - `build.yml` can boot IRIX 5.3 to a login prompt on Linux, macOS and Windows diff --git a/FEATURES.md b/FEATURES.md index 4319a7aac..ea403a290 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -175,8 +175,8 @@ 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)). | -| `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. The first listed is the default. | +| `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)). | +| `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: r10000 (`r10000_180`), r12000 (`r12000_270`). The first listed is the default. | | `machine.cpus` | `1` | Processors fitted. 2 only on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). | ### [graphics] diff --git a/docs/origin200-ip29.md b/docs/origin200-ip29.md new file mode 100644 index 000000000..ce75bcd71 --- /dev/null +++ b/docs/origin200-ip29.md @@ -0,0 +1,63 @@ +# SGI Origin 200 (IP29 System Board, IP27 software platform) + +The Origin 200 is the IP29 System Board on the IP27 software platform: one +Hub node, an R10000 or R12000, a Bridge joined straight to the Hub's XIO port +(no Crossbow), two ISP1040 SCSI controllers and an IOC3. The machine runs +headless on the IOC3's first serial port. + +## Configuration + +Copy [`origin200.toml.example`](../origin200.toml.example) to `origin200.toml`, +provide the PROM and disk named there, and run `iris --config origin200.toml`. +The sample selects `machine.profile = "origin200_ip29"`. + +- `machine.cpu` is `r10000_180` (180 MHz, 1 MiB secondary cache, the default + module; `r10000` names it) or `r12000_270` (270 MHz, 4 MiB; `r12000` names + it). One processor is built. The R10000 module runs on the Octane + R10000's cache type (1 MiB, 128-byte lines); the R12000 module has a 4 MiB + type of its own, the module's size. Both use the Octane part's line size, + not the module's. Only the 180 MHz + module has a one-wire record (`PIMM_1XT5_1MB`); no inventory gives the R12000 + module's, so its part is blank. +- `banks` are the four DIMM-pair banks in MB (64, 128, 256 or 512), filled + from bank 0. A bank of 128 MB or less is one run at its 512 MiB stride; a + larger one is four runs at 128 MiB intervals. +- `[origin200] flash` keeps the 1 MiB node boot flash, where the PROM stores + its environment log. Unset, the flash starts fresh every run. + `controller_store` does the same for the module controller's store. +- `prom` takes the stock IP27 PROM as a raw payload or SGI's distribution + container. The flash configuration block is patched for the product and + processor module before the PROM runs. + +The PROM runs its diagnostics and reaches the System Maintenance Menu and the +Command Monitor on the first serial port, and an installed IRIX 6.5.30 disk +boots to a serial login. + +## Limits + +- One node and one processor. A second module, the GIGAchannel cabinet and + the Origin 2000 are not built. +- Cached accesses use node-relative addresses, which is right for the node + number zero a one-node machine has. +- The module controller's NMI output and the Hub's per-processor enable and + NMI stores are logged, not delivered. +- Program and erase of the node flash finish inside the write that starts + them. +- The Ethernet transceiver is a DP83840A model with a synthetic partner; the + link comes up two seconds after the first status read. +- The Bridge answers at fifteen widget numbers, every one but the Hub's own. + Which numbers the hardware answers is not established. +- The node keeps Hub revision 3 and the IP29 board record 030-1244-001 + revision H for the R12000 module too; real 225 and 270 MHz machines report + Hub revision 5 and board 030-1389-001. +- Memory is the banks the configuration gives. The R12000 module's 512 MB + total is two 256 MB banks on a real machine; here it is whatever + `banks` says (`[256, 256]` for two, `[512]` for one), and nothing prefers + the split. +- A 256 MB bank is refused with the 180 MHz R10000 module, as the PROM + disables it there. +- The node flash and the uncached Hub-special views have no JIT page + generation, so a jitv2 build does not compile code from them. +- Snapshots are not supported. + +Unmodelled register accesses are visible with `log hub on` in the monitor. diff --git a/origin200.toml.example b/origin200.toml.example new file mode 100644 index 000000000..e743190cc --- /dev/null +++ b/origin200.toml.example @@ -0,0 +1,37 @@ +# SGI Origin 200 (IP29 System Board, IP27 software platform), serial-only setup. +# Copy to origin200.toml and run from the repository root: +# iris --config origin200.toml +# Supply the IP27 PROM image (the stock 6.156 image from the IRIX 6.5.30 +# distribution) and, to boot IRIX, a raw IRIX 6.5.30 disk. No images are +# included. Convert QCOW2 disks to raw first. + +prom = "ignore/origin200/ip27prom.img" +nvram = "ignore/origin200/nvram.bin" +# One bank of a DIMM pair, in MB: 64, 128, 256 or 512 each, filled from bank 0. +banks = [64, 0, 0, 0] +headless = true +no_audio = true +monitor_port = 18988 +# The IOC3's first port is the console. +serial_port_a = 18980 +serial_port_b = 18981 + +[machine] +profile = "origin200_ip29" +# r10000 (180 MHz, 1 MiB secondary cache) or r12000 (270 MHz, 4 MiB). +cpu = "r10000" + +[origin200] +# Keep the node boot flash and the module controller's store between runs. +flash = "ignore/origin200/flash.raw" +controller_store = "ignore/origin200/controller.raw" + +[network] +# Disconnected cable. For NAT, use mode = "nat". +mode = "off" + +[scsi.1] +path = "ignore/origin200/irix.raw" +cdrom = false +# Keep the base disk unchanged. Guest writes go to irix.raw.overlay. +overlay = true From 320d7098247d007618aad257f8a1267089711dfb Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 05:03:04 -0700 Subject: [PATCH 33/38] ip27: let a board carry several nodes A multi-node SN0 machine is several of the same node: a Hub, memory, boot flash, module controller and processor each. The machine held exactly one, so a board could not describe a second. Make a board return one BoardSpec per node, including the files that keep each node's flash and controller store, and wire all its nodes at once so it can join them. The machine keeps a Unit per node and starts, stops and resets them together; the first is the one the front ends address. The PCI population the Origin 200 and the Origin 2000 BaseIO share moves to a function both boards call. The processors are marked as sharing memory when the board has more than one node, so a store-conditional is a compare-and-swap. No behaviour changes for the Origin 200. --- src/ip27_machine.rs | 459 +++++++++++++++++++++++--------------- src/ip27_machine/tests.rs | 24 +- 2 files changed, 292 insertions(+), 191 deletions(-) diff --git a/src/ip27_machine.rs b/src/ip27_machine.rs index 598b6f9a7..64d5c7636 100644 --- a/src/ip27_machine.rs +++ b/src/ip27_machine.rs @@ -168,6 +168,9 @@ pub struct BoardSpec { pub banks_mb: [u32; ip27::BANKS], /// What the flash configuration block carries for this product and module. pub flash: FlashValues, + /// The file that keeps the node's boot flash between runs; absent means + /// volatile. + pub flash_file: Option, /// The record on the node's one-wire part. None leaves the part /// unprogrammed, which IRIX reports as missing page data. pub node_record: Option, @@ -190,6 +193,15 @@ impl NodeRecord { pub const IP27: Self = Self { part: "030-0733-003", revision: "G", name: "IP27" }; } +/// What the machine asks of the module controller a board wires. +pub trait ModuleController: Send + Sync { + /// The module resets: the controller's bus side returns to power-on. + fn reset(&self); + /// The controller's outputs since the last call, with its time brought + /// up to date. + fn take_events(&self) -> Vec; +} + /// What a board's I/O hands the machine once it is wired to the node. pub struct BoardIo { /// The devices with threads or timers, in start order. @@ -205,6 +217,8 @@ pub struct BoardIo { /// The `scratch = true` volume. pub scratch: Option, pub nat: Option, + /// The module controller the nodes' I2C controllers reach. + pub controller: Arc, /// Put the board's devices back to power-on. pub reset: Box, /// The Bridge, for a test to drive. @@ -223,23 +237,27 @@ pub struct Nat { /// else about one is the [`BoardSpec`]. pub trait Ip27Board: 'static { const NAME: &'static str; - fn spec(cfg: &MachineConfig) -> Result; - /// Wire the board's I/O to the node. - fn wire(node: &Node, cfg: &MachineConfig) -> Result; + /// The nodes the board carries, in slot order. + fn specs(cfg: &MachineConfig) -> Result, String>; + /// Wire the board's I/O to the nodes, and the nodes to each other. + fn wire(nodes: &[&Node], cfg: &MachineConfig) -> Result; } pub type Origin200Machine = Ip27Machine; +/// A node with its processor and module controller. +struct Unit { + node: Node, + cpu: Arc, +} + pub struct Ip27Machine { io: BoardIo, _board: std::marker::PhantomData B>, - node: Node, - elsc: Arc, + units: Vec, controller_events: Sender, - i2c: Arc, /// The recurring timer that brings the controller up to date. controller_timer: Option, - cpu: Arc, monitor: Arc, monitor_port: u16, ci: bool, @@ -263,15 +281,16 @@ impl InterruptController for CpuIrq { /// What the Hub asks of the machine around it. struct Hooks { + name: &'static str, cpu: Weak, events: Sender, } impl HubHooks for Hooks { fn cpu_enable(&self, slice: usize, enabled: bool) { - crate::dlog!(LogModule::Hub, "Origin 200: PI_CPU_ENABLE {slice} = {enabled} (no effect on the processor)"); + crate::dlog!(LogModule::Hub, "{}: PI_CPU_ENABLE {slice} = {enabled} (no effect on the processor)", self.name); } fn nmi(&self, slice: usize) { - crate::dlog!(LogModule::Hub, "Origin 200: NMI to slice {slice} (not delivered)"); + crate::dlog!(LogModule::Hub, "{}: NMI to slice {slice} (not delivered)", self.name); } fn soft_reset(&self, _slice: usize) { if let Some(cpu) = self.cpu.upgrade() { @@ -280,7 +299,7 @@ impl HubHooks for Hooks { } fn node_number_changed(&self, number: u16) { if number != 0 { - crate::dlog!(LogModule::Hub, "Origin 200: node number {number}: cached accesses still use node-relative offsets"); + crate::dlog!(LogModule::Hub, "{}: node number {number}: cached accesses still use node-relative offsets", self.name); } } fn machine_restart(&self) { @@ -296,7 +315,7 @@ fn controller_output(event: ElscEvent, machine: &Sender) { ElscEvent::Power(false) => machine.send(MachineEvent::PowerOff), ElscEvent::Power(true) | ElscEvent::Reset => machine.send(MachineEvent::HardReset), ElscEvent::Nmi(level) => { - crate::dlog!(LogModule::Hub, "Origin 200: module controller NMI {level} (not delivered)"); + crate::dlog!(LogModule::Hub, "IP27: module controller NMI {level} (not delivered)"); Ok(()) } }; @@ -306,19 +325,19 @@ fn controller_output(event: ElscEvent, machine: &Sender) { struct Unmodelled(&'static str); impl BusDevice for Unmodelled { fn read32(&self, a: u64) -> crate::traits::BusRead32 { - crate::dlog!(LogModule::Hub, "Origin 200: read of unmodelled {} {a:#x}", self.0); + crate::dlog!(LogModule::Hub, "IP27: read of unmodelled {} {a:#x}", self.0); crate::traits::BusRead32::err() } fn read64(&self, a: u64) -> crate::traits::BusRead64 { - crate::dlog!(LogModule::Hub, "Origin 200: read of unmodelled {} {a:#x}", self.0); + crate::dlog!(LogModule::Hub, "IP27: read of unmodelled {} {a:#x}", self.0); crate::traits::BusRead64::err() } fn write32(&self, a: u64, v: u32) -> u32 { - crate::dlog!(LogModule::Hub, "Origin 200: write of {v:#x} to unmodelled {} {a:#x}", self.0); + crate::dlog!(LogModule::Hub, "IP27: write of {v:#x} to unmodelled {} {a:#x}", self.0); BUS_ERR } fn write64(&self, a: u64, v: u64) -> u32 { - crate::dlog!(LogModule::Hub, "Origin 200: write of {v:#x} to unmodelled {} {a:#x}", self.0); + crate::dlog!(LogModule::Hub, "IP27: write of {v:#x} to unmodelled {} {a:#x}", self.0); BUS_ERR } } @@ -326,87 +345,88 @@ impl BusDevice for Unmodelled { impl Ip27Machine { pub fn new(cfg: &MachineConfig) -> Result { cfg.validate()?; - let spec = B::spec(cfg)?; - let module = spec.module; + let specs = B::specs(cfg)?; let prom = prom_image::load(Path::new(&cfg.machine.profile.prom_path(&cfg.prom)), node_flash::SIZE) .map_err(|e| format!("{} {e}", B::NAME))?; - let image = node_flash::initial_image(&prom, &spec.flash)?; - let flash = match &cfg.origin200.flash { - Some(path) => NodeFlash::with_file(image, path).map_err(|e| format!("origin200.flash {path}: {e}"))?, - None => NodeFlash::new(image)?, - }; - let clock = host_clock(); - let timers = Arc::new(TimerManager::new()); - let memory = NodeMemory::new(spec.banks_mb)?; - memory.map_reset_view(&flash); - let hub = Hub::new(spec.hub, clock.clone()); - hub.pi().set_timer_manager(timers.clone()); - // The Hub's one-wire master reads the node board's record, whose - // serial is the node's identity: a peer's PROM reads it as the remote - // NIC. - let mut onewire = w1::Bus::new(); - match &spec.node_record { - Some(r) => { - let record = BoardRecord { barcode: "EMUN00", part: r.part, revision: r.revision, name: r.name }; - onewire.add(Box::new(board_record_part(&record, 0x5145_4d55_4e00))); + let mut parts = Vec::new(); + for (i, spec) in specs.iter().enumerate() { + let image = node_flash::initial_image(&prom, &spec.flash)?; + let flash = match &spec.flash_file { + Some(path) => NodeFlash::with_file(image, path).map_err(|e| format!("node {i} flash {}: {e}", path.display()))?, + None => NodeFlash::new(image)?, + }; + let clock = host_clock(); + let timers = Arc::new(TimerManager::new()); + let memory = NodeMemory::new(spec.banks_mb)?; + memory.map_reset_view(&flash); + let hub = Hub::new(spec.hub, clock.clone()); + hub.pi().set_timer_manager(timers.clone()); + // The Hub's one-wire master reads the node board's record, whose + // serial is the node's identity: the other node's PROM reads it + // as the remote NIC. + let mut onewire = w1::Bus::new(); + let barcode = format!("EMUN{i:02}"); + let serial = 0x5145_4d55_4e00 | i as u64; + match &spec.node_record { + Some(r) => { + let record = BoardRecord { barcode: &barcode, part: r.part, revision: r.revision, name: r.name }; + onewire.add(Box::new(board_record_part(&record, serial))); + } + None => onewire.add(Box::new(w1::Ds2505::new(0x0b, serial, &[]))), } - None => onewire.add(Box::new(w1::Ds2505::new(0x0b, 0x5145_4d55_4e00, &[]))), + hub.md().set_onewire(onewire); + hub.set_space(Space::Memory, memory.physical().clone()); + hub.set_space( + Space::HubSpecial, + ip27::hub_special(memory.physical().clone(), &flash, memory.back_door(), memory.back_door()), + ); + hub.set_space(Space::Io, hub.io_space()); + hub.set_space(Space::Special, Arc::new(Unmodelled("memory-special space"))); + let node = Node { hub, memory, flash, clock: clock.clone(), timers: timers.clone() }; + node.enable_cpu(); + + parts.push(node); } - hub.md().set_onewire(onewire); - hub.set_space(Space::Memory, memory.physical().clone()); - hub.set_space( - Space::HubSpecial, - ip27::hub_special(memory.physical().clone(), &flash, memory.back_door(), memory.back_door()), - ); - hub.set_space(Space::Io, hub.io_space()); - hub.set_space(Space::Special, Arc::new(Unmodelled("memory-special space"))); - let node = Node { hub, memory, flash, clock: clock.clone(), timers: timers.clone() }; - node.enable_cpu(); - - // The module controller on the Hub's I2C controller. - let store = ElscNvram::origin200(cfg.origin200.controller_store.as_deref().map(Path::new)).map(Arc::new) - .map_err(|e| format!("origin200.controller_store: {e}"))?; - let busy = BusBusy::default(); - let elsc = Arc::new(Elsc::new(ElscConfig::default(), store.clone(), clock.clone(), busy.clone())); - let mut bus = I2cBus::new(); - bus.add(elsc.clone()); - bus.add(store); - let i2c = Arc::new(Pcf8584::new(bus, busy)); - node.hub.attach_i2c(i2c.clone()); - let io = B::wire(&node, cfg)?; + let nodes: Vec<&Node> = parts.iter().collect(); + let io = B::wire(&nodes, cfg)?; let (tx, rx) = channel(); - let cpu = Self::build_cpu(cfg, &module, &node)?; - node.hub.pi().set_interrupt_controller(Arc::new(CpuIrq(Arc::downgrade(&cpu)))); - node.hub.set_hooks(Arc::new(Hooks { cpu: Arc::downgrade(&cpu), events: tx.clone() })); - node.hub.register_locks(); + let mut units = Vec::new(); + for (node, spec) in parts.into_iter().zip(&specs) { + let cpu = Self::build_cpu(cfg, &spec.module, &node, spec.hub.cpu_base, specs.len() > 1)?; + node.hub.pi().set_interrupt_controller(Arc::new(CpuIrq(Arc::downgrade(&cpu)))); + node.hub.set_hooks(Arc::new(Hooks { name: B::NAME, cpu: Arc::downgrade(&cpu), events: tx.clone() })); + node.hub.register_locks(); + cpu.register_locks(); + units.push(Unit { node, cpu }); + } let monitor = Monitor::new(); monitor.register_device(init_devlog()); - monitor.register_device(Arc::new(crate::cpu::cpu_monitor::CpuMonitor::new(vec![cpu.clone()]))); - monitor.register_device(node.memory.physical().clone()); - monitor.register_device(node.hub.clone()); + monitor.register_device(Arc::new(crate::cpu::cpu_monitor::CpuMonitor::new(units.iter().map(|u| u.cpu.clone()).collect()))); + for u in &units { + monitor.register_device(u.node.memory.physical().clone()); + monitor.register_device(u.node.hub.clone()); + } for d in &io.monitor { monitor.register_device(d.clone()); } - cpu.register_locks(); eprintln!( - "iris: {}, {}, {} MB", + "iris: {}, {} node{}, {}, {} MB each", B::NAME, - module.part.label(), - node.memory.banks_mb().iter().sum::() + units.len(), + if units.len() == 1 { "" } else { "s" }, + specs[0].module.part.label(), + units[0].node.memory.banks_mb().iter().sum::() ); Ok(Self { io, _board: std::marker::PhantomData, - node, - elsc, + units, controller_events: tx, - i2c, controller_timer: None, - cpu, monitor: Arc::new(monitor), monitor_port: cfg.monitor_port.unwrap_or(DEFAULT_MONITOR_PORT), ci: cfg.ci, @@ -417,13 +437,30 @@ impl Ip27Machine { }) } + /// The first node, the one the front ends and the monitor address. + pub(crate) fn node(&self) -> &Node { + self.node_at(0) + } + + /// Node `n`'s processor, for tests. + #[cfg(test)] + pub(crate) fn cpu_at(&self, n: usize) -> &Arc { + &self.units[n].cpu + } + /// The board's Bridge, for tests. #[cfg(test)] pub(crate) fn bridge(&self) -> &Arc { &self.io.bridge } - fn build_cpu(cfg: &MachineConfig, module: &CpuModule, node: &Node) -> Result, String> { + pub(crate) fn node_at(&self, n: usize) -> &Node { + &self.units[n].node + } + + /// `smp` when the board carries more than one node: the processors then + /// share memory, and a store-conditional has to be a compare-and-swap. + fn build_cpu(cfg: &MachineConfig, module: &CpuModule, node: &Node, cpu_id: usize, smp: bool) -> Result, String> { let bus = Arc::new(Ip27Bus::new( node.hub.cpu_special(), node.hub.processor_io(node.hub.claim(Space::Io)), @@ -437,9 +474,9 @@ impl Ip27Machine { // shared table has only the Octane's 2 MB, which the PROM's tag test // refuses. let cpu = if module.part.l2_kb == 4096 { - Self::cpu::(module, bus, phys, memory, &node.timers, count_hz) + Self::cpu::(module, bus, phys, memory, &node.timers, count_hz, cpu_id, smp) } else { - crate::with_cpu!(module.part, true, false, C => Self::cpu::(module, bus, phys, memory, &node.timers, count_hz)) + crate::with_cpu!(module.part, true, false, C => Self::cpu::(module, bus, phys, memory, &node.timers, count_hz, cpu_id, smp)) }; // The shadow cache and the JIT read RAM straight from the ppmem window. if let Some(sp) = node.memory.physical().ppmem_space() { @@ -483,13 +520,13 @@ impl Ip27Machine { /// recurring timer brings it up to date. Nothing runs per instruction. fn start_controller_timer(&mut self) { self.stop_controller_timer(); - let (elsc, events) = (self.elsc.clone(), self.controller_events.clone()); - let id = self.node.timers.add_recurring( + let (controller, events) = (self.io.controller.clone(), self.controller_events.clone()); + let id = self.units[0].node.timers.add_recurring( std::time::Instant::now() + CONTROLLER_PERIOD, CONTROLLER_PERIOD, (), move |_| { - for event in elsc.take_events() { + for event in controller.take_events() { controller_output(event, &events); } TimerReturn::Continue @@ -500,7 +537,7 @@ impl Ip27Machine { fn stop_controller_timer(&mut self) { if let Some(id) = self.controller_timer.take() { - self.node.timers.remove(id); + self.units[0].node.timers.remove(id); } } @@ -511,6 +548,8 @@ impl Ip27Machine { memory: Arc, timers: &Arc, count_hz: u64, + cpu_id: usize, + smp: bool, ) -> Arc where C: crate::cpu::mips_cache_v2::CpuModel + MipsCache + From> + 'static, @@ -525,7 +564,8 @@ impl Ip27Machine { exec.core.cp0_config = module.config(); exec.core.count_write_samples = true; exec.phys_mask = ip27::CPU_PHYS_MASK; - exec.core.cpu_id = 0; + exec.core.cpu_id = cpu_id as u8; + exec.core.smp = smp; Arc::new(MipsCpu::new(exec)) } } @@ -540,12 +580,16 @@ impl Workstation for Ip27Machine { } } fn stop(&mut self) { - self.cpu.clone().as_device().stop(); + for u in &self.units { + u.cpu.clone().as_device().stop(); + } self.stop_controller_timer(); for d in self.io.devices.iter().rev() { d.stop(); } - self.node.hub.stop(); + for u in &self.units { + u.node.hub.stop(); + } if let Err(e) = self.io.timekeeper.flush_nvram() { crate::dlog!(LogModule::Hub, "{} NVRAM: {e}", B::NAME); } @@ -555,28 +599,33 @@ impl Workstation for Ip27Machine { } fn reset(&mut self) { self.stop(); - self.cpu.power_on(); - self.node.hub.power_on(); - self.node.enable_cpu(); - self.node.flash.power_on(); - self.i2c.power_on(); - self.elsc.power_on(); + for u in &self.units { + u.cpu.power_on(); + u.node.hub.power_on(); + u.node.enable_cpu(); + u.node.flash.power_on(); + } + self.io.controller.reset(); (self.io.reset)(); self.io.timekeeper.power_on(); self.restart_peripherals(); } fn restart_peripherals(&mut self) { - self.node.hub.start(); + for u in &self.units { + u.node.hub.start(); + } for d in &self.io.devices { d.start(); } self.start_controller_timer(); } fn cpu_start(&self) { - self.cpu.clone().as_device().start(); + for u in &self.units { + u.cpu.clone().as_device().start(); + } } fn cpu_is_running(&self) -> bool { - self.cpu.clone().as_device().is_running() + self.units[0].cpu.clone().as_device().is_running() } fn machine_slot(&mut self) -> MachineSlot { let ptr = self as *mut Self as *mut dyn Workstation; @@ -606,13 +655,13 @@ impl Workstation for Ip27Machine { self.io.ps2.clone() } fn get_timer_manager(&self) -> Arc { - self.node.timers.clone() + self.node().timers.clone() } fn get_cpu_debug(&self) -> Arc { - self.cpu.clone().debug_adapter() + self.units[0].cpu.clone().debug_adapter() } fn fasttick_count(&self) -> Arc { - self.cpu.fasttick_count() + self.units[0].cpu.fasttick_count() } fn get_ci_serial(&self) -> Option> { self.io.console.clone() @@ -672,12 +721,16 @@ impl Drop for Ip27Machine { self.stop(); // The compile pool's requests point into the memory freed below. #[cfg(feature = "jitv2")] - crate::cpu::jitv2::Jitv2::stop_for_drop(&self.cpu.jitv2(), || self.cpu.clone().as_device().stop()); + for u in &self.units { + crate::cpu::jitv2::Jitv2::stop_for_drop(&u.cpu.jitv2(), || u.cpu.clone().as_device().stop()); + } crate::workstation::flush_on_drop(self); self.monitor.shutdown(); // The bus holds the devices, and the Hub and Bridge hold its memory, // which owns the bus: let go, or none of it is freed. - self.node.memory.physical().release_devices(); + for u in &self.units { + u.node.memory.physical().release_devices(); + } } } @@ -723,12 +776,42 @@ impl Origin200 { } } +/// The Origin 200's module controller, on its own node's I2C controller. +struct Origin200Controller { + elsc: Arc, + i2c: Arc, +} + +impl Origin200Controller { + fn new(node: &Node, store: Option<&Path>) -> Result, String> { + let store = ElscNvram::origin200(store).map(Arc::new).map_err(|e| format!("origin200.controller_store: {e}"))?; + let busy = BusBusy::default(); + let elsc = Arc::new(Elsc::new(ElscConfig::default(), store.clone(), node.clock.clone(), busy.clone())); + let mut bus = I2cBus::new(); + bus.add(elsc.clone()); + bus.add(store); + let i2c = Arc::new(Pcf8584::new(bus, busy)); + node.hub.attach_i2c(i2c.clone()); + Ok(Arc::new(Self { elsc, i2c })) + } +} + +impl ModuleController for Origin200Controller { + fn reset(&self) { + self.i2c.power_on(); + self.elsc.power_on(); + } + fn take_events(&self) -> Vec { + self.elsc.take_events() + } +} + impl Ip27Board for Origin200 { const NAME: &'static str = "Origin 200"; - fn spec(cfg: &MachineConfig) -> Result { + fn specs(cfg: &MachineConfig) -> Result, String> { let module = *cpu_module(cfg.cpu_part()?).ok_or("an Origin 200 has an R10000 or an R12000")?; - Ok(BoardSpec { + Ok(vec![BoardSpec { // Hub 2.1 on the Origin 200; the node is not in a numbered midplane slot. hub: HubConfig { cpus: 1, ni_revision: 3, ..Default::default() }, module, @@ -738,11 +821,14 @@ impl Ip27Board for Origin200 { cpu_clock_hz: module.clock_hz(), ..FlashValues::ORIGIN200_R10000_180_1M }, + flash_file: cfg.origin200.flash.as_ref().map(PathBuf::from), node_record: (module.part == &parts::R10000_180).then_some(NodeRecord::PIMM_1XT5), - }) + }]) } - fn wire(node: &Node, cfg: &MachineConfig) -> Result { + fn wire(nodes: &[&Node], cfg: &MachineConfig) -> Result { + let node = nodes[0]; + let controller = Origin200Controller::new(node, cfg.origin200.controller_store.as_deref().map(Path::new))?; let bridge = Bridge::new(Upstream::single(HUB_HOST, node.hub.inbound(), node.hub.interrupt_target())); bridge.set_revision(BRIDGE_REVISION); bridge.set_microlan(Self::board_record()); @@ -750,86 +836,101 @@ impl Ip27Board for Origin200 { for widget in (0..16).filter(|&w| w != WIDGET_HUB) { node.hub.io().connect_widget(widget, bridge.clone()); } + let eth = Ioc3Eth::new_origin200(cfg.network().mac); + pci_devices(node, cfg, bridge, Self::ioc3_config(cfg.network().mac), eth, controller, SUPERIO_PIN, "origin200", Self::NAME) + } +} - let heartbeat = Arc::new(AtomicU64::new(0)); - let mac = cfg.network().mac; - let (bus_a, bus_b) = (Isp1040::new_bus(), Isp1040::new_bus()); - let attached = crate::scsi::attach_from_config(cfg, Some("origin200"), &|c| match c { - 0 => Ok(&bus_a), - 1 => Ok(&bus_b), - _ => Err(format!("controller {c} (an Origin 200 has controllers 0 and 1)")), - })?; - if let Some(d) = attached.daynaports.first() { - return Err(format!("{}: kind = \"daynaport\" is not available on the Origin 200", crate::config::scsi_label(d.controller, d.id))); - } - let scsi = vec![Arc::new(Isp1040::new_at(0, bus_a)), Arc::new(Isp1040::new_at(1, bus_b))]; - for s in &scsi { - s.set_heartbeat(heartbeat.clone()); - } - bridge.attach(0, scsi[0].clone()); - bridge.attach(1, scsi[1].clone()); - let ioc3 = Arc::new(Ioc3::new(Self::ioc3_config(mac))); - ioc3.set_heartbeat(heartbeat.clone()); - let eth = Ioc3Eth::new_origin200(mac); - eth.set_heartbeat(heartbeat); - ioc3.attach_eth(eth.clone()); - bridge.attach(2, ioc3.clone()); - ioc3.connect_superio_interrupt(bridge.pin_line(SUPERIO_PIN)); - ioc3.set_timer_manager(node.timers.clone()); - // The Fuel's M48T35 Timekeeper is the same part. - let timekeeper = Arc::new(Timekeeper::fuel_with_nvram_file(Path::new(&cfg.nvram))?); - ioc3.attach_timekeeper(timekeeper.clone()); - - let console = cfg.ci.then(|| Arc::new(CiSerialBackend::new())); - let (port_a, port_b) = crate::dev::serial_host::console_backends(cfg, Self::NAME, console.as_ref()); - ioc3.set_serial_backend(0, port_a); - if let Some(b) = port_b { - ioc3.set_serial_backend(1, b); - } - - let mut nat = None; - let net = GatewayConfig::from_network(&cfg.network()); - if let Some(link) = Link::with_gateway(net.clone())? { - nat = Some(Nat { control: link.nat(), expected: (net.client_ip, net.gateway_ip, net.netmask), frames: link.frames() }); - eth.attach(link); - } - bridge.register_locks(); - ioc3.register_locks(); - eth.register_locks(); - for s in &scsi { - s.register_locks(); +/// The PCI devices both IP27 I/O boards carry behind their Bridge: two +/// ISP1040s and an IOC3 with its Ethernet, serial ports and Timekeeper. +pub(crate) fn pci_devices( + node: &Node, + cfg: &MachineConfig, + bridge: Arc, + ioc3_config: Ioc3Config, + eth: Arc, + controller: Arc, + superio_pin: u8, + scsi_name: &str, + name: &str, +) -> Result { + let heartbeat = Arc::new(AtomicU64::new(0)); + let (bus_a, bus_b) = (Isp1040::new_bus(), Isp1040::new_bus()); + let attached = crate::scsi::attach_from_config(cfg, Some(scsi_name), &|c| match c { + 0 => Ok(&bus_a), + 1 => Ok(&bus_b), + _ => Err(format!("controller {c} ({name} has controllers 0 and 1)")), + })?; + if let Some(d) = attached.daynaports.first() { + return Err(format!("{}: kind = \"daynaport\" is not available on the {name}", crate::config::scsi_label(d.controller, d.id))); + } + let scsi = vec![Arc::new(Isp1040::new_at(0, bus_a)), Arc::new(Isp1040::new_at(1, bus_b))]; + for s in &scsi { + s.set_heartbeat(heartbeat.clone()); + } + bridge.attach(0, scsi[0].clone()); + bridge.attach(1, scsi[1].clone()); + let ioc3 = Arc::new(Ioc3::new(ioc3_config)); + ioc3.set_heartbeat(heartbeat.clone()); + eth.set_heartbeat(heartbeat); + ioc3.attach_eth(eth.clone()); + bridge.attach(2, ioc3.clone()); + ioc3.connect_superio_interrupt(bridge.pin_line(superio_pin)); + ioc3.set_timer_manager(node.timers.clone()); + // The Fuel's M48T35 Timekeeper is the same part. + let timekeeper = Arc::new(Timekeeper::fuel_with_nvram_file(Path::new(&cfg.nvram))?); + ioc3.attach_timekeeper(timekeeper.clone()); + + let console = cfg.ci.then(|| Arc::new(CiSerialBackend::new())); + let (port_a, port_b) = crate::dev::serial_host::console_backends(cfg, name, console.as_ref()); + ioc3.set_serial_backend(0, port_a); + if let Some(b) = port_b { + ioc3.set_serial_backend(1, b); + } + + let mut nat = None; + let net = GatewayConfig::from_network(&cfg.network()); + if let Some(link) = Link::with_gateway(net.clone())? { + nat = Some(Nat { control: link.nat(), expected: (net.client_ip, net.gateway_ip, net.netmask), frames: link.frames() }); + eth.attach(link); + } + bridge.register_locks(); + ioc3.register_locks(); + eth.register_locks(); + for s in &scsi { + s.register_locks(); + } + + let mut monitor: Vec> = scsi.iter().map(|s| s.clone() as Arc).collect(); + monitor.push(ioc3.ps2()); + let devices: Vec> = vec![ioc3.clone(), eth.clone(), scsi[0].clone(), scsi[1].clone()]; + let ps2 = ioc3.ps2(); + #[cfg(test)] + let test_bridge = bridge.clone(); + let reset_scsi = scsi.clone(); + let reset: Box = Box::new(move || { + bridge.reset(); + bridge.reset_pci(); + ioc3.power_on(); + eth.power_on(); + for s in &reset_scsi { + s.reset(); } - - let mut monitor: Vec> = scsi.iter().map(|s| s.clone() as Arc).collect(); - monitor.push(ioc3.ps2()); - let devices: Vec> = vec![ioc3.clone(), eth.clone(), scsi[0].clone(), scsi[1].clone()]; - let ps2 = ioc3.ps2(); + }); + Ok(BoardIo { + devices, + monitor, + scsi, + ps2, + timekeeper, + console, + scratch: attached.scratch, + nat, + controller, + reset, #[cfg(test)] - let test_bridge = bridge.clone(); - let reset_scsi = scsi.clone(); - let reset: Box = Box::new(move || { - bridge.reset(); - bridge.reset_pci(); - ioc3.power_on(); - eth.power_on(); - for s in &reset_scsi { - s.reset(); - } - }); - Ok(BoardIo { - devices, - monitor, - scsi, - ps2, - timekeeper, - console, - scratch: attached.scratch, - nat, - reset, - #[cfg(test)] - bridge: test_bridge, - }) - } + bridge: test_bridge, + }) } #[cfg(test)] diff --git a/src/ip27_machine/tests.rs b/src/ip27_machine/tests.rs index 32ae74c1a..33e812d6a 100644 --- a/src/ip27_machine/tests.rs +++ b/src/ip27_machine/tests.rs @@ -79,9 +79,9 @@ fn a_modules_config_word_part_and_cache_type_say_the_same_secondary_cache() { fn the_r12000_module_has_no_record_on_its_one_wire_part() { let mut cfg = config(&std::env::temp_dir()); cfg.set_cpu(&parts::R12000_270); - assert!(Origin200::spec(&cfg).unwrap().node_record.is_none()); + assert!(Origin200::specs(&cfg).unwrap()[0].node_record.is_none()); cfg.set_cpu(&parts::R10000_180); - assert_eq!(Origin200::spec(&cfg).unwrap().node_record.unwrap().name, "PIMM_1XT5_1MB"); + assert_eq!(Origin200::specs(&cfg).unwrap()[0].node_record.unwrap().name, "PIMM_1XT5_1MB"); } #[test] @@ -127,14 +127,14 @@ fn the_reset_vector_reaches_the_flash_and_the_processor_is_enabled() { let m = Origin200Machine::new(&config(&dir.0)).unwrap(); // Node memory view, the Hub-special views of the flash (own and named), // all show the PROM's first word at the reset address and the window. - let memory = m.node.hub.cpu_memory(); + let memory = m.node().hub.cpu_memory(); assert_eq!(memory.read32(0x1fc0_0000).data, 0x3c08_a600); - let special = m.node.hub.cpu_special(); + let special = m.node().hub.cpu_special(); assert_eq!(special.read32(0x1000_0000).data, 0x3c08_a600); assert_eq!(special.read32(0x3000_0000).data, 0x3c08_a600); // The Hub's own register: PI_CPU_ENABLE_A reads one without the PROM // having stored it. - let io = m.node.hub.claim(Space::Io); + let io = m.node().hub.claim(Space::Io); let enable = crate::dev::hub::IO_REGISTERS_LOCAL + hub_pi::reg::CPU_ENABLE_A; assert_eq!(io.read64(enable).data, 1); // And its slice B, which a one-processor module does not fit, is clear. @@ -147,7 +147,7 @@ fn the_bridge_answers_through_the_hub_at_every_widget_but_its_own() { big_stack(|| { let dir = Scratch::new("bridge"); let m = Origin200Machine::new(&config(&dir.0)).unwrap(); - let io = m.node.hub.claim(Space::Io); + let io = m.node().hub.claim(Space::Io); let widget = |w: u64| w << 24; // The Bridge's identification register, in the data half of its slot. let id = |w: u64| io.read32(widget(w) + 4).data; @@ -165,7 +165,7 @@ fn a_reset_enables_the_processor_again() { let dir = Scratch::new("restart"); let mut m = Origin200Machine::new(&config(&dir.0)).unwrap(); m.reset(); - let io = m.node.hub.claim(Space::Io); + let io = m.node().hub.claim(Space::Io); assert_eq!(io.read64(crate::dev::hub::IO_REGISTERS_LOCAL + hub_pi::reg::CPU_ENABLE_A).data, 1); m.stop(); }); @@ -176,7 +176,7 @@ fn the_flash_carries_the_products_machine_type_and_cycle_counts() { big_stack(|| { let dir = Scratch::new("flash"); let m = Origin200Machine::new(&config(&dir.0)).unwrap(); - let word = |at: u64| m.node.hub.cpu_special().read32(0x1000_0000 + at).data; + let word = |at: u64| m.node().hub.cpu_special().read32(0x1000_0000 + at).data; // Offsets 0x8c, 0x90, 0x94 and 0x9c of the configuration block. assert_eq!(word(0x8c), 15); assert_eq!(word(0x90), 1); @@ -224,10 +224,10 @@ fn a_dropped_machine_frees_its_hub_memory_and_timers() { big_stack(|| { let dir = Scratch::new("free"); let m = Origin200Machine::new(&config(&dir.0)).unwrap(); - let hub = Arc::downgrade(&m.node.hub); - let memory = Arc::downgrade(m.node.memory.physical()); - let timers = Arc::downgrade(&m.node.timers); - let cpu = Arc::downgrade(&m.cpu); + let hub = Arc::downgrade(&m.node().hub); + let memory = Arc::downgrade(m.node().memory.physical()); + let timers = Arc::downgrade(&m.node().timers); + let cpu = Arc::downgrade(m.cpu_at(0)); let bridge = Arc::downgrade(m.bridge()); drop(m); assert_eq!(hub.strong_count(), 0, "the Hub"); From 0a8719a70b0e35419687559ca58759f6628117ec Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 06:12:40 -0700 Subject: [PATCH 34/38] hub: keep a processor's register alias on its own node A processor reaches its own Hub's registers at a fixed I/O offset with a node field of zero, whatever number the node holds. The processor's I/O view claimed every address by node number, so once the PROM numbered a second node 1, its stores to its own registers went to the node still holding 0, and its reads came from that node. The second node of a pair then ran the PROM against the first one's Hub and never finished. Give the processor an I/O view that keeps node-zero addresses in that alias on its own node, and leave every other address, including the alias offset under another node's number, to the claim map. I/O references are not on the memory fast path. --- src/dev/hub.rs | 23 +++++++++++++++++------ src/dev/hub/tests.rs | 26 ++++++++++++++++++++++++++ src/ip27_machine.rs | 2 +- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/dev/hub.rs b/src/dev/hub.rs index d89d05eb0..c986f8a9a 100644 --- a/src/dev/hub.rs +++ b/src/dev/hub.rs @@ -467,6 +467,13 @@ impl Hub { Arc::new(View { hub: self.clone(), space: Space::HubSpecial, local: Local::Always }) } + /// The processor's view of the I/O space: this node's registers at the + /// reader's own alias (node field zero), whatever number the node holds, + /// and any other address claimed by the node number it carries. + pub fn cpu_io(self: &Arc) -> Arc { + Arc::new(View { hub: self.clone(), space: Space::Io, local: Local::IoAlias }) + } + /// This node's I/O space, addressed from the node's base: the crossbar /// widgets' small windows, the Hub's own registers at both offsets, and /// the seven big windows. @@ -521,6 +528,10 @@ enum Local { Calias, /// Node-zero addresses are always this node's own. Always, + /// Node-zero addresses in the registers' own alias are this node's + /// whatever number the node holds: the alias is the one I/O region the + /// reader's Hub answers without asking which node holds zero. + IoAlias, } /// A node-qualified address space as a processor, an engine or a widget sees @@ -534,12 +545,12 @@ impl View { fn run(&self, a: u64, f: impl FnOnce(&dyn BusDevice, u64) -> R) -> Option { let offset = a & (NODE_SPAN - 1); let number = ((a >> NODE_SHIFT) & NODE_MASK) as u8; - let stays = number == 0 - && match self.local { - Local::Never => false, - Local::Calias => a < self.hub.calias.load(Ordering::Acquire), - Local::Always => true, - }; + let stays = match self.local { + Local::Never => false, + Local::Calias => number == 0 && a < self.hub.calias.load(Ordering::Acquire), + Local::Always => number == 0, + Local::IoAlias => number == 0 && (IO_REGISTERS_LOCAL..IO_REGISTERS_NAMED).contains(&offset), + }; if stays { return self.hub.view.spaces[self.space as usize].get().map(|d| f(&**d, offset)); } diff --git a/src/dev/hub/tests.rs b/src/dev/hub/tests.rs index 803ec4770..1a524f95e 100644 --- a/src/dev/hub/tests.rs +++ b/src/dev/hub/tests.rs @@ -476,6 +476,32 @@ fn the_io_space_holds_the_registers_at_both_offsets_and_the_widgets_behind_them( assert_eq!(io.read8(IO_BIG_WINDOWS + 7 * hub_io::BIG_WINDOW_SIZE).status, BUS_ERR); } +#[test] +fn a_processors_own_register_alias_stays_on_its_node_whatever_the_number() { + let (a, b) = pair(); + for hub in [&a, &b] { + hub.set_space(Space::Io, hub.io_space()); + } + number(&b, 1); + let at = IO_REGISTERS_LOCAL + hub_io::reg::SCRATCH_1; + // The claim by node number sends node zero's alias to the node holding + // zero; the processor's view keeps it on its own. + assert_eq!(b.claim(Space::Io).write64(at, 0x11), BUS_OK); + assert_eq!(r64(&*a, hub_io::reg::SCRATCH_1), 0x11); + assert_eq!(b.cpu_io().write64(at, 0x22), BUS_OK); + assert_eq!(r64(&*b, hub_io::reg::SCRATCH_1), 0x22); + assert_eq!(r64(&*a, hub_io::reg::SCRATCH_1), 0x11); + // The node-explicit window still goes by node number. + assert_eq!(b.cpu_io().read64(IO_REGISTERS_NAMED + hub_io::reg::SCRATCH_1).data, 0x11); + assert_eq!(b.cpu_io().read64(NODE(1) + IO_REGISTERS_NAMED + hub_io::reg::SCRATCH_1).data, 0x22); + // The alias is the one with node field zero: another node's number at + // the same offset goes to that node. + assert_eq!(b.cpu_io().read64(NODE(0) + at).data, 0x22); + assert_eq!(a.cpu_io().write64(NODE(1) + at, 0x33), BUS_OK); + assert_eq!(r64(&*b, hub_io::reg::SCRATCH_1), 0x33); + assert_eq!(r64(&*a, hub_io::reg::SCRATCH_1), 0x11); +} + // ---- the engines through the fabric ---- #[test] diff --git a/src/ip27_machine.rs b/src/ip27_machine.rs index 64d5c7636..9ccec4e7c 100644 --- a/src/ip27_machine.rs +++ b/src/ip27_machine.rs @@ -463,7 +463,7 @@ impl Ip27Machine { fn build_cpu(cfg: &MachineConfig, module: &CpuModule, node: &Node, cpu_id: usize, smp: bool) -> Result, String> { let bus = Arc::new(Ip27Bus::new( node.hub.cpu_special(), - node.hub.processor_io(node.hub.claim(Space::Io)), + node.hub.processor_io(node.hub.cpu_io()), node.hub.claim(Space::Special), node.hub.cpu_memory(), )); From 680c43e7751e0e5343a9231296cf996e8d3e4a2f Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 06:12:40 -0700 Subject: [PATCH 35/38] xbow: add the port a processor reaches a widget through The Crossbow routes a request by widget number and passes it only while the link to that widget is up. The Octane keeps that rule in its address map. A Hub's I/O space has no such map: it holds a device per widget number, so a Crossbow in front of it has nothing to gate the access. Add Xbow::port, a wrapper that hands an access to the widget while the link carries traffic and fails it otherwise. A machine puts the wrapper where it would have put the widget. It costs one atomic load per access and only on a machine that uses it. Add Xbow::packets beside it: the face a Hub's big window reaches a peer's I/O view through, which routes a packet by the widget named in address bits 51:48 while that widget's link is up. --- src/dev/xbow.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index 0f1f04672..3f3352254 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -601,6 +601,61 @@ impl crate::traits::Resettable for Xbow { /// address, of which the window's 16 MB is decoded. const WINDOW_MASK: u64 = REG_SPAN as u64 - 1; +/// The widget at one of the Crossbow's ports, as a processor reaches it: the +/// widget answers while the link is up and the access fails while it is not. +/// The Crossbow routes a request by widget number and translates no address, +/// so the window is the widget's own. +struct Port { + xbow: Arc, + widget: u8, + dev: Arc, +} + +/// A packet into a peer's I/O view: the widget it names in bits 51:48 gets it, +/// with those bits cleared, while the link to that widget is up. +struct Packets { + xbow: Arc, + ports: Vec<(u8, Arc)>, +} + +/// An access that a Crossbow passes on to a device, or fails. +macro_rules! routed_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + self.route(a).map_or($out::err(), |(d, at)| d.$r(at)) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.route(a).map_or(BUS_ERR, |(d, at)| d.$w(at, v)) + } + }; +} + +impl Port { + fn route(&self, a: u64) -> Option<(&dyn BusDevice, u64)> { + self.xbow.carries(self.widget).then(|| (&*self.dev, a)) + } +} +impl BusDevice for Port { + routed_access!(read8, write8, u8, BusRead8); + routed_access!(read16, write16, u16, BusRead16); + routed_access!(read32, write32, u32, BusRead32); + routed_access!(read64, write64, u64, BusRead64); +} + +impl Packets { + fn route(&self, a: u64) -> Option<(&dyn BusDevice, u64)> { + let widget = ((a >> 48) & 0xf) as u8; + let (_, dev) = self.ports.iter().find(|(w, _)| *w == widget)?; + self.xbow.carries(widget).then(|| (&**dev, a & ((1 << 48) - 1))) + } +} +impl BusDevice for Packets { + routed_access!(read8, write8, u8, BusRead8); + routed_access!(read16, write16, u16, BusRead16); + routed_access!(read32, write32, u32, BusRead32); + routed_access!(read64, write64, u64, BusRead64); +} + impl Xbow { /// What a Bridge behind this Crossbow sends to: each of `hosts` sits on /// its widget's port, and a request to it is dropped while that link is @@ -612,6 +667,18 @@ impl Xbow { up.with(h.with_link(Arc::new(move || xbow.carries(widget)))) }) } + + /// `dev` as the widget behind port `widget` (8 to 15). + pub fn port(self: &Arc, widget: u8, dev: Arc) -> Arc { + assert!(link_bit(widget).is_some(), "widget {widget} is not a Crossbow port"); + Arc::new(Port { xbow: self.clone(), widget, dev }) + } + + /// The packet face a Hub's big window reaches a peer's I/O view through: + /// each of `ports` is a port widget and the device a packet to it lands on. + pub fn packets(self: &Arc, ports: Vec<(u8, Arc)>) -> Arc { + Arc::new(Packets { xbow: self.clone(), ports }) + } } impl BusDevice for Xbow { @@ -916,4 +983,44 @@ mod tests { assert!(Xbow::new(5, 0x8400).load_state(&v).is_err(), "other links fitted"); assert!(Xbow::new(4, 0x8100).load_state(&v).is_err(), "another revision"); } + + /// A widget that answers every read with a marker. + struct Marker; + impl BusDevice for Marker { + fn read32(&self, a: u64) -> BusRead32 { + BusRead32::ok(0x1234_0000 | a as u32) + } + fn write32(&self, _: u64, _: u32) -> u32 { + BUS_OK + } + } + + #[test] + fn a_port_passes_accesses_while_its_link_is_up_and_fails_them_otherwise() { + let x = Arc::new(octane()); + let fitted = x.port(15, Arc::new(Marker)); + assert_eq!(fitted.read32(0x10).data, 0x1234_0010); + assert_eq!(fitted.write32(0x10, 0), BUS_OK); + // Widget 11 has nothing attached, so its link is down. + let empty = x.port(11, Arc::new(Marker)); + assert_eq!(empty.read32(0x10).status, BUS_ERR); + assert_eq!(empty.write32(0x10, 0), BUS_ERR); + } + + #[test] + #[should_panic(expected = "not a Crossbow port")] + fn only_the_eight_link_widgets_are_ports() { + let _ = Arc::new(octane()).port(7, Arc::new(Marker)); + } + + #[test] + fn a_packet_goes_to_the_widget_it_names_while_that_link_is_up() { + let x = Arc::new(octane()); + let packets = x.packets(vec![(15, Arc::new(Marker)), (11, Arc::new(Marker))]); + // The widget is cleared from the address the device sees. + assert_eq!(packets.read32(15u64 << 48 | 0x10).data, 0x1234_0010); + // A named port with no link, and a widget with no port, fail. + assert_eq!(packets.read32(11u64 << 48 | 0x10).status, BUS_ERR); + assert_eq!(packets.read32(9u64 << 48 | 0x10).status, BUS_ERR); + } } From 93e2a6333c224781eaef425c77a17c68a1e1163f Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 06:12:40 -0700 Subject: [PATCH 36/38] ip27: route cached references to other nodes through the Hub A node's cached references go straight to its own memory, which holds only node-relative addresses. With a second node, a reference that names another node number fell into unfitted memory: reads gave zero and writes vanished, so the PROM's memory test of the second node failed. Let a board route the windows of node numbers 1 to 255 to the Hub's node-qualified memory, which picks the Hub by the number the address carries, across the cable, and fails a number no Hub holds. Node zero's window is the node's own memory only while the node holds zero. Once it holds another number, a node-zero reference inside the cached alias still stays on the node, and one beyond the alias goes to the Hub holding zero. The second node's PROM merges the first node's tables that way; read from its own memory instead, the merge copied the second node's empty table over itself and left the first node's entry unset, and the PROM then faulted on it. The node's RAM sits in the ppmem window the cache and the JIT read directly, so moving the window's reference to the Hub takes that mapping away while the number is nonzero. A node numbered zero, which is a single node and the first node of a pair, keeps the direct mapping and takes no new decision on the hot path. That rerouting is the processor's own cached path only. Memory and Hub-special accesses, which name a node's banks by offset, go to the banks themselves through NodeMemory::home, so renumbering a node does not move what the Hub and the PROM's window reach. A store-conditional to another node's memory has to be as atomic as one to the processor's own, so the Hub's node-qualified views and that home view forward compare-and-swap to the node's banks. --- src/dev/hub.rs | 8 ++ src/ip27.rs | 224 +++++++++++++++++++++++++++++++++++++- src/ip27/tests.rs | 52 +++++++++ src/ip27_machine.rs | 31 +++++- src/ip27_machine/tests.rs | 52 ++++++++- 5 files changed, 355 insertions(+), 12 deletions(-) diff --git a/src/dev/hub.rs b/src/dev/hub.rs index c986f8a9a..26f7a4c9d 100644 --- a/src/dev/hub.rs +++ b/src/dev/hub.rs @@ -578,6 +578,14 @@ macro_rules! view_access { }; } impl BusDevice for View { + // A store-conditional to another node's memory has to be as atomic as + // one to the processor's own. + fn cas32(&self, a: u64, old: u32, new: u32) -> Option { + self.run(a, |d, at| d.cas32(at, old, new)).flatten() + } + fn cas64(&self, a: u64, old: u64, new: u64) -> Option { + self.run(a, |d, at| d.cas64(at, old, new)).flatten() + } view_access!(read8, write8, u8, BusRead8); view_access!(read16, write16, u16, BusRead16); view_access!(read32, write32, u32, BusRead32); diff --git a/src/ip27.rs b/src/ip27.rs index cda9beb5e..74f8df606 100644 --- a/src/ip27.rs +++ b/src/ip27.rs @@ -18,9 +18,12 @@ //! //! # What is approximated here //! -//! - Cached accesses ignore the node number in address bits 39:32 and use -//! node-relative offsets, so they are right for a node numbered zero, which -//! is the one-node Origin 200. +//! - Cached accesses to node zero's window use node-relative offsets while +//! the node holds zero, which is the one-node Origin 200 and the first node +//! of a deskside. A board with several nodes routes the windows of the +//! other node numbers through the Hub ([`NodeMemory::route_nodes`]), and +//! sends node zero's, beyond the cached alias, to the Hub that holds zero +//! once the node holds another number ([`NodeZero`]). //! - The low 128 KiB of the Hub-special memory view is not private to each //! processor of the pair. //! - A bank no larger than 128 MiB is one run at its 512 MiB stride; a larger @@ -40,7 +43,7 @@ use crate::ppmem::devmap::SLOT_SHIFT; use crate::ppmem::MappedMemory; use crate::traits::*; use parking_lot::Mutex; -use std::sync::Arc; +use std::sync::{Arc, Weak}; pub(crate) const PA_MASK: u64 = (1u64 << 40) - 1; /// XKPHYS bits 58:57 ride along with the physical address into the bus. @@ -54,6 +57,8 @@ pub const SLOT_STRIDE: u64 = 128 << 20; pub const BANKS: usize = 4; const MIB: u64 = 1 << 20; +/// Node numbers, whose cached windows [`NodeMemory::route_nodes`] answers. +const NODE_WINDOWS: u64 = 256; /// Where bank `bank` of `mb` MiB lies: `(node offset, length)` runs, in the /// order they take the bank's storage. @@ -75,6 +80,9 @@ pub fn bank_runs(bank: usize, mb: u32) -> Vec<(u64, u64)> { pub struct NodeMemory { phys: Arc, banks_mb: [u32; BANKS], + /// The view [`Self::route_nodes`] sent the other node numbers to, held + /// weakly: it reaches the Hub, which holds this memory's users. + claim: Arc>>>, } impl NodeMemory { @@ -104,7 +112,7 @@ impl NodeMemory { } index += 1; } - Ok(Self { phys, banks_mb }) + Ok(Self { phys, banks_mb, claim: Arc::default() }) } pub fn physical(&self) -> &Arc { @@ -115,6 +123,40 @@ impl NodeMemory { self.banks_mb } + /// Answer cached references to node numbers 1 to 255 through `view`, the + /// Hub's node-qualified memory, so a reference to memory another Hub holds + /// reaches it across the cable and one that no Hub holds fails, logged by + /// the Hub. The windows are 4 GiB apart at address bit 32, above + /// everything the node's own banks occupy. Node zero's window is the + /// node's own memory and is not routed. + pub fn route_nodes(&self, view: Arc) { + *self.claim.lock() = Some(Arc::downgrade(&view)); + for node in 1..NODE_WINDOWS { + self.phys.map(node << 32, (node + 1) << 32, view.clone(), "node-qualified memory"); + } + } + + /// The node's answer to a node-zero cached reference once the node holds + /// another number, for [`NodeZero::update`] to apply. It sends the + /// reference to the view [`Self::route_nodes`] was given, so a board with + /// one node, which routes nothing, never sends it anywhere. + pub fn node_zero(&self) -> Arc { + Arc::new(NodeZero { + phys: Arc::downgrade(&self.phys), + banks_mb: self.banks_mb, + claim: self.claim.clone(), + state: Mutex::new(NodeZeroState { number: 0, calias: 0, away: false }), + }) + } + + /// The node's memory at its node-relative offsets whatever number the + /// node holds, for the Hub's spaces: the Hub-special space, and every + /// reference another Hub or an engine makes by the node's number. + /// [`NodeZero`] reroutes only the processor's own cached references. + pub fn home(&self) -> Arc { + Arc::new(Home::new(self.phys.clone(), self.banks_mb)) + } + /// Place the boot flash's reset view in node memory. pub fn map_reset_view(&self, flash: &Arc) { crate::ip35::map_device(&self.phys, RESET_VIEW, crate::dev::node_flash::SIZE as u64, flash.chip(), "boot flash"); @@ -150,6 +192,88 @@ impl NodeMemory { } } +/// [`NodeMemory::home`]: an offset inside a fitted run reaches its bank +/// directly, past whatever [`NodeZero`] put in the decode table there; any +/// other offset is the decode table's. +struct Home { + phys: Arc, + /// Each bank's fitted runs, built once: a node offset names a bank by + /// its stride, and a bank has at most four runs. + runs: [[Option; 4]; BANKS], +} + +/// One fitted run: where it lies in the node's offsets, which RAM bank holds +/// it and where in that bank it starts. +#[derive(Clone, Copy)] +struct HomeRun { + at: u64, + len: u64, + index: usize, + src: u64, +} + +impl Home { + fn new(phys: Arc, banks_mb: [u32; BANKS]) -> Self { + let mut runs = [[None; 4]; BANKS]; + let mut index = 0; + for (bank, &mb) in banks_mb.iter().enumerate() { + if mb == 0 { + continue; + } + let mut src = 0; + for (n, (at, len)) in bank_runs(bank, mb).into_iter().enumerate() { + runs[bank][n] = Some(HomeRun { at, len, index, src }); + src += len; + } + index += 1; + } + Self { phys, runs } + } + + /// The bank index and the offset within it that node offset `a` names, + /// if a fitted run holds it. No allocation: this is on every access. + #[inline] + fn bank(&self, a: u64) -> Option<(usize, u64)> { + let runs = self.runs.get((a / BANK_STRIDE) as usize)?; + runs.iter().flatten().find(|r| (r.at..r.at + r.len).contains(&a)).map(|r| (r.index, r.src + a - r.at)) + } +} + +macro_rules! home_access { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + match self.bank(a) { + Some((i, at)) => self.phys.banks()[i].$r(at), + None => self.phys.$r(a), + } + } + fn $w(&self, a: u64, v: $t) -> u32 { + match self.bank(a) { + Some((i, at)) => self.phys.banks()[i].$w(at, v), + None => self.phys.$w(a, v), + } + } + }; +} +impl BusDevice for Home { + fn cas32(&self, a: u64, old: u32, new: u32) -> Option { + match self.bank(a) { + Some((i, at)) => self.phys.banks()[i].cas32(at, old, new), + None => self.phys.cas32(a, old, new), + } + } + fn cas64(&self, a: u64, old: u64, new: u64) -> Option { + match self.bank(a) { + Some((i, at)) => self.phys.banks()[i].cas64(at, old, new), + None => self.phys.cas64(a, old, new), + } + } + home_access!(read8, write8, u8, BusRead8); + home_access!(read16, write16, u16, BusRead16); + home_access!(read32, write32, u32, BusRead32); + home_access!(read64, write64, u64, BusRead64); +} + /// Memory that is not fitted: reads zero, drops writes. `phys hole on` logs it. struct Absent; impl BusDevice for Absent { @@ -299,5 +423,95 @@ impl BusDevice for Ip27Bus { bus_access!(read64, write64, u64, BusRead64); } +/// Where a node that holds a nonzero number sends a cached reference that +/// names node zero. Within the cached alias the reference stays on the node. +/// Beyond it the node-qualified memory answers by number, and the reference +/// reaches whichever Hub holds zero, across the cable. The node's own memory +/// is then reached by its own number. +/// +/// Node zero's window is the node's own memory, and its RAM is mapped into +/// the ppmem window the cache and the JIT read directly. Sending a reference +/// elsewhere takes the mapping away, so every cached reference of the node +/// takes the bus path while the number is nonzero. That costs the second node +/// speed and nothing else, since its software addresses its memory by its +/// number. +pub struct NodeZero { + /// Weak: the Hub that owns this through its hooks also owns the memory. + phys: Weak, + banks_mb: [u32; BANKS], + claim: Arc>>>, + state: Mutex, +} + +struct NodeZeroState { + number: u16, + /// The cached alias size in bytes. + calias: u64, + /// Whether the RAM beyond the alias is sent through the claim. + away: bool, +} + +impl NodeZero { + /// The number the node last held. + pub fn number(&self) -> u16 { + self.state.lock().number + } + + /// The node holds `number` and its cached alias spans `calias` bytes + /// (the node's number changed, or the alias did). + pub fn update(&self, number: u16, calias: u64) { + let mut st = self.state.lock(); + st.number = number; + st.calias = calias; + self.apply(&mut st); + } + + fn apply(&self, st: &mut NodeZeroState) { + let Some(phys) = self.phys.upgrade() else { return }; + let claim = self.claim.lock().as_ref().and_then(Weak::upgrade); + let away = st.number != 0 && claim.is_some(); + if !away && !st.away { + return; + } + let ppmem = phys.ppmem_space(); + if let Some(sp) = ppmem { + sp.clear_mappings(); + } + let mut index = 0; + for (bank, &mb) in self.banks_mb.iter().enumerate() { + if mb == 0 { + continue; + } + let mut src = 0u64; + for (at, len) in bank_runs(bank, mb) { + let end = at + len; + // The part of the run past the alias goes through the claim; + // the rest, and every run when the number is zero, is the + // bank. + let split = if away { st.calias.clamp(at, end) } else { end }; + if split > at { + let first = at >> SLOT_SHIFT; + phys.fill_slots(first, (split - at) >> SLOT_SHIFT, phys.bank_view(index, at.wrapping_sub(src))); + } + if split < end { + if let Some(claim) = &claim { + phys.map(split, end, claim.clone(), "node zero beyond the cached alias"); + } + } + if !away { + if let Some(sp) = ppmem { + if end <= sp.window_size() { + let _ = sp.map_range(index, src, at, len); + } + } + } + src += len; + } + index += 1; + } + st.away = away; + } +} + #[cfg(test)] mod tests; diff --git a/src/ip27/tests.rs b/src/ip27/tests.rs index 7015539ce..bbed90abf 100644 --- a/src/ip27/tests.rs +++ b/src/ip27/tests.rs @@ -86,3 +86,55 @@ fn hub_special_repeats_memory_and_the_flash_at_the_named_node_offsets_too() { } assert_eq!(special.read8(0x4000_0000).status, BUS_ERR, "nothing between the windows"); } + +/// A view that answers every read with its address, as the Hub's +/// node-qualified memory does for the Hub it names. +struct Echo; +impl BusDevice for Echo { + fn read64(&self, a: u64) -> BusRead64 { + BusRead64::ok(a) + } +} + +#[test] +fn cached_references_to_other_node_numbers_go_to_the_view() { + let mem = NodeMemory::new([64, 0, 0, 0]).unwrap(); + mem.route_nodes(Arc::new(Echo)); + let phys = mem.physical(); + // Node zero's window is the node's own memory. + assert_eq!(phys.write64(0x1000, 0x55), BUS_OK); + assert_eq!(phys.read64(0x1000).data, 0x55); + // Every other node number reaches the view with the number still in the + // address. + assert_eq!(phys.read64(0x1_0000_1000).data, 0x1_0000_1000); + assert_eq!(phys.read64(0x7_0000_2000).data, 0x7_0000_2000); + assert_eq!(phys.read64(0xff_0000_3000).data, 0xff_0000_3000); +} + +#[test] +fn a_node_that_holds_another_number_sends_node_zero_beyond_the_cached_alias_to_the_view() { + let mem = NodeMemory::new([128, 0, 0, 0]).unwrap(); + let phys = mem.physical(); + let echo: Arc = Arc::new(Echo); + let zero = mem.node_zero(); + mem.route_nodes(echo.clone()); + let alias = 64 << 20; + assert_eq!(phys.write64(alias + 0x1000, 0x77), BUS_OK); + // Holding zero changes nothing. + zero.update(0, alias); + assert_eq!(phys.read64(alias + 0x1000).data, 0x77); + // Holding one: the alias stays local, memory beyond it reaches the view + // with node zero in the address. + zero.update(1, alias); + assert_eq!(phys.write64(0x1000, 0x55), BUS_OK); + assert_eq!(phys.read64(0x1000).data, 0x55); + assert_eq!(phys.read64(alias + 0x1000).data, alias + 0x1000); + // A larger alias keeps more of it local, a smaller one less. + zero.update(1, 96 << 20); + assert_eq!(phys.read64(alias + 0x1000).data, 0x77); + assert_eq!(phys.read64((96 << 20) + 0x1000).data, (96 << 20) + 0x1000); + // The number back to zero returns all of it. + zero.update(0, alias); + assert_eq!(phys.read64(alias + 0x1000).data, 0x77); + assert_eq!(phys.read64(0x1000).data, 0x55); +} diff --git a/src/ip27_machine.rs b/src/ip27_machine.rs index 9ccec4e7c..73bfd0a98 100644 --- a/src/ip27_machine.rs +++ b/src/ip27_machine.rs @@ -284,6 +284,16 @@ struct Hooks { name: &'static str, cpu: Weak, events: Sender, + /// Where a cached reference naming node zero goes, as the node's number + /// and cached alias set it. + zero: Arc, + pi: Weak, +} +impl Hooks { + fn route(&self, number: u16) { + let calias = self.pi.upgrade().map_or(0, |pi| pi.calias_bytes()); + self.zero.update(number, calias); + } } impl HubHooks for Hooks { fn cpu_enable(&self, slice: usize, enabled: bool) { @@ -297,10 +307,12 @@ impl HubHooks for Hooks { cpu.as_device().signal(Signal::Reset(true)); } } + fn cached_alias_changed(&self, bytes: u64) { + let number = self.zero.number(); + self.zero.update(number, bytes); + } fn node_number_changed(&self, number: u16) { - if number != 0 { - crate::dlog!(LogModule::Hub, "{}: node number {number}: cached accesses still use node-relative offsets", self.name); - } + self.route(number); } fn machine_restart(&self) { let _ = self.events.send(MachineEvent::HardReset); @@ -375,10 +387,10 @@ impl Ip27Machine { None => onewire.add(Box::new(w1::Ds2505::new(0x0b, serial, &[]))), } hub.md().set_onewire(onewire); - hub.set_space(Space::Memory, memory.physical().clone()); + hub.set_space(Space::Memory, memory.home()); hub.set_space( Space::HubSpecial, - ip27::hub_special(memory.physical().clone(), &flash, memory.back_door(), memory.back_door()), + ip27::hub_special(memory.home(), &flash, memory.back_door(), memory.back_door()), ); hub.set_space(Space::Io, hub.io_space()); hub.set_space(Space::Special, Arc::new(Unmodelled("memory-special space"))); @@ -396,7 +408,14 @@ impl Ip27Machine { for (node, spec) in parts.into_iter().zip(&specs) { let cpu = Self::build_cpu(cfg, &spec.module, &node, spec.hub.cpu_base, specs.len() > 1)?; node.hub.pi().set_interrupt_controller(Arc::new(CpuIrq(Arc::downgrade(&cpu)))); - node.hub.set_hooks(Arc::new(Hooks { name: B::NAME, cpu: Arc::downgrade(&cpu), events: tx.clone() })); + let zero = node.memory.node_zero(); + node.hub.set_hooks(Arc::new(Hooks { + name: B::NAME, + cpu: Arc::downgrade(&cpu), + events: tx.clone(), + zero, + pi: Arc::downgrade(node.hub.pi()), + })); node.hub.register_locks(); cpu.register_locks(); units.push(Unit { node, cpu }); diff --git a/src/ip27_machine/tests.rs b/src/ip27_machine/tests.rs index 33e812d6a..0ffe63ae0 100644 --- a/src/ip27_machine/tests.rs +++ b/src/ip27_machine/tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::config::{GraphicsBoard, NetMode}; -use crate::dev::hub_pi; +use crate::dev::{hub_ni, hub_pi}; use crate::traits::BUS_OK; struct Scratch(PathBuf); @@ -224,6 +224,10 @@ fn a_dropped_machine_frees_its_hub_memory_and_timers() { big_stack(|| { let dir = Scratch::new("free"); let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + // A single node has nothing that holds its Hub from its own memory + // map. Route the other node numbers through the Hub, as a board with + // a cable does, so that only the release in `Drop` can free it. + m.node().memory.route_nodes(m.node().hub.claim(Space::Memory)); let hub = Arc::downgrade(&m.node().hub); let memory = Arc::downgrade(m.node().memory.physical()); let timers = Arc::downgrade(&m.node().timers); @@ -237,3 +241,49 @@ fn a_dropped_machine_frees_its_hub_memory_and_timers() { assert_eq!(bridge.strong_count(), 0, "the Bridge"); }); } + +/// The PROM of a board's second node keeps its stack in uncached Hub-special +/// memory above the 4 KiB cached alias it programs, and renumbers the node +/// between saving its return address there and reloading it. The Hub-special +/// space and the memory claimed by the node's own number stay the node's own +/// memory; only node zero's window beyond the alias leaves the node, for the +/// Hub that holds zero, of which there is none here. +#[test] +fn renumbering_moves_only_node_zero_beyond_the_alias_off_the_node() { + big_stack(|| { + let dir = Scratch::new("renumber"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + let hub = &m.node().hub; + m.node().memory.route_nodes(hub.claim(Space::Memory)); + const SLOT: u64 = 0x1bef_e78; + assert_eq!(hub.write64(hub_pi::reg::CALIAS_SIZE, 1), BUS_OK); + assert_eq!(hub.cpu_special().write64(SLOT, 0x5a5a), BUS_OK); + assert_eq!(hub.write64(hub_ni::reg::STATUS, 1 << 8), BUS_OK); + assert_eq!(hub.cpu_special().read64(SLOT).data, 0x5a5a, "the stack slot"); + assert_eq!(hub.claim(Space::Memory).read64((1 << 32) + SLOT).data, 0x5a5a, "node 1 by number"); + assert_ne!(m.node().memory.physical().read64(SLOT).status, BUS_OK, "cached, node zero"); + assert_ne!(hub.cpu_memory().read64(SLOT).status, BUS_OK, "uncached memory, node zero"); + }); +} + +/// A store-conditional is a compare-and-swap once processors share memory, +/// and a cached reference to a node by number reaches it through the Hub. +/// Without one there, the processor would fall back to a plain store and two +/// nodes could both take the same lock. +#[test] +fn a_reference_by_node_number_can_compare_and_swap() { + big_stack(|| { + let dir = Scratch::new("cas"); + let m = Origin200Machine::new(&config(&dir.0)).unwrap(); + let hub = &m.node().hub; + m.node().memory.route_nodes(hub.claim(Space::Memory)); + assert_eq!(hub.write64(hub_ni::reg::STATUS, 1 << 8), BUS_OK); + let phys = m.node().memory.physical(); + const WORD: u64 = (1 << 32) + 0x61_c000; + assert_eq!(phys.write64(WORD, 0x99), BUS_OK); + assert_eq!(phys.cas64(WORD, 0x99, 0x9a), Some(true)); + assert_eq!(phys.cas64(WORD, 0x99, 0x9b), Some(false)); + assert_eq!(phys.cas32(WORD + 4, 0x9a, 0x9c), Some(true)); + assert_eq!(hub.cpu_special().read64(0x61_c000).data, 0x9c); + }); +} From 3caed8b7284cae0d8599b0aece741cada0fd7390 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 06:12:57 -0700 Subject: [PATCH 37/38] ip27: add the Origin 2000 deskside Add the second SN0 product: one or two node boards on the deskside midplane, selected with machine.profile = "origin2000_ip27" and [origin2000] nodes. The board is data (slot identity 7 and 6, Hub 2.3, the flash with no machine type) plus its wiring: each node hosts a Crossbow whose port to the Hub is gated by link state, the first carries the BaseIO (a Bridge with the Origin 200's PCI population) at widget 8, the module has one controller with an I2C adapter per node, and two Hubs are joined by the direct cable with their cached windows routed through each other. The Bridge is built with the Hub that owns its Crossbow, so it reaches one Hub. One or two nodes boot the stock PROM to the menu and IRIX 6.5.30 to a serial login. Both are in the guide. --- CHANGELOG.md | 7 + FEATURES.md | 17 ++- docs/origin2000-ip27.md | 48 +++++++ origin2000.toml.example | 38 ++++++ src/config.rs | 82 +++++++++--- src/ip27_origin2000.rs | 249 +++++++++++++++++++++++++++++++++++ src/ip27_origin2000/tests.rs | 175 ++++++++++++++++++++++++ src/lib.rs | 1 + src/machine.rs | 8 +- src/workstation.rs | 1 + 10 files changed, 604 insertions(+), 22 deletions(-) create mode 100644 docs/origin2000-ip27.md create mode 100644 origin2000.toml.example create mode 100644 src/ip27_origin2000.rs create mode 100644 src/ip27_origin2000/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5a61857..08a5a2a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ 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. +### Origin 2000 + +- `machine.profile = "origin2000_ip27"` runs the stock IP27 PROM on one or + two Hub nodes of an Origin 2000 deskside, the second joined by the direct + cable. One node boots IRIX 6.5.30 to a serial login. See + [the guide](docs/origin2000-ip27.md). + ### Origin 200 - `machine.profile = "origin200_ip29"` runs the stock IP27 PROM on one Hub diff --git a/FEATURES.md b/FEATURES.md index ea403a290..50d95f095 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -175,10 +175,23 @@ 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)). | -| `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: r10000 (`r10000_180`), r12000 (`r12000_270`). The first listed is the default. | +| `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)). | +| `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 and Origin 2000: r10000 (`r10000_180`), r12000 (`r12000_270`). The first listed is the default. | | `machine.cpus` | `1` | Processors fitted. 2 only on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). | +### [origin2000] + +Origin 2000 settings apply when `machine.profile = "origin2000_ip27"`. + +| Key | Default | Values and behavior | +|---|---|---| +| `nodes` | `1` | Node boards: 1 or 2. Two are joined by the direct cable. Every node gets the `banks` memory. | +| `flash` | unset | Persistent 1 MiB node boot-flash file of the first node; the second keeps its own at the name with `.1` added. Unset, the flash is volatile. | +| `controller_store` | unset | Persistent file for the module controller's configuration store. Unset, it is volatile. | + +An `[origin2000]` table is written to a saved config only when one of these +keys is set. See the [Origin 2000 guide](docs/origin2000-ip27.md). + ### [graphics] | Key | Default | Values and behavior | diff --git a/docs/origin2000-ip27.md b/docs/origin2000-ip27.md new file mode 100644 index 000000000..d2ca30d46 --- /dev/null +++ b/docs/origin2000-ip27.md @@ -0,0 +1,48 @@ +# SGI Origin 2000 deskside (IP27 node boards) + +The Origin 2000 deskside is one or two IP27 node boards on the module +midplane. Each node is a Hub with its memory, boot flash and one R10000 or +R12000, and the host of its own Crossbow. The first Crossbow carries the BaseIO +card (a Bridge with two ISP1040s and an IOC3). With two nodes the Hubs are +joined by a direct cable, the Null Router. The machine runs headless on the +IOC3's first serial port. + +## Configuration + +Copy [`origin2000.toml.example`](../origin2000.toml.example) to +`origin2000.toml`, provide the PROM and disk named there, and run +`iris --config origin2000.toml`. The sample selects +`machine.profile = "origin2000_ip27"`. + +- `[origin2000] nodes` is 1 or 2. The memory in `banks` is fitted to every + node. +- `machine.cpu` is `r10000` or `r12000`. Each node has one processor. +- `[origin2000] flash` and `controller_store` keep the boot flash (the second + node's at the same name with `.1` added) and the module controller's store + between runs. +- `prom` takes the stock IP27 PROM, as for the [Origin 200](origin200-ip29.md). + +Node slot 1 drives raw slot identity 7 into its Hub and slot 2 drives 6. The +first node's Crossbow has the BaseIO at widget 8 and the Hub at widget 9; the +second's has the Hub at widget 10. The module has one controller, reached +through an I2C adapter per node. + +## Limits + +- One node boots the stock PROM to the System Maintenance Menu and IRIX + 6.5.30 to a serial login. +- Two nodes: the PROM finds both Hubs, numbers them, chooses the first as the + global master, passes its diagnostics with both processors and 256 MB and + reaches the menu. IRIX then stops with "Processor #1 did not start": the + second node's processor takes a TLB exception in the PROM (the master + reports "has taken an exception"), so it never waits to be launched. +- The BaseIO's boot flash is not fitted; the PROM falls back to its own copy + of the I/O PROM. +- The BaseIO is the only board; the second Crossbow has no widgets but its + node. +- The module's midplane one-wire part is not fitted. +- A node that holds a nonzero number sends cached references to node zero's + window beyond its cached alias to the Hub that holds zero, and its own RAM + is then reached through its own number only. The second node gives up the + direct memory window for it, so its cached references are slower. +- Snapshots are not supported. diff --git a/origin2000.toml.example b/origin2000.toml.example new file mode 100644 index 000000000..dce3ae4c5 --- /dev/null +++ b/origin2000.toml.example @@ -0,0 +1,38 @@ +# SGI Origin 2000 deskside (IP27 node boards), serial-only setup. +# Copy to origin2000.toml and run from the repository root: +# iris --config origin2000.toml +# Supply the IP27 PROM image (the stock 6.156 image from the IRIX 6.5.30 +# distribution) and, to boot IRIX, a raw IRIX 6.5.30 disk. No images are +# included. Convert QCOW2 disks to raw first. + +prom = "ignore/origin2000/ip27prom.img" +nvram = "ignore/origin2000/nvram.bin" +# One bank of a DIMM pair per node, in MB: 64, 128, 256 or 512 each. +banks = [128, 0, 0, 0] +headless = true +no_audio = true +monitor_port = 18988 +# The IOC3's first port is the console. +serial_port_a = 18980 +serial_port_b = 18981 + +[machine] +profile = "origin2000_ip27" +# r10000 (180 MHz, 1 MiB secondary cache) or r12000 (270 MHz, 4 MiB). +cpu = "r10000" + +[origin2000] +# One or two node boards; two are joined by the direct cable. +nodes = 1 +flash = "ignore/origin2000/flash.raw" +controller_store = "ignore/origin2000/controller.raw" + +[network] +# Disconnected cable. For NAT, use mode = "nat". +mode = "off" + +[scsi.1] +path = "ignore/origin2000/irix.raw" +cdrom = false +# Keep the base disk unchanged. Guest writes go to irix.raw.overlay. +overlay = true diff --git a/src/config.rs b/src/config.rs index 78da262be..4704b4792 100644 --- a/src/config.rs +++ b/src/config.rs @@ -619,6 +619,10 @@ pub enum MachineProfile { /// IOC3. Configured by the shared keys and `[origin200]`; see /// `src/ip27_machine.rs`. Origin200Ip29, + /// SGI Origin 2000 deskside: one or two IP27 node boards, each the host + /// of a Crossbow, the first carrying the BaseIO. Configured by the shared + /// keys and `[origin2000]`; see `src/ip27_origin2000.rs`. + Origin2000Ip27, } impl MachineProfile { @@ -639,7 +643,7 @@ impl MachineProfile { pub fn bank_sizes(self) -> &'static [u32] { match self { Self::OctaneIp30 => &[0, 64, 128, 256, 512, 1024, 2048], - Self::Origin200Ip29 => &[0, 64, 128, 256, 512], + Self::Origin200Ip29 | Self::Origin2000Ip27 => &[0, 64, 128, 256, 512], Self::O2Ip32 => &[0, 32, 64, 128, 256], Self::Indigo2Ip28 => MC_BANK_SIZES, _ => &[0, 8, 16, 32, 64, 128], @@ -658,7 +662,7 @@ impl MachineProfile { pub fn prom_path(self, prom: &str) -> String { match self { Self::OctaneIp30 => prom_file(prom, crate::ip30_machine::OCTANE_PROM_FILE), - Self::Origin200Ip29 => prom_file(prom, crate::ip27_machine::ORIGIN200_PROM_FILE), + Self::Origin200Ip29 | Self::Origin2000Ip27 => prom_file(prom, crate::ip27_machine::ORIGIN200_PROM_FILE), Self::O2Ip32 => prom_file(prom, crate::ip32_machine::O2_PROM_FILE), _ => prom.to_string(), } @@ -666,13 +670,14 @@ impl MachineProfile { /// Every profile, in the order `workstation::build` lists them. `ALL` is /// only the ones the GUI offers. - pub const EVERY: [Self; 6] = [ + pub const EVERY: [Self; 7] = [ Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28, Self::O2Ip32, Self::OctaneIp30, Self::Origin200Ip29, + Self::Origin2000Ip27, ]; /// The name `[machine] profile` and `--machine` give this profile. @@ -684,6 +689,7 @@ impl MachineProfile { Self::O2Ip32 => "o2_ip32", Self::OctaneIp30 => "octane_ip30", Self::Origin200Ip29 => "origin200_ip29", + Self::Origin2000Ip27 => "origin2000_ip27", } } @@ -739,6 +745,7 @@ impl MachineProfile { Self::O2Ip32 => "SGI O2 (IP32)", Self::OctaneIp30 => "SGI Octane (IP30)", Self::Origin200Ip29 => "SGI Origin 200 (IP29)", + Self::Origin2000Ip27 => "SGI Origin 2000 (IP27)", } } @@ -748,7 +755,7 @@ impl MachineProfile { /// connector), one on the Indy. pub fn scsi_controllers(self) -> u8 { match self { - Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 => 2, + Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 => 2, _ => 1, } } @@ -760,7 +767,7 @@ impl MachineProfile { /// purpose. pub fn max_scsi_id(self) -> u8 { match self { - Self::OctaneIp30 | Self::Origin200Ip29 => 15, + Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 => 15, _ => 7, } } @@ -780,7 +787,7 @@ impl MachineProfile { pub fn cpus(self) -> &'static [&'static Part] { match self { Self::OctaneIp30 => crate::ip30_machine::CPUS, - Self::Origin200Ip29 => crate::ip27_machine::CPUS, + Self::Origin200Ip29 | Self::Origin2000Ip27 => crate::ip27_machine::CPUS, Self::O2Ip32 => crate::ip32_machine::CPUS, Self::Indigo2Ip28 => crate::machine::IP28_CPUS, Self::IndyIp24 | Self::Indigo2Ip22 => crate::machine::CPUS, @@ -895,7 +902,7 @@ impl GraphicsBoard { pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, - MachineProfile::Origin200Ip29 => self == Self::None, + MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 => self == Self::None, MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -905,7 +912,7 @@ impl GraphicsBoard { pub fn default_for(profile: MachineProfile) -> Self { match profile { MachineProfile::OctaneIp30 => Self::SolidImpact, - MachineProfile::Origin200Ip29 => Self::None, + MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 => Self::None, _ => Self::Newport, } } @@ -1250,6 +1257,39 @@ impl Origin200Section { } } +/// `[origin2000]`: the Origin 2000 deskside's node count and persistent +/// stores. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Origin2000Section { + /// Node boards: 1 or 2. The second joins the first by the direct cable. + #[serde(default = "default_origin2000_nodes")] + pub nodes: u8, + /// Persistent 1 MiB node boot flash of the first node; the second keeps + /// its own beside it, at the name with `.1` added. Absent means volatile. + #[serde(default)] + pub flash: Option, + /// Persistent file for the module's one controller store. Absent means + /// volatile. + #[serde(default)] + pub controller_store: Option, +} + +fn default_origin2000_nodes() -> u8 { 1 } + +impl Default for Origin2000Section { + fn default() -> Self { + Self { nodes: default_origin2000_nodes(), flash: None, controller_store: None } + } +} + +impl Origin2000Section { + /// Left out of a written config when nothing in it was set. + fn is_default(&self) -> bool { + self.nodes == default_origin2000_nodes() && self.flash.is_none() && self.controller_store.is_none() + } +} + /// An entry of the deprecated `o2.disks` list; see [`O2Section::disks`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -1950,6 +1990,9 @@ pub struct MachineConfig { /// Persistent flash and controller store for the Origin 200. #[serde(default, skip_serializing_if = "Origin200Section::is_default")] pub origin200: Origin200Section, + /// Node count and persistent stores for the Origin 2000 deskside. + #[serde(default, skip_serializing_if = "Origin2000Section::is_default")] + pub origin2000: Origin2000Section, /// Host performance tuning (`[perf]` section). #[serde(default)] @@ -2180,6 +2223,7 @@ impl Default for MachineConfig { o2: O2Section::default(), octane: OctaneSection::default(), origin200: Origin200Section::default(), + origin2000: Origin2000Section::default(), perf: PerfConfig::default(), clock: ClockConfig::default(), rtc_offset: RtcOffset::default(), @@ -2230,6 +2274,7 @@ impl MachineConfig { // The Octane has a PCI bridge with a free slot. MachineProfile::OctaneIp30 => Ok(()), MachineProfile::Origin200Ip29 => Err("[virtio_serial] is not wired on the Origin 200".into()), + MachineProfile::Origin2000Ip27 => Err("[virtio_serial] is not wired on the Origin 2000".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)" @@ -2429,6 +2474,8 @@ impl MachineConfig { format!("graphics.board \"{name}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)") } else if self.machine.profile == MachineProfile::Origin200Ip29 { format!("graphics.board \"{name}\" is not modelled on the Origin 200 (valid: none)") + } else if self.machine.profile == MachineProfile::Origin2000Ip27 { + format!("graphics.board \"{name}\" is not modelled on the Origin 2000 (valid: none)") } else { format!("graphics.board \"{name}\" is only valid on the Octane") }); @@ -2470,7 +2517,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) { + if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27) { return Err(format!( "scsi.{}: kind = \"daynaport\" is not available on the O2 (its Ethernet is built in)", key )); @@ -2507,12 +2554,13 @@ impl MachineConfig { } } crate::dev::crime_mem::check_kits(&self.o2_kits()).map_err(|e| format!("banks: {e}"))?; - } else if self.machine.profile == MachineProfile::Origin200Ip29 { - // The System Board has four banks of a DIMM pair each, filled from + } else if matches!(self.machine.profile, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27) { + let name = if self.machine.profile == MachineProfile::Origin200Ip29 { "Origin 200" } else { "Origin 2000" }; + // The node has four banks of a DIMM pair each, filled from // bank 0 without a gap. let banks = &self.banks[..bank_count]; if banks[0] == 0 { - return Err("banks: an Origin 200 needs memory in bank 0".into()); + return Err(format!("banks: an {name} needs memory in bank 0")); } if let Some(gap) = banks.iter().position(|&b| b == 0) { if banks[gap..].iter().any(|&b| b != 0) { @@ -2521,7 +2569,7 @@ impl MachineConfig { } for (i, &sz) in banks.iter().enumerate() { if !MachineProfile::Origin200Ip29.bank_sizes().contains(&sz) { - return Err(format!("bank{i} size {sz} MB is invalid for an Origin 200 (valid: 64, 128, 256, 512)")); + return Err(format!("bank{i} size {sz} MB is invalid for an {name} (valid: 64, 128, 256, 512)")); } // The stock PROM disables a 256 MB bank whenever the processor // clock is 180 MHz, which leaves a bank 0 of that size no memory. @@ -2574,8 +2622,8 @@ impl MachineConfig { } } match (self.network.mode, self.machine.profile) { - (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::Origin200Ip29 => { - return Err("network.mode = \"off\" is only available on the O2, the Octane and the Origin 200 for now; the Indy and Indigo2 always have their Ethernet".into()); + (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::Origin200Ip29 && p != MachineProfile::Origin2000Ip27 => { + return Err("network.mode = \"off\" is only available on the O2, the Octane 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() @@ -3639,7 +3687,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)"); + assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, origin200_ip29, origin2000_ip27)"); let list = MachineProfile::list(); assert_eq!(list.lines().count(), MachineProfile::EVERY.len()); assert!(list.starts_with("indy_ip24 SGI Indy (IP24)\n"), "{list}"); @@ -3648,7 +3696,7 @@ mod export_tests { for p in MachineProfile::EVERY { match p { MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 - | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 => {} + | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 => {} } let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"{}\"", p.name())).unwrap(); assert_eq!(cfg.machine.profile, p); diff --git a/src/ip27_origin2000.rs b/src/ip27_origin2000.rs new file mode 100644 index 000000000..499d3dcab --- /dev/null +++ b/src/ip27_origin2000.rs @@ -0,0 +1,249 @@ +//! The Origin 2000 deskside: one or two IP27 node boards on the module +//! midplane, each the host of its own Crossbow, the first Crossbow carrying +//! the BaseIO card, and with two nodes the Hubs joined by a direct CrayLink +//! cable, the Null Router. +//! +//! Each node board's slot identity is what the midplane drives into the Hub: +//! raw 7 in node slot 1 and 6 in slot 2. Node 1's Crossbow takes its node at +//! widget 9 and the BaseIO at widget 8; node 2's takes its node at widget 10 +//! and nothing else. +//! +//! # What is approximated here +//! +//! - The module has one controller, reached through an I2C adapter per node. +//! Each adapter completes a byte inside the register access that starts it, +//! so the nodes cannot contend for the bus bit by bit +//! ([`crate::dev::o2000_module`]). +//! - The BaseIO is a Bridge with the Origin 200's PCI population: two +//! ISP1040s and an IOC3 with the DP83840A transceiver model. The card's +//! boot flash is not fitted, so its flash window fails, and the BASEIO PROM +//! loads from the copy in the node PROM's image. +//! - The module's midplane one-wire part is not fitted, and the Hub's line +//! reaches only the node's own net. +//! - Node 2 has no I/O of its own: a deskside's second Crossbow carries the +//! other six XIO slots, none of them fitted here. +//! - The Bridge reaches only the Hub that owns its Crossbow, node 1's. A +//! Bridge that two Hubs can address is not modelled. +//! - Cached accesses to node numbers 1 and up route through the Hub, and a +//! number no Hub holds fails. A node that holds a nonzero number sends node +//! zero's window beyond its cached alias to the Hub that holds zero. +//! +//! Ported from hw/mips/sgi/sn-machine.c in jamesbraid/qemu-sgi-origami. + +use crate::config::MachineConfig; +use crate::dev::bridge::Bridge; +use crate::dev::elsc::ElscEvent; +use crate::dev::elsc_nvram::ElscNvram; +use crate::dev::hub::{Hub, HubConfig, Space}; +use crate::dev::hub_md::SlotSignals; +use crate::dev::ioc3::Ioc3Config; +use crate::dev::ioc3_eth::Ioc3Eth; +use crate::dev::node_flash::FlashValues; +use crate::dev::o2000_module::{ModuleConfig, ModuleEvent, Origin2000Module}; +use crate::traits::Resettable; +use crate::dev::w1; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; +use crate::dev::xbow::Xbow; +use crate::ip27_machine::{cpu_module, pci_devices, BoardIo, BoardSpec, Ip27Board, Ip27Machine, ModuleController, Node, NodeRecord}; +use crate::xtalk::{Upstream, XtalkHost}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +pub type Origin2000Machine = Ip27Machine; + +/// Node boards the deskside midplane has. +pub const MAX_NODES: usize = 2; +/// The Crossbow's widget for the BaseIO card, in XIO slot 1. +const WIDGET_BASEIO: u8 = 8; +/// The Crossbow's widget for a node board's Hub: 9 in node slot 1, 10 in 2. +const fn widget_hub(node: usize) -> u8 { + 9 + node as u8 +} +/// What node slot `node` (0 or 1) drives onto the Hub's slot identity: 7 for +/// the first slot, 6 for the second. +const fn slot_bits(node: usize) -> u8 { + 7 - node as u8 +} +/// The Crossbow revision: 1.1, the earliest the kernel accepts. +const XBOW_REVISION: u32 = 2; +/// The BaseIO's Bridge revision. +const BRIDGE_REVISION: u32 = 4; +/// The Bridge input the BaseIO's IOC3 SuperIO interrupt is wired to. +const SUPERIO_PIN: u8 = 4; +/// The BaseIO's PCI subsystem identification. +const SUBSYS_BASEIO: u16 = 0xc302; + +/// The Hub as its Crossbow and the widgets below address it. +const fn hub_host(node: usize) -> XtalkHost { + XtalkHost { widget: widget_hub(node), int_packet: 0, int_packet_mask: 0, vectors: 128 } +} + +/// The module number the controller's store starts with. +const MODULE_NUMBER: u8 = 1; +/// The module's serial and its identity part's ROM serial: synthetic, so an +/// inventory from the model never reads as a real unit. +const MODULE_SERIAL: &str = "EMU0000001"; +const MODULE_IDENTITY_SERIAL: u64 = 0x5145_4d55_4d00; + +/// The module management as the machine drives it: the adapters to reset +/// with the module, and the outputs to drain. +struct ModuleManagement { + module: Arc, + adapters: usize, +} + +impl ModuleController for ModuleManagement { + /// A module reset leaves the controller alone and returns the nodes' + /// adapters to power-on. + fn reset(&self) { + for i in 0..self.adapters { + self.module.adapter(i).power_on(); + } + } + fn take_events(&self) -> Vec { + self.module + .take_events() + .into_iter() + .filter_map(|e| match e { + ModuleEvent::Power(on) => Some(ElscEvent::Power(on)), + ModuleEvent::Reset(true) => Some(ElscEvent::Reset), + ModuleEvent::Reset(false) => None, + ModuleEvent::Nmi(level) => Some(ElscEvent::Nmi(level)), + }) + .collect() + } +} + +pub struct Origin2000; + +impl Origin2000 { + /// The BaseIO's board record on its Bridge's one-wire line. The barcode + /// and ROM serial are synthetic. + fn board_record() -> w1::Bus { + let mut bus = w1::Bus::new(); + let record = BoardRecord { barcode: "EMUB00", part: "030-1124-002", revision: "J", name: "BASEIO" }; + bus.add(Box::new(board_record_part(&record, 0x5145_4d55_4200))); + bus + } + + fn ioc3_config(mac: [u8; 6]) -> Ioc3Config { + Ioc3Config { + subsystem_id: SUBSYS_BASEIO, + pci_policy: Default::default(), + reset_pci_on_power_on: false, + mac, + serial: [true, true], + parallel: false, + keyboard_mouse: false, + rtc: false, + gpio_input_mask: 0, + gpio_input_values: 0, + records: Vec::new(), + } + } + + /// A file for node `node` beside the one the configuration names for the + /// first. + fn per_node(path: &Option, node: usize) -> Option { + path.as_ref().map(|p| if node == 0 { PathBuf::from(p) } else { PathBuf::from(format!("{p}.{node}")) }) + } + + /// The module controller: one for the module, in front of every node's + /// I2C controller. + fn controller(nodes: &[&Node], cfg: &MachineConfig) -> Result, String> { + let store = ElscNvram::origin2000(MODULE_NUMBER, cfg.origin2000.controller_store.as_deref().map(Path::new)) + .map(Arc::new) + .map_err(|e| format!("origin2000.controller_store: {e}"))?; + let module = Origin2000Module::new(ModuleConfig { module_number: MODULE_NUMBER, ..Default::default() }, store, nodes[0].clock.clone()); + for (i, node) in nodes.iter().enumerate() { + node.hub.attach_i2c(module.adapter(i)); + } + module + .attach_parts(MODULE_SERIAL, MODULE_IDENTITY_SERIAL, None) + .map_err(|e| format!("module identity: {e}"))?; + module.power_on(); + Ok(Arc::new(ModuleManagement { module, adapters: nodes.len() })) + } +} + +impl Ip27Board for Origin2000 { + const NAME: &'static str = "Origin 2000"; + + fn specs(cfg: &MachineConfig) -> Result, String> { + let module = *cpu_module(cfg.cpu_part()?).ok_or("an Origin 2000 has an R10000 or an R12000")?; + let nodes = usize::from(cfg.origin2000.nodes); + if !(1..=MAX_NODES).contains(&nodes) { + return Err(format!("origin2000.nodes {nodes}: the deskside has one or two node boards")); + } + Ok((0..nodes) + .map(|node| BoardSpec { + // Hub 2.3 on the Origin 2000 module. + hub: HubConfig { + cpus: 1, + // Global processor numbers run two to a node slot. + cpu_base: 2 * node, + ni_revision: 5, + slot: SlotSignals { slot: Some(slot_bits(node)), slave_module: false }, + ..Default::default() + }, + module, + banks_mb: std::array::from_fn(|i| cfg.banks[i]), + flash: FlashValues { + cpu_mode_bits: module.mode_word, + cpu_clock_hz: module.clock_hz(), + // Zero in the stock image: the PROM chooses the module's + // Crossbow tables rather than the Origin 200's. + machine_type: 0, + ..FlashValues::ORIGIN200_R10000_180_1M + }, + flash_file: Self::per_node(&cfg.origin2000.flash, node), + node_record: Some(NodeRecord::IP27), + }) + .collect()) + } + + fn wire(nodes: &[&Node], cfg: &MachineConfig) -> Result { + let controller = Self::controller(nodes, cfg)?; + let mut io = None; + for (i, node) in nodes.iter().enumerate() { + let host = hub_host(i); + let mut widgets = 1u16 << host.widget; + if i == 0 { + widgets |= 1 << WIDGET_BASEIO; + } + let xbow = Arc::new(Xbow::new(XBOW_REVISION, widgets)); + xbow.connect_host(host, node.hub.interrupt_target()); + node.hub.io().connect_widget(0, xbow.clone()); + node.hub.io().connect_widget(host.widget, xbow.port(host.widget, node.hub.widget_face())); + xbow.register_locks(); + if i == 0 { + let bridge = Bridge::new(Upstream::single(host, node.hub.inbound(), node.hub.interrupt_target())); + bridge.set_revision(BRIDGE_REVISION); + bridge.set_microlan(Self::board_record()); + let links = xbow.clone(); + bridge.set_link_alive(Box::new(move |w| links.carries(w))); + let weak = Arc::downgrade(&bridge); + xbow.on_link_reset(Box::new(move |w| { + if w == WIDGET_BASEIO { + if let Some(b) = weak.upgrade() { + b.link_reset(); + } + } + })); + node.hub.io().connect_widget(WIDGET_BASEIO, xbow.port(WIDGET_BASEIO, bridge.clone())); + let mac = cfg.network().mac; + io = Some(pci_devices(node, cfg, bridge, Self::ioc3_config(mac), Ioc3Eth::new_origin200(mac), controller.clone(), SUPERIO_PIN, "origin2000", Self::NAME)?); + } + } + if let [a, b] = nodes { + Hub::cable(&a.hub, &b.hub); + for node in nodes { + node.memory.route_nodes(node.hub.claim(Space::Memory)); + } + } + io.ok_or_else(|| "an Origin 2000 has at least one node".to_string()) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip27_origin2000/tests.rs b/src/ip27_origin2000/tests.rs new file mode 100644 index 000000000..0e817e28e --- /dev/null +++ b/src/ip27_origin2000/tests.rs @@ -0,0 +1,175 @@ +use super::*; +use crate::config::{MachineProfile, NetMode}; +use crate::cpu::parts; +use crate::dev::hub::Space; +use crate::dev::{hub_md, hub_ni}; +use crate::traits::{BusDevice, BUS_ERR, BUS_OK}; +use std::path::Path; + +struct Scratch(PathBuf); +impl Scratch { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("iris-o2k-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + Scratch(dir) + } +} +impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// A PROM stand-in: a recognisable first word and a configuration block in +/// the shape the flash initialiser patches. +fn prom(dir: &Path) -> String { + let mut p = vec![0u8; 0x1000]; + p[..4].copy_from_slice(&0x3c08_a600u32.to_be_bytes()); + p[0x68..0x70].copy_from_slice(&0x6970_3237_636f_6e66u64.to_be_bytes()); + let path = dir.join("prom.bin"); + std::fs::write(&path, p).unwrap(); + path.to_string_lossy().into_owned() +} + +fn config(dir: &Path, nodes: u8) -> MachineConfig { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::Origin2000Ip27; + cfg.set_cpu(&parts::R10000_180); + cfg.origin2000.nodes = nodes; + cfg.banks = [64, 0, 0, 0, 0, 0, 0, 0]; + cfg.prom = prom(dir); + cfg.nvram = dir.join("nvram.bin").to_string_lossy().into_owned(); + cfg.headless = true; + cfg.network.mode = NetMode::Off; + let held: Vec<_> = (0..3).map(|_| std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap()).collect(); + cfg.monitor_port = Some(held[0].local_addr().unwrap().port()); + cfg.serial_port_a = Some(held[1].local_addr().unwrap().port()); + cfg.serial_port_b = Some(held[2].local_addr().unwrap().port()); + let disk = dir.join("disk.raw"); + std::fs::write(&disk, vec![0u8; 4 << 20]).unwrap(); + cfg.scsi.clear(); + cfg.scsi.insert(1, toml::from_str(&format!("path = {:?}\noverlay = true", disk.to_string_lossy())).unwrap()); + cfg +} + +/// Building a machine puts more than a megabyte on the stack. +fn big_stack(body: impl FnOnce() + Send + 'static) { + std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join().expect("thread panicked"); +} + +fn built(name: &str, nodes: u8) -> (Scratch, Origin2000Machine) { + let dir = Scratch::new(name); + let m = Origin2000Machine::new(&config(&dir.0, nodes)).unwrap(); + (dir, m) +} + +#[test] +fn the_profile_is_chosen_by_its_snake_case_name() { + assert_eq!(MachineProfile::from_name("origin2000_ip27"), Ok(MachineProfile::Origin2000Ip27)); + assert_eq!(MachineProfile::Origin2000Ip27.name(), "origin2000_ip27"); +} + +#[test] +fn the_deskside_has_one_or_two_node_boards() { + let dir = Scratch::new("count"); + assert_eq!(config(&dir.0, 2).validate(), Ok(())); + let err = Origin2000::specs(&config(&dir.0, 3)).err().unwrap(); + assert!(err.contains("one or two node boards"), "{err}"); + assert!(Origin2000::specs(&config(&dir.0, 0)).is_err()); +} + +#[test] +fn each_node_slot_drives_its_identity_into_the_hub() { + big_stack(|| { + let (_dir, m) = built("slots", 2); + let slot = |n: usize| m.node_at(n).hub.read64(hub_md::reg::SLOTID_USTAT).data & 7; + assert_eq!(slot(0), 7); + assert_eq!(slot(1), 6); + }); +} + +#[test] +fn the_flash_carries_no_machine_type() { + big_stack(|| { + let (_dir, m) = built("flash", 1); + let word = |at: u64| m.node().hub.cpu_special().read32(0x1000_0000 + at).data; + assert_eq!(word(0x90), 0); + assert_eq!(word(0x8c), 15); + }); +} + +#[test] +fn the_first_crossbow_carries_the_baseio_and_its_node() { + big_stack(|| { + let (_dir, m) = built("xbow", 1); + let io = m.node().hub.claim(Space::Io); + let id = |w: u64| io.read32((w << 24) + 4); + // The Crossbow's identification: revision 2, bit 0 set. + assert_eq!(id(0).data, 0x2000_0001); + // The Bridge answers at widget 8, the Hub at its own port, 9. + assert_eq!(id(8).status, BUS_OK); + assert_ne!(id(8).data, id(9).data); + // A port with nothing fitted fails. + for w in [10, 11, 15] { + assert_eq!(id(w).status, BUS_ERR, "widget {w}"); + } + assert_eq!(io.write32((8 << 24) + crate::dev::bridge::reg::INT_ENABLE as u64 + 4, 0), BUS_OK); + }); +} + +#[test] +fn the_second_node_is_the_host_of_a_crossbow_with_no_io() { + big_stack(|| { + let (_dir, m) = built("second", 2); + let io = m.node_at(1).hub.claim(Space::Io); + let id = |w: u64| io.read32((w << 24) + 4); + assert_eq!(id(0).data, 0x2000_0001); + assert_eq!(id(10).status, BUS_OK); + assert_eq!(id(8).status, BUS_ERR, "the BaseIO is on the first node's Crossbow"); + assert_eq!(id(9).status, BUS_ERR); + }); +} + +#[test] +fn the_cable_joins_the_nodes_once_they_are_numbered() { + big_stack(|| { + let (_dir, m) = built("cable", 2); + let (a, b) = (&m.node_at(0).hub, &m.node_at(1).hub); + // The PROM numbers the second node 1; the first keeps 0. + assert_eq!(b.write64(hub_ni::reg::STATUS, 1 << 8), BUS_OK); + let at = (1u64 << 32) + 0x1000; + // A store through the first node's claim lands in the second's memory. + assert_eq!(a.claim(Space::Memory).write64(at, 0x1122_3344_5566_7788), BUS_OK); + assert_eq!(b.cpu_memory().read64(0x1000).data, 0x1122_3344_5566_7788); + assert_eq!(a.cpu_memory().read64(0x1000).data, 0); + // A cached reference from the first node to node 1 takes the same way. + assert_eq!(m.node_at(0).memory.physical().read64(at).data, 0x1122_3344_5566_7788); + }); +} + +#[test] +fn a_dropped_machine_frees_its_hubs_memory_and_timers() { + big_stack(|| { + let (_dir, m) = built("drop", 2); + let hubs: Vec<_> = (0..2).map(|i| Arc::downgrade(&m.node_at(i).hub)).collect(); + let memory: Vec<_> = (0..2).map(|i| Arc::downgrade(m.node_at(i).memory.physical())).collect(); + let timers: Vec<_> = (0..2).map(|i| Arc::downgrade(&m.node_at(i).timers)).collect(); + let cpus: Vec<_> = (0..2).map(|i| Arc::downgrade(m.cpu_at(i))).collect(); + let bridge = Arc::downgrade(m.bridge()); + drop(m); + for (i, w) in hubs.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "hub {i}"); + } + for (i, w) in memory.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "memory map {i}"); + } + for (i, w) in timers.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "timers {i}"); + } + for (i, w) in cpus.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "cpu {i}"); + } + assert_eq!(bridge.strong_count(), 0, "the Bridge"); + }); +} diff --git a/src/lib.rs b/src/lib.rs index 8e7887d73..f308417ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,7 @@ pub mod ip30_machine; /// the Origin 2000 and Onyx2 desksides are built from it. pub mod ip27; pub mod ip27_machine; +pub mod ip27_origin2000; /// Fuel has an IP34 board and uses the IP35 software platform and processor views. pub mod ip35; pub mod machine_state; diff --git a/src/machine.rs b/src/machine.rs index 9f776fbf9..3e92b3822 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::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27] .into_iter().find(|p| *p as u8 == raw) } @@ -69,6 +69,7 @@ fn emulator_name_thirds(profile: Option) -> &'static [&'static s Some(MachineProfile::O2Ip32) => &["IRIX", "Iris", "IPsomething", "IP32"], Some(MachineProfile::OctaneIp30) => &["IRIX", "Iris", "IPsomething", "IP30"], Some(MachineProfile::Origin200Ip29) => &["IRIX", "Iris", "IPsomething", "IP29"], + Some(MachineProfile::Origin2000Ip27) => &["IRIX", "Iris", "IPsomething", "IP27"], None => &["IRIX", "Iris", "IPsomething"], } } @@ -2170,11 +2171,12 @@ mod emulator_name_tests { Some(MachineProfile::O2Ip32) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "O2"], Some(MachineProfile::OctaneIp30) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP32", "O2"], Some(MachineProfile::Origin200Ip29) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], + Some(MachineProfile::Origin2000Ip27) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "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::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::Origin200Ip29), Some(MachineProfile::Origin2000Ip27)] { let thirds = emulator_name_thirds(p); assert!(!thirds.is_empty()); for w in thirds { @@ -2186,7 +2188,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] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27] { set_emulator_profile(p); assert_eq!(emulator_profile(), Some(p)); } diff --git a/src/workstation.rs b/src/workstation.rs index cb73271ac..e376dafdb 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -45,6 +45,7 @@ pub fn build(cfg: MachineConfig) -> Result, String> { // So is the Octane (HEART, Xbow, Bridge). crate::config::MachineProfile::OctaneIp30 => Ok(Box::new(crate::ip30_machine::OctaneMachine::new(&cfg)?)), crate::config::MachineProfile::Origin200Ip29 => Ok(Box::new(crate::ip27_machine::Origin200Machine::new(&cfg)?)), + crate::config::MachineProfile::Origin2000Ip27 => Ok(Box::new(crate::ip27_origin2000::Origin2000Machine::new(&cfg)?)), _ => Ok(Box::new(Machine::new(cfg))), } } From 81d2486416809ff79cd9aca83d305f19f07e405f Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 7 Nov 2026 07:56:40 -0700 Subject: [PATCH 38/38] ip27: add the Onyx2 deskside Add the Onyx2 deskside as a second board on the platform, selected with machine.profile = "onyx2_ip27" and the same [origin2000] section. Its midplane drives slot identity 3 and 2 into the Hubs, which the PROM reads as the deskside's node slots and so picks the single-Crossbow widget map. Both Hubs are hosts of the one Crossbow: node ports at widgets 10 and 9, the BaseIO-G at 15 and a graphics slot at 8. A packet into a peer's I/O view is routed by widget through the Crossbow, which is how the PROM reads the other Hub's node number. The graphics slot holds a test widget until a pipe is modelled; it logs and fails everything but its identification. Either Hub reaches the BaseIO, and the Bridge delivers DMA and interrupts to the Hub at the widget the guest programs, each behind its Crossbow port's link. One node boots an installed IRIX 6.5.30 to a serial login. --- CHANGELOG.md | 7 ++ FEATURES.md | 6 +- docs/onyx2-ip27.md | 35 +++++++ src/config.rs | 43 +++++--- src/ip27_onyx2.rs | 197 ++++++++++++++++++++++++++++++++++++ src/ip27_onyx2/tests.rs | 218 ++++++++++++++++++++++++++++++++++++++++ src/ip27_origin2000.rs | 14 +-- src/lib.rs | 1 + src/machine.rs | 8 +- src/workstation.rs | 1 + 10 files changed, 503 insertions(+), 27 deletions(-) create mode 100644 docs/onyx2-ip27.md create mode 100644 src/ip27_onyx2.rs create mode 100644 src/ip27_onyx2/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a5a2a9d..edc5d8c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ 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. +### Onyx2 + +- `machine.profile = "onyx2_ip27"` runs the stock IP27 PROM on one or two Hub + nodes of an Onyx2 deskside, sharing one Crossbow, with a test widget in the + graphics slot, and boots an installed IRIX 6.5.30 to a serial login with one + node. See [the guide](docs/onyx2-ip27.md). + ### Origin 2000 - `machine.profile = "origin2000_ip27"` runs the stock IP27 PROM on one or diff --git a/FEATURES.md b/FEATURES.md index 50d95f095..c0a3cf466 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -175,13 +175,13 @@ 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)). | -| `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 and Origin 2000: r10000 (`r10000_180`), r12000 (`r12000_270`). The first listed is the default. | +| `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)). | +| `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`). The first listed is the default. | | `machine.cpus` | `1` | Processors fitted. 2 only on the Octane; see [its guide](docs/octane-ip30-multiprocessor.md). | ### [origin2000] -Origin 2000 settings apply when `machine.profile = "origin2000_ip27"`. +Origin 2000 settings apply when `machine.profile = "origin2000_ip27"` or `"onyx2_ip27"`. | Key | Default | Values and behavior | |---|---|---| diff --git a/docs/onyx2-ip27.md b/docs/onyx2-ip27.md new file mode 100644 index 000000000..ee130df7d --- /dev/null +++ b/docs/onyx2-ip27.md @@ -0,0 +1,35 @@ +# SGI Onyx2 deskside (IP27 node boards) + +The Onyx2 deskside is one or two IP27 node boards on the 4P1G5 midplane. Both +Hubs are hosts of the one Crossbow, which carries the BaseIO-G at widget 15 and +the graphics slot at widget 8. Two nodes are joined by the direct cable. The +machine runs headless on the IOC3's first serial port. There is no +InfiniteReality pipe: the graphics slot holds a test widget that answers its +identification word and nothing else. + +## Configuration + +Use [`origin2000.toml.example`](../origin2000.toml.example) with +`machine.profile = "onyx2_ip27"`; `[origin2000]` (`nodes`, `flash`, +`controller_store`) configures this machine too. Node slot 1 drives raw slot +identity 3 into its Hub and takes widget 10; slot 2 drives 2 and takes widget 9. +With one node, the board is in slot 1. + +A disk installed on an Onyx2 boots; the machine cannot install without a pipe. + +## Limits + +- One node boots the stock PROM to the System Maintenance Menu and an + installed IRIX 6.5.30 to a serial login. +- Two nodes: the PROM finds both Hubs, numbers them, makes the second slot's + board the global master, passes its diagnostics with both processors and + 256 MB, and IRIX boots to a root login with both processors. +- Either Hub's processor reaches the BaseIO-G through the Crossbow. The Bridge + delivers DMA and interrupts to the Hub at the widget the guest programs into + it, and both Hubs are hosts, each behind its Crossbow port's link. With two + nodes the PROM and IRIX program widget 9, the master's Hub; with one, widget + 10. +- The BaseIO-G is its base board: no MediaIO or audio. +- The module controller, the cable and the node windows are the + [Origin 2000 deskside's](origin2000-ip27.md), with its limits. +- Snapshots are not supported. diff --git a/src/config.rs b/src/config.rs index 4704b4792..a7386cb73 100644 --- a/src/config.rs +++ b/src/config.rs @@ -623,6 +623,11 @@ pub enum MachineProfile { /// of a Crossbow, the first carrying the BaseIO. Configured by the shared /// keys and `[origin2000]`; see `src/ip27_origin2000.rs`. Origin2000Ip27, + /// SGI Onyx2 deskside: one or two IP27 node boards sharing one Crossbow, + /// the BaseIO-G at widget 15 and a graphics slot at widget 8, which holds + /// a test widget. Configured by the shared keys and `[origin2000]`; see + /// `src/ip27_onyx2.rs`. + Onyx2Ip27, } impl MachineProfile { @@ -643,7 +648,7 @@ impl MachineProfile { pub fn bank_sizes(self) -> &'static [u32] { match self { Self::OctaneIp30 => &[0, 64, 128, 256, 512, 1024, 2048], - Self::Origin200Ip29 | Self::Origin2000Ip27 => &[0, 64, 128, 256, 512], + Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 => &[0, 64, 128, 256, 512], Self::O2Ip32 => &[0, 32, 64, 128, 256], Self::Indigo2Ip28 => MC_BANK_SIZES, _ => &[0, 8, 16, 32, 64, 128], @@ -662,7 +667,7 @@ impl MachineProfile { pub fn prom_path(self, prom: &str) -> String { match self { Self::OctaneIp30 => prom_file(prom, crate::ip30_machine::OCTANE_PROM_FILE), - Self::Origin200Ip29 | Self::Origin2000Ip27 => prom_file(prom, crate::ip27_machine::ORIGIN200_PROM_FILE), + Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 => prom_file(prom, crate::ip27_machine::ORIGIN200_PROM_FILE), Self::O2Ip32 => prom_file(prom, crate::ip32_machine::O2_PROM_FILE), _ => prom.to_string(), } @@ -670,7 +675,7 @@ impl MachineProfile { /// Every profile, in the order `workstation::build` lists them. `ALL` is /// only the ones the GUI offers. - pub const EVERY: [Self; 7] = [ + pub const EVERY: [Self; 8] = [ Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28, @@ -678,6 +683,7 @@ impl MachineProfile { Self::OctaneIp30, Self::Origin200Ip29, Self::Origin2000Ip27, + Self::Onyx2Ip27, ]; /// The name `[machine] profile` and `--machine` give this profile. @@ -690,6 +696,7 @@ impl MachineProfile { Self::OctaneIp30 => "octane_ip30", Self::Origin200Ip29 => "origin200_ip29", Self::Origin2000Ip27 => "origin2000_ip27", + Self::Onyx2Ip27 => "onyx2_ip27", } } @@ -746,6 +753,7 @@ impl MachineProfile { Self::OctaneIp30 => "SGI Octane (IP30)", Self::Origin200Ip29 => "SGI Origin 200 (IP29)", Self::Origin2000Ip27 => "SGI Origin 2000 (IP27)", + Self::Onyx2Ip27 => "SGI Onyx2 (IP27)", } } @@ -755,7 +763,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 => 2, + Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 => 2, _ => 1, } } @@ -767,7 +775,7 @@ impl MachineProfile { /// purpose. pub fn max_scsi_id(self) -> u8 { match self { - Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 => 15, + Self::OctaneIp30 | Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 => 15, _ => 7, } } @@ -787,7 +795,7 @@ impl MachineProfile { pub fn cpus(self) -> &'static [&'static Part] { match self { Self::OctaneIp30 => crate::ip30_machine::CPUS, - Self::Origin200Ip29 | Self::Origin2000Ip27 => crate::ip27_machine::CPUS, + Self::Origin200Ip29 | Self::Origin2000Ip27 | Self::Onyx2Ip27 => crate::ip27_machine::CPUS, Self::O2Ip32 => crate::ip32_machine::CPUS, Self::Indigo2Ip28 => crate::machine::IP28_CPUS, Self::IndyIp24 | Self::Indigo2Ip22 => crate::machine::CPUS, @@ -902,7 +910,7 @@ impl GraphicsBoard { pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, - MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 => self == Self::None, + MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => self == Self::None, MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -912,7 +920,7 @@ impl GraphicsBoard { pub fn default_for(profile: MachineProfile) -> Self { match profile { MachineProfile::OctaneIp30 => Self::SolidImpact, - MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 => Self::None, + MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => Self::None, _ => Self::Newport, } } @@ -2275,6 +2283,7 @@ impl MachineConfig { MachineProfile::OctaneIp30 => Ok(()), MachineProfile::Origin200Ip29 => Err("[virtio_serial] is not wired on the Origin 200".into()), MachineProfile::Origin2000Ip27 => Err("[virtio_serial] is not wired on the Origin 2000".into()), + MachineProfile::Onyx2Ip27 => Err("[virtio_serial] is not wired on the Onyx2".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)" @@ -2476,6 +2485,8 @@ impl MachineConfig { format!("graphics.board \"{name}\" is not modelled on the Origin 200 (valid: none)") } else if self.machine.profile == MachineProfile::Origin2000Ip27 { format!("graphics.board \"{name}\" is not modelled on the Origin 2000 (valid: none)") + } else if self.machine.profile == MachineProfile::Onyx2Ip27 { + format!("graphics.board \"{name}\" is not modelled on the Onyx2 (valid: none)") } else { format!("graphics.board \"{name}\" is only valid on the Octane") }); @@ -2517,7 +2528,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) { + if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27) { return Err(format!( "scsi.{}: kind = \"daynaport\" is not available on the O2 (its Ethernet is built in)", key )); @@ -2554,8 +2565,12 @@ impl MachineConfig { } } crate::dev::crime_mem::check_kits(&self.o2_kits()).map_err(|e| format!("banks: {e}"))?; - } else if matches!(self.machine.profile, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27) { - let name = if self.machine.profile == MachineProfile::Origin200Ip29 { "Origin 200" } else { "Origin 2000" }; + } else if matches!(self.machine.profile, MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27) { + let name = match self.machine.profile { + MachineProfile::Origin200Ip29 => "Origin 200", + MachineProfile::Onyx2Ip27 => "Onyx2", + _ => "Origin 2000", + }; // The node has four banks of a DIMM pair each, filled from // bank 0 without a gap. let banks = &self.banks[..bank_count]; @@ -2622,7 +2637,7 @@ 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 => { + (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::Origin200Ip29 && p != MachineProfile::Origin2000Ip27 && p != MachineProfile::Onyx2Ip27 => { return Err("network.mode = \"off\" is only available on the O2, the Octane and the Origin 200 and 2000 for now; the Indy and Indigo2 always have their Ethernet".into()); } (NetMode::Stream, _) => { @@ -3687,7 +3702,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)"); + assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, origin200_ip29, origin2000_ip27, onyx2_ip27)"); let list = MachineProfile::list(); assert_eq!(list.lines().count(), MachineProfile::EVERY.len()); assert!(list.starts_with("indy_ip24 SGI Indy (IP24)\n"), "{list}"); @@ -3696,7 +3711,7 @@ 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::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::Origin200Ip29 | MachineProfile::Origin2000Ip27 | MachineProfile::Onyx2Ip27 => {} } let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"{}\"", p.name())).unwrap(); assert_eq!(cfg.machine.profile, p); diff --git a/src/ip27_onyx2.rs b/src/ip27_onyx2.rs new file mode 100644 index 000000000..91dbd48dc --- /dev/null +++ b/src/ip27_onyx2.rs @@ -0,0 +1,197 @@ +//! The Onyx2 deskside: one or two IP27 node boards on the 4P1G5 midplane, +//! both hosts of the one Crossbow, with the BaseIO-G at widget 15 and the +//! graphics slot at widget 8. +//! +//! Node slot 1 drives raw slot identity 3 into its Hub and takes widget 10; +//! slot 2 drives 2 and takes widget 9. The module controller, the Hubs' cable +//! and the node windows are the Origin 2000 deskside's +//! ([`crate::ip27_origin2000`]). +//! +//! # What is approximated here +//! +//! - The graphics slot holds a test widget that answers its identification +//! and nothing else, not an InfiniteReality pipe. +//! - Either Hub's processor reaches the BaseIO-G, and the Bridge takes DMA and +//! interrupts to the Hub whose widget the guest programs into it, so the +//! board the stock PROM makes the global master receives them. +//! - The BaseIO-G is its base board only: the MediaIO daughtercard and the +//! audio chip are not fitted, and the board record is the base board's. +//! - The rest is as the Origin 2000 deskside's list in +//! [`crate::ip27_origin2000`]. +//! +//! Ported from hw/mips/sgi/sn-machine.c in jamesbraid/qemu-sgi-origami. + +use crate::config::MachineConfig; +use crate::dev::bridge::Bridge; +use crate::dev::hub::{Hub, HubConfig, Space}; +use crate::dev::hub_md::SlotSignals; +use crate::dev::ioc3::Ioc3Config; +use crate::dev::ioc3_eth::Ioc3Eth; +use crate::dev::node_flash::FlashValues; +use crate::dev::w1; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; +use crate::dev::xbow::{widget_id, Xbow}; +use crate::devlog::LogModule; +use crate::ip27_machine::{cpu_module, pci_devices, BoardIo, BoardSpec, Ip27Board, Ip27Machine, Node, NodeRecord}; +use crate::ip27_origin2000::{Origin2000, BRIDGE_REVISION, MAX_NODES, SUPERIO_PIN, XBOW_REVISION}; +use crate::traits::{BusDevice, BusRead32, BusRead64, BUS_ERR}; +use crate::xtalk::{HostPort, XtalkHost}; +use std::sync::Arc; + +pub type Onyx2Machine = Ip27Machine; + +/// The graphics slot's widget, and the BaseIO-G's. +const WIDGET_PIPE: u8 = 8; +const WIDGET_BASEIO: u8 = 15; +/// A node board's Hub port: 10 in node slot 1, 9 in slot 2. +const fn widget_hub(node: usize) -> u8 { + 10 - node as u8 +} +/// What node slot `node` drives onto the Hub's slot identity: 3 and 2. +const fn slot_bits(node: usize) -> u8 { + 3 - node as u8 +} +const fn hub_host(node: usize) -> XtalkHost { + XtalkHost { widget: widget_hub(node), int_packet: 0, int_packet_mask: 0, vectors: 128 } +} +/// The test widget's identification: not a real part. +const TEST_WIDGET_ID: u32 = widget_id(0, 0x7e57, 0x36); + +/// A widget that answers only its identification word. +struct TestWidget; +impl BusDevice for TestWidget { + fn read32(&self, a: u64) -> BusRead32 { + if a == 4 { + return BusRead32::ok(TEST_WIDGET_ID); + } + crate::dlog!(LogModule::Hub, "Onyx2: read of {a:#x} in the graphics slot, which holds a test widget"); + BusRead32::err() + } + fn write32(&self, a: u64, v: u32) -> u32 { + crate::dlog!(LogModule::Hub, "Onyx2: write of {v:#x} to {a:#x} in the graphics slot, which holds a test widget"); + BUS_ERR + } + fn read64(&self, a: u64) -> BusRead64 { + crate::dlog!(LogModule::Hub, "Onyx2: 64-bit read of {a:#x} in the graphics slot, which holds a test widget"); + BusRead64::err() + } + fn write64(&self, a: u64, v: u64) -> u32 { + crate::dlog!(LogModule::Hub, "Onyx2: 64-bit write of {v:#x} to {a:#x} in the graphics slot, which holds a test widget"); + BUS_ERR + } +} + +pub struct Onyx2Deskside; + +impl Onyx2Deskside { + /// The BaseIO-G's base board record, with a synthetic barcode and serial. + fn board_record() -> w1::Bus { + let mut bus = w1::Bus::new(); + let record = BoardRecord { barcode: "EMUB01", part: "030-0734-002", revision: "N", name: "BASEIO" }; + bus.add(Box::new(board_record_part(&record, 0x5145_4d55_4201))); + bus + } + + /// The Origin 2000 BaseIO's IOC3 with the keyboard and mouse the BaseIO-G + /// adds. + fn ioc3_config(mac: [u8; 6]) -> Ioc3Config { + Ioc3Config { keyboard_mouse: true, ..Origin2000::ioc3_config(mac) } + } +} + +impl Ip27Board for Onyx2Deskside { + const NAME: &'static str = "Onyx2"; + + fn specs(cfg: &MachineConfig) -> Result, String> { + let module = *cpu_module(cfg.cpu_part()?).ok_or("an Onyx2 has an R10000 or an R12000")?; + let nodes = usize::from(cfg.origin2000.nodes); + if !(1..=MAX_NODES).contains(&nodes) { + return Err(format!("origin2000.nodes {nodes}: the deskside has one or two node boards")); + } + Ok((0..nodes) + .map(|node| BoardSpec { + hub: HubConfig { + cpus: 1, + cpu_base: 2 * node, + ni_revision: 5, + slot: SlotSignals { slot: Some(slot_bits(node)), slave_module: false }, + ..Default::default() + }, + module, + banks_mb: std::array::from_fn(|i| cfg.banks[i]), + flash: FlashValues { + cpu_mode_bits: module.mode_word, + cpu_clock_hz: module.clock_hz(), + machine_type: 0, + ..FlashValues::ORIGIN200_R10000_180_1M + }, + flash_file: Origin2000::per_node(&cfg.origin2000.flash, node), + node_record: Some(NodeRecord::IP27), + }) + .collect()) + } + + fn wire(nodes: &[&Node], cfg: &MachineConfig) -> Result { + let controller = Origin2000::controller(nodes, cfg)?; + let mut widgets = (1u16 << WIDGET_PIPE) | (1 << WIDGET_BASEIO); + for i in 0..nodes.len() { + widgets |= 1 << widget_hub(i); + } + let xbow = Arc::new(Xbow::new(XBOW_REVISION, widgets)); + // The Crossbow's own interrupts go to the last node's Hub, which the + // stock PROM makes the master. Either Hub's processor reaches the + // BaseIO through the shared Crossbow, and the Bridge sends DMA and + // interrupts to the Hub at the widget the guest programs, so the + // Crossbow gives it both, each behind its port's link. + let owner_index = nodes.len() - 1; + let owner = nodes[owner_index]; + xbow.connect_host(hub_host(owner_index), owner.hub.interrupt_target()); + xbow.register_locks(); + + let hosts = nodes + .iter() + .enumerate() + .map(|(i, n)| HostPort::new(hub_host(i), n.hub.inbound(), n.hub.interrupt_target())) + .collect(); + let bridge = Bridge::new(xbow.upstream(hosts)); + bridge.set_revision(BRIDGE_REVISION); + bridge.set_microlan(Self::board_record()); + let links = xbow.clone(); + bridge.set_link_alive(Box::new(move |w| links.carries(w))); + let weak = Arc::downgrade(&bridge); + xbow.on_link_reset(Box::new(move |w| { + if w == WIDGET_BASEIO { + if let Some(b) = weak.upgrade() { + b.link_reset(); + } + } + })); + + let peers: Vec<(u8, Arc)> = nodes + .iter() + .enumerate() + .map(|(i, n)| (widget_hub(i), n.hub.inbound())) + .collect(); + for node in nodes { + let io = node.hub.io(); + io.connect_widget(0, xbow.clone()); + io.connect_widget(WIDGET_PIPE, xbow.port(WIDGET_PIPE, Arc::new(TestWidget))); + io.connect_widget(WIDGET_BASEIO, xbow.port(WIDGET_BASEIO, bridge.clone())); + for (i, n) in nodes.iter().enumerate() { + io.connect_widget(widget_hub(i), xbow.port(widget_hub(i), n.hub.widget_face())); + } + io.connect_packets(xbow.packets(peers.clone())); + } + if let [a, b] = nodes { + Hub::cable(&a.hub, &b.hub); + for node in nodes { + node.memory.route_nodes(node.hub.claim(Space::Memory)); + } + } + let mac = cfg.network().mac; + pci_devices(owner, cfg, bridge, Self::ioc3_config(mac), Ioc3Eth::new_origin200(mac), controller, SUPERIO_PIN, "onyx2", Self::NAME) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip27_onyx2/tests.rs b/src/ip27_onyx2/tests.rs new file mode 100644 index 000000000..e13a7daa5 --- /dev/null +++ b/src/ip27_onyx2/tests.rs @@ -0,0 +1,218 @@ +use super::*; +use crate::config::{MachineProfile, NetMode}; +use crate::cpu::parts; +use crate::dev::hub::Space; +use crate::dev::{hub_md, hub_ni}; +use crate::traits::{BusDevice, BUS_ERR, BUS_OK}; +use std::path::{Path, PathBuf}; + +struct Scratch(PathBuf); +impl Scratch { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("iris-onyx2-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + Scratch(dir) + } +} +impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// A PROM stand-in: a recognisable first word and a configuration block in +/// the shape the flash initialiser patches. +fn prom(dir: &Path) -> String { + let mut p = vec![0u8; 0x1000]; + p[..4].copy_from_slice(&0x3c08_a600u32.to_be_bytes()); + p[0x68..0x70].copy_from_slice(&0x6970_3237_636f_6e66u64.to_be_bytes()); + let path = dir.join("prom.bin"); + std::fs::write(&path, p).unwrap(); + path.to_string_lossy().into_owned() +} + +fn config(dir: &Path, nodes: u8) -> MachineConfig { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::Onyx2Ip27; + cfg.set_cpu(&parts::R10000_180); + cfg.origin2000.nodes = nodes; + cfg.banks = [64, 0, 0, 0, 0, 0, 0, 0]; + cfg.prom = prom(dir); + cfg.nvram = dir.join("nvram.bin").to_string_lossy().into_owned(); + cfg.headless = true; + cfg.network.mode = NetMode::Off; + let held: Vec<_> = (0..3).map(|_| std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap()).collect(); + cfg.monitor_port = Some(held[0].local_addr().unwrap().port()); + cfg.serial_port_a = Some(held[1].local_addr().unwrap().port()); + cfg.serial_port_b = Some(held[2].local_addr().unwrap().port()); + let disk = dir.join("disk.raw"); + std::fs::write(&disk, vec![0u8; 4 << 20]).unwrap(); + cfg.scsi.clear(); + cfg.scsi.insert(1, toml::from_str(&format!("path = {:?}\noverlay = true", disk.to_string_lossy())).unwrap()); + cfg +} + +/// Building a machine puts more than a megabyte on the stack. +fn big_stack(body: impl FnOnce() + Send + 'static) { + std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join().expect("thread panicked"); +} + +fn built(name: &str, nodes: u8) -> (Scratch, Onyx2Machine) { + let dir = Scratch::new(name); + let m = Onyx2Machine::new(&config(&dir.0, nodes)).unwrap(); + (dir, m) +} + +#[test] +fn the_profile_is_chosen_by_its_snake_case_name() { + assert_eq!(MachineProfile::from_name("onyx2_ip27"), Ok(MachineProfile::Onyx2Ip27)); + assert_eq!(MachineProfile::Onyx2Ip27.name(), "onyx2_ip27"); +} + + +#[test] +fn two_nodes_share_one_crossbow_and_both_reach_the_baseio() { + big_stack(|| { + let (_dir, m) = built("two", 2); + let (n1, n2) = (&m.node_at(0).hub, &m.node_at(1).hub); + // Slot identity 3 in the first node slot, 2 in the second. + assert_eq!(n1.read64(hub_md::reg::SLOTID_USTAT).data & 7, 3); + assert_eq!(n2.read64(hub_md::reg::SLOTID_USTAT).data & 7, 2); + // The flash carries no machine type: the slot code selects the table. + assert_eq!(n1.cpu_special().read32(0x1000_0090).data, 0); + + for hub in [n1, n2] { + let io = hub.claim(Space::Io); + let id = |w: u64| io.read32((w << 24) + 4); + // The one Crossbow, revision 2. + assert_eq!(id(0).data, 0x2000_0001); + // Both Hubs' ports, and the test widget in the graphics slot. + assert_eq!(id(9).status, BUS_OK); + assert_eq!(id(10).status, BUS_OK); + assert_eq!(id(8).data, TEST_WIDGET_ID); + // The test widget refuses anything else. + assert_eq!(io.write32((8 << 24) + 0x10, 1), BUS_ERR); + assert_eq!(io.read32((8 << 24) + 0x10).status, BUS_ERR); + // Nothing is fitted at 11 to 14. + for w in 11..15 { + assert_eq!(id(w).status, BUS_ERR, "widget {w}"); + } + } + // The BaseIO-G answers at widget 15 from either Hub. + let id = |hub: &Arc| hub.claim(Space::Io).read32((15 << 24) + 4); + assert_eq!(id(n2).status, BUS_OK); + assert_eq!(id(n1).status, BUS_OK); + assert_eq!(id(n1).data, id(n2).data); + + // A big-window probe of the peer's port, as the PROM does it, reads + // the peer's node number once the Hubs are numbered. + assert_eq!(n2.write64(hub_ni::reg::STATUS, 1 << 8), BUS_OK); + let io = n1.cpu_io(); + let bwin = crate::dev::hub::IO_BIG_WINDOWS; + assert_eq!(n1.write64(crate::dev::hub_io::reg::BWIN_TABLE, (9 << 8) | 0x1000), BUS_OK); + let status = io.read64(bwin + 0x0160_0000); + assert_eq!(status.status, BUS_OK); + assert_eq!((status.data >> 8) & 0x1ff, 1); + }); +} + +#[test] +fn a_dropped_machine_frees_its_hubs_memory_and_timers() { + big_stack(|| { + let (_dir, m) = built("drop", 2); + let hubs: Vec<_> = (0..2).map(|i| Arc::downgrade(&m.node_at(i).hub)).collect(); + let memory: Vec<_> = (0..2).map(|i| Arc::downgrade(m.node_at(i).memory.physical())).collect(); + let timers: Vec<_> = (0..2).map(|i| Arc::downgrade(&m.node_at(i).timers)).collect(); + let cpus: Vec<_> = (0..2).map(|i| Arc::downgrade(m.cpu_at(i))).collect(); + let bridge = Arc::downgrade(m.bridge()); + drop(m); + for (i, w) in hubs.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "hub {i}"); + } + for (i, w) in memory.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "memory map {i}"); + } + for (i, w) in timers.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "timers {i}"); + } + for (i, w) in cpus.iter().enumerate() { + assert_eq!(w.strong_count(), 0, "cpu {i}"); + } + assert_eq!(bridge.strong_count(), 0, "the Bridge"); + }); +} + +/// A PCI function with nothing behind it but the DMA and interrupt line the +/// Bridge hands it. +struct Card { + cfg: crate::pci::PciConfig, + irq: parking_lot::Mutex>, + dma: parking_lot::Mutex>>, +} + +impl Card { + fn new() -> Arc { + Arc::new(Self { + cfg: crate::pci::PciConfig::new(0x1234, 0x5678, 0x01_0000, 1, [crate::pci::Bar::Unused; 6], crate::pci::PCI_INTA), + irq: Default::default(), + dma: Default::default(), + }) + } +} + +impl crate::pci::PciBusDevice for Card { + fn config(&self) -> &crate::pci::PciConfig { + &self.cfg + } + fn bar_read(&self, _bar: usize, _off: u32, _width: usize) -> u32 { + 0 + } + fn bar_write(&self, _bar: usize, _off: u32, _val: u32, _width: usize) {} + fn connect_interrupt(&self, line: crate::irq::IrqLine) { + *self.irq.lock() = Some(line); + } + fn connect_dma(&self, dma: Arc) { + *self.dma.lock() = Some(dma); + } +} + +/// The BaseIO-G's Bridge sends DMA and interrupts to the Hub at the widget +/// the guest programs into it, not to a fixed one: the second node's Hub +/// (widget 9) gets both when the guest names it, and the first's (10) when +/// the guest names that. +#[test] +fn the_bridge_sends_dma_and_interrupts_to_the_hub_the_guest_names() { + use crate::dev::bridge::{reg, DIRECT_WIDGET_SHIFT}; + big_stack(|| { + let (_dir, m) = built("bridge", 2); + let bridge = m.bridge().clone(); + let card = Card::new(); + bridge.attach(7, card.clone()); + let set = |off: u32, v: u32| assert_eq!(bridge.write32(u64::from(off + 4), v), BUS_OK); + let dma = card.dma.lock().clone().unwrap(); + let memory = |i: usize, off: u64| m.node_at(i).memory.physical().read32(off).data; + let pending = |i: usize| m.node_at(i).hub.read64(crate::dev::hub_pi::reg::INT_PEND0).data; + + // Node slot 2 is widget 9 and node slot 1 is widget 10. + for (widget, node, other) in [(9u32, 1usize, 0usize), (10, 0, 1)] { + set(reg::DIRECT_MAP, widget << DIRECT_WIDGET_SHIFT); + let at = 0x1000 * u64::from(widget); + assert!(dma.dma_write(0x8000_0000 + at, &[1, 2, 3, 4]).is_ok(), "widget {widget}"); + assert_ne!(memory(node, at), 0, "widget {widget}: the named node did not get it"); + assert_eq!(memory(other, at), 0, "widget {widget}: the other node saw it"); + + // The interrupt goes the same way, to the vector the pin names. + set(reg::INT_DEST_UPPER, widget << 16); + set(reg::INT_DEST_LOWER, 0); + set(reg::int_host(7), 5); + set(reg::INT_ENABLE, 1 << 7); + card.irq.lock().as_ref().unwrap().set(true); + assert_eq!(pending(node) & 1 << 5, 1 << 5, "widget {widget}"); + assert_eq!(pending(other) & 1 << 5, 0, "widget {widget}: the other node heard it"); + card.irq.lock().as_ref().unwrap().set(false); + // The guest's handler clears the pending bit. + assert_eq!(m.node_at(node).hub.write64(crate::dev::hub_pi::reg::INT_PEND_MOD, 5), BUS_OK); + } + }); +} diff --git a/src/ip27_origin2000.rs b/src/ip27_origin2000.rs index 499d3dcab..c9e210e0a 100644 --- a/src/ip27_origin2000.rs +++ b/src/ip27_origin2000.rs @@ -65,11 +65,11 @@ const fn slot_bits(node: usize) -> u8 { 7 - node as u8 } /// The Crossbow revision: 1.1, the earliest the kernel accepts. -const XBOW_REVISION: u32 = 2; +pub(crate) const XBOW_REVISION: u32 = 2; /// The BaseIO's Bridge revision. -const BRIDGE_REVISION: u32 = 4; +pub(crate) const BRIDGE_REVISION: u32 = 4; /// The Bridge input the BaseIO's IOC3 SuperIO interrupt is wired to. -const SUPERIO_PIN: u8 = 4; +pub(crate) const SUPERIO_PIN: u8 = 4; /// The BaseIO's PCI subsystem identification. const SUBSYS_BASEIO: u16 = 0xc302; @@ -87,7 +87,7 @@ const MODULE_IDENTITY_SERIAL: u64 = 0x5145_4d55_4d00; /// The module management as the machine drives it: the adapters to reset /// with the module, and the outputs to drain. -struct ModuleManagement { +pub(crate) struct ModuleManagement { module: Arc, adapters: usize, } @@ -126,7 +126,7 @@ impl Origin2000 { bus } - fn ioc3_config(mac: [u8; 6]) -> Ioc3Config { + pub(crate) fn ioc3_config(mac: [u8; 6]) -> Ioc3Config { Ioc3Config { subsystem_id: SUBSYS_BASEIO, pci_policy: Default::default(), @@ -144,13 +144,13 @@ impl Origin2000 { /// A file for node `node` beside the one the configuration names for the /// first. - fn per_node(path: &Option, node: usize) -> Option { + pub(crate) fn per_node(path: &Option, node: usize) -> Option { path.as_ref().map(|p| if node == 0 { PathBuf::from(p) } else { PathBuf::from(format!("{p}.{node}")) }) } /// The module controller: one for the module, in front of every node's /// I2C controller. - fn controller(nodes: &[&Node], cfg: &MachineConfig) -> Result, String> { + pub(crate) fn controller(nodes: &[&Node], cfg: &MachineConfig) -> Result, String> { let store = ElscNvram::origin2000(MODULE_NUMBER, cfg.origin2000.controller_store.as_deref().map(Path::new)) .map(Arc::new) .map_err(|e| format!("origin2000.controller_store: {e}"))?; diff --git a/src/lib.rs b/src/lib.rs index f308417ba..48915580d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,7 @@ pub mod ip30_machine; /// the Origin 2000 and Onyx2 desksides are built from it. pub mod ip27; pub mod ip27_machine; +pub mod ip27_onyx2; pub mod ip27_origin2000; /// Fuel has an IP34 board and uses the IP35 software platform and processor views. pub mod ip35; diff --git a/src/machine.rs b/src/machine.rs index 3e92b3822..a6cf66a9a 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::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27] .into_iter().find(|p| *p as u8 == raw) } @@ -70,6 +70,7 @@ fn emulator_name_thirds(profile: Option) -> &'static [&'static s Some(MachineProfile::OctaneIp30) => &["IRIX", "Iris", "IPsomething", "IP30"], Some(MachineProfile::Origin200Ip29) => &["IRIX", "Iris", "IPsomething", "IP29"], Some(MachineProfile::Origin2000Ip27) => &["IRIX", "Iris", "IPsomething", "IP27"], + Some(MachineProfile::Onyx2Ip27) => &["IRIX", "Iris", "IPsomething", "IP27"], None => &["IRIX", "Iris", "IPsomething"], } } @@ -2172,11 +2173,12 @@ mod emulator_name_tests { Some(MachineProfile::OctaneIp30) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP32", "O2"], Some(MachineProfile::Origin200Ip29) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], Some(MachineProfile::Origin2000Ip27) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], + Some(MachineProfile::Onyx2Ip27) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "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::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::Origin200Ip29), Some(MachineProfile::Origin2000Ip27), Some(MachineProfile::Onyx2Ip27)] { let thirds = emulator_name_thirds(p); assert!(!thirds.is_empty()); for w in thirds { @@ -2188,7 +2190,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] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::Origin200Ip29, MachineProfile::Origin2000Ip27, MachineProfile::Onyx2Ip27] { set_emulator_profile(p); assert_eq!(emulator_profile(), Some(p)); } diff --git a/src/workstation.rs b/src/workstation.rs index e376dafdb..2bd5798da 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -46,6 +46,7 @@ pub fn build(cfg: MachineConfig) -> Result, String> { crate::config::MachineProfile::OctaneIp30 => Ok(Box::new(crate::ip30_machine::OctaneMachine::new(&cfg)?)), crate::config::MachineProfile::Origin200Ip29 => Ok(Box::new(crate::ip27_machine::Origin200Machine::new(&cfg)?)), crate::config::MachineProfile::Origin2000Ip27 => Ok(Box::new(crate::ip27_origin2000::Origin2000Machine::new(&cfg)?)), + crate::config::MachineProfile::Onyx2Ip27 => Ok(Box::new(crate::ip27_onyx2::Onyx2Machine::new(&cfg)?)), _ => Ok(Box::new(Machine::new(cfg))), } }