diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ecf8f26..65c9d53dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -262,6 +262,10 @@ 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'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/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/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/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..cea54a8c3 --- /dev/null +++ b/src/dev/mace_id.rs @@ -0,0 +1,250 @@ +//! 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::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}; + +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, + /// The LEDs as the status bar shows them: `Rex3::HB_LED_RED` and + /// `HB_LED_GREEN`. + pub heartbeat: Arc, +} + +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(); + 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 + /// 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) { + 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); + } + } + } +} + +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); + } + self.show_leds(reg & (RED_LED | GREEN_LED)); + Ok(()) + } +} + +impl crate::traits::Resettable for MaceId { + fn power_on(&self) { + self.st.lock().unwrap().reset(); + self.show_leds(GREEN_LED); + } +} 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/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..054944406 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,8 +456,10 @@ 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); } + // 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 @@ -483,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(); @@ -691,7 +685,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..9cf86c727 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -771,189 +771,143 @@ 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); +} + +/// 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() { + 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..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, @@ -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}"),