From d592f174b2b752f308f49d1244df1a4c6184ac9f Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:12:06 -0700 Subject: [PATCH 01/15] w1: the 1-wire bus and the SGI board records stand alone golden.rs pinned the bus's reply streams to FNV digests and head bytes generated once from per-master code that no longer exists. A digest says a stream changed but not what it should be, and nothing in this repo can regenerate the fixtures. The multi-part cases it covered (Search ROM, Read ROM, Skip ROM with Read Memory, a bad Match ROM over a DS2502, DS2501 and DS2505 on one wire) now sit in tests.rs and assert the wired-AND the protocol defines, computed from the parts' own ROMs and streams. The w1 tests' MAC constant is now a made-up address. sgi_nic's mac_nic and board_nic only boxed the result of ethernet_address_part and board_record_part so it could be added to a w1::Bus. HEART, the Bridge and the IOC3 box the part themselves, and the wrappers, which left two names for each constructor, are gone. Name the qemu-sgi-origami source each of heart.rs, heart_mem.rs, sgi_nic.rs and the w1 modules was ported from, as ioc3.rs, ioc3_eth.rs, isp1040.rs and rad1.rs already do, so the provenance is uniform. --- src/dev/heart.rs | 2 + src/dev/heart_mem.rs | 2 + src/dev/ioc3.rs | 2 +- src/dev/sgi_nic.rs | 29 ++--- src/dev/w1/bus.rs | 2 + src/dev/w1/crc.rs | 2 + src/dev/w1/ds2502.rs | 2 + src/dev/w1/ds2505.rs | 2 + src/dev/w1/golden.rs | 303 ------------------------------------------- src/dev/w1/mod.rs | 2 - src/dev/w1/slot.rs | 2 + src/dev/w1/tests.rs | 106 ++++++++++++++- src/ip30_machine.rs | 4 +- 13 files changed, 127 insertions(+), 333 deletions(-) delete mode 100644 src/dev/w1/golden.rs diff --git a/src/dev/heart.rs b/src/dev/heart.rs index a02bc66d..9c602579 100644 --- a/src/dev/heart.rs +++ b/src/dev/heart.rs @@ -15,6 +15,8 @@ //! both to this device, which takes the full physical address. Memory is //! `heart_mem::HeartRam`, which a descriptor write reaches through //! [`Heart::on_memcfg`]. +//! +//! Ported from hw/misc/sgi-heart.c in jamesbraid/qemu-sgi-origami. use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering}; use std::sync::Arc; diff --git a/src/dev/heart_mem.rs b/src/dev/heart_mem.rs index f84790a1..76c1aa35 100644 --- a/src/dev/heart_mem.rs +++ b/src/dev/heart_mem.rs @@ -26,6 +26,8 @@ //! The ppmem window maps whole 32 MiB units, the guard included: the CPU's //! cached accesses through the window reach those 16 KiB. Only uncached //! accesses, which go through the table, see the bus error. +//! +//! Ported from hw/misc/sgi-heart.c in jamesbraid/qemu-sgi-origami. use std::sync::atomic::{fence, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 6ffe56d2..ca135d0e 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -948,7 +948,7 @@ fn default_wire(cfg: &Ioc3Config) -> w1::Bus { bus.add(Box::new(sgi_nic::ethernet_address_part(cfg.mac, cfg.ethernet_part_serial))); for (r, serial) in &cfg.records { let rec = BoardRecord { barcode: &r.barcode, part: &r.part, revision: &r.revision, name: &r.name }; - bus.add(sgi_nic::board_nic(&rec, *serial)); + bus.add(Box::new(sgi_nic::board_record_part(&rec, *serial))); } bus } diff --git a/src/dev/sgi_nic.rs b/src/dev/sgi_nic.rs index 8116c795..59e79ce0 100644 --- a/src/dev/sgi_nic.rs +++ b/src/dev/sgi_nic.rs @@ -3,9 +3,11 @@ //! the parts and the master's slot decode live in `crate::dev::w1`; this //! module only knows the SGI record formats and builds parts that carry //! them. The barcodes and ROM serials we ship are synthetic. +//! +//! Ported from hw/misc/sgi-board-record.c in jamesbraid/qemu-sgi-origami. use crate::dev::w1::crc::crc16 as crc16_seed; -use crate::dev::w1::{self, Part}; +use crate::dev::w1; /// Text fields of an SGI board record, as a part's first two 32-byte pages /// hold them, each page closed by an inverted CRC-16. @@ -105,23 +107,12 @@ pub fn ethernet_address_part(mac: [u8; 6], serial: u64) -> w1::Ds2502 { /// "EMU" marker and instance 0. pub const ETHERNET_PART_SERIAL: u64 = 0x5e74_54d5_5000; -/// `ethernet_address_part` with the synthetic serial the machine uses, as -/// a part for a `w1::Bus`. -pub fn mac_nic(mac: [u8; 6]) -> Box { - Box::new(ethernet_address_part(mac, ETHERNET_PART_SERIAL)) -} - -/// `board_record_part` as a part for a `w1::Bus`. -pub fn board_nic(record: &BoardRecord, serial: u64) -> Box { - Box::new(board_record_part(record, serial)) -} - #[cfg(test)] mod tests { use super::*; use crate::dev::w1::crc::crc8; use crate::dev::w1::slot; - use crate::dev::w1::Bus; + use crate::dev::w1::{Bus, Part}; /// The 1-wire protocol, the way Linux's sgi_w1.c drives the register: /// `pulse << 10 | sample << 2`. @@ -192,7 +183,7 @@ mod tests { #[test] fn the_mac_nic_answers_read_rom_and_read_memory() { let mac = [0x08, 0x00, 0x69, 0x0b, 0x7e, 0xba]; - let mut m = bus_of(vec![mac_nic(mac)]); + let mut m = bus_of(vec![Box::new(ethernet_address_part(mac, ETHERNET_PART_SERIAL))]); assert!(m.reset(), "presence pulse"); m.byte(0x33); let rom: Vec = (0..8).map(|_| m.byte(0xff)).collect(); @@ -221,9 +212,9 @@ mod tests { #[test] fn search_rom_finds_every_part_on_the_wire() { let mut m = bus_of(vec![ - mac_nic([1, 2, 3, 4, 5, 6]), - board_nic(&rec("EDJ162", "030-0891-003", "A", "FP1"), 0x1234), - board_nic(&rec("AAE3230158", "060-0035-003", "A", "PWR.SPPLY.ER"), 0x5678), + Box::new(ethernet_address_part([1, 2, 3, 4, 5, 6], ETHERNET_PART_SERIAL)), + Box::new(board_record_part(&rec("EDJ162", "030-0891-003", "A", "FP1"), 0x1234)), + Box::new(board_record_part(&rec("AAE3230158", "060-0035-003", "A", "PWR.SPPLY.ER"), 0x5678)), ]); let roms = m.search(); let mut fams: Vec = roms.iter().map(|r| r[0]).collect(); @@ -238,7 +229,7 @@ mod tests { #[test] fn a_board_nic_reads_as_openbsd_and_linux_expect() { - let mut m = bus_of(vec![board_nic(&rec("FGT687", "030-0887-003", "G", "IP30"), 0x1234)]); + let mut m = bus_of(vec![Box::new(board_record_part(&rec("FGT687", "030-0887-003", "G", "IP30"), 0x1234))]); // owserial: the redirection table from the status memory at 0x100, // eight bytes and a CRC16 at a time. assert!(m.reset()); @@ -292,7 +283,7 @@ mod tests { assert_eq!(p.programmable(), 64); assert_eq!(p.rom()[0], 0x91); assert_eq!((p.memory()[0], p.memory()[5], p.memory()[10]), (0x0a, 3, 8), "marker, station address reversed"); - assert_eq!(mac_nic([8, 0, 0x69, 1, 2, 3]).rom()[6], 0x5e, "UniqueWare prefix"); + assert_eq!(ethernet_address_part([8, 0, 0x69, 1, 2, 3], ETHERNET_PART_SERIAL).rom()[6], 0x5e, "UniqueWare prefix"); } #[test] diff --git a/src/dev/w1/bus.rs b/src/dev/w1/bus.rs index ee19d486..136aec21 100644 --- a/src/dev/w1/bus.rs +++ b/src/dev/w1/bus.rs @@ -1,6 +1,8 @@ //! The bus: which parts are selected, the ROM command phases (Read, Match, //! Search and Skip ROM), then a memory function command, its two address //! bytes and the selected parts' reply streams, wired-AND. +//! +//! Ported from hw/1wire/core.c in jamesbraid/qemu-sgi-origami. /// A part on the bus. pub trait Part: Send { diff --git a/src/dev/w1/crc.rs b/src/dev/w1/crc.rs index 7330a4ef..be2439ca 100644 --- a/src/dev/w1/crc.rs +++ b/src/dev/w1/crc.rs @@ -1,4 +1,6 @@ //! The Dallas/Maxim CRCs the parts frame their streams with. +//! +//! Ported from hw/1wire/crc.c in jamesbraid/qemu-sgi-origami. /// CRC-8 (x^8 + x^5 + x^4 + 1, reflected), as in a ROM's last byte. A /// ROM's CRC over all eight bytes is zero. diff --git a/src/dev/w1/ds2502.rs b/src/dev/w1/ds2502.rs index fe911adc..3ec15f25 100644 --- a/src/dev/w1/ds2502.rs +++ b/src/dev/w1/ds2502.rs @@ -1,6 +1,8 @@ //! DS2501 (512 bits, two 32-byte pages) and DS2502 (1 Kbit, four pages) //! add-only memories: Read Memory (0xf0), Read Data/Generate CRC (0xc3) //! and Read Status (0xaa). Writes are refused. +//! +//! Ported from hw/1wire/ds2502.c in jamesbraid/qemu-sgi-origami. use super::crc::crc8; use super::Part; diff --git a/src/dev/w1/ds2505.rs b/src/dev/w1/ds2505.rs index e1b9204a..406577cf 100644 --- a/src/dev/w1/ds2505.rs +++ b/src/dev/w1/ds2505.rs @@ -1,6 +1,8 @@ //! DS2505 (16 Kbit, 64 pages of 32 bytes) add-only memory: Read Memory //! (0xf0), Read Status (0xaa) and Extended Read Memory (0xa5). Writes are //! refused. +//! +//! Ported from hw/1wire/ds2505.c in jamesbraid/qemu-sgi-origami. use super::crc::{crc16, crc8}; use super::Part; diff --git a/src/dev/w1/golden.rs b/src/dev/w1/golden.rs deleted file mode 100644 index 8a13aee2..00000000 --- a/src/dev/w1/golden.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! Reply streams equal to what the earlier per-master 1-wire code produced. -//! The fixtures were generated once by running that code (the bus and parts -//! the IOC3 module carried) over the same parts and driver, and pasted here -//! as lengths, FNV-1a digests and head bytes. -//! -//! Three things changed on purpose, and the fixtures account for them: the -//! CRC in front of (or seeded by) a start address above the top covers the -//! address as masked, so those cases were generated at the masked address; -//! a DS2501 now answers the DS2502's 128-byte address space (the part's -//! 64 programmable bytes, then erased), so its fixtures stop where the old -//! 64-byte stream ended; and a DS2502 Read Status start above 7 is no longer -//! masked, so it has no fixture (see `tests.rs`). - -use super::*; - -trait Wire { - fn reset(&mut self) -> bool; - fn slot(&mut self, bit: bool) -> bool; -} - -fn wr(w: &mut impl Wire, b: u8) { - for i in 0..8 { - w.slot(b >> i & 1 != 0); - } -} - -fn rd(w: &mut impl Wire) -> u8 { - (0..8).fold(0, |a, i| a | (w.slot(true) as u8) << i) -} - -/// Skip ROM, a memory command with its address, then `n` bytes read. -fn stream(w: &mut impl Wire, cmd: u8, ta: u16, n: usize) -> Vec { - assert!(w.reset()); - wr(w, 0xcc); - wr(w, cmd); - wr(w, ta as u8); - wr(w, (ta >> 8) as u8); - (0..n).map(|_| rd(w)).collect() -} - -fn fnv(d: &[u8]) -> u64 { - d.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3)) -} - -fn hex(d: &[u8]) -> String { - d.iter().map(|b| format!("{b:02x}")).collect() -} - -/// A Search ROM walk: the level of each (bit, complement) pair and the -/// master's choice, as 0/1 characters; then a Read Memory at 0 from the -/// part it leaves selected, four bytes. `policy` 0/1 takes that branch at a -/// discrepancy; 2+k follows part k's ROM. -fn search_trace(w: &mut impl Wire, policy: usize, roms: &[[u8; 8]]) -> String { - assert!(w.reset()); - wr(w, 0xf0); - let mut s = String::new(); - for i in 0..64 { - let b = w.slot(true); - let nb = w.slot(true); - let choice = match (b, nb) { - (false, false) => match policy { - 0 => false, - 1 => true, - k => roms[k - 2][i / 8] >> (i % 8) & 1 != 0, - }, - (true, true) => panic!("nobody answered bit {i}"), - (b, _) => b, - }; - w.slot(choice); - s.push(if b { '1' } else { '0' }); - s.push(if nb { '1' } else { '0' }); - s.push(if choice { '1' } else { '0' }); - } - wr(w, 0xf0); - wr(w, 0); - wr(w, 0); - let got: Vec = (0..4).map(|_| rd(w)).collect(); - format!("{s}|{}", hex(&got)) -} - -/// Read ROM, Skip ROM Read Memory, and a Match ROM with one bit wrong, all -/// on the same multi-part wire. -fn broadcast_trace(w: &mut impl Wire, roms: &[[u8; 8]]) -> String { - assert!(w.reset()); - wr(w, 0x33); - let rom: Vec = (0..8).map(|_| rd(w)).collect(); - let skip = stream(w, 0xf0, 0, 16); - assert!(w.reset()); - wr(w, 0x55); - let mut bad = roms[0]; - bad[3] ^= 1; - for b in bad { - wr(w, b); - } - wr(w, 0xf0); - wr(w, 0); - wr(w, 0); - let nobody: Vec = (0..2).map(|_| rd(w)).collect(); - format!("{}|{}|{}", hex(&rom), hex(&skip), hex(&nobody)) -} - -const N: usize = 2420; - -/// FNV-1a of the first `n` bytes of the stream, and its first 24 bytes. -fn digest(w: &mut impl Wire, cmd: u8, ta: u16, n: usize) -> (u64, String) { - let s = stream(w, cmd, ta, n); - (fnv(&s), hex(&s[..24.min(n)])) -} - -// name, cmd, start address, bytes read, fnv64 of them, first 24 bytes -const STREAMS: &[(&str, u8, u16, usize, u64, &str)] = &[ - ("ds2502", 0xf0, 0x0000, 2420, 0x759379ac36b89e69, "8d0104070a0d101316191c1f2225282b2e3134373a3d4043"), - ("ds2502", 0xf0, 0x0040, 2420, 0xcc58573e64e58d1d, "16c1c4c7cacdd0d3d6d9dcdfe2e5e8ebeef1f4f7fafd0003"), - ("ds2502", 0xf0, 0x007f, 2420, 0x31c1b2888c00e122, "237ee7ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xf0, 0x0080, 2420, 0x759379ac36b89e69, "8d0104070a0d101316191c1f2225282b2e3134373a3d4043"), - ("ds2502", 0xf0, 0x01c5, 2420, 0xece0db9e4b82a4ff, "e9d0d3d6d9dcdfe2e5e8ebeef1f4f7fafd000306090c0f12"), - ("ds2502", 0xf0, 0x0003, 2420, 0xf59d04b26a0cdd26, "d80a0d101316191c1f2225282b2e3134373a3d404346494c"), - ("ds2502", 0xf0, 0x0007, 2420, 0x983af73cc07d4f0d, "e316191c1f2225282b2e3134373a3d404346494c4f525558"), - ("ds2502", 0xf0, 0x0008, 2420, 0x8f0a2f28f16eb9b7, "fb191c1f2225282b2e3134373a3d404346494c4f5255585b"), - ("ds2502", 0xc3, 0x0000, 2420, 0x9b0a60c9ec0ac10b, "b70104070a0d101316191c1f2225282b2e3134373a3d4043"), - ("ds2502", 0xc3, 0x0040, 2420, 0xe41dddd6ef067e42, "2cc1c4c7cacdd0d3d6d9dcdfe2e5e8ebeef1f4f7fafd0003"), - ("ds2502", 0xc3, 0x007f, 2420, 0x6e827f68f554275c, "197ee7ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xc3, 0x0080, 2420, 0x9b0a60c9ec0ac10b, "b70104070a0d101316191c1f2225282b2e3134373a3d4043"), - ("ds2502", 0xc3, 0x01c5, 2420, 0x54b2d0923a32bccf, "d3d0d3d6d9dcdfe2e5e8ebeef1f4f7fafd000306090c0f12"), - ("ds2502", 0xc3, 0x0003, 2420, 0xae37348d2c76b278, "e20a0d101316191c1f2225282b2e3134373a3d404346494c"), - ("ds2502", 0xc3, 0x0007, 2420, 0x99b55abcef47c9de, "d916191c1f2225282b2e3134373a3d404346494c4f525558"), - ("ds2502", 0xc3, 0x0008, 2420, 0xd472bed2057e4c6d, "c1191c1f2225282b2e3134373a3d404346494c4f5255585b"), - ("ds2502", 0xaa, 0x0000, 2420, 0xeec80c7b4da3d930, "9cffffffffffffff00fcffffffffffffffffffffffffffff"), - ("ds2502", 0xaa, 0x0003, 2420, 0x874dfe58929d1ac8, "c9ffffffff0071ffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xaa, 0x0007, 2420, 0x1657f0ad38674196, "f20000ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0040, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x007f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0080, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x01c5, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0003, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0007, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x0f, 0x0008, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0040, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x007f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0080, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x01c5, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0003, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0007, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0x55, 0x0008, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0040, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x007f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0080, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x01c5, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0003, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0007, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2502", 0xa5, 0x0008, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2501", 0xf0, 0x0000, 65, 0x85a328b18cf42978, "8d02070c11161b20252a2f34393e43484d52575c61666b70"), - ("ds2501", 0xf0, 0x0020, 33, 0x76388bcbea60f7eb, "4ca2a7acb1b6bbc0c5cacfd4d9dee3e8edf2f7fc01060b10"), - ("ds2501", 0xf0, 0x003f, 2, 0x09ca8b07b64641ce, "b83d"), - ("ds2501", 0xf0, 0x0003, 62, 0xec746aedbc1aec46, "d811161b20252a2f34393e43484d52575c61666b70757a7f"), - ("ds2501", 0xc3, 0x0000, 67, 0x40b81739158a51bf, "b702070c11161b20252a2f34393e43484d52575c61666b70"), - ("ds2501", 0xc3, 0x0020, 34, 0xcd89fc0b1d6b1256, "76a2a7acb1b6bbc0c5cacfd4d9dee3e8edf2f7fc01060b10"), - ("ds2501", 0xc3, 0x003f, 3, 0x400a9d1aea715c11, "823d43"), - ("ds2501", 0xc3, 0x0003, 64, 0x77edb83fbe59e380, "e211161b20252a2f34393e43484d52575c61666b70757a7f"), - ("ds2501", 0xaa, 0x0000, 2420, 0xeec80c7b4da3d930, "9cffffffffffffff00fcffffffffffffffffffffffffffff"), - ("ds2501", 0xaa, 0x0003, 2420, 0x874dfe58929d1ac8, "c9ffffffff0071ffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x0000, 2420, 0x65469d858abb0def, "0910171e252c333a41484f565d646b727980878e959ca3aa"), - ("ds2505", 0xf0, 0x0045, 2420, 0x885beba33ef1dcf2, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x0100, 2420, 0x72c898dc37f1ac2f, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x013f, 2420, 0xcd224fc8781e954c, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x07e0, 2420, 0x4321d37b35a99e54, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x07f9, 2420, 0x4f4fa7ad21d50514, "ffffffffffffffdb0effffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x07ff, 2420, 0x3886bdfe47c25e36, "ffbebfffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf0, 0x0800, 2420, 0x65469d858abb0def, "0910171e252c333a41484f565d646b727980878e959ca3aa"), - ("ds2505", 0xf0, 0x0a45, 2420, 0x916bb21bfb4554c9, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xaa, 0x0000, 2420, 0xd1ef3532e9d0c33c, "ffffffffffffffff9da1ffffffffffffffffbe7bffffffff"), - ("ds2505", 0xaa, 0x0045, 2420, 0xc106fdfd7abb8d87, "ffffff1bbaffffffffffffffffbe7bffffffffffffffffbe"), - ("ds2505", 0xaa, 0x0100, 2420, 0x0d9ced4413785e95, "ffffffffffffffff9031ffffffffffffffffbe7bffffffff"), - ("ds2505", 0xaa, 0x013f, 2420, 0x90e38dee0bbadeb5, "ffaffbffffffffffffffffbe7bffffffffffffffffbe7bff"), - ("ds2505", 0xaa, 0x07e0, 2420, 0xe4ea65fe804d2f04, "ffffffffffffffffbfc7ffffffffffffffffbe7bffffffff"), - ("ds2505", 0xaa, 0x07f9, 2420, 0x7ab2ecc11e510a3c, "fffffffffffffff86fffffffffffffffffffffffffffffff"), - ("ds2505", 0xaa, 0x07ff, 2420, 0x851235987bb70ef4, "ffac67ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xaa, 0x0800, 2420, 0xd1ef3532e9d0c33c, "ffffffffffffffff9da1ffffffffffffffffbe7bffffffff"), - ("ds2505", 0xaa, 0x0a45, 2420, 0x77d0e16b3f038470, "ffffff1a02ffffffffffffffffbe7bffffffffffffffffbe"), - ("ds2505", 0xa5, 0x0000, 2420, 0xeb0fdfa03db9af37, "ff9d730910171e252c333a41484f565d646b727980878e95"), - ("ds2505", 0xa5, 0x0045, 2420, 0x3a6c2d08e317ee65, "ff8ca6ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x0100, 2420, 0xb4ea00f63777fe98, "ff9ce3ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x013f, 2420, 0x23dc601f0a81e6f2, "ffacefffbfbfffbfbfffffffffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x07e0, 2420, 0xb22c762a76215087, "ff9eb5ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x07f9, 2420, 0xa6e25b494ec35966, "ff4f72fffffffffffffff17fffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x07ff, 2420, 0xdcecb0a627b69f1d, "ffaf73ffbfbfffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xa5, 0x0800, 2420, 0xeb0fdfa03db9af37, "ff9d730910171e252c333a41484f565d646b727980878e95"), - ("ds2505", 0xa5, 0x0a45, 2420, 0x1f52d1c1ea5fb4b2, "ff8dc6ffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x0045, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x0100, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x013f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x07e0, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x07f9, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x07ff, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x0800, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xc3, 0x0a45, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x0045, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x0100, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x013f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x07e0, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x07f9, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x07ff, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x0800, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x0f, 0x0a45, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x0045, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x0100, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x013f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x07e0, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x07f9, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x07ff, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x0800, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf3, 0x0a45, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x0045, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x0100, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x013f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x07e0, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x07f9, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x07ff, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x0800, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0x55, 0x0a45, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x0000, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x0045, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x0100, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x013f, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x07e0, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x07f9, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x07ff, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x0800, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), - ("ds2505", 0xf5, 0x0a45, 2420, 0xee65e5922f97cfa1, "ffffffffffffffffffffffffffffffffffffffffffffffff"), -]; -const SEARCHES: &[&str] = &[ - "101000010000101010010101010010010010010010010010010010010010101010101010101010101010101010101101010010101010101010101010010010101010101101101010010101101101101010101010010010010101010101101101|8d02070c", - "101001010101010010010010010010010010010010010010010101101010010010101010101010101010101010101010101010101101010010101010101010101010010010101010101010010010101010101010010101010101101010101010|0910171e", - "101000010001010010010010101101010101010101010101101010010101010010010101101101101010010101101010101010101010010010101010101101010010010101010010101010010010010010010010010010010101010010010101|8d010407", - "101000010000101010010101010010010010010010010010010010010010101010101010101010101010101010101101010010101010101010101010010010101010101101101010010101101101101010101010010010010101010101101101|8d02070c", - "101001010101010010010010010010010010010010010010010101101010010010101010101010101010101010101010101010101101010010101010101010101010010010101010101010010010101010101010010101010101101010101010|0910171e", -]; -const BROADCAST: &str = "0100004544000008|090004040004100000080c1400240000|ffff"; - -impl Wire for Bus { - fn reset(&mut self) -> bool { - Bus::reset(self) - } - fn slot(&mut self, bit: bool) -> bool { - Bus::slot(self, bit) - } -} - -fn pat(n: usize, m: usize, a: usize) -> Vec { - (0..n).map(|i| (i * m + a) as u8).collect() -} - -/// The three parts the fixtures were generated with. -fn parts() -> [(&'static str, Box); 3] { - [ - ("ds2502", Box::new(Ds2502::new(0x09, 0x0123_4567_89ab, &pat(128, 3, 1)))), - ("ds2501", Box::new(Ds2502::ds2501(0x91, 0x5e74_54d5_5000, &pat(64, 5, 2)))), - ("ds2505", Box::new(Ds2505::new(0x0b, 0x5145_4d55_4600, &pat(64, 7, 9)))), - ] -} - -fn part(name: &str) -> Box { - parts().into_iter().find(|(n, _)| *n == name).unwrap().1 -} - -fn wire() -> Bus { - let mut b = Bus::new(); - for (_, p) in parts() { - b.add(p); - } - b -} - -#[test] -fn reply_streams_match_the_earlier_code_byte_for_byte() { - for &(name, cmd, ta, n, want, head) in STREAMS { - let mut b = Bus::new(); - b.add(part(name)); - let (got, got_head) = digest(&mut b, cmd, ta, n); - assert_eq!((got_head.as_str(), got), (head, want), "{name} {cmd:#04x} at {ta:#06x}"); - } -} - -#[test] -fn search_rom_on_three_parts_matches_the_earlier_code() { - let roms = wire().roms(); - for (policy, want) in SEARCHES.iter().enumerate() { - assert_eq!(search_trace(&mut wire(), policy, &roms), *want, "policy {policy}"); - } -} - -#[test] -fn read_rom_skip_rom_and_a_bad_match_on_three_parts_match_the_earlier_code() { - let roms = wire().roms(); - assert_eq!(broadcast_trace(&mut wire(), &roms), BROADCAST); -} diff --git a/src/dev/w1/mod.rs b/src/dev/w1/mod.rs index 92672a44..c4262101 100644 --- a/src/dev/w1/mod.rs +++ b/src/dev/w1/mod.rs @@ -19,7 +19,5 @@ pub use bus::{Bus, Part}; pub use ds2502::Ds2502; pub use ds2505::Ds2505; -#[cfg(test)] -mod golden; #[cfg(test)] mod tests; diff --git a/src/dev/w1/slot.rs b/src/dev/w1/slot.rs index 28c16d86..d7f4859e 100644 --- a/src/dev/w1/slot.rs +++ b/src/dev/w1/slot.rs @@ -5,6 +5,8 @@ //! The 400 and 40 thresholds are modelling choices, not hardware values: //! real pulse widths in register units are unknown, and every responder //! the guests have been run against uses these. +//! +//! Ported from include/hw/misc/sgi-onewire-slot.h in jamesbraid/qemu-sgi-origami. use super::Bus; diff --git a/src/dev/w1/tests.rs b/src/dev/w1/tests.rs index 1c835a4b..eaa7c830 100644 --- a/src/dev/w1/tests.rs +++ b/src/dev/w1/tests.rs @@ -1,11 +1,13 @@ //! The bus, the parts and the slot decode, driven the way a guest does. +//! +//! Ported from tests/unit/test-onewire.c in jamesbraid/qemu-sgi-origami. use super::crc::{crc16, crc8}; use super::slot::{self, fields, ONE_PULSE, RESET_PULSE}; use super::*; -use crate::dev::sgi_nic::{board_nic, mac_nic, BoardRecord}; +use crate::dev::sgi_nic::{board_record_part, ethernet_address_part, BoardRecord, ETHERNET_PART_SERIAL}; -const MAC: [u8; 6] = [0x08, 0x00, 0x69, 0x0b, 0x7e, 0xba]; +const MAC: [u8; 6] = [0x08, 0x00, 0x69, 0x12, 0x34, 0x56]; /// A master on one bus: slots at pulse widths, as a MicroLAN register does. struct M(Bus); @@ -97,12 +99,15 @@ fn image(n: usize) -> Vec { /// The IOC3's wire: the Ethernet part and two board records. fn octane_wire() -> M { let mut b = Bus::new(); - b.add(mac_nic(MAC)); - b.add(board_nic(&BoardRecord { barcode: "EMUF00", part: "030-0891-003", revision: "A", name: "FP1" }, 0x5145_4d55_4600)); - b.add(board_nic( + b.add(Box::new(ethernet_address_part(MAC, ETHERNET_PART_SERIAL))); + b.add(Box::new(board_record_part( + &BoardRecord { barcode: "EMUF00", part: "030-0891-003", revision: "A", name: "FP1" }, + 0x5145_4d55_4600, + ))); + b.add(Box::new(board_record_part( &BoardRecord { barcode: "EMUS00", part: "060-0028-003", revision: "A", name: "PWR.SPPLY.SR" }, 0x5145_4d55_5300, - )); + ))); M(b) } @@ -263,6 +268,95 @@ fn a_wrong_match_rom_leaves_every_part_unselected() { assert!(m.reset()); } +/// A DS2502, a DS2501 and a DS2505 with distinct images: families 0x09, +/// 0x91 and 0x0b, so their ROMs part ways in the second bit. +fn three_parts() -> [Box; 3] { + let img = |n: usize, mul: usize, add: usize| (0..n).map(|i| (i * mul + add) as u8).collect::>(); + [ + Box::new(Ds2502::new(0x09, 0x0123_4567_89ab, &img(128, 3, 1))), + Box::new(Ds2502::ds2501(0x91, 0x5e74_54d5_5000, &img(64, 5, 2))), + Box::new(Ds2505::new(0x0b, 0x5145_4d55_4600, &img(64, 7, 9))), + ] +} + +fn three_part_wire() -> M { + let mut b = Bus::new(); + for p in three_parts() { + b.add(p); + } + M(b) +} + +#[test] +fn search_rom_on_three_parts_answers_the_and_of_the_parts_still_selected() { + let roms: Vec<[u8; 8]> = three_parts().iter().map(|p| p.rom()).collect(); + let mut discrepancies = 0; + // The master follows each part's ROM in turn. + for target in 0..3 { + let mut m = three_part_wire(); + assert!(m.reset()); + m.write(0xf0); + let mut selected = vec![0, 1, 2]; + for i in 0..64 { + let bit = |k: usize| roms[k][i / 8] >> (i % 8) & 1 != 0; + // Every selected part sends its bit, then its complement; the + // wire is low if any of them sends a zero. + let want_bit = selected.iter().all(|&k| bit(k)); + let want_complement = selected.iter().all(|&k| !bit(k)); + assert_eq!(m.bit(true), want_bit, "target {target} bit {i}"); + assert_eq!(m.bit(true), want_complement, "target {target} complement {i}"); + if !want_bit && !want_complement { + discrepancies += 1; + } + let choice = bit(target); + m.bit(choice); + selected.retain(|&k| bit(k) == choice); + } + assert_eq!(selected, [target], "only the followed part is left"); + // The part left selected answers the next memory command. + m.write(0xf0); + m.write(0); + m.write(0); + let want = one(three_parts().into_iter().nth(target).unwrap()).skip(0xf0, 0, 5); + assert_eq!(m.read_n(5), want, "target {target}"); + } + assert!(discrepancies > 0, "the families differ, so the parts disagree somewhere"); +} + +#[test] +fn skip_rom_read_memory_on_three_parts_wires_the_streams_together() { + let mut want = vec![0xffu8; 16]; + for p in three_parts() { + for (w, b) in want.iter_mut().zip(one(p).skip(0xf0, 0, 16)) { + *w &= b; + } + } + assert_eq!(three_part_wire().skip(0xf0, 0, 16), want); +} + +#[test] +fn read_rom_on_three_parts_is_the_and_of_their_roms_and_a_bad_match_selects_none() { + let roms: Vec<[u8; 8]> = three_parts().iter().map(|p| p.rom()).collect(); + let mut and = [0xffu8; 8]; + for rom in &roms { + for i in 0..8 { + and[i] &= rom[i]; + } + } + let mut m = three_part_wire(); + assert!(m.reset()); + m.write(0x33); + assert_eq!(m.read_n(8), and); + // One wrong bit in a real ROM matches no part, so none answers. + let mut bad = roms[0]; + bad[3] ^= 1; + m.match_rom(&bad); + m.write(0xf0); + m.write(0); + m.write(0); + assert_eq!(m.read_n(2), [0xff; 2]); +} + #[test] fn a_part_leaves_a_match_at_the_first_differing_bit() { // Two parts whose ROMs differ in the first byte: after the bit that diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 0dada67d..76e6242c 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -28,7 +28,7 @@ use crate::dev::isp1040::Isp1040; use crate::dev::mgras::hq4::ImpactXio; use crate::dev::rad1::Rad1; use crate::dev::sflash::{CfiFlash, FLASH_SIZE}; -use crate::dev::sgi_nic::{board_nic, BoardRecord}; +use crate::dev::sgi_nic::{board_record_part, BoardRecord}; use crate::dev::w1; use crate::dev::xbow::Xbow; use crate::hptimer::TimerManager; @@ -71,7 +71,7 @@ pub fn heart_banks(cfg: &MachineConfig) -> [u64; 4] { /// Octane's. fn record_bus(barcode: &str, part: &str, revision: &str, name: &str, serial: u64) -> w1::Bus { let mut bus = w1::Bus::new(); - bus.add(board_nic(&BoardRecord { barcode, part, revision, name }, serial)); + bus.add(Box::new(board_record_part(&BoardRecord { barcode, part, revision, name }, serial))); bus } From cd79bc7ecdb3143462c354cea070837d65bf8b0c Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:15:27 -0700 Subject: [PATCH 02/15] dev: compile test-only hooks only for tests Ioc3::set_parallel_backend, Bridge::set_dma_64bit and w1's slot_wired exist so tests can exercise paths no machine uses yet. In a normal build they are dead code. Compile them only for tests and say why in each doc comment. --- src/dev/bridge.rs | 4 +++- src/dev/ioc3.rs | 4 +++- src/dev/w1/slot.rs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index a3e1f589..976e9f0f 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -652,7 +652,9 @@ impl Bridge { } /// Mark `slot`'s data phases 64 bits wide (its card drove REQ64# at - /// reset). Affects the byte placement of its DMA. + /// reset). Affects the byte placement of its DMA. No machine marks a card + /// 64 bits wide yet, so only the tests call it. + #[cfg(test)] pub fn set_dma_64bit(&self, slot: u8, wide: bool) { let mut st = self.st.lock(); st.dma_64bit = (st.dma_64bit & !(1 << slot)) | ((wide as u8) << slot); diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index ca135d0e..004bca4f 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -2566,7 +2566,9 @@ impl Ioc3 { self.sh.st.lock().uart[port].backend = backend; } - /// Where parallel-port output goes. Without one it is dropped. + /// Where parallel-port output goes. Without one it is dropped. No machine + /// wires one, so only the tests set it. + #[cfg(test)] pub fn set_parallel_backend(&self, backend: Arc) { self.sh.st.lock().par.backend = Some(backend); } diff --git a/src/dev/w1/slot.rs b/src/dev/w1/slot.rs index d7f4859e..4f297aa7 100644 --- a/src/dev/w1/slot.rs +++ b/src/dev/w1/slot.rs @@ -31,7 +31,9 @@ pub fn slot(bus: &mut Bus, pulse: u32, _sample: u32) -> bool { } /// A slot on a master whose wire also reaches a second net: both buses -/// see it, and the wire reads low if a part on either pulls it low. +/// see it, and the wire reads low if a part on either pulls it low. No +/// machine joins two nets yet, so only the tests build it. +#[cfg(test)] pub fn slot_wired(bus: &mut Bus, shared: Option<&mut Bus>, pulse: u32, sample: u32) -> bool { let level = slot(bus, pulse, sample); match shared { From 065654f294c6a30006757614281cd7079a6ac0b4 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:17:35 -0700 Subject: [PATCH 03/15] logging: route guest-triggerable device messages through dlog! A message a guest driver can provoke on every access goes to dlog!, not stderr: an unsupported register width, a ring that is not programmed, a DMA that misses memory, a target error. A guest that repeats one would flood the host console, and nothing could silence it. dlog! output is per module and can be turned off. The ISP1040 messages use the existing scsi module, and the target error is also recorded in the device's note log like its siblings. RAD1 had no module, so it gets rad1. A host failure is not guest-triggered and stays on stderr. The IOC3's NVRAM save fails when the disk is full and is retried on every scheduled save, so it printed on each one. It now prints once until a save succeeds again. HEART and the Crossbow already record their refusals in bounded lists the monitor reads, and the Bridge reports its own at log debug level, so they need no change. --- src/dev/ioc3.rs | 23 +++++++++++++++++------ src/dev/isp1040.rs | 6 ++++-- src/dev/rad1.rs | 19 ++++++++++--------- src/devlog.rs | 10 ++++++++-- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 004bca4f..8d5b3c87 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -820,6 +820,21 @@ impl Rtc { } } +/// Say that an NVRAM save failed, once until a save succeeds again: the +/// guest's writes keep scheduling saves, and a full disk would otherwise +/// print on every one. +fn report_nvram_save(path: &std::path::Path, result: std::io::Result<()>) { + static FAILING: AtomicBool = AtomicBool::new(false); + match result { + Ok(()) => FAILING.store(false, Ordering::Relaxed), + Err(e) => { + if !FAILING.swap(true, Ordering::Relaxed) { + eprintln!("ioc3: NVRAM {}: {e}", path.display()); + } + } + } +} + /// Replace the NVRAM file in one step (a temporary file, synced and /// renamed), so a kill mid-write leaves the previous image whole. fn save_nvram(path: &std::path::Path, image: &[u8]) -> std::io::Result<()> { @@ -2403,9 +2418,7 @@ impl Shared { None => return, } }; - if let Err(e) = save_nvram(&job.0, &job.1) { - eprintln!("ioc3: NVRAM {}: {e}", job.0.display()); - } + report_nvram_save(&job.0, save_nvram(&job.0, &job.1)); } } @@ -2458,9 +2471,7 @@ impl Shared { fn flush_nvram(&self, quiet: Duration) { let Some((path, image)) = self.st.lock().rtc.take_dirty(quiet) else { return }; - if let Err(e) = save_nvram(&path, &image) { - eprintln!("ioc3: NVRAM {}: {e}", path.display()); - } + report_nvram_save(&path, save_nvram(&path, &image)); } /// One synchronous pass of what the receive threads and timers do: diff --git a/src/dev/isp1040.rs b/src/dev/isp1040.rs index afde5c08..e066f87a 100644 --- a/src/dev/isp1040.rs +++ b/src/dev/isp1040.rs @@ -44,6 +44,7 @@ use std::sync::{Arc, OnceLock}; use parking_lot::{Condvar, Mutex, MutexGuard}; +use crate::devlog::LogModule; use crate::irq::IrqLine; use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PCI_INTA}; use crate::scsi::{scsi_cmd, ScsiDataLength, ScsiDevice, ScsiIdentity, ScsiRequest}; @@ -743,7 +744,7 @@ impl Core { }; if first { let what = if write { "write to" } else { "read from" }; - eprintln!("isp1040: DMA {what} {addr:#x} is not memory"); + crate::dlog!(LogModule::Scsi, "isp1040: DMA {what} {addr:#x} is not memory"); self.note(format!("DMA {what} {addr:#x} is not memory")); } } @@ -1150,7 +1151,8 @@ impl Core { Some(match dev.request(&req) { Ok(r) => (r.data, r.status), Err(e) => { - eprintln!("isp1040: target {target}: {e}"); + crate::dlog!(LogModule::Scsi, "isp1040: target {target}: {e}"); + self.note(format!("target {target}: {e}")); (Vec::new(), CHECK_CONDITION) } }) diff --git a/src/dev/rad1.rs b/src/dev/rad1.rs index eb3036f2..f9e1e4af 100644 --- a/src/dev/rad1.rs +++ b/src/dev/rad1.rs @@ -48,6 +48,7 @@ use parking_lot::Mutex; use std::time::{Duration, Instant}; use crate::config::AudioConfig; +use crate::devlog::LogModule; use crate::host_audio::{HostOut, Resampler}; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; use crate::traits::Device; @@ -459,20 +460,20 @@ impl State { let tag = (ctrl >> CTRL_TAG_SHIFT) & CTRL_TAG_MASK; if base == 0 || len == 0 { - eprintln!("rad1: {} enabled with no ring programmed (address {base:#x}, control {ctrl:#x})", info.name); + crate::dlog!(LogModule::Rad1, "rad1: {} enabled with no ring programmed (address {base:#x}, control {ctrl:#x})", info.name); return; } if len > DMA_MAX_BYTES { - eprintln!("rad1: {} ring length {len:#x} exceeds the modelled cap; clamping", info.name); + crate::dlog!(LogModule::Rad1, "rad1: {} ring length {len:#x} exceeds the modelled cap; clamping", info.name); len = DMA_MAX_BYTES; } // What hardware does with a partial frame is unknown; start nothing. if !len.is_multiple_of(info.frame_bytes) { - eprintln!("rad1: {} ring length {len:#x} is not a whole number of frames", info.name); + crate::dlog!(LogModule::Rad1, "rad1: {} ring length {len:#x} is not a whole number of frames", info.name); return; } if tag != info.tag { - eprintln!("rad1: {} control word {ctrl:#x} carries tag {tag}, not the channel type {}", info.name, info.tag); + crate::dlog!(LogModule::Rad1, "rad1: {} control word {ctrl:#x} carries tag {tag}, not the channel type {}", info.name, info.tag); } self.chan[idx] = Chan { running: true, base, len, offset: 0, start_ns: now, frames: 0 }; } @@ -779,7 +780,7 @@ impl Inner { s.ust_update(now); return s.ust_count; } - eprintln!("rad1: unsupported {width}-byte BAR0 read at {off:#x}"); + crate::dlog!(LogModule::Rad1, "rad1: unsupported {width}-byte BAR0 read at {off:#x}"); 0 } @@ -790,7 +791,7 @@ impl Inner { let now = self.now(); let mut s = self.state.lock(); if self.write_locked(&mut s, off, val, width, now).is_none() { - eprintln!("rad1: unsupported {width}-byte BAR0 write at {off:#x} value {val:#x}"); + crate::dlog!(LogModule::Rad1, "rad1: unsupported {width}-byte BAR0 write at {off:#x} value {val:#x}"); } self.arm_audio(&mut s); } @@ -941,7 +942,7 @@ impl Inner { let interval = (ctrl >> STATUS_INTERVAL_SHIFT) & STATUS_INTERVAL_MASK; s.status_gen += 1; if interval == 0 { - eprintln!("rad1: continuous status DMA started with a zero interval (control {ctrl:#x}); stopping"); + crate::dlog!(LogModule::Rad1, "rad1: continuous status DMA started with a zero interval (control {ctrl:#x}); stopping"); s.status_running = false; self.cancel_status(s); return; @@ -1058,7 +1059,7 @@ impl Inner { && put(SLOT_FLAGS_OFFSET, &0u32.to_le_bytes()) && put(SLOT_DONE_OFFSET, &SLOT_DONE_WORD.to_le_bytes()); if !ok { - eprintln!("rad1: status DMA at {slot:#x} failed"); + crate::dlog!(LogModule::Rad1, "rad1: status DMA at {slot:#x} failed"); } ok } @@ -1124,7 +1125,7 @@ impl Inner { } }); if !ok { - eprintln!("rad1: {} DMA at {:#x} + {:#x} failed", info.name, c.base, c.offset); + crate::dlog!(LogModule::Rad1, "rad1: {} DMA at {:#x} + {:#x} failed", info.name, c.base, c.offset); return false; } if info.output { diff --git a/src/devlog.rs b/src/devlog.rs index e27dc116..ea17db92 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -49,6 +49,9 @@ pub enum LogModule { Nveeprom = 25, /// GR2 (XZ / Extreme) graphics: HQ2, GE7, RE3, VC1, XMAP5, Bt457. Gr2 = 26, + /// The Octane's RAD1 PCI audio: rings, status DMA and register accesses + /// the model does not support. + Rad1 = 27, } // Mask bits shared by L1i / L1d / L2c @@ -57,7 +60,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 = 27; + pub const COUNT: usize = 28; pub fn from_str(s: &str) -> Option { match s { @@ -88,6 +91,7 @@ impl LogModule { "l2c" => Some(Self::L2c), "ultra" => Some(Self::Ultra), "gr2" => Some(Self::Gr2), + "rad1" => Some(Self::Rad1), _ => None, } } @@ -121,6 +125,7 @@ impl LogModule { Self::L2c => "l2c", Self::Ultra => "ultra", Self::Gr2 => "gr2", + Self::Rad1 => "rad1", } } @@ -131,7 +136,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::Nveeprom, Self::Gr2, Self::Rad1, ] } @@ -271,6 +276,7 @@ impl DevLog { ModuleLog::new(), // Scsi1 ModuleLog::new(), // Nveeprom ModuleLog::new(), // Gr2 + ModuleLog::new(), // Rad1 ], } } From 2465793eb02a3f8ba50c8b6a1f0a9ccca92a39ea Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:18:14 -0700 Subject: [PATCH 04/15] cpu: test the Compare delay without waiting on the wall clock The Octane Compare-across-the-wrap test armed a real hptimer one-shot and polled for IP7 for up to two seconds, so it depended on host timing and was compiled out under ci_clock. Move the delay arithmetic into compare_delay_ns and assert its result directly: a target just past the wrap waits the short distance, a target equal to Count waits a full wrap, and one behind Count waits out the wrap. The test no longer sleeps, so it runs under ci_clock too. --- src/cpu/mips_core.rs | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/cpu/mips_core.rs b/src/cpu/mips_core.rs index fbe19449..a39e6300 100644 --- a/src/cpu/mips_core.rs +++ b/src/cpu/mips_core.rs @@ -1830,11 +1830,7 @@ impl MipsCore { // The Compare *write* path classifies that case (a deliberate ack vs // a deadline we overran) and delivers IP7 itself when warranted; this // function only schedules the next architectural match. - let delta = match (self.cp0_compare as u32).wrapping_sub(self.cp0_count as u32) as u64 { - 0 => 1u64 << 32, - d => d, - }; - let ns = ((delta as u128 * 1_000_000_000) / self.count_hz as u128) as u64; + let ns = compare_delay_ns(self.cp0_compare as u32, self.cp0_count as u32, self.count_hz); #[cfg(feature = "ci_clock")] { self.count_fire_cycle = self.hot.cycles.saturating_add(ns / NS_PER_GUEST_CYCLE); @@ -2679,6 +2675,17 @@ impl Default for MipsCore { } } +/// Host nanoseconds until a Count at `count` next equals `compare`, at `hz` +/// ticks per second. The distance is taken modulo 2^32, so a target just +/// past the wrap is a short wait; a target equal to Count is a full wrap. +fn compare_delay_ns(compare: u32, count: u32, hz: u64) -> u64 { + let delta = match compare.wrapping_sub(count) as u64 { + 0 => 1u64 << 32, + d => d, + }; + ((delta as u128 * 1_000_000_000) / hz as u128) as u64 +} + #[cfg(test)] mod ip7_ticket_tests { use super::*; @@ -2939,22 +2946,17 @@ mod ip7_ticket_tests { /// The one-shot armed across the wrap waits the short distance, not a /// full wrap (about 44 s at 97.5 MHz). - #[cfg(not(feature = "ci_clock"))] #[test] - fn octane_one_shot_armed_across_the_wrap_fires_promptly() { - let mut core = octane_core(); - // The one-shot runs on the host clock: the real rate, so the short - // distance is microseconds and a full wrap would be 44 s. - core.set_count_hz(97_500_000); - core.set_timer_manager(Arc::new(crate::hptimer::TimerManager::new())); - core.write_cp0(9, 0xFFFF_FF00); - core.hot.cycles += 1; - // 0x1000 ticks past the wrap: about 45 us. - core.write_cp0(11, 0x1000); - let t0 = std::time::Instant::now(); - while !ip7(&core) && t0.elapsed() < std::time::Duration::from_secs(2) { - std::thread::sleep(std::time::Duration::from_millis(1)); - } - assert!(ip7(&core), "the Compare one-shot across the wrap did not fire within 2 s"); + fn octane_one_shot_armed_across_the_wrap_waits_the_short_distance() { + let hz = 97_500_000; + // 0x1000 ticks past the wrap from 0x100 before it: 0x1100 ticks, + // about 45 us. + let ns = compare_delay_ns(0x1000, 0xFFFF_FF00, hz); + assert_eq!(ns, 0x1100u64 * 1_000_000_000 / hz); + assert!(ns < 1_000_000, "{ns} ns is not the short distance"); + // A target equal to Count is a full wrap away, about 44 s. + assert_eq!(compare_delay_ns(5, 5, hz), (1u64 << 32) * 1_000_000_000 / hz); + // A target behind Count waits out the wrap. + assert!(compare_delay_ns(0x10, 0x20, hz) > 40_000_000_000); } } From 48ef885c514c14641db45868abc266017254624b Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:49:48 -0700 Subject: [PATCH 05/15] ip30: test the memory map's bus-level behaviour Add bus-level tests for parts of the map the other tests leave open: a valid descriptor over an empty bank, the PROM's bank-size probe answers, a bank above 4 GiB, a bus master reaching the guarded 16 KiB, and the alias sharing page generations with SDRAM under jitv2. One test shows that the window path (mem_ptr) reaches the bytes at 0x2000_3000, because the ppmem window maps the whole 32 MiB unit, while an uncached read faults. Real HEART faults cached accesses there too. It is named as a limitation and listed in docs/octane-ip30.md, so a change to it is deliberate. --- docs/octane-ip30.md | 6 ++ src/ip30/tests.rs | 155 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 2 deletions(-) diff --git a/docs/octane-ip30.md b/docs/octane-ip30.md index c16dc677..36c43359 100644 --- a/docs/octane-ip30.md +++ b/docs/octane-ip30.md @@ -180,6 +180,12 @@ through its event channel. `HACKING.md` lists the thread names. which the processor reaches only there (0x2000_0000..+16 KiB is a bus error, as the stock IDE memory diagnostics expect). Whether bus masters reach that range is not known; the DMA view does. +- A cached access to 0x2000_0000..+16 KiB reaches the bytes. On an Octane2 + (HEART rev F) cached and uncached reads there both take a data bus + error; here only uncached accesses do, because the processor's cache + reads memory straight from the ppmem window, which maps the whole first + 32 MiB. Faulting the cached access would put that whole 32 MiB unit of + SDRAM on the slower bus path. No supported guest touches the range. - The PROM's cached stores to its own data in the flash are kept in a per-line copy, as a write-back cache would hold them; they are never written back. diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 68758b1d..baed3842 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -54,8 +54,13 @@ struct Board { } fn board(prom: &[u8]) -> Board { - let ram = Arc::new(HeartRam::new([128, 0, 0, 0])); - let heart = Arc::new(Heart::new(HeartConfig::octane([128 << 20, 0, 0, 0]), crate::dev::heart::host_clock())); + board_with(prom, [128, 0, 0, 0]) +} + +/// `board` with the DIMMs fitted in each bank, in MB. +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()); let bridge = Bridge::new(ram.dma_view(), heart.clone()); @@ -209,6 +214,152 @@ fn sdram_follows_the_descriptors() { assert_eq!(p.read64(0x1_1000_0000).status, BUS_ERR); } +// The memory map as the processor and a bus master see it. Each of these +// holds on the bus path; the one that does not hold for a cached access is +// named as a limitation. + +/// Write HEART's descriptor for `bank`, as the PROM does through the PIU. +fn set_bank(p: &Physical, bank: u32, desc: u32) { + assert_eq!(p.write32(PIU_BASE + 0x20 + 4 * bank as u64, desc), BUS_OK); +} + +#[test] +fn map_a_valid_descriptor_over_an_empty_bank_reads_zero_and_drops_writes() { + let b = board(&[]); + let p = &b.phys; + let dma = b.bus.memory(); + // Bank 1 is not fitted. Its descriptor claims 64 MiB at the third unit. + set_bank(p, 1, 0x8001_0002); + let at = 0x2000_0000 + 2 * 0x200_0000 + 0x100; + assert_eq!(p.write64(at, u64::MAX), BUS_OK, "the write is accepted"); + assert_eq!(p.read64(at).data, 0, "and goes nowhere"); + assert_eq!(p.read8(at).status, BUS_OK); + assert_eq!(dma.read64(at).status, BUS_ERR, "a bus master gets an error"); + assert_eq!(p.read64(at + 0x400_0000).status, BUS_ERR, "past the descriptor's span"); + // With no other descriptor over it, the start of SDRAM is empty memory + // too: low memory answers as that bank does, past the guard. + set_bank(p, 1, 0x8001_0000); + assert_eq!(p.write32(0x100, 5), BUS_OK); + assert_eq!(p.read32(0x100).data, 0); + assert_eq!(p.read8(0x2000_0000).status, BUS_ERR, "the guard still faults"); + assert_eq!(p.read32(0x2000_4000).data, 0); + assert_eq!(dma.read32(0x2000_0100).status, BUS_ERR); +} + +#[test] +fn map_the_proms_bank_size_probe_gets_each_size_s_answer() { + // The stock PROM sizes a bank through a temporary 2 GiB descriptor + // (0x81bf0040), writing patterns and watching which address bits alias, + // then looks the result up. These are the descriptors it settles on. + const PROBE: u64 = 0x2000_0000 + 0x40 * 0x200_0000; + const UNIT: u64 = 0x200_0000; + for (mib, want) in [(64u32, 0x8101_0040u32), (128, 0x8103_0040), (256, 0x8147_0040)] { + let b = board_with(&[], [mib, 0, 0, 0]); + let p = &b.phys; + set_bank(p, 0, 0x81bf_0040); + let distinct = |step: u64, pat: u64| { + assert_eq!(p.write64(PROBE, pat), BUS_OK); + assert_eq!(p.write64(PROBE + step, !pat), BUS_OK); + p.read64(PROBE).data == pat + }; + let mut bits = 0; + assert_eq!(p.write64(PROBE + 0x100, 0x1111_1111_1111_1111), BUS_OK); + assert_eq!(p.write64(PROBE, !0x1111_1111_1111_1111), BUS_OK); + if p.read64(PROBE + 0x100).data == 0x1111_1111_1111_1111 { + bits |= 0x20; + } + if distinct(4 * UNIT, 0x2222_2222_2222_2222) { + bits |= 0x10; + } else if distinct(UNIT, 0x4444_4444_4444_4444) { + bits |= 0x08; + } + if distinct(32 * UNIT, 0x8888_8888_8888_8888) { + bits |= 0x04; + } + // The PROM's table, indexed by the three bits it found. + const TABLE: [u32; 16] = [ + 0x0000_0040, 0x0001_0040, 0x0043_0040, 0x0047_0040, 0x008f_0040, 0x009f_0040, 0x0000_0040, 0x0000_0040, + 0x0101_0040, 0x0103_0040, 0x0147_0040, 0x014f_0040, 0x019f_0040, 0x01bf_0040, 0x0000_0040, 0x0000_0040, + ]; + let word = TABLE[bits / 4] | 0x8000_0000; + assert_eq!(word, want, "{mib} MiB"); + set_bank(p, 0, word); + assert_eq!(b.heart.memcfg()[0], want); + // The descriptor it settles on holds the fitted memory where the probe was, nothing past it. + let end = PROBE + ((mib as u64) << 20); + assert_eq!(p.write64(end - 8, 3), BUS_OK, "{mib} MiB: last doubleword"); + assert_eq!(p.read64(end - 8).data, 3); + assert_eq!(p.read64(end).status, BUS_ERR, "{mib} MiB: past the end"); + } +} + +#[test] +fn map_a_bank_above_4_gib_is_reached_through_the_bus_only() { + let b = board_with(&[], [64, 64, 0, 0]); + let p = &b.phys; + let dma = b.bus.memory(); + // Bank 0 at the start of SDRAM space; bank 1 at unit 0x80, 4 GiB up. + set_bank(p, 0, 0x8001_0000); + set_bank(p, 1, 0x8001_0080); + let high = 0x2000_0000 + 0x80 * 0x200_0000; + assert_eq!(high, 0x1_2000_0000); + assert_eq!(p.write64(high + 0x8000, 0x0123_4567_89ab_cdef), BUS_OK); + assert_eq!(p.read64(high + 0x8000).data, 0x0123_4567_89ab_cdef); + assert_eq!(dma.read64(high + 0x8000).data, 0x0123_4567_89ab_cdef); + assert_eq!(p.read64(0x2000_8000).data, 0, "bank 0 is a different bank"); + assert!(p.mem_ptr(high + 0x8000).is_some(), "it has a direct pointer"); + assert_eq!(p.read64(high + 0x400_0000).status, BUS_ERR, "past the bank"); +} + +#[test] +fn map_a_bus_master_reaches_the_guarded_bytes() { + let b = board(&[]); + let p = &b.phys; + set_bank(p, 0, 0x8003_0000); + let dma = b.bus.memory(); + assert_eq!(p.read64(0x2000_0000).status, BUS_ERR); + assert_eq!(p.write64(0x2000_3000, 1), BUS_ERR); + assert_eq!(dma.write64(0x2000_3000, 0x0a0b_0c0d_0e0f_1011), BUS_OK); + assert_eq!(dma.read64(0x2000_3000).data, 0x0a0b_0c0d_0e0f_1011); + assert_eq!(p.read64(0x3000).data, 0x0a0b_0c0d_0e0f_1011, "the same bytes, at low memory"); + let mut bytes = [0u8; 8]; + assert_eq!(read_bytes(&*dma, 0x2000_3000, &mut bytes), BUS_OK); + assert_eq!(bytes, [0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11]); +} + +/// Low memory and SDRAM are one page-generation counter, whichever way a +/// store comes in: through the alias, at the SDRAM address by a bus master, +/// or through a block write. +#[cfg(feature = "jitv2")] +#[test] +fn map_the_alias_and_sdram_share_page_generations() { + let b = board(&[]); + let p = &b.phys; + set_bank(p, 0, 0x8003_0000); + let dma = b.bus.memory(); + let gen = |g: *const AtomicU64| unsafe { (*g).load(Ordering::Acquire) }; + let low = p.gen_ptr(0x2000); + assert!(!low.is_null()); + assert_eq!(low, dma.gen_ptr(0x2000_2000)); + let v = gen(low); + assert_eq!(p.write_block(0x2000, &[1, 2]), BUS_OK); + assert_ne!(gen(low), v, "a block store through the alias"); + let v = gen(low); + assert_eq!(dma.write_block(0x2000_2000, &[3]), BUS_OK); + assert_ne!(gen(low), v, "a bus master's block store at the SDRAM address"); +} + +/// Limitation: real HEART faults cached accesses to the guard as well, but +/// the CPU's fast paths go through the ppmem window, which maps the whole +/// 32 MiB unit, guard included. +#[test] +fn limitation_the_window_reaches_the_guard() { + let b = board(&[]); + set_bank(&b.phys, 0, 0x8003_0000); + assert_eq!(b.phys.read32(0x2000_3000).status, BUS_ERR, "uncached reads fault"); + assert!(b.phys.mem_ptr(0x2000_3000).is_some(), "limitation: the window path reaches the bytes"); +} + #[test] fn widgets_route_by_number() { let b = board(&[]); From 91332587597cba62f12a5386190449bb3d7d3e98 Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 11:57:46 -0700 Subject: [PATCH 06/15] heart_mem: skip a MEMCFG write that leaves the decode alone A descriptor write rebuilt the bus entries and cleared and remapped the ppmem window every time, including when the PROM rewrites the same words. That empties the window under a running CPU for no change and sends its cached accesses through the bus meanwhile. Compare the bits the decode reads and return early when they match; the monitor still sees the words written. A real change already bumps every page generation in the rebuild, and a test now says so. --- src/dev/heart_mem.rs | 56 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/dev/heart_mem.rs b/src/dev/heart_mem.rs index 76c1aa35..2c6dc7d7 100644 --- a/src/dev/heart_mem.rs +++ b/src/dev/heart_mem.rs @@ -223,9 +223,22 @@ impl HeartRam { /// HEART's descriptors changed: place the banks as they now say. /// Called on the CPU thread (a MEMCFG write, which the PROM makes before /// anything else runs) or with the CPU stopped. + /// + /// A write that leaves the decode as it was does nothing to the window + /// or the page generations: the PROM rewrites the same descriptors, and + /// a rebuild empties the window under a running CPU. pub fn set_memcfg(&self, words: [u32; 4]) { + let same = Self::decoded(&self.desc.load()) == Self::decoded(&words); self.desc.store(words); - self.rebuild(); + if !same { + self.rebuild(); + } + } + + /// The bits of each descriptor the decode reads: all of a valid one + /// (valid, density, span, base), nothing of an invalid one. + fn decoded(words: &[u32; BANKS]) -> [u32; BANKS] { + words.map(|d| if d & DESC_VALID == 0 { 0 } else { d & (DESC_VALID | 0x7 << 22 | 0x3f << 16 | 0x1ff) }) } /// Place the banks as the descriptors in force say: after `set_memcfg`, @@ -561,6 +574,47 @@ mod tests { assert_eq!(m.phys().read64(RAM_BASE + 0x8000).status, BUS_ERR, "and the table forgets the units"); } + #[test] + fn a_write_that_leaves_the_decode_alone_does_not_touch_the_window() { + let m = ram(128); + let d = DESC_VALID | (3 << 16); + bank0(&m, d); + let space = m.phys().ppmem_space().unwrap(); + let mapped = space.mapped_bitmap(); + assert_ne!(mapped, 0); + // Drop the mappings behind its back: a rebuild would put them back. + space.clear_mappings(); + bank0(&m, d); + assert_eq!(space.mapped_bitmap(), 0, "the same descriptors: no rebuild"); + // Bits the decode ignores, and the rest of an invalid descriptor. + m.set_memcfg([d | 1 << 29, 0x7fff_ffff, 0, 0]); + assert_eq!(space.mapped_bitmap(), 0, "ignored bits: no rebuild"); + assert_eq!(m.memcfg()[1], 0x7fff_ffff, "but the monitor sees what was written"); + bank0(&m, DESC_VALID | (1 << 16)); + assert_eq!(space.mapped_bitmap(), 1 << (RAM_BASE >> 26), "a change lays it out"); + assert_ne!(mapped, space.mapped_bitmap()); + } + + /// A rebuild bumps every page's generation, so compiled code from a page + /// that now holds other bytes is dropped; a repeated write does not. + #[cfg(feature = "jitv2")] + #[test] + fn a_changed_descriptor_bumps_the_page_generations() { + let gen = |p: *const AtomicU64| unsafe { (*p).load(Ordering::Acquire) }; + let m = ram(128); + let d = DESC_VALID | (3 << 16); + bank0(&m, d); + let g = m.phys().gen_ptr(RAM_BASE + 0x8000); + let v = gen(g); + bank0(&m, d); + assert_eq!(gen(g), v, "the same descriptors leave it"); + bank0(&m, DESC_VALID | (1 << 16)); + assert_ne!(gen(g), v, "a change advances it"); + let v = gen(g); + m.set_memcfg([0; 4]); + assert_ne!(gen(g), v, "so does taking the memory away"); + } + #[test] fn the_higher_bank_answers_where_two_overlap() { let m = HeartRam::new([64, 64, 0, 0]); From e9d98e9acc51f30fd8ef091e467801199d65a420 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 22:18:10 -0700 Subject: [PATCH 07/15] ip30: stop locking once a miss or unsupported list is full The bus's miss list and the HEART and Crossbow unsupported-access lists keep their first 64 distinct entries. Once full they still took the lock and scanned the list on every access, which a guest polling an absent register does continuously. Set a flag when the list fills and return on one relaxed load, as the O2's miss list does. --- src/dev/heart.rs | 27 +++++++++++++++++++++++++-- src/dev/xbow.rs | 26 ++++++++++++++++++++++++-- src/ip30.rs | 13 +++++++++++-- src/ip30/tests.rs | 14 ++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/dev/heart.rs b/src/dev/heart.rs index 9c602579..f98f3bc9 100644 --- a/src/dev/heart.rs +++ b/src/dev/heart.rs @@ -18,7 +18,7 @@ //! //! Ported from hw/misc/sgi-heart.c in jamesbraid/qemu-sgi-origami. -use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; use std::sync::Arc; use parking_lot::Mutex; @@ -195,6 +195,8 @@ pub struct Heart { /// Accesses this model does not support, first 64 distinct, for the /// monitor: `(offset, size, write)`. unsupported: Mutex>, + /// `unsupported` holds 64: a further access costs one relaxed load. + unsupported_full: AtomicBool, /// Packets that reached HEART's interrupt register, for the monitor. pub packets: AtomicU64, } @@ -210,6 +212,7 @@ impl Heart { memcfg_hook: Mutex::new(None), mlan: Mutex::new(w1::Bus::new()), unsupported: Mutex::new(Vec::new()), + unsupported_full: AtomicBool::new(false), packets: AtomicU64::new(0), } } @@ -256,12 +259,18 @@ impl Heart { } fn note(&self, off: u32, size: usize, write: bool) { + if self.unsupported_full.load(Ordering::Relaxed) { + return; + } let mut u = self.unsupported.lock(); - if u.len() < 64 && !u.iter().any(|&(o, s, w)| o == off && s as usize == size && w == write) { + if !u.iter().any(|&(o, s, w)| o == off && s as usize == size && w == write) { if trace() { eprintln!("ip30: HEART: unsupported {size}-byte {} at {off:#x}", if write { "write" } else { "read" }); } u.push((off, size as u8, write)); + if u.len() == 64 { + self.unsupported_full.store(true, Ordering::Relaxed); + } } } @@ -759,6 +768,20 @@ mod tests { h.piu_read(off, 8).unwrap() } + #[test] + fn the_unsupported_list_keeps_the_first_64_and_then_stops() { + let f = heart(); + let h = &f.heart; + for i in 0..100u32 { + h.note(0x1000 + 8 * i, 8, false); + } + let u = h.unsupported(); + assert_eq!(u.len(), 64); + assert_eq!(u[63], (0x1000 + 8 * 63, 8, false)); + h.note(0x9000, 4, true); + assert_eq!(h.unsupported(), u, "a full list is left alone"); + } + #[test] fn descriptor_writes_reach_memory_and_reset_clears_them() { let f = heart(); diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index edd34224..5243076f 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -20,7 +20,7 @@ //! //! Ported from hw/misc/sgi-xbow.c in jamesbraid/qemu-sgi-origami. -use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, OnceLock}; use parking_lot::Mutex; @@ -194,6 +194,8 @@ pub struct Xbow { /// has settled: the widget at the far end resets too. on_link_reset: OnceLock>, unsupported: Mutex>, + /// `unsupported` holds 64: a further access costs one relaxed load. + unsupported_full: AtomicBool, } fn link_bit(widget: u8) -> Option { @@ -215,6 +217,7 @@ impl Xbow { on_link_change: OnceLock::new(), on_link_reset: OnceLock::new(), unsupported: Mutex::new(Vec::new()), + unsupported_full: AtomicBool::new(false), } } @@ -369,9 +372,15 @@ impl Xbow { } fn note(&self, off: u32, size: usize, write: bool) { + if self.unsupported_full.load(Ordering::Relaxed) { + return; + } let mut u = self.unsupported.lock(); - if u.len() < 64 && !u.iter().any(|&(o, s, w)| o == off && s as usize == size && w == write) { + if !u.iter().any(|&(o, s, w)| o == off && s as usize == size && w == write) { u.push((off, size as u8, write)); + if u.len() == 64 { + self.unsupported_full.store(true, Ordering::Relaxed); + } } } @@ -623,6 +632,19 @@ mod tests { assert!(x.carries(8) && x.carries(15) && !x.carries(11) && !x.carries(3)); } + #[test] + fn the_unsupported_list_keeps_the_first_64_and_then_stops() { + let x = octane(); + for i in 0..100u32 { + x.note(0x1000 + 8 * i, 4, false); + } + let u = x.unsupported(); + assert_eq!(u.len(), 64); + assert_eq!(u[63], (0x1000 + 8 * 63, 4, false)); + x.note(0x9000, 4, true); + assert_eq!(x.unsupported(), u, "a full list is left alone"); + } + #[test] fn malformed_accesses_are_refused() { let x = octane(); diff --git a/src/ip30.rs b/src/ip30.rs index 7f129c96..b8141d02 100644 --- a/src/ip30.rs +++ b/src/ip30.rs @@ -24,7 +24,7 @@ //! //! Set IRIS_IP30_TRACE=1 to log unmapped accesses as they happen. -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use parking_lot::Mutex; @@ -68,18 +68,27 @@ pub static LAST_PC: AtomicU64 = AtomicU64::new(0); struct Unmapped { /// First 64 distinct: `(addr, write, pc)`. misses: Mutex>, + /// `misses` holds 64: a further miss costs one relaxed load, so a guest + /// polling an absent device does not lock and scan the list each time. + full: AtomicBool, } impl Unmapped { #[cold] fn miss(&self, addr: u64, write: bool) { + if self.full.load(Ordering::Relaxed) { + return; + } let pc = LAST_PC.load(Ordering::Relaxed); let mut u = self.misses.lock(); - if u.len() < 64 && !u.iter().any(|&(a, w, _)| a == addr && w == write) { + if !u.iter().any(|&(a, w, _)| a == addr && w == write) { if std::env::var_os("IRIS_IP30_TRACE").is_some() { eprintln!("ip30: unmapped {} at {addr:#x} (pc {pc:#x})", if write { "write" } else { "read" }); } u.push((addr, write, pc)); + if u.len() == 64 { + self.full.store(true, Ordering::Relaxed); + } } } } diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index baed3842..963f1b2c 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -360,6 +360,20 @@ fn limitation_the_window_reaches_the_guard() { assert!(b.phys.mem_ptr(0x2000_3000).is_some(), "limitation: the window path reaches the bytes"); } +#[test] +fn the_miss_list_keeps_the_first_64_and_then_stops() { + let b = board(&[]); + let p = &b.phys; + for i in 0..100u64 { + assert_eq!(p.read32(0xf0_0000_0000 + i * 0x1_0000).status, BUS_ERR); + } + let u = b.bus.unmapped(); + assert_eq!(u.len(), 64); + assert_eq!(u[63].0, 0xf0_0000_0000 + 63 * 0x1_0000, "the first 64, in order"); + assert_eq!(p.write32(0xf1_0000_0000, 0), BUS_ERR); + assert_eq!(b.bus.unmapped(), u, "a full list is left alone"); +} + #[test] fn widgets_route_by_number() { let b = board(&[]); From 462f5cb39c2aef770d06078c457ee1d4a4bf2680 Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 11:59:51 -0700 Subject: [PATCH 08/15] bridge: swap whole DMA units in place A swapped DMA transfer, which is every one on a slot with the swap bit clear, built a spanning vector and moved each byte through an index calculation, for reads and writes alike. Disk payloads are page aligned and a whole number of units, so the common case needs none of that. When the start and length are whole 4 or 8 byte units, read straight into the caller's buffer and reverse each unit where it lies. A write reverses a copy on the stack, or on the heap past 4 KiB, and stores it in one go. Other transfers keep the byte-exact path. The test places aligned and unaligned transfers around the 4 KB split and the stack copy's size under each swap rule and checks every byte. Not measured on this base. --- src/dev/bridge.rs | 23 +++++++++++++++++++++++ src/dev/bridge_tests.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index 976e9f0f..9a415ec3 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -1313,6 +1313,14 @@ impl Bridge { if mask == 0 { return self.mem_read(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); + 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]; @@ -1328,6 +1336,21 @@ impl Bridge { if mask == 0 { return self.mem_write(xio, data); } + let unit = mask as usize + 1; + if xio & mask == 0 && data.len() % unit == 0 { + // Whole units: reverse each into a copy and store it in one go. + let mut stack = [0u8; 4096]; + let mut heap = Vec::new(); + let swapped: &mut [u8] = if data.len() <= stack.len() { + &mut stack[..data.len()] + } else { + heap.resize(data.len(), 0); + &mut heap + }; + swapped.copy_from_slice(data); + swapped.chunks_exact_mut(unit).for_each(<[u8]>::reverse); + return self.mem_write(xio, swapped); + } let lo = xio & !mask; let hi = (xio + data.len() as u64 + mask) & !mask; let mut span = vec![0u8; (hi - lo) as usize]; diff --git a/src/dev/bridge_tests.rs b/src/dev/bridge_tests.rs index 0c5aaf27..d8658066 100644 --- a/src/dev/bridge_tests.rs +++ b/src/dev/bridge_tests.rs @@ -1115,3 +1115,34 @@ fn the_window_decodes_the_low_24_bits_of_a_physical_address() { let r = Rig::new(); assert_eq!(r.bridge.read32(0x1f00_0000 + u64::from(reg::IDENTIFICATION + 4)).data, r.get(reg::IDENTIFICATION)); } + +#[test] +fn long_and_short_transfers_place_every_byte_by_the_swap_rule() { + let r = Rig::new(); + let c = r.card(2, PCI_INTA); + r.direct_to_heart(false); + // (swap bit, 64-bit slot, mask): the PCI byte at p is memory byte p ^ mask. + for (swap, wide, mask) in [(true, false, 0usize), (false, false, 3), (false, true, 7)] { + let d = r.get(reg::device(2)) & !DEV_SWAP_DIRECT; + r.set(reg::device(2), if swap { d | DEV_SWAP_DIRECT } else { d }); + r.bridge.set_dma_64bit(2, wide); + // Aligned and unaligned starts and lengths, around the 4 KB split + // and the size a stack copy holds. + for (off, len) in [(0usize, 8usize), (0, 4096), (0, 4104), (0, 8192), (8, 12288), (1, 4096), (0, 4095), (3, 9), (5, 8192)] { + let data: Vec = (0..len).map(|i| (i * 31 + 7) as u8).collect(); + r.mem.poke(0x2000, &vec![0xee; 16384]); + assert!(c.dma().dma_write(0x8000_2000 + off as u64, &data).is_ok()); + let lo = 0x2000 + off & !7; + let hi = (0x2000 + off + len + 7) & !7; + let mut want = vec![0xee; hi - lo]; + for (i, &b) in data.iter().enumerate() { + let p = 0x2000 + off + i; + want[(p ^ mask) - lo] = b; + } + assert_eq!(r.mem.peek(lo as u64, hi - lo), want, "write mask {mask} at {off} for {len}"); + let mut back = vec![0u8; len]; + assert!(c.dma().dma_read(0x8000_2000 + off as u64, &mut back).is_ok()); + assert_eq!(back, data, "read mask {mask} at {off} for {len}"); + } + } +} From ca50670e77638fe42e15ae0d3a46b8b09ad50b83 Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 12:43:43 -0700 Subject: [PATCH 09/15] ioc3: drop the 114-byte NVRAM file format The 114-byte register dump came from an earlier port of the chip and nothing writes it any more. Keeping a second reader meant a second layout to describe and test. The 512-byte image is the only format now. Any other size is ignored with the existing warning. --- src/dev/ioc3.rs | 48 +++++++++++++++---------------------------- src/dev/ioc3_tests.rs | 16 --------------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 8d5b3c87..542000f4 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -39,7 +39,7 @@ //! Host serial input is fed into the FIFOs by one thread per port. //! * The DS1685 is the O2's DS1687 model behind an index and a data port; it //! has no alarm or periodic interrupts. Its RAM, including the 128-byte -//! extended RAM, persists in a 512-byte file (an old 114-byte dump loads). +//! extended RAM, persists in a 512-byte file. //! //! Ported from hw/misc/sgi-ioc3.c and its serial, parallel and keyboard siblings //! in jamesbraid/qemu-sgi-origami. @@ -673,9 +673,7 @@ impl Parallel { const NVRAM_QUIET: Duration = Duration::from_millis(500); /// Registers 0x0e.. are storage rather than clock in the file. const NVRAM_FIRST: usize = 0x0e; -/// The old file format: registers 0x0e..0x80 as the bank-flat cell array. -const NVRAM_OLD_LEN: usize = 0x80 - NVRAM_FIRST; -/// The current file format is a 512-byte image: common registers 0x00..0x40, +/// The file is a 512-byte image: common registers 0x00..0x40, /// bank-0 RAM 0x40..0x80, bank-1 registers 0x80..0xc0, extended RAM /// 0xc0..0x140, zero padding after. const NVRAM_IMAGE_LEN: usize = 0x200; @@ -779,33 +777,21 @@ impl Rtc { img } - /// Load a file: the old 114-byte register dump, or the 512-byte image. - /// The clock registers (0x00..0x0d) are never loaded; time starts from - /// the host. Returns false for any other length. + /// Load a 512-byte image. The clock registers (0x00..0x0d) are never + /// loaded; time starts from the host. Returns false for any other length. fn load(&mut self, img: &[u8]) -> bool { - if img.len() == NVRAM_OLD_LEN { - for (i, b) in img.iter().enumerate() { - let r = NVRAM_FIRST + i; - if r < 0x40 { - self.dev.poke(r, *b); - } else { - self.nv[r - 0x40] = *b; - } - } - true - } else if img.len() == NVRAM_IMAGE_LEN { - for (r, &b) in img.iter().enumerate().take(0x40).skip(NVRAM_FIRST) { - self.dev.poke(r, b); - } - self.nv.copy_from_slice(&img[NVRAM_OFF_NV..NVRAM_OFF_NV + 64]); - self.ext.copy_from_slice(&img[NVRAM_OFF_EXT..NVRAM_OFF_EXT + 64]); - self.xram.copy_from_slice(&img[NVRAM_OFF_XRAM..NVRAM_OFF_XRAM + RTC_XRAM_SIZE]); - // Loading never powers the machine off. - self.dev.poke(0x4a, self.ext[0x0a] & !0x08); - true - } else { - false + if img.len() != NVRAM_IMAGE_LEN { + return false; } + for (r, &b) in img.iter().enumerate().take(0x40).skip(NVRAM_FIRST) { + self.dev.poke(r, b); + } + self.nv.copy_from_slice(&img[NVRAM_OFF_NV..NVRAM_OFF_NV + 64]); + self.ext.copy_from_slice(&img[NVRAM_OFF_EXT..NVRAM_OFF_EXT + 64]); + self.xram.copy_from_slice(&img[NVRAM_OFF_XRAM..NVRAM_OFF_XRAM + RTC_XRAM_SIZE]); + // Loading never powers the machine off. + self.dev.poke(0x4a, self.ext[0x0a] & !0x08); + true } /// The file and the image to put in it, if the RAM changed at least @@ -2586,7 +2572,7 @@ impl Ioc3 { /// Back the DS1685's RAM (user RAM, bank-1 registers and the 128-byte /// extended RAM) with a file. A 512-byte file (the layout the QEMU model - /// uses) is loaded, as is an old 114-byte register dump. The file is + /// uses) is loaded. The file is /// written as 512 bytes now and replaced whole once the guest's writes /// to the RAM pause (see [`Ioc3::flush_nvram`]). The clock starts from /// host time. @@ -2596,7 +2582,7 @@ impl Ioc3 { let mut st = self.sh.st.lock(); if let Ok(img) = std::fs::read(&path) { if !st.rtc.load(&img) { - log::warn!("ioc3: NVRAM file {} has {} bytes, not 114 or 512; ignored", path.display(), img.len()); + log::warn!("ioc3: NVRAM file {} has {} bytes, not 512; ignored", path.display(), img.len()); } } st.rtc.file = Some(path.clone()); diff --git a/src/dev/ioc3_tests.rs b/src/dev/ioc3_tests.rs index f47f79cf..8446939d 100644 --- a/src/dev/ioc3_tests.rs +++ b/src/dev/ioc3_tests.rs @@ -1203,22 +1203,6 @@ fn nvram_and_extended_ram_survive_in_a_512_byte_file() { let _ = std::fs::remove_file(&path); } -/// An old 114-byte dump still loads; the file becomes a 512-byte image. -#[test] -fn an_old_114_byte_file_still_loads() { - let path = std::env::temp_dir().join(format!("ioc3-old-{}", std::process::id())); - let mut old = vec![0u8; 114]; - old[0] = 0xab; // register 0x0e - old[(0x45 - 0x0e) as usize] = 0xcd; // bank 0 RAM 0x45 - std::fs::write(&path, &old).unwrap(); - let r = Rig::new(); - r.c.set_nvram_file(&path).unwrap(); - assert_eq!(rtc_r(&r, 0x0e), 0xab); - assert_eq!(rtc_r(&r, 0x45), 0xcd); - assert_eq!(std::fs::metadata(&path).unwrap().len(), 0x200); - let _ = std::fs::remove_file(&path); -} - /// A full image in the QEMU layout loads, extended RAM included. #[test] fn a_qemu_layout_image_loads() { From 098e7179657e4338a8b70f8c00a121cd3b502df2 Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 12:44:55 -0700 Subject: [PATCH 10/15] w1: move the SGI board records into the 1-wire module The board-record format and the parts built from it only make sense on a 1-wire bus, and the module that owns the bus already holds the other Dallas parts. Move dev/sgi_nic.rs to dev/w1/sgi.rs and point the IOC3, the HQ4, the Octane machine and the w1 tests at the new path. The moved file's tests also carried barcodes, a part revision and an Ethernet address read off real boards. Use the synthetic barcodes and part numbers the machine's own records carry, and a made-up station address, so nothing in the tree is a real unit's identity. --- src/dev/ioc3.rs | 6 +++--- src/dev/mgras/hq4.rs | 2 +- src/dev/mod.rs | 1 - src/dev/w1/mod.rs | 1 + src/dev/{sgi_nic.rs => w1/sgi.rs} | 14 +++++++------- src/dev/w1/tests.rs | 2 +- src/ip30_machine.rs | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename src/dev/{sgi_nic.rs => w1/sgi.rs} (97%) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 542000f4..19008ace 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -55,8 +55,8 @@ use std::time::{Duration, Instant}; use crate::dev::ds1687::MaceRtc; use crate::dev::ioc3_port::Ioc3EthPort; use crate::dev::ps2::Ps2Controller; -use crate::dev::sgi_nic::{self, BoardRecord}; use crate::dev::w1; +use crate::dev::w1::sgi::{self, BoardRecord}; use crate::dev::z85c30::{null_serial_backend, SerialBackend}; use crate::irq::{InterruptController, IrqLine}; use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PciDmaStatus, PCI_INTA}; @@ -946,10 +946,10 @@ impl Ioc3Config { /// records, the front plane and the power supply on the Octane. fn default_wire(cfg: &Ioc3Config) -> w1::Bus { let mut bus = w1::Bus::new(); - bus.add(Box::new(sgi_nic::ethernet_address_part(cfg.mac, cfg.ethernet_part_serial))); + bus.add(Box::new(sgi::ethernet_address_part(cfg.mac, cfg.ethernet_part_serial))); for (r, serial) in &cfg.records { let rec = BoardRecord { barcode: &r.barcode, part: &r.part, revision: &r.revision, name: &r.name }; - bus.add(Box::new(sgi_nic::board_record_part(&rec, *serial))); + bus.add(Box::new(sgi::board_record_part(&rec, *serial))); } bus } diff --git a/src/dev/mgras/hq4.rs b/src/dev/mgras/hq4.rs index 1dbccaa5..c146cf6e 100644 --- a/src/dev/mgras/hq4.rs +++ b/src/dev/mgras/hq4.rs @@ -59,8 +59,8 @@ use parking_lot::Mutex; use super::{Gl, Hq3Board, Mgras, MGRAS_SLOT_GFX_BASE}; use crate::config::GraphicsBoard; -use crate::dev::sgi_nic::{board_record_part, BoardRecord}; use crate::dev::w1; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; use crate::ip30::widget_small_base; use crate::irq::{InterruptController, IrqLine}; use crate::traits::{ diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 658b490f..a0d97e43 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -29,7 +29,6 @@ pub mod mgras; pub mod ng1; pub mod serial_log; pub mod sflash; -pub mod sgi_nic; pub mod xbow; pub mod bridge; pub mod w1; diff --git a/src/dev/w1/mod.rs b/src/dev/w1/mod.rs index c4262101..4e0f29d4 100644 --- a/src/dev/w1/mod.rs +++ b/src/dev/w1/mod.rs @@ -13,6 +13,7 @@ pub mod bus; pub mod crc; pub mod ds2502; pub mod ds2505; +pub mod sgi; pub mod slot; pub use bus::{Bus, Part}; diff --git a/src/dev/sgi_nic.rs b/src/dev/w1/sgi.rs similarity index 97% rename from src/dev/sgi_nic.rs rename to src/dev/w1/sgi.rs index 59e79ce0..370acc52 100644 --- a/src/dev/sgi_nic.rs +++ b/src/dev/w1/sgi.rs @@ -1,13 +1,13 @@ //! SGI's "Number In a Can" records: what a board's Dallas add-only memory //! holds, a part number, name and barcode, or the Ethernet address. The bus, -//! the parts and the master's slot decode live in `crate::dev::w1`; this +//! the parts and the master's slot decode live in the parent module `w1`; this //! module only knows the SGI record formats and builds parts that carry //! them. The barcodes and ROM serials we ship are synthetic. //! //! Ported from hw/misc/sgi-board-record.c in jamesbraid/qemu-sgi-origami. -use crate::dev::w1::crc::crc16 as crc16_seed; use crate::dev::w1; +use crate::dev::w1::crc::crc16 as crc16_seed; /// Text fields of an SGI board record, as a part's first two 32-byte pages /// hold them, each page closed by an inverted CRC-16. @@ -182,7 +182,7 @@ mod tests { #[test] fn the_mac_nic_answers_read_rom_and_read_memory() { - let mac = [0x08, 0x00, 0x69, 0x0b, 0x7e, 0xba]; + let mac = [0x08, 0x00, 0x69, 0x12, 0x34, 0x56]; let mut m = bus_of(vec![Box::new(ethernet_address_part(mac, ETHERNET_PART_SERIAL))]); assert!(m.reset(), "presence pulse"); m.byte(0x33); @@ -213,8 +213,8 @@ mod tests { fn search_rom_finds_every_part_on_the_wire() { let mut m = bus_of(vec![ Box::new(ethernet_address_part([1, 2, 3, 4, 5, 6], ETHERNET_PART_SERIAL)), - Box::new(board_record_part(&rec("EDJ162", "030-0891-003", "A", "FP1"), 0x1234)), - Box::new(board_record_part(&rec("AAE3230158", "060-0035-003", "A", "PWR.SPPLY.ER"), 0x5678)), + Box::new(board_record_part(&rec("EMUF00", "030-0891-003", "A", "FP1"), 0x1234)), + Box::new(board_record_part(&rec("EMUS00", "060-0028-003", "A", "PWR.SPPLY.SR"), 0x5678)), ]); let roms = m.search(); let mut fams: Vec = roms.iter().map(|r| r[0]).collect(); @@ -229,7 +229,7 @@ mod tests { #[test] fn a_board_nic_reads_as_openbsd_and_linux_expect() { - let mut m = bus_of(vec![Box::new(board_record_part(&rec("FGT687", "030-0887-003", "G", "IP30"), 0x1234))]); + let mut m = bus_of(vec![Box::new(board_record_part(&rec("EMUN00", "030-0887-003", "F", "IP30"), 0x1234))]); // owserial: the redirection table from the status memory at 0x100, // eight bytes and a CRC16 at a time. assert!(m.reset()); @@ -262,7 +262,7 @@ mod tests { m.byte(0); let buf: Vec = (0..64).map(|_| m.byte(0xff)).collect(); assert_eq!(buf[0], 0x01); - assert_eq!(&buf[1..7], b"FGT687"); + assert_eq!(&buf[1..7], b"EMUN00"); assert_eq!(&buf[21..30], b"030-0887-"); assert_eq!(&buf[32 + 3..32 + 6], b"003"); assert!(buf[32 + 10..32 + 16].iter().all(|&b| b == 0xff)); diff --git a/src/dev/w1/tests.rs b/src/dev/w1/tests.rs index eaa7c830..7f59f70d 100644 --- a/src/dev/w1/tests.rs +++ b/src/dev/w1/tests.rs @@ -5,7 +5,7 @@ use super::crc::{crc16, crc8}; use super::slot::{self, fields, ONE_PULSE, RESET_PULSE}; use super::*; -use crate::dev::sgi_nic::{board_record_part, ethernet_address_part, BoardRecord, ETHERNET_PART_SERIAL}; +use crate::dev::w1::sgi::{board_record_part, ethernet_address_part, BoardRecord, ETHERNET_PART_SERIAL}; const MAC: [u8; 6] = [0x08, 0x00, 0x69, 0x12, 0x34, 0x56]; diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 76e6242c..30e3c652 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -28,8 +28,8 @@ use crate::dev::isp1040::Isp1040; use crate::dev::mgras::hq4::ImpactXio; use crate::dev::rad1::Rad1; use crate::dev::sflash::{CfiFlash, FLASH_SIZE}; -use crate::dev::sgi_nic::{board_record_part, BoardRecord}; use crate::dev::w1; +use crate::dev::w1::sgi::{board_record_part, BoardRecord}; use crate::dev::xbow::Xbow; use crate::hptimer::TimerManager; use crate::ip30::{ From 28747ca4ba30867d3fcc0237c9bb9c2afef59ecf Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 12:46:56 -0700 Subject: [PATCH 11/15] ip30: remove unused leftovers from the Octane devices A pass over the Octane's files for code nothing uses. None of it changes behaviour. - Ioc3Config::ethernet_part_serial was always the one shared constant, so the field only held a second copy. The wire takes the constant. - Ioc3::service and its helper are called by tests alone. Compile them for tests only and say so, instead of leaving a public method that reads as part of the periodic machinery. - WIDGET_GFX_FIRST had no user. - The ip30 test rig kept an Ioc3 handle behind allow(dead_code). The bridge already holds the chip. - The w1 tests spelled the Ethernet part's serial out three times. - A format! with no arguments in the HEART bank check, and an IOC3 comment that named a service thread the chip no longer has. --- src/dev/ioc3.rs | 14 ++++++-------- src/ip30.rs | 2 -- src/ip30/tests.rs | 4 +--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 19008ace..f55e6e9a 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -884,8 +884,6 @@ pub struct Ioc3Config { pub subsystem_id: u16, /// The board's station address, which the one-wire part holds. pub mac: [u8; 6], - /// 48-bit ROM serial of the station-address part. - pub ethernet_part_serial: u64, pub serial: [bool; 2], pub parallel: bool, pub keyboard_mouse: bool, @@ -925,8 +923,6 @@ impl Ioc3Config { Self { subsystem_id: SUBSYS_IP30_SYSBOARD, mac, - // UniqueWare prefix 0x5e7, then the "EMU" marker and instance 0. - ethernet_part_serial: 0x5e74_54d5_5000, serial: [true, true], parallel: true, keyboard_mouse: true, @@ -946,7 +942,7 @@ impl Ioc3Config { /// records, the front plane and the power supply on the Octane. fn default_wire(cfg: &Ioc3Config) -> w1::Bus { let mut bus = w1::Bus::new(); - bus.add(Box::new(sgi::ethernet_address_part(cfg.mac, cfg.ethernet_part_serial))); + bus.add(Box::new(sgi::ethernet_address_part(cfg.mac, sgi::ETHERNET_PART_SERIAL))); for (r, serial) in &cfg.records { let rec = BoardRecord { barcode: &r.barcode, part: &r.part, revision: &r.revision, name: &r.name }; bus.add(Box::new(sgi::board_record_part(&rec, *serial))); @@ -1623,7 +1619,7 @@ impl St { } /// Transitions already due happen before a read, not whenever the - /// service thread next runs. + /// receive thread or a timer next runs. fn io_catch_up(&mut self, now: Instant) { for _ in 0..100_000 { match self.int_out.next { @@ -2463,6 +2459,7 @@ impl Shared { /// One synchronous pass of what the receive threads and timers do: /// host input into the FIFOs, expired receive timers and transmit /// delays, the interrupt output, and any PS/2 character that arrived. + #[cfg(test)] fn service(&self) { self.with_st(false, |st, _| { let now = Instant::now(); @@ -2645,8 +2642,9 @@ impl Ioc3 { } /// One synchronous pass of the work the receive threads and the timers - /// do between guest accesses. Nothing calls it periodically; tests use it - /// to settle the chip without waiting on a thread. + /// do between guest accesses. Tests use it to settle the chip without + /// waiting on a thread. + #[cfg(test)] pub fn service(&self) { self.sh.service(); } diff --git a/src/ip30.rs b/src/ip30.rs index b8141d02..f343306b 100644 --- a/src/ip30.rs +++ b/src/ip30.rs @@ -39,8 +39,6 @@ use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_ER pub const WIDGET_XBOW: u8 = 0x0; pub const WIDGET_HEART: u8 = 0x8; pub const WIDGET_BRIDGE: u8 = 0xf; -/// The four ports graphics and expansion boards take. -pub const WIDGET_GFX_FIRST: u8 = 0x9; /// Widget `w`'s small window: 16 MiB at `0x1000_0000 + (w << 24)`. pub const fn widget_small_base(w: u8) -> u64 { diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 963f1b2c..84c8bba8 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -450,8 +450,6 @@ struct Octane { heart: Arc, mem: Arc, bridge: Arc, - #[allow(dead_code)] - ioc3: Arc, isp: [Arc; 2], sent: rtrb::Consumer>, inject: rtrb::Producer>, @@ -479,7 +477,7 @@ impl Octane { bridge.attach(SLOT_ISP0 as u8, isp[0].clone()); bridge.attach(SLOT_ISP1 as u8, isp[1].clone()); bridge.attach(SLOT_IOC3 as u8, ioc3.clone()); - Octane { heart, mem, bridge, ioc3, isp, sent, inject, eth } + Octane { heart, mem, bridge, isp, sent, inject, eth } } // ---- the processor's side ---- From b144829d4a39fb777916822f47d6e7910941c31a Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 27 Oct 2026 21:18:41 -0700 Subject: [PATCH 12/15] docs: bring the Octane notes up to date The Octane's bus is the shared Physical, but several notes still described the old arrangement or the Octane as unfinished. The bring-up note was a log of an early bring-up: "where it stands", a not-done list that the working machine has since finished, and a second half for the full machine. It described the one-wire parts through a type the tree no longer has and said the processor module's part sits on the Bridge's wire, which is wrong now that HEART has its own MicroLAN. Reduce it to what the PROM insists on and what booting through to IRIX added, with the memory-sizing entry corrected to the probe the code models. INDEX.md now lists it as a rule rather than an open item. The Octane guide, the device-timing note and the changelog said nothing polls. The IOC3's receive thread looks for host input every 2 ms, one per fitted serial port; everything else is paced by hptimer one-shots. State both. The guide's limitation entry now says why a cached access to the guarded 16 KiB reaches the bytes while an uncached one faults, and its provenance sentence no longer claims the same behaviour and approximations as the models it was ported from. debug.md says a device's own refusal is not in `ip30 unmapped`. The changelog gets an entry for the memory and bus changes in this series. Stale names of the bus type in two comments and the test header are corrected. HACKING.md still opened with three workstations and described each machine as having an embedded PROM fallback. The O2 and the Octane are their own machines (CRIME, MACE and GBE; HEART, the Crossbow and the Bridge) and have no embedded PROM, so add them and say so. FEATURES.md and the VALID_BANK_SIZES comment said only IP28 accepts 256 and 512 MB banks. The Octane's banks are matched DIMM pairs of 64 to 512 MB, so the Indy and Indigo2 limit is now stated as theirs. --- CHANGELOG.md | 16 +++- FEATURES.md | 13 +-- HACKING.md | 24 +++-- clanker/INDEX.md | 2 +- .../devices/ip30-device-timing-on-hptimer.md | 2 +- clanker/devices/ip30-octane-bringup.md | 93 +++++++------------ debug.md | 4 +- docs/octane-ip30.md | 27 +++--- src/config.rs | 2 +- src/dev/heart.rs | 6 +- src/ip30/tests.rs | 2 +- 11 files changed, 94 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0cd44cc..69f72eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -176,13 +176,21 @@ hashes are given where a change is easiest to understand by reading the commit. IMPACT graphics on widget 11. The stock IP30 PROM boots IRIX 6.5.30 to a serial login or, on the graphics console, to the X login and desktop. Needs the IP30 PROM image. Runs under jitv2 as well as the interpreter. - The devices are time-driven by `hptimer` and per-port receive threads, with - no polling thread; every thread is joined when the machine stops. + The devices are time-driven by `hptimer`. Serial input is polled every + 2 ms by one receive thread per port; every thread is joined when the + machine stops. iris-gui can run it. +- **The Octane's memory and bus bookkeeping are cheaper.** A MEMCFG write + that leaves the decode as it was no longer rebuilds the table and empties + ppmem's window under the running CPU. The first 64 distinct misses and + unsupported accesses are still listed, but once a list is full it costs one + atomic load instead of a lock and a scan. A swapped Bridge DMA transfer + that is whole 4- or 8-byte units reverses the bytes in place rather than + through a spanning copy. - **One 1-wire bus for the board records.** `dev::w1` holds the bus, the slot decode and the Dallas parts (DS2501/2502, DS2505); HEART, the Bridge, - IOC3 and the graphics board each own a wire. `sgi_nic` only builds the SGI - record formats. + IOC3 and the graphics board each own a wire. `dev::w1::sgi` only builds the + SGI record formats. - **CP0 Count writes can open the late-Compare window** (`count_write_samples`, opt-in; the Octane sets it): IRIX on the Octane arms Compare a few instructions after writing Count, and a host stall in between no longer diff --git a/FEATURES.md b/FEATURES.md index 1c5663c8..8d2a7b15 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -335,16 +335,17 @@ is the internal bus, 1 the external) and `[graphics] board`. See [the Octane gui ### Hardware constraints and precedence -Bank values are 0, 8, 16, 32, 64, or 128 MB; only IP28 accepts 256/512 MB. On -the O2 a bank is a DIMM pair's kit, 32, 64, 128 or 256 MB, fitted from pair A -largest first; on the Octane it is a matched DIMM pair, 64, 128, 256 or 512 MB, filled from bank 0. `banks` has eight entries; every machine today uses the first -four and the rest must be 0. +On the Indy and Indigo2 bank values are 0, 8, 16, 32, 64, or 128 MB, and only +IP28 accepts 256/512 MB. On the O2 a bank is a DIMM pair's kit, 32, 64, 128 or +256 MB, fitted from pair A largest first; on the Octane it is a matched DIMM +pair, 64, 128, 256 or 512 MB, filled from bank 0. `banks` has eight entries; +every machine today uses the first four and the rest must be 0. IP22/IP24 four-bank IRIX 6.5 behavior remains separate from IP28's verified two-bank 1 GB layout. Extreme requires IP22. All non-Newport boards require `heads=1` and `resolution="guest"`. IP28 IRIX needs IMPACT; its PROM has an embedded fallback. -The config validator permits SCSI controller 1 only on IP22, the O2 and the Octane, -the machines with two controllers. +The config validator permits SCSI controller 1 only on IP22, the O2 and the +Octane, the machines with two controllers. Standalone CLI options update the loaded config where supplied; boolean enable switches do not provide a general way to clear every saved true setting. diff --git a/HACKING.md b/HACKING.md index 1cdd9cdf..848a4a6d 100644 --- a/HACKING.md +++ b/HACKING.md @@ -16,10 +16,12 @@ Other documents: ## 1. What IRIS is -IRIS emulates three SGI workstations: the Indy (IP24), the Indigo2 (IP22) and -the Indigo2 IMPACT (IP28). The CPU is an R4400, R5000 or R10000, chosen at -runtime. Graphics is Newport (REX3), GR2 (XZ/Extreme) or IMPACT. It boots the -real PROMs and IRIX 5.3 and 6.5 to a desktop with networking. +IRIS emulates five SGI workstations: the Indy (IP24), the Indigo2 (IP22), the +Indigo2 IMPACT (IP28), the O2 (IP32) and the Octane (IP30). The CPU is an +R4400, R5000 or R10000, chosen at runtime (the Octane takes the R10000 only). +Graphics is Newport (REX3), GR2 (XZ/Extreme) or IMPACT on the Indy and +Indigo2, GBE on the O2 and Solid IMPACT on the Octane. It boots the real PROMs +and IRIX 5.3 and 6.5 to a desktop with networking. It is **not cycle-accurate**, anywhere. IRIX is interrupt-driven and does not care how many cycles an instruction takes, so IRIS models behaviour @@ -460,9 +462,17 @@ introspection commands are in [debug.md](debug.md#jit-v2). cascade, a serial EEPROM for NVRAM and the MAC address. - **IP28 (Indigo2 IMPACT)**: IP22 devices plus an R10000, IMPACT graphics, a 16 MB MEMCFG granule and RAM at `0x20000000`. - -Each has an embedded PROM fallback. `src/platform.rs` and `src/machine.rs` -wire the differences; `platform_profile_tests.rs` pins them down. +- **IP32 (O2)**: its own machine (`src/ip32_machine.rs`): CRIME, MACE + and GBE, with an Adaptec 7880 for SCSI. +- **IP30 (Octane)**: its own machine (`src/ip30_machine.rs`): HEART, the + Crossbow and the Bridge with IOC3 and two ISP1040 SCSI controllers, an + R10000, and IMPACT graphics (see the [Octane guide](docs/octane-ip30.md) + for the boards). + +The Indy and Indigo2 profiles each have an embedded PROM fallback; +`src/platform.rs` and `src/machine.rs` wire their differences, and +`platform_profile_tests.rs` pins them down. The O2 and the Octane have no +embedded PROM and are built by `workstation::build`. ## 10. Building and testing diff --git a/clanker/INDEX.md b/clanker/INDEX.md index aba2d138..0f0ad3e6 100644 --- a/clanker/INDEX.md +++ b/clanker/INDEX.md @@ -108,7 +108,7 @@ Non-graphics devices: MC/MEMCFG and RAM banks, HPC3, VDMA, EEPROMs and NVRAM, PS | [o2-readback-transfer-registers.md](devices/o2-readback-transfer-registers.md) | rule | O2 glReadPixels: the frame-buffer-to-linear transfer, GL vertices, edge type, src steps and linStride as IRIX uses them | | [ip32-o2-bringup-log.md](devices/ip32-o2-bringup-log.md) | history | The O2 (IP32) bring-up log: PROM, POST, CRIME/MACE/GBE, SCSI, PCI, the OSes | | [ip32-headless-irix-panics.md](devices/ip32-headless-irix-panics.md) | rule | O2 TLBMISS at 0xff800000 in early boot was `crmFifoWait` timing out on our host-thread RE; status polls now wait for the engine (fixed) | -| [ip30-octane-bringup.md](devices/ip30-octane-bringup.md) | open | The Octane (IP30) bring-up: PROM 4.17 to its menu on the serial console; what it insists on (memory sizing, the Bridge status bit, empty slots, IOC3 bits, the NIC parts' formats, its device tree); the full machine's findings (cached flash lines, low-memory JIT counters, the ISP stall, HQ4 before a context switch) | +| [ip30-octane-bringup.md](devices/ip30-octane-bringup.md) | rule | What the Octane's PROM insists on (memory sizing, the Bridge status bit, empty slots, IOC3 bits, the one-wire parts' formats, its device tree) and what booting it through to IRIX added (cached flash lines, low-memory JIT counters, the ISP stall, HQ4 before a context switch) | | [ip30-low-memory-aliases-sdram.md](devices/ip30-low-memory-aliases-sdram.md) | rule | Octane: the 16 KiB at physical 0 is the first 16 KiB of SDRAM, which the processor reaches only there (0x2000_0000..+16 KiB bus-errors, as the stock IDE memory diagnostics expect); shares its JIT page generations; the guard holds on the table path only (the ppmem window maps whole units); DMA reach unknown | | [ip30-prom-stores-into-flash-stay-in-cache.md](devices/ip30-prom-stores-into-flash-stay-in-cache.md) | rule | Octane: the PROM's cached stores into its own flash never reach the Bridge; `CachedFlashLines` holds them (approximation: no write-back) | | [ip30-device-timing-on-hptimer.md](devices/ip30-device-timing-on-hptimer.md) | design | Octane: IOC3, RAD1 and ISP1040 pace themselves on `hptimer` one-shots, with threads only where something blocks (the ISP1040 ring, serial receive, NVRAM writer) | diff --git a/clanker/devices/ip30-device-timing-on-hptimer.md b/clanker/devices/ip30-device-timing-on-hptimer.md index c3cc60e5..b3dfec37 100644 --- a/clanker/devices/ip30-device-timing-on-hptimer.md +++ b/clanker/devices/ip30-device-timing-on-hptimer.md @@ -2,7 +2,7 @@ Status: design (read the code before relying on details). -**What to know.** The Octane's devices avoid polling threads. Timed work is a +**What to know.** The Octane's devices poll only for serial input. Timed work is a one-shot or recurring `hptimer` that does the same catch-up a register access would, so an idle guest still makes progress. Threads exist only where something blocks. diff --git a/clanker/devices/ip30-octane-bringup.md b/clanker/devices/ip30-octane-bringup.md index 90987843..8c291045 100644 --- a/clanker/devices/ip30-octane-bringup.md +++ b/clanker/devices/ip30-octane-bringup.md @@ -1,33 +1,28 @@ -# The Octane (IP30) bring-up: the PROM to its serial prompt +# What the Octane's PROM insists on -Status: open (started 2026-10-19 on branch ip30 of slaapliedje/iris, rebuilt -2026-10-26 on branch ip30-machine as its own machine; the PROM's menu and -command monitor work on the serial console). +Status: rule (PROM 4.17; each item below was found by a stall or a wrong +answer in the PROM's own checks). -**Where it stands.** `[machine] profile = "octane_ip30"` builds an -`OctaneMachine` (`src/ip30_machine.rs`) over `Ip30Bus` (`src/ip30.rs`, on `Physical` since 2026-10-28): -HEART, the Xbow, the Bridge with its PCI configuration space and device -windows, the IOC3's two 16550s, DS1687 and 1-wire parts, the flash, and -memory placed by MEMCFG. The QL1040s, the RAD and the graphics widget are -register files. The map every offset came from is -`docs/Octane/ip30-address-map.md`. PROM 4.17 runs its power-on -diagnostics, builds its device tree (`hinv` shows the IOC3's serial -ports, keyboard, Ethernet, both QLISP1040s and the RAD) and reaches the -System Maintenance Menu and the Command Monitor on UART A -(`serial_port_a`). `IRIS_IP30_TRACE=1` logs the first access to each -register, `=2` every IOC3 access in order; the monitor's `ip30` command -shows memory, HEART, the Bridge and the unmapped accesses. +**What to know.** The IP30 PROM probes the hardware far harder than a +kernel does. Memory sizing, the Bridge, the IOC3 and the one-wire parts must +each answer the way the real parts do, or the PROM skips a bus, panics with no +console, or loops. The bus is `Ip30Bus` (`src/ip30.rs`), the machine's +`Physical`. `docs/octane-ip30.md` says what the machine covers and +`docs/Octane/ip30-address-map.md` holds the map every offset came from. The +monitor's `ip30` command (memory, the Bridge, the graphics link, refused +Crossbow accesses, unmapped accesses) and `heart unsupported` show what the +PROM touched that the model does not answer. `IRIS_IP30_TRACE=1` prints the +accesses nothing answers as they happen. -**What the PROM insists on, found one stall at a time.** +**What the PROM insists on.** -- *Memory sizing.* The PROM programs a bank as 1 GB at 2 GB (MEMCFG - 0x819f0040) and looks for where its contents repeat: at +256 bytes, - +128 MB, then +1 GB or +64 MB, one DIMM address line each. A rev D HEART - (the Octane's) sizes only 1 GB and 2 GB pairs correctly from a plain - mirror, so configure 1024 MB banks. It also probes every bank, so a - bank with no DIMMs must take writes and read back zero (`Decode::Empty`) - rather than bus-error; the probe places banks beyond 4 GB for a moment, - where the 32-bit bus cannot follow. +- *Memory sizing.* The PROM sizes a bank by writing through a temporary + 2 GiB descriptor (`0x81bf0040`) and checking which address bits alias. + HEART's wiring for that probe is not documented, so + `heart_mem::bank_offset` projects each fitted size so the PROM picks the + right descriptor; that is an approximation. It also probes every bank, so + a bank with no DIMMs must take writes and read back zero rather than + bus-error. - *The Bridge's device windows at reset are memory space at base 0.* The PROM enables the RAD's memory decode and writes through window 3 with neither the RAD's BAR nor device register 3 programmed; the window has @@ -47,11 +42,11 @@ shows memory, HEART, the Bridge and the unmapped accesses. byte bus 2 (+0xc0000), as OpenBSD's dsrtc.c has it, not the SuperIO RTC slot at 0x20168. The 16550s are at 0x20178 (A, the console) and 0x20170 (B), registers at consecutive bytes. -- *The NICs.* The MAC part is a DS1981U (family 0x91): the PROM reads 14 - bytes after Read Memory at 0 (the first the CRC8 of the command) into - nic[13..0] and takes the address from nic[2..8], so the MAC sits - reversed at memory 5..11 as the end of a TMEX page. The board parts are - DS2505s (family 0x0b, 2 KB): Read Status at 0x100 returns the page +- *The one-wire parts.* The MAC part is a DS1981U (family 0x91): the PROM + reads 14 bytes after Read Memory at 0 (the first the CRC8 of the + command) into nic[13..0] and takes the address from nic[2..8], so the MAC + sits reversed at memory 5..11 as the end of a TMEX page. The board parts + are DS2505s (family 0x0b, 2 KB): Read Status at 0x100 returns the page redirection table in groups of eight bytes each followed by an inverted CRC16 (over the command, address and bytes for the first group, the bytes alone after; the PROM's `nic_read_mfg` checks them: "invalid crc16 @@ -60,11 +55,9 @@ shows memory, HEART, the Bridge and the unmapped accesses. barcode (20), nine characters of the part number; page 1: three more, the revision, six 0xff bytes, the name (14); each page ends in its inverted CRC16. The PROM uses Search ROM on each wire, so several parts - share one `OneWire`. The system board's and CPU module's parts are on - the Bridge's wire, the front plane's and power supply's on the IOC3's - (with the MAC part); the PROM never drives HEART's MicroLAN master. The - CPU board NIC diagnostic still fails: what the PROM wants from the PM10 - part is not known yet. + share one `w1::Bus`. The system board's part is on the Bridge's wire, the + processor module's on HEART's MicroLAN, and the front plane's and power + supply's on the IOC3's (with the MAC part). - *What the PROM programs in the Bridge.* IOC3 BAR0 = 0x00400000 and device register 2 = 0x1004 (memory space, base 0x004 << 20, no PIO swap); interrupt destination upper 0x09080000 / lower 0x80 (vector 9 for @@ -82,31 +75,13 @@ shows memory, HEART, the Bridge and the unmapped accesses. PCI offset ^ 2 through a window whose swap bit is clear. A little-endian function's model therefore takes CPU-view offsets and applies the ^2/^3 itself; the window's swap bit (device register bit - 13) is modelled as lane and data swapping but nothing has used it yet. + 13) is modelled as lane and data swapping. - *The device tree.* Finding out why a console device will not open is quickest from the tree: the root pointer is at 0xa800000020f24040, a node's type is at +4, key at +16, peer at +64, child at +72, and one of the doublewords from +24 on points at its name. -**Not done.** The CPU board NIC check; the keyboard (the PROM polls -K_RD after writing 0xf2 to K_WD and gives up, which is fine without a -keyboard); large and huge widget windows (physical 0x8_0000_0000 and up) -and memory above 4 GB need the bus widened past `u32`, which the O2's -`phys_alias` trick does not cover; SCSI needs the ISP1040 model -(`docs/Octane/qla1040.md`: the PROM's "QL SCSI controller initialization -failed" is the register file answering its mailbox); Ethernet the IOC3's -ring DMA; graphics HQ4 in front of the IMPACT emulation (the widget is -left off the Xbow's links so the PROM takes the serial console); -snapshots the usual `Saveable` work. - -## Full machine - -The sections above stop at the PROM menu. The machine now boots the stock -PROM to IRIX 6.5.30 (serial login, graphics console and desktop, networking, -sound); the device models are ports of the author's QEMU models, one file -each (`docs/octane-ip30.md` says what is covered). What the bring-up above -lists as not done is done, except the CPU board NIC check. What the full -boot added: +**What booting the PROM through to IRIX added.** - *The PROM's stores into the flash are cached lines.* The PROM keeps its initialised data at flash addresses and stores to it with cached accesses @@ -125,9 +100,9 @@ boot added: advanced by every store, including a bus master's at the SDRAM address. - *An ISP1040 reset must not wait for the disk.* A soft reset and a queue initialisation waited on the CPU thread for the command in flight, so a - slow disk stalled the guest and the scheduler's timer ticks were missed - . The reset now moves a generation counter - on and the ring thread does the DMA with no lock held. + slow disk stalled the guest and the scheduler's timer ticks were missed. + The reset now moves a generation counter on and the ring thread does the + DMA with no lock held. - *The PROM console draws on HQ4 before any context switch.* Its graphical console sends direct raster writes and a few other one-word packets to the HQ4's command port before the board has ever selected a context, and a diff --git a/debug.md b/debug.md index 68dbce44..58f1279e 100644 --- a/debug.md +++ b/debug.md @@ -323,8 +323,8 @@ shows the command total and anything odd the driver did; `regs`, `wdt`, (HEART registers touched but not modelled) and `ip30 memcfg|bridge|gfx|xbow|unmapped` (the banks as HEART places them, the Bridge's interrupt routing, the graphics widget's link, refused Crossbow accesses, and accesses to physical addresses -nothing is mapped at, with the PC). The `phys` commands work on the Octane as -well. +nothing is mapped at, with the PC; a device's own refusal is not listed). The +`phys` commands work on the Octane as well. Queue position `[0]` is the mounted disc, `[1]` the next on eject. A disc change reaches IRIX as a Unit Attention ("medium changed") on its next diff --git a/docs/octane-ip30.md b/docs/octane-ip30.md index 36c43359..c7210e9d 100644 --- a/docs/octane-ip30.md +++ b/docs/octane-ip30.md @@ -161,10 +161,11 @@ HEART's bank mapping as they happen. ## Threads -Nothing polls. IOC3 runs one receive thread per fitted serial port and an -NVRAM writer, and arms its deadlines (serial receive and transmit delays, -the timer interrupt, the NVRAM flush) as `hptimer` one-shots, so they fire -without the guest touching the chip. RAD audio is paced by `hptimer` too and +Serial input is the one thing that polls. IOC3 runs one receive thread per +fitted serial port, which looks for host input every 2 ms, and an NVRAM +writer. It arms its deadlines (serial receive and transmit delays, the timer +interrupt, the NVRAM flush) as `hptimer` one-shots, so they fire without the +guest touching the chip. RAD audio is paced by `hptimer` too and has no thread of its own. Each ISP1040 runs a request-ring thread, started with the machine and joined when it stops. The Ethernet has a receive pump and the NAT engine (`ip30-eth-rx`, `ip30-eth-nat`). A power-off from the @@ -177,15 +178,18 @@ through its event channel. `HACKING.md` lists the thread names. - HEART: the answer to the PROM's bank-size probe is an approximation; ECC error injection is not modelled (a mode write that asks for it is recorded); the 16 KiB at physical 0 are the first 16 KiB of SDRAM, - which the processor reaches only there (0x2000_0000..+16 KiB is a bus - error, as the stock IDE memory diagnostics expect). Whether bus masters - reach that range is not known; the DMA view does. + which the processor reaches only there (an uncached access to + 0x2000_0000..+16 KiB is a bus error, as the stock IDE memory diagnostics + expect; cached accesses are the next item). Whether bus masters reach + that range is not known; the DMA view does. - A cached access to 0x2000_0000..+16 KiB reaches the bytes. On an Octane2 (HEART rev F) cached and uncached reads there both take a data bus error; here only uncached accesses do, because the processor's cache - reads memory straight from the ppmem window, which maps the whole first - 32 MiB. Faulting the cached access would put that whole 32 MiB unit of - SDRAM on the slower bus path. No supported guest touches the range. + reads memory straight from the ppmem window, which maps the whole 32 MiB + unit at 0x2000_0000. Leaving the 16 KiB out of the window would put every + cached access in the first 64 MiB of SDRAM on the slower bus path (the + window's fast-path flag covers 64 MiB at a time). No supported guest + touches the range. - The PROM's cached stores to its own data in the flash are kept in a per-line copy, as a write-back cache would hold them; they are never written back. @@ -203,8 +207,7 @@ through its event channel. `HACKING.md` lists the thread names. The device models are ported from the author's QEMU models in [Origami](https://github.com/jamesbraid/origami) (the -[sgi-origami branch](https://github.com/jamesbraid/qemu-sgi-origami)), with -the same behaviour and approximations. The boot flash (`src/dev/sflash.rs`) +[sgi-origami branch](https://github.com/jamesbraid/qemu-sgi-origami)). The boot flash (`src/dev/sflash.rs`) derives from MAME's [intelfsh](https://github.com/mamedev/mame/blob/master/src/devices/machine/intelfsh.cpp). The physical map is in [Octane/ip30-address-map.md](Octane/ip30-address-map.md); diff --git a/src/config.rs b/src/config.rs index 22972c44..e26d04b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,7 @@ use clap::Parser; use serde::{Deserialize, Serialize}; use std::net::Ipv4Addr; -/// Memory bank sizes in MB; validation restricts 256/512 MB banks to IP28. +/// Memory bank sizes in MB; validation restricts 256/512 MB banks to IP28 and the Octane. pub const VALID_BANK_SIZES: &[u32] = &[0, 8, 16, 32, 64, 128, 256, 512]; /// What sits at a SCSI id. `cdrom = true` remains the historical spelling for diff --git a/src/dev/heart.rs b/src/dev/heart.rs index f98f3bc9..eeb3991d 100644 --- a/src/dev/heart.rs +++ b/src/dev/heart.rs @@ -11,7 +11,7 @@ //! `asm/sgi/heart.h`. //! //! The processor sees HEART's registers at [`PIU_BASE`] and its widget -//! registers at the start of widget 8's small window; `Ip30Physical` maps +//! registers at the start of widget 8's small window; `Ip30Bus` maps //! both to this device, which takes the full physical address. Memory is //! `heart_mem::HeartRam`, which a descriptor write reaches through //! [`Heart::on_memcfg`]. @@ -188,7 +188,7 @@ pub struct Heart { /// The CPU's interrupt word (`MipsCore`'s), once the machine has a CPU. cpu_irq: AtomicPtr, /// Where descriptor writes go: the machine points it at - /// `Ip30Physical::remap_banks`. + /// `Ip30Bus::remap_banks`. memcfg_hook: Mutex>>, /// The processor module's MicroLAN: the PM10 board record. mlan: Mutex, @@ -629,7 +629,7 @@ impl crate::irq::InterruptController for Heart { /// The two register blocks, at full physical addresses: the processor /// interface at [`PIU_BASE`] and the widget registers at [`WIDGET_BASE`]. -/// Anything else in the slots `Ip30Physical` gives HEART is a bus error. +/// Anything else in the slots `Ip30Bus` gives HEART is a bus error. impl BusDevice for Heart { fn read8(&self, addr: u64) -> BusRead8 { self.bus_read(addr, 1).map_or(BusRead8::err(), |v| BusRead8::ok(v as u8)) diff --git a/src/ip30/tests.rs b/src/ip30/tests.rs index 84c8bba8..c6dede34 100644 --- a/src/ip30/tests.rs +++ b/src/ip30/tests.rs @@ -1,7 +1,7 @@ //! The Octane's chips wired together as `ip30_machine` wires them, driven //! without a CPU. //! -//! The first group is the processor's bus: `Ip30Physical` over HEART, the +//! The first group is the processor's bus: `Ip30Bus` over HEART, the //! Crossbow, the Bridge with the boot flash, SDRAM and its first 16 KiB at physical 0. //! //! The rest are the real HEART, Bridge, IOC3, IOC3 Ethernet and ISP1040, From 103387209f54d520b68bf06e2874a4c1e28104ca Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 23:02:12 -0700 Subject: [PATCH 13/15] ip30: light the status bar's SCSI and network indicators The Octane created the shared activity word but handed it only to the graphics board, so the SCSI and Ethernet lights never came on. Give it to both ISP1040 controllers and the IOC3 Ethernet, as the Indy does for its SCSI and Ethernet parts. The controllers set the light for the target a command runs against, for ids 0 to 5, whichever bus it is on. The Ethernet sets transmit when frames go out and receive when one is delivered to the guest. --- src/dev/ioc3_eth.rs | 20 ++++++++++++++++++-- src/dev/isp1040.rs | 12 ++++++++++++ src/ip30_machine.rs | 7 ++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/dev/ioc3_eth.rs b/src/dev/ioc3_eth.rs index 8b950d39..83480bdb 100644 --- a/src/dev/ioc3_eth.rs +++ b/src/dev/ioc3_eth.rs @@ -41,8 +41,8 @@ //! Ported from hw/net/sgi-ioc3-eth.c and hw/net/ics1890.c in //! jamesbraid/qemu-sgi-origami. -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock, Weak}; use parking_lot::Mutex; use std::time::{Duration, Instant}; @@ -1045,6 +1045,8 @@ pub struct Ioc3Eth { /// The NAT and receive threads, joined in `stop`. threads: Mutex>>, me: Weak, + /// The status bar's activity bits, shared with the display thread. + heartbeat: OnceLock>, } /// How long the receive thread sleeps with no negotiation to wait for: it is @@ -1073,6 +1075,7 @@ impl Ioc3Eth { nat: Arc::new(Mutex::new(None)), threads: Mutex::new(Vec::new()), me: me.clone(), + heartbeat: OnceLock::new(), }) } @@ -1080,6 +1083,17 @@ impl Ioc3Eth { self.mac } + /// Light the status bar's Ethernet activity bits in `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: Arc) { + let _ = self.heartbeat.set(heartbeat); + } + + fn blink(&self, bit: u64) { + if let Some(hb) = self.heartbeat.get() { + hb.fetch_or(bit, Ordering::Relaxed); + } + } + /// A machine reset: the MAC's registers and the PHY as at power-on, /// the interrupt dropped. The network link stays. The drop is delivered /// under `irq_order`, so a raise the receive thread was delivering @@ -1123,6 +1137,7 @@ impl Ioc3Eth { if frames.is_empty() { return; } + self.blink(crate::dev::ng1::rex3::Rex3::HB_ENET_TX); if let Some(link) = self.link.lock().as_mut() { for f in frames { let _ = link.to_net.push(f); @@ -1264,6 +1279,7 @@ impl Ioc3Eth { let mut st = self.st.lock(); if st.rx_ready() && st.rx_filter(&frame) { st.deliver(&*dma, &frame, false); + self.blink(crate::dev::ng1::rex3::Rex3::HB_ENET_RX); } } self.irq_flush(); diff --git a/src/dev/isp1040.rs b/src/dev/isp1040.rs index e066f87a..0c3f84e9 100644 --- a/src/dev/isp1040.rs +++ b/src/dev/isp1040.rs @@ -432,6 +432,8 @@ pub struct Core { notes: Mutex>, dma_misses: Mutex>, odd_accesses: Mutex, + /// The status bar's activity bits, shared with the display thread. + heartbeat: OnceLock>, } impl Default for Isp1040 { @@ -530,6 +532,7 @@ impl Core { notes: Mutex::new(Vec::new()), dma_misses: Mutex::new(std::collections::BTreeSet::new()), odd_accesses: Mutex::new(0), + heartbeat: OnceLock::new(), } } @@ -566,6 +569,11 @@ impl Core { (self.gen.load(Ordering::Acquire) == gen).then(store) } + /// Light the status bar's SCSI activity bits in `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: Arc) { + let _ = self.heartbeat.set(heartbeat); + } + /// Wire the adapter's interrupt pin to `line` (a level). pub fn connect_irq(&self, line: IrqLine) { let _ = self.irq.set(line); @@ -1214,6 +1222,10 @@ impl Core { /// stored, and no completion is built. fn run_job(&self, job: Job, gen: u64) -> Option { let Job { handle, target, cdb, xfer, to_dev, segs, described } = job; + // The bar has lights for SCSI ids 0-5. + if let (Some(hb), 0..=5) = (self.heartbeat.get(), target) { + hb.fetch_or(1u64 << (crate::dev::ng1::rex3::Rex3::HB_SCSI_BASE as u64 + target as u64), Ordering::Relaxed); + } let (status, moved); if to_dev { let mut buf = Vec::new(); diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 30e3c652..37e53efc 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -159,12 +159,18 @@ impl OctaneMachine { let links = xbow.clone(); bridge.set_link_alive(Box::new(move |w| links.carries(w))); + // The status bar's activity bits, shared with the graphics board. + let heartbeat = Arc::new(AtomicU64::new(0)); let mac = cfg.network().mac; let scsi = [Arc::new(Isp1040::new_at(0)), Arc::new(Isp1040::new_at(1))]; + 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::octane(mac, false)); let eth = Ioc3Eth::new(mac); + eth.set_heartbeat(heartbeat.clone()); ioc3.attach_eth(eth.clone()); bridge.attach(2, ioc3.clone()); ioc3.connect_superio_interrupt(bridge.pin_line(SUPERIO_PIN)); @@ -183,7 +189,6 @@ impl OctaneMachine { // The status bar's activity bits and the CPU's Compare matches, // shared with the graphics board before the CPU exists. - let heartbeat = Arc::new(AtomicU64::new(0)); let fasttick = Arc::new(AtomicU64::new(0)); let gfx = board.map(|b| ImpactXio::with_board(WIDGET_GFX, b, heart.clone(), mem.clone(), heartbeat, fasttick.clone())); From 3cc296879305aacb47ad2447b43aeabe747eab6d Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 23:47:53 -0700 Subject: [PATCH 14/15] ioc3: show the Octane's front lightbar in the status bar The system-board IOC3 drives the lightbar from its first two general-purpose pins: pin 0 the white power LED, pin 1 the red fault LED, 1 lit (Linux leds-ip30.c). Mirror them into the status bar's persistent front LED, white as the green LED and red as red. The update runs after every register access, so a write to a pin register, a write to the pin-data register and a reset all reach it. It only applies to the system-board IOC3. The polarity is one constant. --- docs/octane-ip30.md | 4 ++++ src/dev/ioc3.rs | 39 ++++++++++++++++++++++++++++++++++++++- src/ip30_machine.rs | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/octane-ip30.md b/docs/octane-ip30.md index c7210e9d..38dfa7b2 100644 --- a/docs/octane-ip30.md +++ b/docs/octane-ip30.md @@ -114,6 +114,10 @@ move them. The PROM and IRIX run the keyboard in scan code set 3. How the controller works and what each driver does with it is in [Octane/ioc3-ps2.md](Octane/ioc3-ps2.md). +The front lightbar is two IOC3 general-purpose pins: pin 0 the white power +LED, pin 1 the red fault LED. The GUI's front LED shows them, green for white +and red for red. + ## Graphics The graphics board sits on widget 11: the same raster chips as the Indigo2 diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index f55e6e9a..0a057d71 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -45,7 +45,7 @@ //! in jamesbraid/qemu-sgi-origami. use std::collections::VecDeque; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::SyncSender; use std::sync::{Arc, OnceLock, Weak}; @@ -2218,6 +2218,8 @@ struct Shared { pin_line: Mutex>, /// The SuperIO line the board wires to a Bridge input. superio_line: Mutex>, + /// The status bar's activity word, shared with the graphics board. + heartbeat: OnceLock>, /// What each line was last driven to, and the Ethernet block's level. pin_level: AtomicBool, superio_level: AtomicBool, @@ -2288,7 +2290,9 @@ impl Shared { let (r, want, power_off) = { let mut st = self.st.lock(); let ctx = Ctx { ps2: &self.ps2, eth_parity: parity }; + let pins = st.pin_data; let r = f(&mut st, &ctx); + self.show_lightbar(&st, pins); (r, st.deadlines(), std::mem::take(&mut st.power_off_pending)) }; self.arm(want); @@ -2301,6 +2305,32 @@ impl Shared { r } + /// Mirror the system board's front lightbar into the status bar when + /// pins 0 and 1 changed from `before`. + fn show_lightbar(&self, st: &St, before: u32) { + use crate::dev::ng1::rex3::Rex3; + let Some(hb) = self.heartbeat.get() else { return }; + if st.cfg.subsystem_id != SUBSYS_IP30_SYSBOARD { + return; + } + // Pin 0 is the white power LED, pin 1 the red fault LED (Linux + // drivers/leds/leds-ip30.c writes 1 to light one). The status bar + // shows white as its green LED. Set to false if the pins turn out + // to be active low. + const LIT_WHEN_HIGH: bool = true; + for (pin, led) in [(0, Rex3::HB_LED_GREEN), (1, Rex3::HB_LED_RED)] { + let (old, new) = ((before >> pin) & 1, (st.pin_data >> pin) & 1); + if old == new { + continue; + } + if (new == 1) == LIT_WHEN_HIGH { + hb.fetch_or(led, Ordering::Relaxed); + } else { + hb.fetch_and(!led, Ordering::Relaxed); + } + } + } + /// Make the pending timers match `want`: one one-shot per deadline. A /// timer whose deadline moved is replaced; with none set or the chip not /// running, deadlines are only evaluated lazily, by the next access. @@ -2513,6 +2543,7 @@ impl Ioc3 { raw_dma: Mutex::new(None), pin_line: Mutex::new(None), superio_line: Mutex::new(None), + heartbeat: OnceLock::new(), pin_level: AtomicBool::new(false), superio_level: AtomicBool::new(false), eth_level: AtomicBool::new(false), @@ -2603,6 +2634,12 @@ impl Ioc3 { self.sh.flush_nvram(Duration::ZERO); } + /// Show the system board's front lightbar in the status bar through + /// `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: Arc) { + let _ = self.sh.heartbeat.set(heartbeat); + } + /// Attach the Ethernet MAC. It is told about DMA and the PCI interrupt /// pin as soon as the bridge provides them. pub fn attach_eth(&self, eth: Arc) { diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 37e53efc..07a71254 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -169,6 +169,7 @@ impl OctaneMachine { bridge.attach(0, scsi[0].clone()); bridge.attach(1, scsi[1].clone()); let ioc3 = Arc::new(Ioc3::octane(mac, false)); + ioc3.set_heartbeat(heartbeat.clone()); let eth = Ioc3Eth::new(mac); eth.set_heartbeat(heartbeat.clone()); ioc3.attach_eth(eth.clone()); From d06b4adba799ff262c95b8c6387ffa869c5a919c Mon Sep 17 00:00:00 2001 From: James Braid Date: Wed, 28 Oct 2026 23:54:02 -0700 Subject: [PATCH 15/15] ioc3: the Octane board drives GPIO inputs 2 and 3 high The model read input 2 as an Octane/Octane2 product pin, high on an Octane and low on an Octane2. An Octane2's PROM Command Monitor reads pin data 0x12d: inputs 2 and 3 both high, and nothing public shows a pin that tells the two machines apart. Drive both high on every Octane board; an Octane's inputs are unmeasured and assumed the same. --- src/dev/ioc3.rs | 12 +++++++----- src/dev/ioc3_tests.rs | 36 +++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 0a057d71..22fb5aa9 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -911,8 +911,8 @@ impl BoardRecordText { } impl Ioc3Config { - /// The Octane's IOC3 (`octane2` selects the Octane2's board records and - /// product input). The fictitious barcodes and ROM serials are the + /// The Octane's IOC3 (`octane2` selects the Octane2's board records). + /// The fictitious barcodes and ROM serials are the /// marked-synthetic ones (barcode `EMU...`). pub fn octane(mac: [u8; 6], octane2: bool) -> Self { let (fp_rev, ps_name, ps_part, ps_rev) = if octane2 { @@ -927,9 +927,11 @@ impl Ioc3Config { parallel: true, keyboard_mouse: true, rtc: true, - // GPIO 2 is the read-only product input: 1 on Octane, 0 on Octane2. - gpio_input_mask: 1 << 2, - gpio_input_values: if octane2 { 0 } else { 1 << 2 }, + // The board drives GPIO inputs 2 and 3 high: read on an Octane2's + // PROM Command Monitor (pin data 0x12d). An Octane is unmeasured + // and assumed the same; nothing shows a pin that tells them apart. + gpio_input_mask: 1 << 2 | 1 << 3, + gpio_input_values: 1 << 2 | 1 << 3, records: vec![ (BoardRecordText::new("EMUF00", "030-0891-003", fp_rev, "FP1"), 0x5145_4d55_4600), (BoardRecordText::new("EMUS00", ps_part, ps_rev, ps_name), 0x5145_4d55_5300), diff --git a/src/dev/ioc3_tests.rs b/src/dev/ioc3_tests.rs index 8446939d..49efbd2e 100644 --- a/src/dev/ioc3_tests.rs +++ b/src/dev/ioc3_tests.rs @@ -350,26 +350,28 @@ fn a_pin_register_reaches_the_shared_data_word() { assert_eq!(r.r32(reg::PIN_DATA) & (1 << 6), 0); } -/// Pin 2 is the board's read-only product input: 1 on an Octane, 0 on an -/// Octane2. Writes and direction changes cannot replace it. +/// Pins 2 and 3 are read-only inputs the board drives high, on the Octane +/// and the Octane2 alike. Writes and direction changes cannot replace them. #[test] -fn the_product_input_is_read_only() { - for (octane2, want) in [(false, 1u32), (true, 0)] { +fn the_board_inputs_are_read_only() { + for octane2 in [false, true] { let r = Rig::with(Ioc3::octane(MAC, octane2)); - let pin2 = reg::PIN_REG_FIRST + 8; - assert_eq!(r.r32(pin2), want); - assert_eq!(r.r32(reg::PIN_DATA) >> 2 & 1, want); - r.w32(pin2, 1 - want); - r.w32(reg::PIN_DATA, if want == 1 { 0 } else { 0xffff_ffff }); - r.w32(reg::PIN_CONTROL_SET, 1 << 2); - r.w32(reg::PIN_CONTROL_CLR, 1 << 2); - assert_eq!(r.r32(pin2), want); - assert_eq!(r.r32(reg::PIN_DATA) >> 2 & 1, want); - r.c.reset(); - assert_eq!(r.r32(pin2), want, "a reset keeps the board's value"); + for pin in [2u32, 3] { + let reg = reg::PIN_REG_FIRST + 4 * pin; + assert_eq!(r.r32(reg), 1); + assert_eq!(r.r32(reg::PIN_DATA) >> pin & 1, 1); + r.w32(reg, 0); + r.w32(reg::PIN_DATA, 0); + r.w32(reg::PIN_CONTROL_SET, 1 << pin); + r.w32(reg::PIN_CONTROL_CLR, 1 << pin); + assert_eq!(r.r32(reg), 1); + assert_eq!(r.r32(reg::PIN_DATA) >> pin & 1, 1); + r.c.reset(); + assert_eq!(r.r32(reg), 1, "a reset keeps the board's value"); + } // An unrelated pin still writes. - r.w32(reg::PIN_REG_FIRST + 4 * 3, 1); - assert_eq!(r.r32(reg::PIN_REG_FIRST + 4 * 3), 1); + r.w32(reg::PIN_REG_FIRST + 4 * 6, 1); + assert_eq!(r.r32(reg::PIN_REG_FIRST + 4 * 6), 1); } }