From 10c548684516a34d5fdf07e5ca5da43853d203e1 Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 01:29:08 -0700 Subject: [PATCH 1/3] mace: model the identity register as one device on the shared w1 bus The O2 carried its own 1-wire state machine, CRC and MAC-to-ROM builder next to the w1 module the Octane uses, and MACE's flash/NIC register was split between a stub cell and that model. Replace both with one device for the whole register, ported from the QEMU model: flash enable, the identity line to a w1::Bus holding the board's DS2502, and the two front LED bits. The line is two bits, so the device times each low pulse on the guest clock and hands the finished reset or slot to the bus. It keeps the QEMU model's structure but not its absolute datasheet windows: the PROM's delay loops are counted in emulated instructions, so a fast emulator (jitv2, a fast host) runs them short, and a pulse outside a window was ignored, which left the Ethernet address reading as all ff. A low of at least 300 us is a reset, at least 30 us a zero, anything shorter a one, and a part's zero holds the line until the next slot. READ ROM, SKIP ROM and READ MEMORY replies are byte-for-byte what the old model sent, and a test pins them. The register trace and the protocol log go; the monitor keeps an onewire status line. Snapshots keep the register, as the QEMU model does. Differences: READ STATUS and READ DATA/CRC now answer as the shared DS2502 does. --- CHANGELOG.md | 2 + debug.md | 4 +- src/dev/mace.rs | 77 +++---- src/dev/mace_id.rs | 221 +++++++++++++++++++ src/dev/mod.rs | 2 +- src/dev/onewire.rs | 501 -------------------------------------------- src/dev/w1/sgi.rs | 11 + src/ip32.rs | 20 +- src/ip32/tests.rs | 250 +++++++++------------- src/ip32_machine.rs | 12 +- 10 files changed, 364 insertions(+), 736 deletions(-) create mode 100644 src/dev/mace_id.rs delete mode 100644 src/dev/onewire.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ecf8f26..e0bdcca17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -262,6 +262,8 @@ hashes are given where a change is easiest to understand by reading the commit. audio registers now brings the rings up to date, so IRIX, which keeps only about 2 ms of sound queued, no longer lets them run dry between service passes. +- **The O2's Ethernet-address chip is on the shared 1-wire bus** (`dev::mace_id`, + ported from the QEMU model); the MAC reads correctly at any emulator speed. - **The O2 is on `Physical`.** Its bus is the same decode table and memory window as the Indy's: CRIME's banks are RAM banks a bank-control write remaps, CRIME, GBE, VICE, MACE and the flash are table entries, and diff --git a/debug.md b/debug.md index 59d174f09..173e4e572 100644 --- a/debug.md +++ b/debug.md @@ -542,7 +542,7 @@ its SCSI controllers answer to `scsi`/`cow` and `scsi1`/`cow1` as described unde | `audio status` / `audio codec` | MACE audio rings and rates / AD1843 codec registers | | `ust status` | MACE UST counter | | `rtc status` / `rtc dump` / `rtc r ` / `rtc w ` | DS1687 RTC | -| `onewire status` / `onewire log` | 1-Wire ID chip (the MAC address) | +| `onewire status` | MACE identity register and the ID chip holding the MAC address | `o2 ` runs the same commands `[o2] control_port` serves, one per line: @@ -559,7 +559,7 @@ its SCSI controllers answer to `scsi`/`cow` and `scsi1`/`cow1` as described unde | `o2 retrace PATH` | Recorded rendering-engine primitives (`IRIS_IP32_RE_TRACE`) | | `o2 scsi PATH` | The SCSI controllers' recent commands | | `o2 unmapped` | Bus accesses that reached no device (the first 64 distinct), with the PC that made each (not kept in a `lightning` build) | -| `o2 ps2log PATH` / `o2 crimelog PATH` / `o2 nictrace PATH` | PS/2 traffic / CRIME interrupt-register accesses recorded since `crime irqlog on` / the MAC-address chip's register trace and 1-Wire protocol log | +| `o2 ps2log PATH` / `o2 crimelog PATH` | PS/2 traffic / CRIME interrupt-register accesses recorded since `crime irqlog on` | ## Recipes diff --git a/src/dev/mace.rs b/src/dev/mace.rs index 866ee9f05..d4f418507 100644 --- a/src/dev/mace.rs +++ b/src/dev/mace.rs @@ -3,7 +3,7 @@ //! and hands the access to the block it names, as HPC3 does for its //! children on the Indigo2. The blocks themselves are in their own files //! (`mace_pci`, `mec`, `mace_isa`, `mace_ps2`, `mace_ust`, `mace_audio`, -//! `mace_sdma`, `ns16550`, `ds1687`, `onewire`); what has no model yet is a +//! `mace_sdma`, `ns16550`, `ds1687`, `mace_id`); what has no model yet is a //! storing register file (`regs`). use std::sync::{Arc, Mutex}; @@ -19,9 +19,9 @@ use crate::dev::mace_sdma::*; use crate::dev::mace_ust::MaceUst; use crate::dev::mec::Mec; use crate::dev::ns16550::Com16550; -use crate::dev::onewire::{NicTrace, OneWireId}; +use crate::dev::mace_id::{MaceId, DEFAULT_MAC}; use crate::ip32::{ - nic_bit, Stub, IP32_MEC_CRIME_INT, MACE_BASE, MACE_COM0, MACE_COM1, MACE_COM_SIZE, MACE_ISA_FLASH_NIC_REG, + Stub, IP32_MEC_CRIME_INT, MACE_BASE, MACE_COM0, MACE_COM1, MACE_COM_SIZE, MACE_ISA_FLASH_NIC_REG, MACE_MEC, MACE_MEC_SIZE, MACE_PS2, MACE_PS2_SIZE, MACE_RTC, MACE_RTC_SIZE, MACE_SIZE, MACE_UST_MSC, MACE_UST_MSC_SIZE, MACEPCI_BASE, MACEPCI_SIZE, }; @@ -38,10 +38,9 @@ pub struct Mace { /// The PCI host bridge: its registers here, its windows outside MACE. pub pci: MacePci, pub ust: Arc, - pub nic_trace: NicTrace, pub rtc: MaceRtc, pub ps2: MacePs2, - pub onewire: OneWireId, + pub id: MaceId, pub com0: Com16550, pub com1: Com16550, /// The serial ports' DMA rings, which IRIX's console runs on. @@ -63,11 +62,10 @@ impl Mace { regs: Stub::new("mace"), pci: MacePci::new(crime.clone(), ram.clone()), ust, - nic_trace: NicTrace::new(), rtc: MaceRtc::new(), ps2: MacePs2::new(), // A locally-administered address; nothing depends on the value yet. - onewire: OneWireId::from_mac([0x08, 0x00, 0x69, 0x12, 0x34, 0x56]), + id: MaceId::new(DEFAULT_MAC), com0: Com16550::named("com0"), com1: Com16550::named("com1"), sdma: MaceSerialDma::new(), @@ -113,7 +111,7 @@ impl Mace { /// commands through on the Indigo2. pub fn devices(&self) -> Vec<&dyn crate::traits::Device> { vec![ - &self.pci, &*self.ust, &self.rtc, &self.ps2, &self.onewire, &self.com0, &self.com1, &self.sdma, + &self.pci, &*self.ust, &self.rtc, &self.ps2, &self.id, &self.com0, &self.com1, &self.sdma, &self.audio, &*self.mec, &*self.isa, ] } @@ -234,33 +232,18 @@ impl Mace { (addr & !3) == MACE_ISA_FLASH_NIC_REG } - /// Feed a write of the NIC register through to the 1-Wire device. - fn nic_write(&self, val: u8) { - self.nic_trace.record(self.ust.now(), self.ust.guest_us(), true, val); - // DEASSERT alone decides it: clear means the master is pulling the - // line down, set means it has let go. The firmware writes DATA=1 in - // both cases, so DATA is an input here, not a level to drive — which - // is exactly what the bus trace shows (only 0x08 and 0x0c are ever - // written). - let low = val & nic_bit::DEASSERT == 0; + /// The identity/flash/LED register: its own device holds the value. + fn id_write(&self, val: u8) { self.ust.onewire_busy(); - self.onewire.master_drive(self.ust.guest_us(), low); - } - - /// The NIC register, with the 1-Wire line as it stands now. A read does - /// not hold the guest clock (only the master driving the line does): - /// IRIX reads this register about a hundred times a second for its - /// other bits, and each read holding the clock for 5 ms ran UST at half - /// speed. - fn nic_read(&self) -> u8 { - let mut v = self.regs.cell(MACE_ISA_FLASH_NIC_REG) as u8; - if self.onewire.line_level(self.ust.guest_us()) { - v |= nic_bit::DATA; - } else { - v &= !nic_bit::DATA; - } - self.nic_trace.record(self.ust.now(), self.ust.guest_us(), false, v); - v + self.id.write(self.ust.guest_us(), val); + } + + /// A read does not hold the guest clock (only the master driving the + /// line does): IRIX reads this register about a hundred times a second + /// for its other bits, and each read holding the clock for 5 ms ran UST + /// at half speed. + fn id_read(&self) -> u8 { + self.id.read(self.ust.guest_us()) } } @@ -296,7 +279,7 @@ impl Mace { ("pci", &self.pci), ("rtc", &self.rtc), ("ps2", &self.ps2), - ("onewire", &self.onewire), + ("id", &self.id), ("com0", &self.com0), ("com1", &self.com1), ("sdma", &self.sdma), @@ -319,7 +302,7 @@ impl BusDevice for Mace { return BusRead8::ok(self.pci.read32(addr & !3).data as u8); } if Self::is_nic_reg(a) { - return BusRead8::ok(self.nic_read()); + return BusRead8::ok(self.id_read()); } if let Some(c) = self.com_for(a) { return c.read8(addr); @@ -397,7 +380,7 @@ impl BusDevice for Mace { return self.pci.read32(addr); } if Self::is_nic_reg(a) { - return BusRead32::ok(self.nic_read() as u32); + return BusRead32::ok(self.id_read() as u32); } if let Some(c) = self.com_for(a) { return c.read32(addr); @@ -442,7 +425,7 @@ impl BusDevice for Mace { return self.pci.read64(addr); } if Self::is_nic_reg(a) { - return BusRead64::ok(self.nic_read() as u64); + return BusRead64::ok(self.id_read() as u64); } if let Some(c) = self.com_for(a) { return c.read64(addr); @@ -479,10 +462,8 @@ impl BusDevice for Mace { return self.pci.write32(addr & !3, val as u32); } if Self::is_nic_reg(a) { - self.nic_write(val); - let r = self.regs.write8(addr, val); - self.regs_wrote(a); - return r; + self.id_write(val); + return BUS_OK; } if let Some(c) = self.com_for(a) { return c.write8(addr, val); @@ -574,10 +555,8 @@ impl BusDevice for Mace { return self.pci.write32(addr, val); } if Self::is_nic_reg(a) { - self.nic_write(val as u8); - let r = self.regs.write32(addr, val); - self.regs_wrote(a); - return r; + self.id_write(val as u8); + return BUS_OK; } if let Some(c) = self.com_for(a) { return c.write32(addr, val); @@ -627,10 +606,8 @@ impl BusDevice for Mace { return self.pci.write64(addr, val); } if Self::is_nic_reg(a) { - self.nic_write(val as u8); - let r = self.regs.write64(addr, val); - self.regs_wrote(a); - return r; + self.id_write(val as u8); + return BUS_OK; } if let Some(c) = self.com_for(a) { return c.write64(addr, val); diff --git a/src/dev/mace_id.rs b/src/dev/mace_id.rs new file mode 100644 index 000000000..e6886e6bf --- /dev/null +++ b/src/dev/mace_id.rs @@ -0,0 +1,221 @@ +//! MACE's miscellaneous control register at 0x1f310008: the flash write +//! enable, the identity line to the board's DS2502 and the two front LEDs. +//! The line is two bits, not a slot register: the master clears RELEASE to +//! pull it low and sets it to let go, and SAMPLE reads it back. What a pulse +//! means depends on how long the line stayed low, so the device times it on +//! the guest clock and hands each finished reset or bit slot to the shared +//! [`w1::Bus`]. +//! +//! The QEMU model keeps the DS2502 datasheet's absolute windows (reset +//! 480-960 us, zero 60-120, one under 24, 480 high before a reset). The +//! PROM's delay loops are counted in emulated instructions, so a fast +//! emulator (jitv2, a fast host) runs them short, and a pulse outside a +//! window was ignored, leaving the Ethernet address reading as all ff. So +//! the classes are only ordered: a low of at least 300 us is a reset, one of +//! at least 30 a zero, anything shorter a one. The widths the PROM produces, +//! about 500, 90 and 8, land in the same classes. +//! +//! Ported from hw/mips/sgi/mace-id.c in jamesbraid/qemu-sgi-origami. + +use std::sync::Mutex; + +use crate::dev::w1::{self, sgi}; +use crate::snapshot::{get_field, toml_u64}; + +pub const FLASH_ENABLE: u8 = 1; +const PASSWORD_CLEAR: u8 = 1 << 1; +/// The master lets the line float high. +pub const RELEASE: u8 = 1 << 2; +/// The line as it reads back. +pub const SAMPLE: u8 = 1 << 3; +const RED_LED: u8 = 1 << 4; +const GREEN_LED: u8 = 1 << 5; +const WRITABLE: u8 = FLASH_ENABLE | RELEASE | RED_LED | GREEN_LED; + +/// Shortest low, in microseconds of guest time, that is a reset, and the +/// shortest that is a zero. +const RESET_MIN: u64 = 300; +const ZERO_MIN: u64 = 30; +/// After a reset releases, the part answers presence from this long after +/// until this long after. +const PRESENCE_DELAY: u64 = 30; +const PRESENCE_END: u64 = 150; + +/// The station address the board carries when none is configured. +pub const DEFAULT_MAC: [u8; 6] = [0x08, 0x00, 0x69, 0x12, 0x34, 0x56]; + +/// Where the PROM's start-up handshake stands: its first low, however long, +/// is a reset when the line is first released. +#[derive(Default)] +enum Start { + /// Waiting for the master to pull the line down (`None`), or since when + /// it has held it down. + #[default] + Pending, + Low(u64), + Done, +} + +#[derive(Default)] +struct State { + bus: w1::Bus, + low_start: u64, + start: Start, + /// The window in which the part answers presence, once a reset has + /// been seen; none before. + presence: Option<(u64, u64)>, + released: bool, + flash_write_enabled: bool, + led_control: u8, + /// The part is holding the line low for a zero it is sending. + slot_pull: bool, +} + +pub struct MaceId { + st: Mutex, +} + +impl State { + /// Reset changes the drive outputs, not the connected board input. + fn reset(&mut self) { + let bus = std::mem::take(&mut self.bus); + *self = State { bus, led_control: GREEN_LED, ..Default::default() }; + self.bus.reset(); + } + + fn presence_at(&self, now: u64) -> bool { + self.presence.is_some_and(|(from, to)| now >= from && now < to) + } + + fn value(&self, now: u64) -> u8 { + let level = self.released && !self.presence_at(now) && !self.slot_pull; + self.led_control + | if self.flash_write_enabled { FLASH_ENABLE } else { 0 } + | if self.released { RELEASE } else { 0 } + | if level { SAMPLE } else { 0 } + } + + fn reset_pulse(&mut self, now: u64) { + self.bus.reset(); + self.slot_pull = false; + self.presence = Some((now + PRESENCE_DELAY, now + PRESENCE_END)); + } + + fn write(&mut self, now: u64, value: u8) { + let release = value & RELEASE != 0; + self.led_control = value & (RED_LED | GREEN_LED); + self.flash_write_enabled = value & FLASH_ENABLE != 0; + if !release && matches!(self.start, Start::Pending) { + self.start = Start::Low(now); + } + if release == self.released { + return; + } + self.released = release; + if !release { + // The slot ends where the next one starts. + self.slot_pull = false; + self.low_start = now; + return; + } + if let Start::Low(since) = self.start { + self.start = Start::Done; + if now.saturating_sub(since) >= RESET_MIN { + self.reset_pulse(now); + } + return; + } + let low = now.saturating_sub(self.low_start); + if low >= RESET_MIN { + self.reset_pulse(now); + return; + } + let Some((_, end)) = self.presence else { return }; + if now < end { + return; + } + if low >= ZERO_MIN { + self.bus.slot(false); + } else { + // A one is also a read slot: a zero from the part holds the line + // low until the next slot starts. + self.slot_pull = !self.bus.slot(true); + } + } +} + +impl MaceId { + /// The board's Ethernet-address chip for station address `mac`. + pub fn new(mac: [u8; 6]) -> Self { + let mut st = State::default(); + st.bus.add(Box::new(sgi::o2_ethernet_address_part(mac))); + st.reset(); + Self { st: Mutex::new(st) } + } + + /// The register at guest time `now` (microseconds). The password-clear + /// input is not wired, so bit 1 reads zero. + pub fn read(&self, now: u64) -> u8 { + self.st.lock().unwrap().value(now) & !PASSWORD_CLEAR + } + + pub fn write(&self, now: u64, value: u8) { + self.st.lock().unwrap().write(now, value & WRITABLE); + } +} + +impl crate::traits::Device for MaceId { + fn step(&self, _cycles: u64) {} + fn stop(&self) {} + fn start(&self) {} + fn is_running(&self) -> bool { + true + } + fn get_clock(&self) -> u64 { + 0 + } + + fn register_commands(&self) -> Vec<(String, String)> { + vec![("onewire".to_string(), "MACE identity line: onewire status".to_string())] + } + + fn execute_command(&self, cmd: &str, _args: &[&str], mut w: Box) -> Result<(), String> { + use std::io::Write; + if cmd != "onewire" { + return Err("Command not found".to_string()); + } + let st = self.st.lock().unwrap(); + let rom: String = st.bus.roms()[0].iter().map(|b| format!("{b:02x}")).collect(); + writeln!(w, "ID chip ROM {rom}, register {:#04x}", st.value(0)).ok(); + Ok(()) + } +} + +/// The register's state only, as the QEMU model keeps it. The part comes +/// from the configured MAC, and a restored guest is past the PROM's start-up +/// handshake. +impl crate::traits::Saveable for MaceId { + fn save_state(&self) -> toml::Value { + let st = self.st.lock().unwrap(); + let mut t = toml::map::Map::new(); + t.insert("register".into(), toml::Value::Integer((st.value(0) & !SAMPLE) as i64)); + toml::Value::Table(t) + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + let reg = get_field(v, "register").and_then(toml_u64).unwrap_or(0) as u8; + let mut st = self.st.lock().unwrap(); + st.reset(); + st.start = Start::Done; + st.released = reg & RELEASE != 0; + st.flash_write_enabled = reg & FLASH_ENABLE != 0; + st.led_control = reg & (RED_LED | GREEN_LED); + Ok(()) + } +} + +impl crate::traits::Resettable for MaceId { + fn power_on(&self) { + self.st.lock().unwrap().reset(); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index a0d97e439..8b2f5e2ad 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -18,12 +18,12 @@ pub mod ioc3; pub mod ioc3_eth; pub mod ioc3_port; pub mod isp1040; +pub mod mace_id; pub mod mace_pci; pub mod mace_ps2; pub mod mace_sdma; pub mod mace_ust; pub mod ns16550; -pub mod onewire; pub mod rad1; pub mod mgras; pub mod ng1; diff --git a/src/dev/onewire.rs b/src/dev/onewire.rs deleted file mode 100644 index fd1671ecf..000000000 --- a/src/dev/onewire.rs +++ /dev/null @@ -1,501 +0,0 @@ -//! The O2's 1-Wire bus and its DS2502 Ethernet-address chip. - -use std::sync::Mutex; - -use crate::snapshot::{get_field, hex_u64, hex_u8, load_u8_slice, toml_u64, toml_u8, u8_slice_to_toml}; - - -/// The Dallas 1-Wire identity chip: 64 bits of ROM holding a family code, a -/// 48-bit serial number and a CRC. SGI machines keep the Ethernet address here. -/// -/// The bus is one wire, so everything is encoded in how long the master holds -/// it low. Durations are read from the UST clock, which is why that had to -/// become time-based first. -pub struct OneWireId { - rom: [u8; 8], - /// The EPROM data area. Erased cells read as 0xff on a real part, so an - /// unprogrammed chip is all-ones rather than all-zeros — a distinction - /// firmware notices. - data: Vec, - /// The status/redirection bytes, likewise erased. - status: Vec, - st: Mutex, -} - -/// What the device is doing between slots. It decides how the next slot is -/// read: while the master still has things to say a slot is a write and is -/// classified by width, and once the device is answering every slot is a read. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -enum OwPhase { - /// Waiting for a command byte. - #[default] - Idle, - /// Collecting the address bytes a memory command needs. - WantAddr, - /// Streaming a reply. - Sending, -} - -#[derive(Default)] -struct OneWireState { - /// Time the master last pulled the line low. - low_since: Option, - /// Bits received from the master in the current command byte. - in_bits: u8, - in_count: u8, - /// Command being executed, once a whole byte has arrived. - command: Option, - /// Bit position within [`OneWireState::out`]. - out_pos: usize, - phase: OwPhase, - /// The memory command waiting for its address bytes. - pending: u8, - addr: [u8; 2], - addr_count: usize, - /// The bytes the device is sending, LSB first within each. - out: Vec, - /// True when the reply has a definite end after which the device listens - /// for another command — READ ROM does, a memory read does not. - out_then_idle: bool, - /// Decoded protocol events, for the bring-up trace. - log: Vec, - /// True once the master has opened a read slot, so a bit is on the line. - presenting: bool, - /// The presence pulse, as a window in UST time. Equal bounds mean none. - presence_from: u64, - presence_to: u64, -} - -pub mod onewire_cmd { - pub const READ_ROM: u8 = 0x33; - pub const SKIP_ROM: u8 = 0xcc; - /// DS2502 memory read: two address bytes follow, then the device sends a - /// CRC of what it was told and streams data from that address. - pub const READ_MEMORY: u8 = 0xf0; - /// As above, but the device also emits a CRC after each page. - pub const READ_DATA_CRC: u8 = 0xc3; - /// DS2502 status (the redirection/write-protect bytes), same shape. - pub const READ_STATUS: u8 = 0xaa; -} - -/// Microsecond thresholds. A reset is held for at least 480 µs; within a time -/// slot, a short low is a 1 and a long low is a 0. -/// After the master releases a reset, the device waits this long and then -/// pulls the line low for [`OW_PRESENCE_LEN_US`]. The master samples somewhere -/// in that window; holding the pulse for a fixed time rather than "until -/// somebody looks" is what makes it independent of how often it polls. -/// The part SGI fits, and the one the firmware insists on: a DS2502, family -/// code 0x09. Getting this wrong is not subtle but it is quiet — the firmware -/// reads the first eight bits of the ROM, sees a family it does not recognise, -/// abandons the read and reports `ds2502_read_rom failed`, with the other -/// fifty-six bits never requested. A DS1990A's 0x01 is the obvious wrong -/// answer because it is the one every 1-Wire example uses. -pub const OW_FAMILY_DS2502: u8 = 0x09; - -pub const OW_PRESENCE_DELAY_US: u64 = 20; -pub const OW_PRESENCE_LEN_US: u64 = 150; - -/// Thresholds, and a lesson about calibration. -/// -/// These were first set from the widths the PROM produced, which were -/// `32 / 357 / 1980` microseconds — four to six times the 1-Wire spec's -/// `6 / 60 / 480`. That looked like the PROM simply bit-banging slowly, and -/// the thresholds were moved out to match. They were in fact compensating for -/// CRIME's counter, which advanced once per *read* instead of with time, so -/// every delay the firmware asked for came out stretched. -/// -/// With [`CRIME_TICKS_PER_US`] driving that counter from the same clock as -/// the UST, the measured widths are `8 / 90 / 500` and the datasheet numbers -/// work unmodified. A device model that needs constants tuned away from the -/// specification is usually telling you something about the clock, not about -/// the device. -pub const OW_RESET_US: u64 = 300; -pub const OW_WRITE0_US: u64 = 30; - -impl OneWireId { - /// Build a ROM from a MAC address: the family code, the six address bytes - /// as the serial number, and a CRC-8 over the first seven. - pub fn from_mac(mac: [u8; 6]) -> Self { - let mut rom = [0u8; 8]; - rom[0] = OW_FAMILY_DS2502; - rom[1..7].copy_from_slice(&mac); - rom[7] = crc8_dallas(&rom[..7]); - // The firmware reads six bytes from offset 0 and reverses them to - // form the address, so store it backwards. - let mut data = vec![0xff; Self::DATA_LEN]; - for (i, b) in mac.iter().rev().enumerate() { - data[i] = *b; - } - Self { - rom, - data, - status: vec![0xff; Self::STATUS_LEN], - st: Mutex::new(Default::default()), - } - } - - /// 1 kbit of EPROM, in four 32-byte pages. - pub const DATA_LEN: usize = 128; - pub const STATUS_LEN: usize = 8; - - pub fn rom(&self) -> [u8; 8] { - self.rom - } - - /// Program the EPROM data area. - pub fn set_data(&mut self, at: usize, bytes: &[u8]) { - for (i, b) in bytes.iter().enumerate() { - if at + i < self.data.len() { - self.data[at + i] = *b; - } - } - } - - /// The bit the device is currently presenting, LSB first within each byte. - /// Past the end of the reply the line simply floats high, which is what a - /// real part does. - fn reply_bit(&self, st: &OneWireState) -> bool { - let i = st.out_pos; - match st.out.get(i / 8) { - Some(b) => (b >> (i % 8)) & 1 != 0, - None => true, - } - } - - /// A whole byte arrived from the master. - fn take_byte(&self, st: &mut OneWireState, b: u8) { - match st.phase { - OwPhase::Idle => { - st.command = Some(b); - self.note(st, format!("cmd 0x{b:02x}")); - match b { - onewire_cmd::READ_ROM => { - st.out = self.rom.to_vec(); - st.out_pos = 0; - // Exactly 64 bits, and then the master may issue a - // memory command without an intervening reset. - st.out_then_idle = true; - st.phase = OwPhase::Sending; - } - onewire_cmd::READ_MEMORY - | onewire_cmd::READ_DATA_CRC - | onewire_cmd::READ_STATUS => { - st.pending = b; - st.addr_count = 0; - st.phase = OwPhase::WantAddr; - } - // SKIP_ROM and anything else: nothing to say. - _ => st.phase = OwPhase::Idle, - } - } - OwPhase::WantAddr => { - if st.addr_count < 2 { - st.addr[st.addr_count] = b; - st.addr_count += 1; - } - if st.addr_count == 2 { - let a = u16::from_le_bytes(st.addr) as usize; - // The device answers with a CRC over everything it was - // told, then streams memory from that address. - // The firmware reads exactly 1 + (len - addr) + 1 bytes: - // a CRC over what it just sent, the rest of the memory - // from that address, and a CRC over that data. Omitting - // the trailing CRC costs nothing visible — the master - // simply reads one byte past the end, gets the floating - // 0xff, and rejects the whole transfer. - let mut out = vec![crc8_dallas(&[st.pending, st.addr[0], st.addr[1]])]; - let src: &[u8] = if st.pending == onewire_cmd::READ_STATUS { - &self.status - } else { - &self.data - }; - let body: Vec = src.iter().skip(a).copied().collect(); - let body_crc = crc8_dallas(&body); - out.extend(body); - out.push(body_crc); - self.note(st, format!("{} from 0x{a:04x}", - if st.pending == onewire_cmd::READ_STATUS { "status" } else { "memory" })); - st.out = out; - st.out_pos = 0; - // A memory read streams until the master resets. - st.out_then_idle = false; - st.phase = OwPhase::Sending; - } - } - // Once the device is talking, the master listens. - OwPhase::Sending => {} - } - } - - fn note(&self, st: &mut OneWireState, what: String) { - if st.log.len() < 64 { - st.log.push(what); - } - } - - /// The protocol the device saw, decoded — resets, commands, addresses. - pub fn protocol_log(&self) -> Vec { - self.st.lock().unwrap().log.clone() - } - - /// The master changed the line. `low` is true when it is pulling low. - /// - /// Everything is decided by how long the master held the line down, which - /// is why the UST has to be a real clock: a reset, a write-0, a write-1 and - /// a read slot differ only in duration. - pub fn master_drive(&self, now: u64, low: bool) { - let mut st = self.st.lock().unwrap(); - if low { - if st.low_since.is_none() { - // Falling edge. It opens the next slot, so the bit the last - // slot presented is finished with now — advance past it here - // rather than on release, where the master has not yet had a - // chance to sample it. - if st.phase == OwPhase::Sending && st.presenting { - st.out_pos += 1; - st.presenting = false; - // A finite reply ends, and the device goes back to - // listening. Without this the master's next command is - // read as more read slots and silently lost. - if st.out_then_idle && st.out_pos >= st.out.len() * 8 { - st.phase = OwPhase::Idle; - st.in_bits = 0; - st.in_count = 0; - } - } - st.low_since = Some(now); - } - return; - } - // Rising edge: classify by how long it was held. - let Some(since) = st.low_since.take() else { return }; - let held = now.saturating_sub(since); - if held >= OW_RESET_US { - // How much of the reply the master actually took before giving up - // is the difference between "it hated the CRC" and "it hated the - // contents". - if st.phase == OwPhase::Sending { - let (got, total) = (st.out_pos, st.out.len() * 8); - self.note(&mut st, format!("master took {got} of {total} bits")); - } - let log = std::mem::take(&mut st.log); - *st = OneWireState { - presence_from: now + OW_PRESENCE_DELAY_US, - presence_to: now + OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US, - log, - ..Default::default() - }; - self.note(&mut st, "reset".into()); - return; - } - if st.phase == OwPhase::Sending { - // A read slot: the master pulsed briefly, and for the rest of the - // slot the device owns the line. - st.presenting = true; - return; - } - // A write slot, LSB first. - let bit = held < OW_WRITE0_US; - st.in_bits |= (bit as u8) << st.in_count; - st.in_count += 1; - if st.in_count == 8 { - let b = st.in_bits; - st.in_bits = 0; - st.in_count = 0; - self.take_byte(&mut st, b); - } - } - - /// What the master sees on the line: true is high (idle), false is low. - pub fn line_level(&self, now: u64) -> bool { - let st = self.st.lock().unwrap(); - if now >= st.presence_from && now < st.presence_to { - return false; - } - if st.low_since.is_some() { - // The master is holding it down itself. - return false; - } - if st.phase == OwPhase::Sending && st.presenting { - return self.reply_bit(&st); - } - true - } -} - -/// Dallas/Maxim CRC-8, polynomial x^8 + x^5 + x^4 + 1 reflected (0x8c). -pub fn crc8_dallas(data: &[u8]) -> u8 { - let mut crc = 0u8; - for &b in data { - let mut byte = b; - for _ in 0..8 { - let mix = (crc ^ byte) & 1; - crc >>= 1; - if mix != 0 { - crc ^= 0x8c; - } - byte >>= 1; - } - } - crc -} - -/// Records what the PROM does to the 1-Wire register and when, so the protocol -/// can be reconstructed from its own behaviour rather than guessed at. -pub struct NicTrace { - /// (host us, guest-clock us, write, value) - events: Mutex>, -} - -impl Default for NicTrace { - fn default() -> Self { Self::new() } -} - -impl NicTrace { - pub fn new() -> Self { Self { events: Mutex::new(Vec::new()) } } - pub fn record(&self, t: u64, guest: u64, write: bool, val: u8) { - let mut e = self.events.lock().unwrap(); - if e.len() < 65536 { - e.push((t, guest, write, val)); - } - } - pub fn events(&self) -> Vec<(u64, bool, u8)> { - self.events.lock().unwrap().iter().map(|&(t, _, w, v)| (t, w, v)).collect() - } - /// One line per access: host us, guest-clock us, R/W, value. - pub fn text(&self) -> String { - self.events.lock().unwrap().iter().map(|&(t, g, w, v)| format!("{t} {g} {} {v:02x}\n", if w { 'W' } else { 'R' })).collect() - } -} - -impl crate::traits::Device for OneWireId { - fn step(&self, _cycles: u64) {} - fn stop(&self) {} - fn start(&self) {} - fn is_running(&self) -> bool { true } - fn get_clock(&self) -> u64 { 0 } - - fn register_commands(&self) -> Vec<(String, String)> { - vec![("onewire".to_string(), "1-Wire ID chip: onewire status | onewire log".to_string())] - } - - fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { - use std::io::Write; - if cmd != "onewire" { - return Err("Command not found".to_string()); - } - match args.first().copied().unwrap_or("status") { - "status" => { - let rom: Vec = self.rom.iter().map(|b| format!("{b:02x}")).collect(); - let mac: Vec = self.rom[1..7].iter().map(|b| format!("{b:02x}")).collect(); - let st = self.st.lock().unwrap(); - writeln!(w, "1-Wire DS2502: ROM {} (Ethernet {}), phase {:?}, command {:?}", - rom.join(""), mac.join(":"), st.phase, st.command).ok(); - } - "log" => { - for l in self.protocol_log() { - writeln!(w, " {l}").ok(); - } - } - _ => return Err("Usage: onewire status | log".to_string()), - } - Ok(()) - } -} - -/// A transaction in progress, so a machine saved mid-read finishes it. The -/// ROM and EPROM come from the configured MAC; the trace log is not state. -impl crate::traits::Saveable for OneWireId { - fn save_state(&self) -> toml::Value { - let st = self.st.lock().unwrap(); - let mut t = toml::map::Map::new(); - if let Some(l) = st.low_since { - t.insert("low_since".into(), hex_u64(l)); - } - t.insert("in_bits".into(), hex_u8(st.in_bits)); - t.insert("in_count".into(), hex_u8(st.in_count)); - if let Some(c) = st.command { - t.insert("command".into(), hex_u8(c)); - } - t.insert("out_pos".into(), toml::Value::Integer(st.out_pos as i64)); - let phase = match st.phase { OwPhase::Idle => 0, OwPhase::WantAddr => 1, OwPhase::Sending => 2 }; - t.insert("phase".into(), toml::Value::Integer(phase)); - t.insert("pending".into(), hex_u8(st.pending)); - t.insert("addr".into(), u8_slice_to_toml(&st.addr)); - t.insert("addr_count".into(), toml::Value::Integer(st.addr_count as i64)); - t.insert("out".into(), u8_slice_to_toml(&st.out)); - t.insert("out_then_idle".into(), toml::Value::Boolean(st.out_then_idle)); - t.insert("presenting".into(), toml::Value::Boolean(st.presenting)); - t.insert("presence_from".into(), hex_u64(st.presence_from)); - t.insert("presence_to".into(), hex_u64(st.presence_to)); - toml::Value::Table(t) - } - - fn load_state(&self, v: &toml::Value) -> Result<(), String> { - let mut st = self.st.lock().unwrap(); - *st = Default::default(); - let u8_of = |k: &str| get_field(v, k).and_then(toml_u8).unwrap_or(0); - let int_of = |k: &str| get_field(v, k).and_then(|x| x.as_integer()).unwrap_or(0); - let bool_of = |k: &str| get_field(v, k).and_then(|x| x.as_bool()).unwrap_or(false); - st.low_since = get_field(v, "low_since").and_then(toml_u64); - st.in_bits = u8_of("in_bits"); - st.in_count = u8_of("in_count"); - st.command = get_field(v, "command").and_then(toml_u8); - st.out_pos = int_of("out_pos") as usize; - st.phase = match int_of("phase") { 1 => OwPhase::WantAddr, 2 => OwPhase::Sending, _ => OwPhase::Idle }; - st.pending = u8_of("pending"); - if let Some(a) = get_field(v, "addr") { - load_u8_slice(a, &mut st.addr); - } - st.addr_count = int_of("addr_count") as usize; - if let Some(toml::Value::Array(a)) = get_field(v, "out") { - st.out = a.iter().map(|b| toml_u8(b).unwrap_or(0)).collect(); - } - st.out_then_idle = bool_of("out_then_idle"); - st.presenting = bool_of("presenting"); - st.presence_from = get_field(v, "presence_from").and_then(toml_u64).unwrap_or(0); - st.presence_to = get_field(v, "presence_to").and_then(toml_u64).unwrap_or(0); - Ok(()) - } -} - -impl crate::traits::Resettable for OneWireId { - /// The ROM and EPROM are the chip's; a reset only drops a transaction - /// in progress. - fn power_on(&self) { - *self.st.lock().unwrap() = Default::default(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Saveable; - - #[test] - fn save_load_round_trip() { - let src = OneWireId::from_mac([0x08, 0x00, 0x69, 0x12, 0x34, 0x56]); - { - let mut st = src.st.lock().unwrap(); - st.low_since = Some(123_456); - st.in_bits = 0x5a; - st.in_count = 3; - st.command = Some(onewire_cmd::READ_MEMORY); - st.out_pos = 17; - st.phase = OwPhase::Sending; - st.pending = onewire_cmd::READ_DATA_CRC; - st.addr = [0x12, 0x34]; - st.addr_count = 2; - st.out = vec![0xde, 0xad, 0xbe, 0xef]; - st.out_then_idle = true; - st.presenting = true; - st.presence_from = 1000; - st.presence_to = 1150; - } - let v1 = src.save_state(); - - let dst = OneWireId::from_mac([0x08, 0x00, 0x69, 0x12, 0x34, 0x56]); - dst.load_state(&v1).expect("load_state"); - assert_eq!(v1, dst.save_state(), "OneWireId save_state mismatch after load_state round-trip"); - } -} diff --git a/src/dev/w1/sgi.rs b/src/dev/w1/sgi.rs index 370acc526..dd3bb2cd1 100644 --- a/src/dev/w1/sgi.rs +++ b/src/dev/w1/sgi.rs @@ -102,6 +102,17 @@ pub fn ethernet_address_part(mac: [u8; 6], serial: u64) -> w1::Ds2502 { w1::Ds2502::ds2501(0x91, serial, &page) } +/// The O2's Ethernet-address part: a DS2502 (family 0x09, the one its PROM +/// insists on) whose serial is the station address itself, in order, and +/// whose memory starts with the address reversed, the rest erased. +pub fn o2_ethernet_address_part(mac: [u8; 6]) -> w1::Ds2502 { + let mut rev = mac; + rev.reverse(); + let mut serial = [0u8; 8]; + serial[..6].copy_from_slice(&mac); + w1::Ds2502::new(0x09, u64::from_le_bytes(serial), &rev) +} + /// The synthetic ROM serial of the Ethernet-address part: the UniqueWare /// prefix 0x5e7 in its top twelve bits (what OpenBSD matches on), then the /// "EMU" marker and instance 0. diff --git a/src/ip32.rs b/src/ip32.rs index cdebe618c..dc9891069 100644 --- a/src/ip32.rs +++ b/src/ip32.rs @@ -46,20 +46,10 @@ pub const VICE_SIZE: u32 = 0x0100_0000; /// MACE, the I/O ASIC (`crate::dev::mace`): PCI, Ethernet, the ISA block. pub const MACE_BASE: u32 = 0x1f00_0000; pub const MACE_SIZE: u32 = 0x0080_0000; -/// MACE's ISA-block register holding the flash write-enable and the Dallas -/// 1-Wire ("NIC") data line. The PROM bit-bangs the serial-ID chip here. +/// MACE's ISA-block register holding the flash write-enable, the Dallas +/// identity line and the front LEDs (`crate::dev::mace_id`). pub const MACE_ISA_FLASH_NIC_REG: u32 = MACE_BASE + 0x0031_0008; -/// Bits in [`MACE_ISA_FLASH_NIC_REG`], as named by NetBSD's `macereg.h`. -pub mod nic_bit { - /// 1 => flash writes enabled. - pub const FLASH_WE: u8 = 0x01; - /// Release the 1-Wire line (let it float high). - pub const DEASSERT: u8 = 0x04; - /// The 1-Wire data line itself. - pub const DATA: u8 = 0x08; -} - /// `com0`, the PROM's console UART. NetBSD attaches it at `mace0 offset /// 0x390000`; `com1` follows at 0x398000. /// The Ethernet, MAC-110 (`crate::dev::mec`), and its CRIME interrupt. @@ -129,7 +119,7 @@ pub use crate::dev::mace_ps2::*; pub use crate::dev::mace_sdma::*; pub use crate::dev::mace_ust::*; pub use crate::dev::ns16550::*; -pub use crate::dev::onewire::*; +pub use crate::dev::mace_id::*; pub const RAM_WINDOW_SIZE: u32 = 8 * 128 * 1024 * 1024; @@ -466,7 +456,7 @@ impl Ip32Bus { scsi: Mutex::new(Vec::new()), }; if let Some(mac) = opts.mac { - bus.mace.onewire = crate::dev::onewire::OneWireId::from_mac(mac); + bus.mace.id = crate::dev::mace_id::MaceId::new(mac); } if let Some(mouse) = opts.keyboard { // With a keyboard the PROM and IRIX take the graphics console; @@ -691,7 +681,7 @@ impl Ip32Bus { self.crime.regs.power_on(); let m = &self.mace; let devices: [&dyn Resettable; 8] = - [&*m.ust, &m.rtc, &m.ps2, &m.onewire, &m.com0, &m.com1, &m.sdma, &*m.isa]; + [&*m.ust, &m.rtc, &m.ps2, &m.id, &m.com0, &m.com1, &m.sdma, &*m.isa]; for d in devices { d.power_on(); } diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 14c7669b9..611f194f5 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -771,189 +771,127 @@ fn resetting_a_channel_clears_its_pointers_latches_and_level() { -/// The CRC is how firmware decides the chip is real. Maxim's published -/// example ROM is the standard check. -#[test] -fn the_dallas_crc_matches_the_published_example() { - assert_eq!(crc8_dallas(&[0x02, 0x1c, 0xb8, 0x01, 0x00, 0x00, 0x00]), 0xa2); -} - -#[test] -fn a_rom_built_from_a_mac_is_self_consistent() { - let ow = OneWireId::from_mac([0x08, 0x00, 0x69, 0x12, 0x34, 0x56]); - let rom = ow.rom(); - assert_eq!(rom[0], OW_FAMILY_DS2502, "family code must be the DS2502's"); - assert_eq!(&rom[1..7], &[0x08, 0x00, 0x69, 0x12, 0x34, 0x56]); - assert_eq!(rom[7], crc8_dallas(&rom[..7]), "CRC must cover the first seven bytes"); -} - -/// A reset long enough to qualify must be answered with a presence pulse — -/// the line pulled low — or the master concludes nothing is attached. -/// The pulse is a window in time, not a one-shot cleared by the first -/// read: the firmware polls it around 150 times, and a pulse that -/// vanished after the first look would be missed by every later one. -#[test] -fn a_reset_is_answered_with_a_presence_pulse_of_real_duration() { - let ow = OneWireId::from_mac([1, 2, 3, 4, 5, 6]); - ow.master_drive(0, true); - let released = OW_RESET_US + 100; - ow.master_drive(released, false); - - assert!(ow.line_level(released + 1), - "the device must not answer instantly; the master is still letting go"); - let inside = released + OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US / 2; - assert!(!ow.line_level(inside), "presence pulse must pull the line low"); - // Every sample inside the window sees it, however many there are. - for k in 0..20 { - let t = released + OW_PRESENCE_DELAY_US + k * (OW_PRESENCE_LEN_US / 25); - assert!(!ow.line_level(t), "presence pulse vanished at +{k}"); - } - let after = released + OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US + 1; - assert!(ow.line_level(after), "and the device must let go afterwards"); -} - -/// A low too short to be a reset is a write slot, and must not be -/// mistaken for one — otherwise every command byte restarts the device. -#[test] -fn a_short_low_is_not_a_reset() { - let ow = OneWireId::from_mac([1, 2, 3, 4, 5, 6]); - ow.master_drive(0, true); - let released = OW_RESET_US - 1; - ow.master_drive(released, false); - assert!(ow.line_level(released + OW_PRESENCE_DELAY_US + 1), - "a sub-reset low must not produce a presence pulse"); -} - -/// READ ROM, clocked in a bit at a time, must stream the ROM back LSB -/// first — that is what carries the MAC address to the firmware. -/// A tiny 1-Wire master, so the protocol tests read like the transaction -/// they are rather than a list of pulse widths. -struct OwMaster<'a> { - ow: &'a OneWireId, +/// A 1-wire master on the identity register's terms: it clears RELEASE to +/// hold the line low for a time and reads SAMPLE back, `t` standing in for +/// the guest clock in microseconds. +struct IdMaster<'a> { + id: &'a MaceId, t: u64, + /// How long a reset, a zero and a one hold the line low. + widths: (u64, u64, u64), } -impl<'a> OwMaster<'a> { - fn new(ow: &'a OneWireId) -> Self { - Self { ow, t: 0 } +impl<'a> IdMaster<'a> { + fn new(id: &'a MaceId) -> Self { + Self { id, t: 0, widths: (500, 90, 8) } } - fn reset(&mut self) { - self.ow.master_drive(self.t, true); - self.t += OW_RESET_US + 100; - self.ow.master_drive(self.t, false); - let pulse = self.t + OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US / 2; - assert!(!self.ow.line_level(pulse), "no presence pulse after a reset"); - self.t += OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US + 10; + fn pulse(&mut self, low: u64) { + self.id.write(self.t, 0); + self.t += low; + self.id.write(self.t, RELEASE); } - fn write_bit(&mut self, bit: bool) { - self.ow.master_drive(self.t, true); - self.t += if bit { 20 } else { OW_WRITE0_US + 200 }; - self.ow.master_drive(self.t, false); - self.t += 50; + fn reset(&mut self) { + self.t += 500; + self.pulse(self.widths.0); + assert_eq!(self.id.read(self.t + 80) & SAMPLE, 0, "no presence pulse after a reset"); + self.t += 160; } fn write_byte(&mut self, b: u8) { for i in 0..8 { - self.write_bit((b >> i) & 1 != 0); + self.pulse(if (b >> i) & 1 != 0 { self.widths.2 } else { self.widths.1 }); + self.t += 70; } } - fn read_bit(&mut self) -> bool { - self.ow.master_drive(self.t, true); - self.t += 20; - self.ow.master_drive(self.t, false); - self.t += 20; - let v = self.ow.line_level(self.t); - self.t += 50; - v + fn read_byte(&mut self) -> u8 { + (0..8).fold(0, |b, i| { + self.pulse(self.widths.2); + let bit = self.id.read(self.t + 1) & SAMPLE != 0; + self.t += 70; + b | (bit as u8) << i + }) } - fn read_byte(&mut self) -> u8 { - let mut b = 0u8; - for i in 0..8 { - if self.read_bit() { - b |= 1 << i; - } - } - b + fn read_bytes(&mut self, n: usize) -> Vec { + (0..n).map(|_| self.read_byte()).collect() } } -/// The whole transaction the firmware performs: identify the part, then -/// read its memory. Every byte of it is checked, because each one was a -/// separate gate — the family code, the address CRC, the data, and the -/// trailing CRC that nothing in the datasheet summary mentions. +const TEST_MAC: [u8; 6] = [0x08, 0x00, 0x69, 0x12, 0x34, 0x56]; + +/// The bytes the O2's Ethernet-address chip gives the PROM, recorded from +/// the model before it moved onto the shared bus, for the address 08:00:69:12:34:56. #[test] -fn the_firmwares_whole_ds2502_transaction_is_answered() { - let mac = [0x08u8, 0x00, 0x69, 0x12, 0x34, 0x56]; - let ow = OneWireId::from_mac(mac); - let mut m = OwMaster::new(&ow); +fn the_ethernet_address_chip_answers_with_the_recorded_bytes() { + let rom = [0x09, 0x08, 0x00, 0x69, 0x12, 0x34, 0x56, 0x7b]; + let mut memory = vec![0x8d, 0x56, 0x34, 0x12, 0x69, 0x00, 0x08]; + memory.extend([0xff; 122]); + memory.push(0x50); + // READ ROM, then straight on to READ MEMORY without a reset. + let id = MaceId::new(TEST_MAC); + let mut m = IdMaster::new(&id); m.reset(); - m.write_byte(onewire_cmd::READ_ROM); - let mut rom = [0u8; 8]; - for b in rom.iter_mut() { - *b = m.read_byte(); - } - assert_eq!(rom, ow.rom(), "READ ROM must stream the identity back"); + m.write_byte(0x33); + assert_eq!(m.read_bytes(8), rom); + m.write_byte(0xf0); + m.write_byte(0); + m.write_byte(0); + assert_eq!(m.read_bytes(130), memory); - // No reset in between: the firmware goes straight on to the memory - // read, and a device still stuck in "sending" would swallow it. - m.write_byte(onewire_cmd::READ_MEMORY); + // SKIP ROM gets the same memory. + let id = MaceId::new(TEST_MAC); + let mut m = IdMaster::new(&id); + m.reset(); + m.write_byte(0xcc); + m.write_byte(0xf0); m.write_byte(0); m.write_byte(0); - assert_eq!( - m.read_byte(), - crc8_dallas(&[onewire_cmd::READ_MEMORY, 0, 0]), - "the device must answer with a CRC over the command and address" - ); + assert_eq!(m.read_bytes(130), memory); - let data: Vec = (0..OneWireId::DATA_LEN).map(|_| m.read_byte()).collect(); - assert_eq!( - m.read_byte(), - crc8_dallas(&data), - "and a CRC over the data it just sent" - ); + // The firmware reverses the first six data bytes to get the address. + let eaddr: Vec = memory[1..7].iter().rev().copied().collect(); + assert_eq!(eaddr, TEST_MAC); +} - let eaddr: Vec = data[..6].iter().rev().copied().collect(); - assert_eq!(eaddr, mac, "the firmware reverses the first six bytes to get the MAC"); -} - -#[test] -fn read_rom_streams_the_identity_back() { - let mac = [0x08u8, 0x00, 0x69, 0x12, 0x34, 0x56]; - let ow = OneWireId::from_mac(mac); - let mut t = 0u64; - // Reset, then wait out the presence pulse. - ow.master_drive(t, true); - t += OW_RESET_US + 100; - ow.master_drive(t, false); - t += OW_PRESENCE_DELAY_US + OW_PRESENCE_LEN_US + 10; - // Clock in READ ROM, LSB first: a short low is a 1, a long low a 0. - for i in 0..8 { - let bit = (onewire_cmd::READ_ROM >> i) & 1 != 0; - ow.master_drive(t, true); - t += if bit { 5 } else { OW_WRITE0_US + 20 }; - ow.master_drive(t, false); - t += 10; - } - // Read 64 bits back. Each slot is the master pulsing the line low and - // then sampling what the device leaves on it. - let mut got = [0u8; 8]; - for i in 0..64 { - ow.master_drive(t, true); - t += 5; - ow.master_drive(t, false); - t += 5; - if ow.line_level(t) { - got[i / 8] |= 1 << (i % 8); - } - t += 10; - } - assert_eq!(got, ow.rom(), "the master must read back exactly the ROM"); - assert_eq!(&got[1..7], &mac, "and the MAC must survive the round trip"); +/// The PROM's delay loops run short when the emulator runs fast, so pulses +/// well under the datasheet's widths must still read as the same classes. +#[test] +fn short_pulses_still_read_the_rom() { + let id = MaceId::new(TEST_MAC); + let mut m = IdMaster::new(&id); + m.widths = (320, 35, 3); + m.reset(); + m.write_byte(0x33); + assert_eq!(m.read_bytes(8), [0x09, 0x08, 0x00, 0x69, 0x12, 0x34, 0x56, 0x7b]); +} + +/// A low shorter than a reset is a slot, not a reset: no presence pulse. +#[test] +fn a_short_low_is_not_a_reset() { + let id = MaceId::new(TEST_MAC); + let mut m = IdMaster::new(&id); + m.reset(); + m.t += 500; + m.pulse(200); + assert_ne!(id.read(m.t + 80) & SAMPLE, 0); +} + +/// The register survives a save, and a cold reset puts it back. +#[test] +fn the_register_survives_save_and_load_and_resets() { + use crate::traits::{Resettable, Saveable}; + let id = MaceId::new(TEST_MAC); + id.write(0, FLASH_ENABLE | RELEASE | 0x10); + let v = id.save_state(); + let restored = MaceId::new(TEST_MAC); + restored.load_state(&v).expect("load_state"); + assert_eq!(restored.read(1) & 0x3f, FLASH_ENABLE | RELEASE | 0x10 | SAMPLE); + restored.power_on(); + assert_eq!(restored.read(2), 0x20); } #[test] diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index 0ee302c62..de8abdecf 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -1088,7 +1088,7 @@ impl Device for O2Control { fn register_commands(&self) -> Vec<(String, String)> { vec![("o2".to_string(), "O2 control: o2 shot PATH | status | pixel X Y | type TEXT | key|keydown|keyup NAME | \ mouse DX DY [BUTTONS] | click [BUTTON] | pc | regs | peek ADDR LEN PATH | \ - retrace PATH | scsi PATH | unmapped | ps2log PATH | crimelog PATH | nictrace PATH".to_string())] + retrace PATH | scsi PATH | unmapped | ps2log PATH | crimelog PATH".to_string())] } fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { @@ -1141,16 +1141,6 @@ fn control_command(bus: &Ip32Bus, cmd: &str, arg: &str, rgba: &mut [u8]) -> Stri ), // `crimelog PATH`: the guest's recent CRIME interrupt-register // accesses, oldest first. - "nictrace" => { - let mut t = bus.mace.nic_trace.text(); - for l in bus.mace.onewire.protocol_log() { - t.push_str(&format!("# {l}\n")); - } - match std::fs::write(arg.trim(), t) { - Ok(()) => "ok".into(), - Err(e) => format!("err {e}"), - } - } "crimelog" => match std::fs::write(arg.trim(), bus.crime.regs.irqlog_text()) { Ok(()) => "ok".into(), Err(e) => format!("err {e}"), From 17b882e8a956acf431be2ba9d79008dcc65dc81d Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 01:29:42 -0700 Subject: [PATCH 2/3] ip32: show the O2's front LEDs in the status bar The identity register already holds the red and green LED bits. Have the device set or clear the status bar's persistent red and green LED bits on every write, reset and load, and give the O2 display that heartbeat instead of the zero it passed. An LED lights when its bit is clear: the O2 register description in docs/datasheets/o2-prom-probe-registers.md says the register resets with green disabled and that writing zero enables both outputs. That is the datasheet's reading, not a hardware measurement. Observed in a boot to login: the PROM writes 0x00 (both lit), pulses flash enable, then writes 0x3c and straight away 0x1c (red dark, green lit). IRIX only toggles flash enable afterwards, to write the environment, so a booted machine shows green. The status bar itself could not be checked: the screenshot command captures a REX3 framebuffer the O2 does not have. --- CHANGELOG.md | 1 + src/dev/mace_id.rs | 47 ++++++++++++++++++++++++++++++++++++--------- src/ip32/tests.rs | 16 +++++++++++++++ src/ip32_machine.rs | 2 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0bdcca17..a7b65dd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -264,6 +264,7 @@ hashes are given where a change is easiest to understand by reading the commit. service passes. - **The O2's Ethernet-address chip is on the shared 1-wire bus** (`dev::mace_id`, ported from the QEMU model); the MAC reads correctly at any emulator speed. +- The O2's front LEDs show in the status bar. - **The O2 is on `Physical`.** Its bus is the same decode table and memory window as the Indy's: CRIME's banks are RAM banks a bank-control write remaps, CRIME, GBE, VICE, MACE and the flash are table entries, and diff --git a/src/dev/mace_id.rs b/src/dev/mace_id.rs index e6886e6bf..cea54a8c3 100644 --- a/src/dev/mace_id.rs +++ b/src/dev/mace_id.rs @@ -17,7 +17,10 @@ //! //! Ported from hw/mips/sgi/mace-id.c in jamesbraid/qemu-sgi-origami. -use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use crate::dev::ng1::rex3::Rex3; use crate::dev::w1::{self, sgi}; use crate::snapshot::{get_field, toml_u64}; @@ -73,6 +76,9 @@ struct State { pub struct MaceId { st: Mutex, + /// The LEDs as the status bar shows them: `Rex3::HB_LED_RED` and + /// `HB_LED_GREEN`. + pub heartbeat: Arc, } impl State { @@ -150,7 +156,9 @@ impl MaceId { let mut st = State::default(); st.bus.add(Box::new(sgi::o2_ethernet_address_part(mac))); st.reset(); - Self { st: Mutex::new(st) } + let id = Self { st: Mutex::new(st), heartbeat: Arc::new(AtomicU64::new(0)) }; + id.show_leds(GREEN_LED); + id } /// The register at guest time `now` (microseconds). The password-clear @@ -160,7 +168,24 @@ impl MaceId { } pub fn write(&self, now: u64, value: u8) { - self.st.lock().unwrap().write(now, value & WRITABLE); + let led = { + let mut st = self.st.lock().unwrap(); + st.write(now, value & WRITABLE); + st.led_control + }; + self.show_leds(led); + } + + /// An LED lights when its bit is clear: writing zero enables both + /// outputs, and the register resets with only the green one disabled. + fn show_leds(&self, bits: u8) { + for (bit, led) in [(RED_LED, Rex3::HB_LED_RED), (GREEN_LED, Rex3::HB_LED_GREEN)] { + if bits & bit == 0 { + self.heartbeat.fetch_or(led, Ordering::Relaxed); + } else { + self.heartbeat.fetch_and(!led, Ordering::Relaxed); + } + } } } @@ -204,12 +229,15 @@ impl crate::traits::Saveable for MaceId { fn load_state(&self, v: &toml::Value) -> Result<(), String> { let reg = get_field(v, "register").and_then(toml_u64).unwrap_or(0) as u8; - let mut st = self.st.lock().unwrap(); - st.reset(); - st.start = Start::Done; - st.released = reg & RELEASE != 0; - st.flash_write_enabled = reg & FLASH_ENABLE != 0; - st.led_control = reg & (RED_LED | GREEN_LED); + { + let mut st = self.st.lock().unwrap(); + st.reset(); + st.start = Start::Done; + st.released = reg & RELEASE != 0; + st.flash_write_enabled = reg & FLASH_ENABLE != 0; + st.led_control = reg & (RED_LED | GREEN_LED); + } + self.show_leds(reg & (RED_LED | GREEN_LED)); Ok(()) } } @@ -217,5 +245,6 @@ impl crate::traits::Saveable for MaceId { impl crate::traits::Resettable for MaceId { fn power_on(&self) { self.st.lock().unwrap().reset(); + self.show_leds(GREEN_LED); } } diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 611f194f5..9cf86c727 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -880,6 +880,22 @@ fn a_short_low_is_not_a_reset() { assert_ne!(id.read(m.t + 80) & SAMPLE, 0); } +/// Writing zero lights both LEDs; the register resets with green dark, and +/// the status bar's bits follow. +#[test] +fn the_front_leds_follow_the_register() { + use crate::dev::ng1::rex3::Rex3; + use std::sync::atomic::Ordering::Relaxed; + let id = MaceId::new(TEST_MAC); + let hb = || id.heartbeat.load(Relaxed); + assert_eq!(hb(), Rex3::HB_LED_RED); + id.write(0, 0); + assert_eq!(hb(), Rex3::HB_LED_RED | Rex3::HB_LED_GREEN); + id.write(1, 0x30); + assert_eq!(hb(), 0); + assert_eq!(id.read(2) & 0x30, 0x30); +} + /// The register survives a save, and a cold reset puts it back. #[test] fn the_register_survives_save_and_load_and_resets() { diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index de8abdecf..d31a85635 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -985,7 +985,7 @@ impl O2Display { let take_shot = self.screenshot_pending.swap(false, Ordering::Relaxed); let stats = crate::disp::BarStats { now: start, - hb: 0, + hb: self.bus.mace.id.heartbeat.fetch_and(crate::dev::ng1::rex3::Rex3::HB_PERSISTENT, Ordering::Relaxed), cycles: self.cycles.get(), fasttick: self.ticks.load(Ordering::Relaxed), decoded_delta: 0, From b4844c04ccda6dc09f553e4db4956fd69426a5bb Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 02:38:51 -0700 Subject: [PATCH 3/3] ip32: light the status bar's SCSI and network indicators The O2 display now reads the shared activity word for the front LEDs, but the SCSI and Ethernet parts never set their bits, so those lights stayed dark. Give the word to both AIC-7880 controllers and the MEC, as the Octane does for its controllers and Ethernet. 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. --- CHANGELOG.md | 1 + src/dev/aic7880.rs | 14 +++++++++++++- src/dev/mec.rs | 19 +++++++++++++++++-- src/ip32.rs | 4 ++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b65dd13..65c9d53dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,6 +265,7 @@ hashes are given where a change is easiest to understand by reading the commit. - **The O2's Ethernet-address chip is on the shared 1-wire bus** (`dev::mace_id`, ported from the QEMU model); the MAC reads correctly at any emulator speed. - The O2's front LEDs show in the status bar. +- The O2's SCSI and Ethernet lights show in the status bar. - **The O2 is on `Physical`.** Its bus is the same decode table and memory window as the Indy's: CRIME's banks are RAM banks a bank-control write remaps, CRIME, GBE, VICE, MACE and the flash are table entries, and diff --git a/src/dev/aic7880.rs b/src/dev/aic7880.rs index 769560f95..ce858eb9b 100644 --- a/src/dev/aic7880.rs +++ b/src/dev/aic7880.rs @@ -28,7 +28,7 @@ use crate::locks::{SpinGuard, SpinLock}; use crate::scsi::{scsi_cmd, ScsiBus, ScsiDataLength, ScsiIdentities, ScsiRequest, CDROM_IDENTITY, DISK_IDENTITY}; use std::collections::VecDeque; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, Weak}; use crate::snapshot::{get_field, hex_u16, hex_u32, hex_u64, hex_u8, load_u8_slice, toml_u16, toml_u32, toml_u64, toml_u8, u8_slice_to_toml}; @@ -427,6 +427,8 @@ pub struct Aic7880 { dma: std::sync::OnceLock>, /// The SCSI id a disk attached without one answers at. target: u8, + /// The status bar's activity bits, shared with the display thread. + heartbeat: std::sync::OnceLock>, /// Which of the O2's two controllers this is (0 internal, 1 external): /// the monitor's `scsi`/`cow` address the first, `scsi1`/`cow1` the second. index: u8, @@ -486,9 +488,15 @@ impl Aic7880 { [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA) }, target: 1, + heartbeat: std::sync::OnceLock::new(), }) } + /// Light the status bar's SCSI activity bits in `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: Arc) { + let _ = self.heartbeat.set(heartbeat); + } + /// `n` bytes of host memory from bus address `addr`, through the bridge. /// Its two views reach the same bytes, and the driver uses both in the /// same breath: the SCB's own address is given as `0x410752fc` while the @@ -839,6 +847,10 @@ impl Aic7880 { /// the controller's own sense, if it holds one. fn request(&self, target: u8, cdb: &[u8], data_in: Option<&[u8]>) -> (Vec, u8) { const CHECK_CONDITION: u8 = 0x02; + // 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); + } if cdb[0] == scsi_cmd::REQUEST_SENSE { if let Some((key, asc, ascq)) = self.sense.lock().unwrap().take() { let mut d = vec![0u8; 18]; diff --git a/src/dev/mec.rs b/src/dev/mec.rs index 5d1bce855..b2bed94d6 100644 --- a/src/dev/mec.rs +++ b/src/dev/mec.rs @@ -26,7 +26,7 @@ //! Indy's SEEQ 8003 meets IRIS's NAT gateway: `start_nat` runs one. use std::collections::VecDeque; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use crate::snapshot::{get_field, hex_u32, hex_u64, load_u16_slice, toml_u32, toml_u64, u16_slice_to_toml}; @@ -241,11 +241,24 @@ pub struct Mec { link: Mutex>, /// CRIME's Ethernet input. irq: std::sync::OnceLock, + /// The status bar's activity bits, shared with the display thread. + heartbeat: std::sync::OnceLock>, } impl Mec { pub fn new(ram: Arc) -> Self { - Self { ram, st: Mutex::new(State::new()), link: Mutex::new(None), irq: std::sync::OnceLock::new() } + Self { ram, st: Mutex::new(State::new()), link: Mutex::new(None), irq: std::sync::OnceLock::new(), heartbeat: std::sync::OnceLock::new() } + } + + /// 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); + } } /// Wire the chip's interrupt to `line`. @@ -531,6 +544,7 @@ impl Mec { if frames.is_empty() { return; } + self.blink(crate::dev::ng1::rex3::Rex3::HB_ENET_TX); if let Some(link) = self.link.lock().unwrap().as_mut() { for f in frames { Self::trace("tx", &f); @@ -641,6 +655,7 @@ impl Mec { self.ram_write(u64::from(buf), &stat.to_be_bytes()); s.int_status |= bits::INT_RX_THRESHOLD; s.rx_frames += 1; + self.blink(crate::dev::ng1::rex3::Rex3::HB_ENET_RX); } } } diff --git a/src/ip32.rs b/src/ip32.rs index dc9891069..054944406 100644 --- a/src/ip32.rs +++ b/src/ip32.rs @@ -458,6 +458,8 @@ impl Ip32Bus { if let Some(mac) = opts.mac { bus.mace.id = crate::dev::mace_id::MaceId::new(mac); } + // The status bar's lights, from the register's heartbeat. + bus.mace.mec.set_heartbeat(bus.mace.id.heartbeat.clone()); if let Some(mouse) = opts.keyboard { // With a keyboard the PROM and IRIX take the graphics console; // without one, the serial console. The graphics hardware is @@ -473,6 +475,8 @@ impl Ip32Bus { // anything is plugged into it, and a kernel that expects two // waits on the one that is missing. let scsi_b = crate::dev::aic7880::Aic7880::new_at(1, bus_b); + scsi.set_heartbeat(bus.mace.id.heartbeat.clone()); + scsi_b.set_heartbeat(bus.mace.id.heartbeat.clone()); bus.mace.pci.attach(IP32_SCSI_SLOT_B, "aic7880-b", scsi_b.clone()); bus.mace.pci.attach(IP32_SCSI_SLOT, "aic7880", scsi.clone()); let mut list = bus.scsi.lock().unwrap();