diff --git a/CHANGELOG.md b/CHANGELOG.md index e73a77b8d..98c44d12b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,26 @@ hashes are given where a change is easiest to understand by reading the commit. ## November 2026 +### O2 PCI add-in cards + +- The O2's one add-in slot (PCI device 3) takes a card from the configuration: + `[[pci]]` with `slot`, `card` and the card's options inline, or `--pci + [BUS:]SLOT=CARD[,KEY=VALUE]...`. Only a real add-in slot can be named; the + onboard AIC-7880s cannot. A mistake stops iris before it starts and the + message names the entry. The slot's INTA reaches CRIME input 10 and INTB to INTD + the shared inputs 13 to 15, as the BSDs and Linux route them. +- Two cards: `aic7880` (the onboard Adaptec chip, one SCSI bus) and + `isp12160` (QLogic, two). A card's buses are the SCSI controllers after the + onboard two, for `[scsi.N]` (key 16×controller+id, ids 1–15). IRIX 6.5.30 + drives the `aic7880` card and `hinv` lists it with its disk; it finds the + `isp12160` but has no driver for it. +- The monitor's `pci slots` lists the bus's devices, onboard or add-in, with + what is fitted and where each interrupt pin goes. +- Snapshots record the fitted cards and refuse a restore into a machine with + others. Snapshots without cards are unchanged. +- The Octane's Bridge has a slot table with its four onboard devices and no + add-in slot: its PCI boards are not modelled. + ### O2 graphics tracing - `re trace FILE [re,cpu,gbe,crime,irq|all]` (also `gbe trace`, `crime trace`, @@ -203,6 +223,13 @@ hashes are given where a change is easiest to understand by reading the commit. - Memory past 4 GB is as fast as the rest: such a machine gets a 16 GB direct window, and the JIT compiles code anywhere in it. - A store-conditional on memory outside the window is now atomic with two CPUs. +### PCI configuration + +- A PCI function can describe its own writable header fields and + write-one-to-clear status bits. Byte and halfword configuration writes + through a Bridge carry their byte enables, so such a function keeps the + status events a narrow write did not select. No Octane function has + write-one-to-clear status bits, so the Octane behaves as before. ### VNC, and screen and pointer commands diff --git a/FEATURES.md b/FEATURES.md index 487c73b05..3690e3bc5 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -236,6 +236,21 @@ Omit `[nfs]` to disable the share; the default forward list is empty. | `port_forward[].guest_port` | `required` | Guest destination port. | | `port_forward[].bind` | `localhost` | localhost or any; any listens on all interfaces. | +### [[pci]] + +PCI add-in cards, one table per card. Only a real add-in slot can be named: the onboard devices are fixed. +Today that is the O2's one slot (PCI device 3, bus `pci`); the Octane's PCI boards are not modelled. +The monitor's `pci slots` lists a machine's slots. + +| Key | Default | Values and behavior | +|---|---|---| +| `pci[].slot` | `required` | The PCI device number (`3`, a number, not text), or the label printed on the chassis as text (`"left"`) where the slot has one. Labels work in the configuration file only; `--pci` takes device numbers. The O2's slot has no label. | +| `pci[].card` | `required` | `aic7880`: Adaptec AIC-7880 SCSI, one bus. `isp12160`: dual-channel QLogic ISP12160 SCSI, two buses. A card's buses are the SCSI controllers after the machine's own (2, then 3 on the O2; the next card's follow), for `[scsi.N]` keys 16×controller+id, ids 1–15. IRIX 6.5.30 on the O2 drives the `aic7880` card and lists its disks in `hinv`; it finds the `isp12160` on the bus but has no driver for it. | +| `pci[].bus` | the machine's only bus | The PCI bus id (`pci` on the O2). Needed only where a machine has several. | +| `pci[].` | none | The card's own options, inline. `isp12160` takes none. | + +Several cards of one kind are allowed where there are slots for them. A mistake (an onboard device, an unknown slot, a slot used twice, an unknown card or option) stops iris before it starts, with a message naming the entry. A snapshot records the fitted cards and loads only into a machine with the same ones. + ### [vino] | Key | Default | Values and behavior | @@ -427,6 +442,7 @@ iris-gui does not parse this standalone option set; use its named-machine UI. | `--list-net-interfaces` | Print the host network interfaces libpcap can bridge onto, then exit. Requires a build with --features pcap. | | `--no-scsi-deferred-int` | Disable deferred SCSI status interrupts (default: enabled for OpenBSD/NetBSD compatibility). | | `--cpu` | Emulated CPU: `r4400` (default), `r5000`, or `r10000`. Overrides `[machine] cpu`. | +| `--pci` | Fit a PCI card: `[BUS:]SLOT=CARD[,KEY=VALUE]...`, for example `--pci 3=aic7880` on the O2. Repeatable; replaces a `[[pci]]` entry for the same slot. | | `--graphics` | Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`), `v6`, `v8`, `v10`, `v12` (`vpro:v12` and so on, Octane only). | | `--gdb-port` | Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). Connect with: target remote localhost: | | `--monitor-port` | Monitor console port on 127.0.0.1 (default 8888). Give each iris running at once its own. | diff --git a/HACKING.md b/HACKING.md index e62f2ecae..3fbb9c9c6 100644 --- a/HACKING.md +++ b/HACKING.md @@ -97,6 +97,12 @@ way: `scsi::attach_from_config` walks the `[scsi.N]` tables for every machine, where the key is a slot (controller × 16 + id, see `config::scsi_slot`), and the machine supplies only a route from controller number to bus. +`ScsiTarget::bus_reset` defaults to no action. Disk targets record reset as +unit attention. Only the ISP12160 calls it; the other controllers keep +their earlier behaviour of not reporting resets to targets. +The ISP12160 releases its controller state lock before walking either bus. +Target callbacks run under that bus lock and must not reacquire it. + ## 2. Data path and endianness **Word-transparent architecture.** Host `u32`/`u64` values are bit diff --git a/HELP.md b/HELP.md index 8b2494b9c..58f60e5f6 100644 --- a/HELP.md +++ b/HELP.md @@ -656,6 +656,15 @@ path = "cdrom4.iso" cdrom = true # discs = ["irix65.iso", "extras.iso", "patches.iso"] # changer; "scsi eject 4" cycles +# PCI add-in cards. The O2 has one slot, PCI device 3. An "aic7880" card adds one SCSI +# bus, controller 2: [scsi.33] is id 1 on it. An "isp12160" adds two (controllers 2 and 3; +# IRIX on the O2 has no driver for it). The monitor's `pci slots` lists the slots. +# [[pci]] +# slot = 3 +# card = "aic7880" +# [scsi.33] +# path = "card-disk.raw" + # Scratch volume for file injection without networking (see README). # [scsi.2] # path = "scratch.raw" @@ -786,6 +795,7 @@ Configuration --nveeprom Indigo2 NVRAM EEPROM file (default: nveeprom.bin) --machine Machine to emulate (a [machine] profile name); `help` lists them --ip22 Same as --machine indigo2_ip22 + --pci <[BUS:]SLOT=CARD[,K=V]...> Fit a PCI add-in card, e.g. 3=aic7880 on the O2 (repeatable) --graphics newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12 --cpu r4400 (default), r5000, or r10000 --bank0..--bank3 RAM bank sizes (0/8/16/32/64/128; IP28: also 256/512) diff --git a/debug.md b/debug.md index 3f2dfb5fd..63ebcc836 100644 --- a/debug.md +++ b/debug.md @@ -579,7 +579,7 @@ its SCSI controllers answer to `scsi`/`cow` and `scsi1`/`cow1` as described unde | `re dither [on\|off]` | Show or set whether the engine dithers, to compare renderings; on by default | | `gbe status` | GBE display engine: timing, frame buffer, tiles | | `isa status` | MACE ISA interrupts | -| `pci status` / `pci config ` | MACE PCI bridge / a function's configuration space | +| `pci status` / `pci slots` / `pci config ` | MACE PCI bridge / each device number the bus table knows, onboard or add-in, with what is in it and where its interrupt pins go / a function's configuration space | | `mec status` | MACE Ethernet: rings, receive buffers, frame counts | | `sdma status` | MACE serial DMA | | `maceps2 status` / `maceps2 traffic` | PS/2 ports / recent bytes each way | diff --git a/docs/o2-ip32.md b/docs/o2-ip32.md index a1ee2daf5..3072bd6a7 100644 --- a/docs/o2-ip32.md +++ b/docs/o2-ip32.md @@ -107,6 +107,22 @@ overlay } ]`. That key is deprecated: iris folds it into `[scsi.N]` (with `controller = bus`) when it loads the file and says so; move the entries over to silence the note. +## PCI add-in cards + +The O2 has one add-in slot, PCI device 3 on the MACE bus, and `[[pci]]` (or +`--pci 3=aic7880`) fits a card in it ([FEATURES.md](../FEATURES.md#pci)). The +slot's INTA reaches CRIME input 10 and INTB to INTD reach the shared inputs +13 to 15, as the MACE specification has them (and Linux, OpenBSD and NetBSD route them); what the board's connector +wires INTB to INTD to is not known. Devices 1 and 2 are the onboard AIC-7880s and cannot be replaced. The +cards are the Adaptec AIC-7880 (`aic7880`, one SCSI bus) and the QLogic +ISP12160 (`isp12160`, two). A card's buses are SCSI controllers 2 onwards: put +disks on them with `[scsi.N]` keys 16×controller+id. IRIX 6.5.30 drives the +`aic7880` card, and `hinv` lists "PCI SCSI controller 2" with its disk; it finds +the `isp12160` on the bus ("PCI Adapter ID ... PCI slot 3" in `hinv -v`) but +has no driver for it. A card's buses have no monitor `scsi` command yet; set +disks in the configuration. +`pci slots` in the monitor shows the slot and what is in it. + ## Snapshots The monitor's `save NAME` and `load NAME`, and iris-ci's `save` and @@ -114,7 +130,7 @@ The monitor's `save NAME` and `load NAME`, and iris-ci's `save` and CPU, every device, all of memory (in the chunk store the snapshots share, so memory that did not change costs nothing), the flash, and the changes on `overlay` disks. A snapshot loads only into an O2 with the same CPU, memory, -disks and build. Use `overlay` disks for snapshots you mean to go back to: a +disks, PCI cards and build. Use `overlay` disks for snapshots you mean to go back to: a disk written in place keeps changing after the save, and the saved memory will not match it. diff --git a/src/config.rs b/src/config.rs index 1ddc8fce1..e22e44043 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,8 @@ use clap::{CommandFactory, FromArgMatches, Parser}; use serde::{Deserialize, Serialize}; use std::net::Ipv4Addr; +use crate::pci_addin::PciEntry; + /// Every memory bank size in MB, for the front-ends' menus; `bank_sizes` says /// which of them a machine takes. pub const VALID_BANK_SIZES: &[u32] = &[0, 8, 16, 32, 64, 128, 256, 512, 1024, 2048]; @@ -1819,6 +1821,13 @@ pub struct MachineConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub port_forward: Vec, + /// PCI add-in cards, one `[[pci]]` table per card: `slot` (a PCI device + /// number or the chassis label), `card`, `bus` where the machine has + /// several, and the card's own options. Only a real add-in slot can be + /// named; the onboard devices are not configurable. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub pci: Vec, + /// VINO video-in configuration (IndyCam emulation source). #[serde(default)] pub vino: VinoConfig, @@ -2054,6 +2063,7 @@ impl Default for MachineConfig { scale: default_scale(), nfs: None, port_forward: vec![], + pci: vec![], headless: false, no_audio: false, gdb_port: None, @@ -2326,13 +2336,18 @@ impl MachineConfig { return Err("graphics.resolution presets require Newport (graphics.board = newport)".into()); } } + // The cards' SCSI buses come after the machine's own controllers. + let cards = crate::pci_addin::validate_pci(self.machine.profile, &self.pci)?; + let own = self.machine.profile.scsi_controllers(); + let controllers = cards.iter().fold(own, |n, c| n.saturating_add(c.card.scsi_controllers)); let mut placed: std::collections::HashMap<(u8, u8), u8> = std::collections::HashMap::new(); let mut keys: Vec = self.scsi.keys().copied().collect(); keys.sort(); for key in keys { let dev = &self.scsi[&key]; let (controller, id) = dev.placement(key); - let max_id = self.machine.profile.max_scsi_id(); + // A card's buses are wide, whatever the machine's own are. + let max_id = if controller >= own.max(2) { 15 } else { self.machine.profile.max_scsi_id() }; if id == 0 || id > max_id { return Err(format!("scsi.{}: SCSI ID {} is out of range (1–{}; the controller itself is 0)", key, id, max_id)); } @@ -2342,8 +2357,9 @@ impl MachineConfig { key, controller, dev.controller )); } - if controller > 1 { - return Err(format!("scsi.{}: controller {} is invalid (valid: 0, 1)", key, controller)); + if controller >= controllers.max(2) { + let valid = if controllers <= 2 { "0, 1".to_string() } else { format!("0 to {}", controllers - 1) }; + return Err(format!("scsi.{}: controller {} is invalid (valid: {valid})", key, controller)); } if controller != 0 && self.machine.profile.scsi_controllers() < 2 { return Err(format!( @@ -2730,6 +2746,13 @@ pub struct Cli { #[arg(long = "cache", value_name = "MODEL")] pub cache: Option, + /// Fit a PCI add-in card: `[BUS:]SLOT=CARD[,KEY=VALUE]...`, for example + /// `--pci 3=aic7880` on the O2. SLOT is a PCI device number (a chassis + /// label works in the configuration file only). Repeat it for more cards; it replaces a `[[pci]]` entry for the + /// same slot. The monitor's `pci slots` lists a machine's slots. + #[arg(long = "pci", value_name = "[BUS:]SLOT=CARD[,K=V]...", value_parser = PciEntry::parse_cli)] + pub pci: Vec, + /// Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`). #[arg(long = "graphics", visible_alias = "board", visible_alias = "graphics-board", value_name = "BOARD")] pub graphics: Option, @@ -2909,6 +2932,7 @@ impl Cli { if let Some(cpu) = self.cpu { cfg.machine.cpu = cpu; } if let Some(cache) = self.cache { cfg.machine.cache = cache; } if let Some(b) = self.graphics { cfg.graphics.board = Some(b); } + crate::pci_addin::merge_cli(cfg.machine.profile, &mut cfg.pci, &self.pci); if self.scale2x { cfg.scale = 2; } if self.headless { cfg.headless = true; } if self.no_audio { cfg.no_audio = true; } @@ -3881,3 +3905,77 @@ mod ci_overlay_tests { assert!(a.contains(&format!("iris-ci-{}-", std::process::id())), "{a}"); } } + +#[cfg(test)] +mod pci_tests { + use super::*; + + /// `[[pci]]` reads and writes back inline, the card's own keys beside + /// `slot` and `card`, and a chassis label is as good as a device number. + #[test] + fn pci_entries_round_trip_through_toml() { + let text = "[machine]\nprofile = \"o2_ip32\"\ncpu = \"r10000\"\n\n[[pci]]\nslot = 3\ncard = \"isp12160\"\n"; + let cfg: MachineConfig = toml::from_str(text).unwrap(); + assert_eq!(cfg.pci.len(), 1); + assert_eq!((cfg.pci[0].slot.clone(), cfg.pci[0].card.as_str()), (crate::pci_addin::SlotRef::Device(3), "isp12160")); + cfg.validate().expect("the O2's slot takes the card"); + let back: MachineConfig = toml::from_str(&toml::to_string(&cfg).unwrap()).unwrap(); + assert_eq!(back.pci, cfg.pci); + let labelled: MachineConfig = toml::from_str("[[pci]]\nslot = \"left\"\ncard = \"isp12160\"\nbus = \"pci\"\nspeed = 2\n").unwrap(); + assert_eq!(labelled.pci[0].slot, crate::pci_addin::SlotRef::Label("left".into())); + assert_eq!(labelled.pci[0].options["speed"], toml::Value::Integer(2), "other keys are the card's options"); + assert_eq!(MachineConfig::default().pci.len(), 0); + assert!(!toml::to_string(&MachineConfig::default()).unwrap().contains("pci"), "an empty list is not written"); + } + + /// A bad card entry stops `validate` with the entry's own message, and + /// a machine with no PCI refuses any. + #[test] + fn validate_reports_a_bad_pci_entry() { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::O2Ip32; + cfg.machine.cpu = CpuModel::R10000; + cfg.pci.push(PciEntry::parse_cli("2=isp12160").unwrap()); + assert!(cfg.validate().unwrap_err().contains("is the onboard external SCSI controller"), "slot 2 is the second AIC-7880"); + cfg.pci[0] = PciEntry::parse_cli("3=isp12160").unwrap(); + cfg.validate().unwrap(); + cfg.machine.profile = MachineProfile::IndyIp24; + cfg.machine.cpu = CpuModel::default(); + assert_eq!(cfg.validate().unwrap_err(), "pci[0]: the Indy has no PCI bus."); + } + + /// The card's two SCSI buses are controllers 2 and 3, and their targets + /// may use the wide bus's ids. + #[test] + fn a_card_adds_scsi_controllers_to_the_machines_own() { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::O2Ip32; + cfg.machine.cpu = CpuModel::R10000; + cfg.scsi.insert(2 * 16 + 9, ScsiDeviceConfig { path: "card.raw".into(), ..Default::default() }); + assert!(cfg.validate().unwrap_err().contains("controller 2 is invalid (valid: 0, 1)"), "no card, no controller 2"); + cfg.pci.push(PciEntry::parse_cli("3=isp12160").unwrap()); + cfg.validate().expect("controller 2, id 9: the card's first bus"); + cfg.scsi.insert(3 * 16 + 1, ScsiDeviceConfig { path: "card2.raw".into(), ..Default::default() }); + cfg.validate().expect("controller 3: the card's second bus"); + cfg.scsi.insert(4 * 16 + 1, ScsiDeviceConfig { path: "x.raw".into(), ..Default::default() }); + assert!(cfg.validate().unwrap_err().contains("controller 4 is invalid (valid: 0 to 3)")); + cfg.scsi.remove(&(4 * 16 + 1)); + cfg.scsi.insert(1, ScsiDeviceConfig { path: "onboard.raw".into(), ..Default::default() }); + cfg.scsi.insert(8, ScsiDeviceConfig { path: "onboard8.raw".into(), ..Default::default() }); + assert!(cfg.validate().unwrap_err().contains("SCSI ID 8 is out of range (1–7"), "the O2's own buses stay narrow"); + } + + #[test] + fn the_pci_option_parses_and_replaces_a_config_entry() { + let cli = Cli::try_parse_from(["iris", "--pci", "pci:3=isp12160", "--pci", "4=isp12160"]).unwrap(); + assert_eq!(cli.pci.len(), 2); + assert!(Cli::try_parse_from(["iris", "--pci", "nonsense"]).is_err()); + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::O2Ip32; + cfg.pci.push(PciEntry::parse_cli("3=isp12160").unwrap()); + let cfg = cli.apply(cfg); + assert_eq!(cfg.pci.len(), 2, "slot 3 replaced, 4 appended"); + assert_eq!(cfg.pci[0].bus.as_deref(), Some("pci")); + } + +} diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index 41fcf4fa9..ac92c6a91 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -553,12 +553,10 @@ impl Bridge { pub fn attach(self: &Arc, slot: u8, dev: Arc) { let slot = slot as usize; assert!(slot < DEVICES, "Bridge has {DEVICES} slots"); - dev.connect_dma(Arc::new(SlotDma { bridge: Arc::downgrade(self), slot })); - let pin = dev.config().interrupt_pin(); - if (1..=4).contains(&pin) { + crate::pci::plug(&*dev, Arc::new(SlotDma { bridge: Arc::downgrade(self), slot }), |pin| { let router = Arc::new(PciPin { bridge: Arc::downgrade(self) }); - dev.connect_interrupt(IrqLine::new(router, (slot * 4 + pin as usize - 1) as u32)); - } + IrqLine::new(router, (slot * 4 + pin as usize - 1) as u32) + }); self.slots.write()[slot] = Some(dev); self.publish_bars(); } @@ -1057,7 +1055,7 @@ impl Bridge { let i = (reg >> 2) as usize; let shift = 8 * (reg & 3); let mask = (u64::MAX >> (64 - 8 * size)) << shift; - cfg.write(i, ((cfg.read(i) as u64 & !mask) | ((val << shift) & mask)) as u32); + cfg.write_masked(i, (val << shift) as u32, mask as u32); true } diff --git a/src/dev/isp1040.rs b/src/dev/isp1040.rs index 1dc60951f..ae655f888 100644 --- a/src/dev/isp1040.rs +++ b/src/dev/isp1040.rs @@ -50,8 +50,9 @@ //! again when the hold ends: an approximation, a chip that had fetched it //! would plausibly move on. A parked entry the guest rewrites meanwhile is //! answered incomplete. A snapshot carries each bus's remaining hold and -//! the parked positions. The ISP12160 would widen `BUSES` and decode the -//! bus from mailbox 2 and its request entries; the per-bus state is there. +//! the parked positions. Only the ISP1040's bus reset starts a hold-off: +//! the ISP12160 (two buses, per-bus state is there) keeps its own bus-reset +//! handling, and its hold-off is not modelled. //! * Only logical unit 0 of a present target exists. A command for another //! unit is passed to the target with that unit number, which answers as //! SCSI says (INQUIRY: no device at this unit; anything else: check @@ -81,7 +82,7 @@ use parking_lot::{Condvar, Mutex, MutexGuard}; use crate::devlog::LogModule; use crate::irq::IrqLine; -use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PCI_INTA}; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciRegisterPolicy, PCI_INTA}; use crate::scsi::{scsi_cmd, ScsiBus, ScsiDataLength, ScsiIdentities, ScsiRequest, CDROM_IDENTITY, DISK_IDENTITY}; use crate::snapshot::{get_field, hex_u16, hex_u32, load_u16_slice, toml_u16, toml_u32, toml_u8, u16_slice_to_toml, u8_slice_to_toml}; @@ -115,7 +116,7 @@ pub mod reg { pub const WINDOW_SIZE: u32 = 0x100; } -const MAILBOX_COUNT: usize = 8; +pub(crate) const MAILBOX_COUNT: usize = 8; const MAILBOX_END: u32 = reg::MAILBOX_BASE + MAILBOX_COUNT as u32 * 2; const BANK_END: u32 = 0xc0; const BANK_WORDS: usize = ((BANK_END - reg::BANK_BASE) / 2) as usize; @@ -163,6 +164,8 @@ pub mod mbox { pub const INIT_REQ_QUEUE: u16 = 0x0010; pub const INIT_RSP_QUEUE: u16 = 0x0011; pub const ABORT_COMMAND: u16 = 0x0015; + pub const ABORT_DEVICE: u16 = 0x0016; + pub const ABORT_TARGET: u16 = 0x0017; pub const BUS_RESET: u16 = 0x0018; pub const GET_FIRMWARE_STATUS: u16 = 0x001f; pub const GET_TARGET_PARAMETERS: u16 = 0x0028; @@ -187,13 +190,14 @@ pub mod mbox { pub const INVALID: u16 = 0x4001; pub const HOST_INTERFACE_ERROR: u16 = 0x4002; pub const TEST_FAILED: u16 = 0x4003; + pub const COMMAND_ERROR: u16 = 0x4005; pub const PARAM_ERROR: u16 = 0x4006; } const RAM_WORDS: usize = 0x10000; /// The command budget the adapter announces in its firmware status. pub const MAX_REQUESTS: u16 = 512; -const FW_VERSION: [u16; 3] = [1, 0, 0]; +pub(crate) const FW_VERSION: [u16; 3] = [1, 0, 0]; pub const HOST_CMD_SHIFT: u32 = 12; pub const RISC_FLAG_HOST_INT: u16 = 0x0080; @@ -229,6 +233,7 @@ pub const SCSI_STATUS_SENSE_VALID: u16 = 0x0200; pub const IOCB_COMPLETE: u16 = 0x0000; pub const IOCB_INCOMPLETE: u16 = 0x0001; pub const IOCB_RESET: u16 = 0x0004; +pub const IOCB_ABORTED: u16 = 0x0005; pub const IOCB_DATA_UNDERRUN: u16 = 0x0015; pub const STATE_GOT_BUS: u16 = 0x0100; @@ -256,13 +261,14 @@ const DEFAULT_SELECTION_TIMEOUT: u16 = 250; /// source for a device, so it runs on the adapter's clock, host wall time /// less the time the machine was stopped, like the selection timeout. const BUS_RESET_DELAY_UNIT: Duration = Duration::from_secs(1); -/// The SCSI buses this part drives. The hold-off, the parked commands and the -/// selection are all kept per bus, so a two-bus part widens this and decodes -/// the bus from its own request entries and from mailbox 2. -const BUSES: usize = 1; +/// The most SCSI buses any part drives (the ISP12160's two; the ISP1040 has +/// one, `IspPart::channels`). The hold-off, the parked commands and the +/// selection are all kept per bus. Only the ISP1040's bus reset starts a +/// hold-off: the ISP12160 keeps its own bus-reset handling. +const BUSES: usize = 2; const CHECK_CONDITION: u8 = 0x02; -const MAX_TARGET: u8 = 15; +pub(crate) const MAX_TARGET: u8 = 15; const MAX_LUN: u8 = 7; /// A completion held for response-ring room. @@ -374,6 +380,49 @@ enum Prepared { Run(Job), } +/// Part-specific register and firmware behavior; the ring engine owns the state. +/// Defaults preserve the ISP1040 contract, including wrapping firmware RAM. +pub(crate) trait IspPart: Send + Sync { + fn name(&self) -> &'static str; + fn channels(&self) -> usize { 1 } + fn pci_config(&self, bars: [Bar; 6]) -> PciConfig { + PciConfig::new(PCI_VENDOR_QLOGIC, PCI_DEVICE_ISP1040, PCI_CLASS_SCSI, PCI_REVISION_ISP1040, bars, PCI_INTA) + } + fn firmware_version(&self) -> [u16; 3] { FW_VERSION } + fn active_bank(&self, cfg1: u16) -> Bank { + if cfg1 & CFG1_BANK_SELECT != 0 { Bank::Sxp } else { Bank::Risc } + } + fn checks_ram_bounds(&self) -> bool { false } + fn has_abort_commands(&self) -> bool { false } + fn target_on(&self, _channel: u8, target: u8) -> u8 { target } + fn decode_target(&self, target: u8) -> (usize, u8) { (0, target) } + fn id_high(&self) -> u16 { PCI_DEVICE_ISP1040 } + /// The ISP1040 register revision runs one ahead of its PCI revision. + fn bus_cfg0(&self) -> u16 { PCI_REVISION_ISP1040 as u16 + 1 } + fn cfg1_write_mask(&self) -> u16 { CFG1_WRITE_MASK } + fn unmodelled_dma_register(&self, _off: u32) -> bool { false } + fn completion_phases(&self, _moved: u64, _total: u64) -> u16 { STATE_ALL_PHASES & !STATE_GOT_SENSE } + fn mailbox_ready(&self, _semaphore: u16) -> bool { true } + /// Extra target-parameter results, beyond the common asynchronous/narrow reply. + fn clear_target_parameters(&self, _mailbox: &mut [u16; MAILBOX_COUNT]) {} + fn set_reset_delay(&self, st: &mut State) { st.reset_delay = st.mailbox[1]; } + /// Queue target notifications for the worker; the CPU never locks a bus. + fn reset_targets(&self, _st: &mut State) -> bool { false } + /// Handle extra mailbox commands, leaving result signalling to the engine. + fn execute_mailbox(&self, _core: &Core, _st: &mut MutexGuard<'_, State>) -> bool { false } + fn save_extra(&self, _st: &State, _t: &mut toml::map::Map) {} + fn load_extra(&self, _st: &mut State, _v: &toml::Value) {} +} + +struct Isp1040Part; + +impl IspPart for Isp1040Part { + fn name(&self) -> &'static str { "ISP1040" } +} + +/// The register window selected by BUS_CFG1, independent of its encoding. +pub(crate) enum Bank { Risc, Sxp, Sxp1, Dma } + /// A command waiting out its bus's post-reset hold-off. Only the ring /// position is kept: the entry, its data segments included, is read again /// from the guest's ring when the hold ends. @@ -392,7 +441,15 @@ struct Parked { started: bool, } -struct State { +pub(crate) struct State { + pub(crate) reset_targets: bool, + pub(crate) pending_target_resets: VecDeque<(u8, Option)>, + pub(crate) active: Option<(u8, u8, u32)>, + cancelled: bool, + pub(crate) outstanding: Vec<(u8, u8, u32)>, + pub(crate) sxp1_bank: [u16; BANK_WORDS], + pub(crate) dma_bank: [u16; BANK_WORDS], + pub(crate) reset_delay1: u16, ctl: Ctl, bus_cfg1: u16, bus_ctrl: u16, @@ -402,11 +459,11 @@ struct State { risc_flags: u16, dma_cmd_cfg: u16, dma_data_cfg: u16, - mailbox: [u16; MAILBOX_COUNT], + pub(crate) mailbox: [u16; MAILBOX_COUNT], risc_bank: [u16; BANK_WORDS], sxp_bank: [u16; BANK_WORDS], risc_ram: Vec, - reset_delay: u16, + pub(crate) reset_delay: u16, /// Mailbox command 0x0031's value, in [`SELECTION_TIMEOUT_UNIT`]s. selection_timeout: u16, rqst_base: u64, @@ -434,6 +491,14 @@ struct State { impl State { fn new() -> Self { let mut s = State { + reset_targets: false, + pending_target_resets: VecDeque::new(), + active: None, + cancelled: false, + outstanding: Vec::new(), + sxp1_bank: [0; BANK_WORDS], + dma_bank: [0; BANK_WORDS], + reset_delay1: 0, ctl: Ctl::default(), bus_cfg1: 0, bus_ctrl: 0, @@ -485,6 +550,12 @@ impl State { self.mailbox = [0, SIG1, SIG2, SIG3, SIG4, 0, 0, 0]; self.risc_bank = [0; BANK_WORDS]; self.sxp_bank = [0; BANK_WORDS]; + self.sxp1_bank = [0; BANK_WORDS]; + self.dma_bank = [0; BANK_WORDS]; + self.reset_delay1 = 0; + self.active = None; + self.cancelled = false; + self.outstanding.clear(); // The driver re-downloads firmware after every reset. self.risc_ram.iter_mut().for_each(|w| *w = 0); self.reset_delay = 0; @@ -499,6 +570,8 @@ impl State { self.resp_seen = 0; self.resp_cons = 0; self.pending.clear(); + self.reset_targets = false; + self.pending_target_resets.clear(); self.quiet_until = [None; BUSES]; self.parked.clear(); } @@ -524,9 +597,15 @@ impl State { self.resp_len != 0 && (u32::from(self.resp_prod) + 1) % u32::from(self.resp_len) != u32::from(self.resp_cons) } - fn active_bank(&mut self) -> &mut [u16; BANK_WORDS] { - if self.bus_cfg1 & CFG1_BANK_SELECT != 0 { &mut self.sxp_bank } else { &mut self.risc_bank } + fn active_bank(&mut self, bank: Bank) -> &mut [u16; BANK_WORDS] { + match bank { + Bank::Risc => &mut self.risc_bank, + Bank::Sxp => &mut self.sxp_bank, + Bank::Sxp1 => &mut self.sxp1_bank, + Bank::Dma => &mut self.dma_bank, + } } + } /// The adapter. Its state lives in a [`Core`] shared with the thread that @@ -543,8 +622,8 @@ impl std::ops::Deref for Isp1040 { /// Lock order: `line_order`, then `st`. Nothing else is taken with `st` /// held except the leaf locks (`notes`, `dma_misses`, `odd_accesses`): DMA, -/// the targets and `fence` are reached with it released. `fence` is held -/// across one guest-memory store at a time, and `targets` across one +/// either bus and `fence` are reached with it released. `fence` is held +/// across one guest-memory store at a time, and either bus across one /// target command; neither is held with the other. pub struct Core { st: Mutex, @@ -560,13 +639,17 @@ pub struct Core { wake: Condvar, /// Serialises driving the interrupt line; see [`Core::sync_irq`]. line_order: Mutex<()>, - /// The targets on the bus, by SCSI id. - bus: ScsiBus, - /// A bus reset the targets have not yet seen. `st` and `bus` are never - /// held together, so the reset (taken under `st`) is passed on by the - /// next command to take `bus`, before it runs: no command can see the - /// targets as they were. See [`Core::scsi`]. - targets_reset: AtomicBool, + /// Generation for commands cancelled by aborts without resetting the chip. + cancel: AtomicU64, + /// Immutable part policy, shared by register, worker and snapshot paths. + part: &'static dyn IspPart, + /// The controller's one bus, or both channels of the ISP12160. + buses: [ScsiBus; 2], + /// A bus reset the targets on that bus have not yet seen. `st` and the + /// buses are never held together, so the reset (taken under `st`) is + /// passed on by the next command to take the bus, before it runs: no + /// command can see the targets as they were. See [`Core::scsi`]. + targets_reset: [AtomicBool; 2], dma: OnceLock>, irq: OnceLock, /// Which of the Octane's two controllers this is (0 internal, 1 @@ -603,7 +686,11 @@ impl Isp1040 { /// The controller at `index` (0 internal, 1 external) on `bus`. pub fn new_at(index: u8, bus: ScsiBus) -> Self { - Self { core: Arc::new(Core::new_at(index, bus)), worker: Mutex::new(None) } + Self { core: Arc::new(Core::new_at(index, &Isp1040Part, [bus, Self::new_bus()])), worker: Mutex::new(None) } + } + + pub(crate) fn new_12160(bus0: ScsiBus, bus1: ScsiBus) -> Self { + Self { core: Arc::new(Core::new_at(0, &crate::dev::isp12160::Isp12160Part, [bus0, bus1])), worker: Mutex::new(None) } } /// Start the ring thread (idempotent) and let it take whatever was @@ -617,7 +704,7 @@ impl Isp1040 { let c = self.core.clone(); *worker = Some( std::thread::Builder::new() - .name(format!("ISP1040-{}", self.index)) + .name(format!("{}-{}", self.core.model_name(), self.index)) .spawn(move || c.run_ring()) .expect("spawn isp1040 thread"), ); @@ -641,13 +728,17 @@ impl Isp1040 { /// Name the adapter's long-lived locks in the lock registry, which /// holds only a weak reference to them. pub fn register_locks(&self) { - for (what, probe) in [ + let mut locks: Vec<(&str, fn(&Core) -> bool)> = vec![ ("st", (|c: &Core| c.st.is_locked()) as fn(&Core) -> bool), ("fence", |c| c.fence.is_locked()), - ("targets", |c| c.bus.is_locked()), - ] { + ("targets", |c| c.buses[0].is_locked()), + ]; + if self.core.part.channels() > 1 { + locks.push(("targets1", |c| c.buses[1].is_locked())); + } + for (what, probe) in locks { let core = Arc::downgrade(&self.core); - crate::locks::register_lock(format!("isp1040-{}::{what}", self.index), move || { + crate::locks::register_lock(format!("{}-{}::{what}", self.core.model_name().to_ascii_lowercase(), self.index), move || { core.upgrade().is_some_and(|c| probe(&c)) }); } @@ -662,27 +753,27 @@ impl Drop for Isp1040 { } impl Core { - fn new_at(index: u8, bus: ScsiBus) -> Self { + fn new_at(index: u8, part: &'static dyn IspPart, buses: [ScsiBus; 2]) -> Self { let st = State::new(); let gen = st.ctl.gen; + let bars = [Bar::Io(reg::WINDOW_SIZE), Bar::Mem(reg::WINDOW_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused]; + let pci = part.pci_config(bars); Self { + part, + cancel: AtomicU64::new(0), st: Mutex::new(st), gen: AtomicU64::new(gen), fence: Mutex::new(()), wake: Condvar::new(), line_order: Mutex::new(()), - bus, - targets_reset: AtomicBool::new(false), + buses, + targets_reset: [AtomicBool::new(false), AtomicBool::new(false)], dma: OnceLock::new(), irq: OnceLock::new(), index, // BAR 0 is the I/O window and BAR 1 its memory-space alias, one // register file. - pci: PciConfig::new( - PCI_VENDOR_QLOGIC, PCI_DEVICE_ISP1040, PCI_CLASS_SCSI, PCI_REVISION_ISP1040, - [Bar::Io(reg::WINDOW_SIZE), Bar::Mem(reg::WINDOW_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], - PCI_INTA, - ), + pci, notes: Mutex::new(Vec::new()), dma_misses: Mutex::new(std::collections::BTreeSet::new()), odd_accesses: Mutex::new(0), @@ -692,6 +783,8 @@ impl Core { } } + fn model_name(&self) -> &'static str { self.part.name() } + pub fn index(&self) -> u8 { self.index } /// A machine reset: the chip as at power-on (no firmware, no queues), @@ -702,6 +795,7 @@ impl Core { { let mut st = self.st.lock(); st.reset(); + if self.part.reset_targets(&mut st) { self.kick(&mut st); } self.publish_gen(&st); self.wake.notify_all(); } @@ -929,7 +1023,7 @@ impl Core { } /// Tell the ring thread the ring may have moved. - fn kick(&self, st: &mut State) { + pub(crate) fn kick(&self, st: &mut State) { st.ctl.work = true; self.wake.notify_all(); } @@ -957,11 +1051,16 @@ impl Core { /// The SCSI bus: attach targets to it and ask what is on it. pub fn bus(&self) -> &ScsiBus { - &self.bus + &self.buses[0] + } + + pub(crate) fn bus_for_channel(&self, channel: u8) -> Option<&ScsiBus> { + ((channel as usize) < self.part.channels()).then(|| &self.buses[channel as usize]) } - fn has_target(&self, id: u8) -> bool { - self.bus.has_target(id) + fn bus_for_target(&self, target: u8) -> (&ScsiBus, u8) { + let (channel, id) = self.part.decode_target(target); + (&self.buses[channel], id) } // ---- DMA --------------------------------------------------------- @@ -1045,12 +1144,18 @@ impl Core { /// A RISC RAM load or dump of up to 128 KiB, with the state unlocked /// for the DMA. The whole transfer is read before a word of it is kept, /// so a refused DMA leaves the array as it was. Words travel - /// little-endian. This part has no edge to its array: a transfer past - /// the end folds to the start. - fn ram_transfer(&self, st: &mut MutexGuard<'_, State>, load: bool) -> u16 { + /// little-endian. The ISP1040 wraps past the end; the ISP12160 rejects + /// transfers that would cross it. + pub(crate) fn ram_transfer(&self, st: &mut MutexGuard<'_, State>, load: bool) -> u16 { let base = st.mailbox[1]; let count = st.mailbox[4] as usize; - let host = Self::mbox_host_addr(&st.mailbox); + if self.part.checks_ram_bounds() && base as usize + count > RAM_WORDS { + return mbox::PARAM_ERROR; + } + let mut host = Self::mbox_host_addr(&st.mailbox); + if st.mailbox[0] == mbox::LOAD_RAM_A64 { + host |= ((st.mailbox[7] as u64) << 32) | ((st.mailbox[6] as u64) << 48); + } if count == 0 { return mbox::COMPLETE; } @@ -1077,6 +1182,20 @@ impl Core { /// Synchronous: the protocol has no way to leave a command pending. fn mailbox_command(&self, st: &mut MutexGuard<'_, State>) { let cmd = st.mailbox[0]; + if self.part.has_abort_commands() && matches!(cmd, mbox::ABORT_COMMAND | mbox::ABORT_DEVICE | mbox::ABORT_TARGET | mbox::BUS_RESET) { + self.with_dispatch_held(st, |st| self.execute_mailbox_command(st)); + } else { + self.execute_mailbox_command(st); + } + } + + fn execute_mailbox_command(&self, st: &mut MutexGuard<'_, State>) { + let cmd = st.mailbox[0]; + if self.part.execute_mailbox(self, st) { + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } match cmd { mbox::NOP => st.mailbox[0] = mbox::COMPLETE, mbox::WRITE_RAM_WORD => { @@ -1098,7 +1217,19 @@ impl Core { // zero. mbox::VERIFY_CHECKSUM => { let start = st.mailbox[1]; + if self.part.checks_ram_bounds() && start as usize + 4 > RAM_WORDS { + st.mailbox[0] = mbox::PARAM_ERROR; + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } let len = st.risc_ram[start.wrapping_add(3) as usize]; + if self.part.checks_ram_bounds() && start as usize + len as usize > RAM_WORDS { + st.mailbox[0] = mbox::PARAM_ERROR; + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } let mut sum = 0u16; for i in 0..len { sum = sum.wrapping_add(st.risc_ram[start.wrapping_add(i) as usize]); @@ -1111,9 +1242,7 @@ impl Core { // Accepted, never executed. mbox::EXEC_FIRMWARE => st.mailbox[0] = mbox::COMPLETE, mbox::ABOUT_FIRMWARE => { - st.mailbox[1] = FW_VERSION[0]; - st.mailbox[2] = FW_VERSION[1]; - st.mailbox[3] = FW_VERSION[2]; + st.mailbox[1..4].copy_from_slice(&self.part.firmware_version()); st.mailbox[6] = 0; st.mailbox[0] = mbox::COMPLETE; } @@ -1128,6 +1257,7 @@ impl Core { mbox::GET_TARGET_PARAMETERS => { st.mailbox[2] = 0; st.mailbox[3] = 0; + self.part.clear_target_parameters(&mut st.mailbox); st.mailbox[0] = mbox::COMPLETE; } // Setters for state nothing reads back; answering invalid is a @@ -1149,7 +1279,7 @@ impl Core { st.mailbox[0] = mbox::COMPLETE; } mbox::SET_RESET_DELAY_PARAMS => { - st.reset_delay = st.mailbox[1]; + self.part.set_reset_delay(st); st.mailbox[0] = mbox::COMPLETE; } mbox::INIT_REQ_QUEUE | mbox::INIT_REQ_QUEUE_A64 | mbox::INIT_RSP_QUEUE | mbox::INIT_RSP_QUEUE_A64 => { @@ -1204,7 +1334,7 @@ impl Core { mbox::BUS_RESET => { let now = self.now_ns(st); let bus = usize::from(st.mailbox[2]); - if bus >= BUSES { + if bus >= self.part.channels() { // A bus this part does not have: nothing to quiet or // retire, and the datasheet's per-bus hold leaves the // real bus alone. Noted so a driver that leaves mailbox 2 @@ -1228,7 +1358,7 @@ impl Core { st.pending.push_back(Pending { handle: p.handle, completion: IOCB_RESET, ..Default::default() }); } } - self.targets_reset.store(true, Ordering::Release); + self.targets_reset[bus].store(true, Ordering::Release); let delay = BUS_RESET_DELAY_UNIT * u32::from(st.mailbox[1]); st.quiet_until[bus] = (!delay.is_zero()).then(|| now.saturating_add(delay.as_nanos() as i64)); self.kick(st); @@ -1245,6 +1375,67 @@ impl Core { st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; } + /// Prevent the worker from taking another entry while a mailbox drops `st`. + fn with_dispatch_held(&self, st: &mut MutexGuard<'_, State>, f: impl FnOnce(&mut MutexGuard<'_, State>) -> R) -> R { + struct Hold<'a, 'b> { + core: &'a Core, + st: &'a mut MutexGuard<'b, State>, + } + impl Drop for Hold<'_, '_> { + fn drop(&mut self) { + self.st.ctl.hold -= 1; + self.core.kick(self.st); + } + } + st.ctl.hold += 1; + let held = Hold { core: self, st }; + f(held.st) + } + + /// A reset of `channel`'s bus: each disk on it puts its mode parameters back before the next + /// command, see [`ScsiTarget::reset_mode_parameters`]. A channel the part lacks is ignored. + pub(crate) fn note_bus_reset(&self, channel: usize) { + if let Some(flag) = self.targets_reset.get(channel) { flag.store(true, Ordering::Release); } + } + + /// Notify targets with `st` released; a blocked target must not stall register access. + fn reset_targets(&self, st: &mut MutexGuard<'_, State>, channel: u8, target: Option) { + let Some(bus) = self.bus_for_channel(channel) else { return }; + self.with_dispatch_held(st, |st| MutexGuard::unlocked(st, || bus.for_each(|id, dev| { + if target.is_none_or(|target| target as usize == id) { + dev.bus_reset(); + } + }))); + } + + pub(crate) fn retire_outstanding(&self, st: &mut State, matches: impl Fn((u8, u8, u32)) -> bool, completion: u16) -> usize { + let mut retired = 0; + let mut keep = Vec::new(); + for item in st.outstanding.drain(..) { + if matches(item) { + st.pending.push_back(Pending { handle: item.2, completion, ..Pending::default() }); + retired += 1; + } else { + keep.push(item); + } + } + st.outstanding = keep; + if retired != 0 { self.kick(st); } + retired + } + + /// Queue one cancellation reply, then fence its remaining guest-memory stores. + pub(crate) fn cancel_active(&self, st: &mut MutexGuard<'_, State>, completion: u16) { + if st.cancelled { return; } + if let Some((_, _, handle)) = st.active { + self.cancel.fetch_add(1, Ordering::AcqRel); + st.cancelled = true; + st.pending.push_back(Pending { handle, completion, ..Pending::default() }); + self.kick(st); + MutexGuard::unlocked(st, || self.retire_stores()); + } + } + fn host_command(&self, st: &mut MutexGuard<'_, State>, val: u16) { match val >> HOST_CMD_SHIFT { 0x0 => {} @@ -1255,6 +1446,10 @@ impl Core { 0x3 => st.risc_flags &= !(RISC_FLAG_PAUSED | RISC_FLAG_RESET), // "A mailbox command is ready": the trigger to process one. 0x5 => { + if !self.part.mailbox_ready(st.semaphore) { + self.note("mailbox submitted with an unacknowledged result".into()); + return; + } st.risc_flags |= RISC_FLAG_HOST_INT; self.mailbox_command(st); } @@ -1440,17 +1635,19 @@ impl Core { /// Run a CDB against the target; `data` is what a write sends it. None /// when a reset cancelled the command while it waited for the target. - fn scsi(&self, gen: u64, target: u8, cdb: &[u8], data: Option>) -> Option<(Vec, u8)> { - let mut targets = self.bus.lock(); - if self.targets_reset.swap(false, Ordering::AcqRel) { + fn scsi(&self, gen: u64, cancel: u64, target: u8, cdb: &[u8], data: Option>) -> Option<(Vec, u8)> { + let (channel, _) = self.part.decode_target(target); + let (bus, id) = self.bus_for_target(target); + let mut targets = bus.lock(); + if self.targets_reset[channel].swap(false, Ordering::AcqRel) { // The bus reset puts each target's mode parameters back; see // [`ScsiTarget::reset_mode_parameters`]. targets.iter_mut().flatten().for_each(|t| t.reset_mode_parameters()); } - if self.gen.load(Ordering::Acquire) != gen { + if self.gen.load(Ordering::Acquire) != gen || self.cancel.load(Ordering::Acquire) != cancel { return None; } - let Some(dev) = targets.get_mut(target as usize).and_then(|t| t.as_mut()) else { + let Some(dev) = targets.get_mut(id as usize).and_then(|t| t.as_mut()) else { return Some((Vec::new(), CHECK_CONDITION)); }; let req = ScsiRequest { cdb: cdb.to_vec(), data_len: ScsiDataLength::Unlimited, data_in: data }; @@ -1481,14 +1678,15 @@ impl Core { let lun = (device & 0xff) as u8; // A bus this part does not have. - if usize::from(channel) >= BUSES { + if channel as usize >= self.part.channels() { return Prepared::Answered(Self::incomplete(handle, 0, 0)); } // GOT_BUS alone is Linux's bad-target; the timeout bit beside it is // what lets the empty slot be treated as benign. - if target > MAX_TARGET || !self.has_target(target) { + if target > MAX_TARGET || !self.buses[channel as usize].has_target(target) { return Prepared::Absent(channel as u8, Self::incomplete(handle, STATE_GOT_BUS, STATUS_FLAG_TIMEOUT)); } + let id = target; if lun > MAX_LUN { return Prepared::Answered(Self::incomplete(handle, STATE_GOT_BUS, STATUS_FLAG_TIMEOUT)); } @@ -1497,9 +1695,9 @@ impl Core { cdb[1] = (cdb[1] & 0x1f) | (lun << 5); let cdb = &cdb[..(cdb_len as usize).clamp(1, CDB_FIELD_LEN)]; let block = self - .bus + .buses[channel as usize] .lock() - .get(target as usize) + .get(id as usize) .and_then(|t| t.as_ref()) .and_then(|t| t.block_size()) .map(|b| b as usize) @@ -1507,6 +1705,7 @@ impl Core { .unwrap_or(BLOCK_SIZE); let (xfer, to_dev) = Self::cdb_transfer(cdb, block); let (segs, described) = self.read_segments(ring, addr, entry_count, seg_count, xfer as u64, a64); + let target = self.part.target_on(channel as u8, target); Prepared::Run(Job { handle, target, cdb: cdb.to_vec(), xfer, to_dev, segs, described }) } @@ -1522,7 +1721,7 @@ impl Core { /// None too when a reset cancelled the command (`gen` is the generation /// it was read under): what it had not yet stored in host memory is not /// stored, and no completion is built. - fn run_job(&self, job: Job, gen: u64) -> Option { + fn run_job(&self, job: Job, gen: u64, cancel: u64) -> Option { let Job { handle, target, cdb, xfer, to_dev, segs, described } = job; // The bar has lights for SCSI ids 0-5. if let (Some(hb), 0..=5) = (self.heartbeat.get(), target) { @@ -1540,9 +1739,10 @@ impl Core { buf.extend_from_slice(&chunk); } moved = buf.len() as u64; - status = self.scsi(gen, target, &cdb, Some(buf))?.1; + if self.cancel.load(Ordering::Acquire) != cancel { return None; } + status = self.scsi(gen, cancel, target, &cdb, Some(buf))?.1; } else { - let (data, s) = self.scsi(gen, target, &cdb, None)?; + let (data, s) = self.scsi(gen, cancel, target, &cdb, None)?; status = s; // Offer the target's bytes to the descriptors in order. let take = (data.len() as u64).min(described).min(xfer as u64) as usize; @@ -1550,7 +1750,7 @@ impl Core { for &(a, l) in &segs { if off >= take { break; } let n = (l as usize).min(take - off); - if !self.fenced(gen, || self.dma_write(a, &data[off..off + n]))? { + if !self.fenced(gen, || self.cancel.load(Ordering::Acquire) == cancel && self.dma_write(a, &data[off..off + n]))? { self.note(format!("payload DMA for handle {handle:#x} refused by the fabric; command stopped")); return None; } @@ -1561,12 +1761,13 @@ impl Core { // Auto-sense runs only after a CHECK CONDITION. let mut sense = Vec::new(); + if self.cancel.load(Ordering::Acquire) != cancel { return None; } if status == CHECK_CONDITION { - sense = self.scsi(gen, target, &[scsi_cmd::REQUEST_SENSE, 0, 0, 0, SENSE_MAX as u8, 0], None)?.0; + sense = self.scsi(gen, cancel, target, &[scsi_cmd::REQUEST_SENSE, 0, 0, 0, SENSE_MAX as u8, 0], None)?.0; sense.truncate(SENSE_MAX); } let mut scsi_status = status as u16; - let mut state_flags = STATE_ALL_PHASES & !STATE_GOT_SENSE; + let mut state_flags = self.part.completion_phases(moved, described.max(xfer as u64)); if !sense.is_empty() { scsi_status |= SCSI_STATUS_SENSE_VALID; state_flags |= STATE_GOT_SENSE; @@ -1663,6 +1864,17 @@ impl Core { st.ctl.work = true; return st; } + if std::mem::take(&mut st.reset_targets) { + for channel in 0..self.part.channels() { + self.note_bus_reset(channel); + self.reset_targets(&mut st, channel as u8, None); + } + continue; + } + if let Some((channel, target)) = st.pending_target_resets.pop_front() { + self.reset_targets(&mut st, channel, target); + continue; + } // Completions held for room go first, in order, as the driver // frees it; while any is held the request ring waits. if !st.pending.is_empty() { @@ -1804,8 +2016,16 @@ impl Core { } Prepared::Run(job) => { st.total_cmds += 1; + st.active = Some((job.target, job.cdb.get(1).copied().unwrap_or(0) >> 5, job.handle)); + st.cancelled = false; st.ctl.busy = true; - let done = MutexGuard::unlocked(&mut st, || self.run_job(job, ring.gen)); + let cancel = self.cancel.load(Ordering::Acquire); + let mut done = MutexGuard::unlocked(&mut st, || self.run_job(job, ring.gen, cancel)); + if st.cancelled { done = None; } + else if self.part.has_abort_commands() && done.is_none() && st.ctl.gen == ring.gen { + if let Some(active) = st.active { st.outstanding.push(active); } + } + st.active = None; st.ctl.busy = false; self.wake.notify_all(); // A reset cancelled the command. @@ -1838,13 +2058,16 @@ impl Core { return st.mailbox[((off - reg::MAILBOX_BASE) / 2) as usize]; } if (reg::BANK_BASE..BANK_END).contains(&off) { - return st.active_bank()[((off - reg::BANK_BASE) / 2) as usize]; + let bank = self.part.active_bank(st.bus_cfg1); + return st.active_bank(bank)[((off - reg::BANK_BASE) / 2) as usize]; + } + if self.part.unmodelled_dma_register(off) { + return 0; } match off { reg::ID_LOW => PCI_VENDOR_QLOGIC, - reg::ID_HIGH => PCI_DEVICE_ISP1040, - // The revision field runs one ahead of the PCI byte. - reg::BUS_CFG0 => PCI_REVISION_ISP1040 as u16 + 1, + reg::ID_HIGH => self.part.id_high(), + reg::BUS_CFG0 => self.part.bus_cfg0(), reg::BUS_CFG1 => st.bus_cfg1, reg::BUS_CTRL => st.bus_ctrl, reg::BUS_STATUS => st.bus_status, @@ -1862,6 +2085,10 @@ impl Core { } fn reg_write(&self, off: u32, val: u16) { + if self.part.unmodelled_dma_register(off) { + self.note(format!("write to ISP12160 DMA register {off:#04x} is not modelled")); + return; + } let mut st = self.st.lock(); if off == reg::RESPONSE_QUEUE { // The consumer acknowledgement: recorded, never over the @@ -1884,15 +2111,17 @@ impl Core { return; } if (reg::BANK_BASE..BANK_END).contains(&off) { - st.active_bank()[((off - reg::BANK_BASE) / 2) as usize] = val; + let bank = self.part.active_bank(st.bus_cfg1); + st.active_bank(bank)[((off - reg::BANK_BASE) / 2) as usize] = val; return; } match off { reg::BUS_CFG1 => { - if val & !CFG1_WRITE_MASK != 0 { - self.note(format!("bus configuration 1 bits {:#06x} are not modelled", val & !CFG1_WRITE_MASK)); + let mask = self.part.cfg1_write_mask(); + if val & !mask != 0 { + self.note(format!("bus configuration 1 bits {:#06x} are not modelled", val & !mask)); } - st.bus_cfg1 = val & CFG1_WRITE_MASK; + st.bus_cfg1 = val & mask; } // Applied after the enables are stored, so the reset wins. reg::BUS_CTRL => { @@ -1900,6 +2129,7 @@ impl Core { // The command in flight is cancelled, not waited for. if val & CTRL_SOFT_RESET != 0 { st.reset(); + if self.part.reset_targets(&mut st) { self.kick(&mut st); } self.publish_gen(&st); self.wake.notify_all(); drop(st); @@ -2005,7 +2235,7 @@ impl crate::traits::Device for Isp1040 { impl crate::scsi_monitor::ScsiTargets for Isp1040 { fn scsi_bus(&self) -> &ScsiBus { - &self.bus + &self.buses[0] } } @@ -2041,14 +2271,15 @@ impl crate::traits::Saveable for Isp1040 { // A hold-off already over but not acted on still crosses: the // commands parked behind it start when the load re-arms it at 0. let quiet_left = st.quiet_until.map(|until| until.map(|u| u - now)); - Self::save_locked(st, left, quiet_left) + self.save_locked(st, left, quiet_left) }) } fn load_state(&self, v: &toml::Value) -> Result<(), String> { self.quiesced(|st| { let now = self.now_ns(st); - Self::load_locked(st, v, now); + self.load_locked(st, v, now); + self.cancel.store(0, Ordering::Release); self.publish_gen(st); }); self.sync_irq(); @@ -2057,7 +2288,7 @@ impl crate::traits::Saveable for Isp1040 { } impl Isp1040 { - fn save_locked(st: &State, selecting_left: Option, quiet_left: [Option; BUSES]) -> toml::Value { + fn save_locked(&self, st: &State, selecting_left: Option, quiet_left: [Option; BUSES]) -> toml::Value { let mut t = toml::map::Map::new(); for (k, v) in [ ("bus_cfg1", st.bus_cfg1), ("bus_ctrl", st.bus_ctrl), ("bus_status", st.bus_status), @@ -2130,16 +2361,18 @@ impl Isp1040 { m.insert("sense".into(), u8_slice_to_toml(&sel.done.sense)); t.insert("selecting".into(), toml::Value::Table(m)); } + self.part.save_extra(st, &mut t); toml::Value::Table(t) } /// Unprocessed request entries survive: the ring thread is kicked and /// runs them after the load. - fn load_locked(st: &mut State, v: &toml::Value, now: i64) { + fn load_locked(&self, st: &mut State, v: &toml::Value, now: i64) { { let ctl = std::mem::take(&mut st.ctl); *st = State::new(); st.ctl = ctl; + self.part.load_extra(st, v); st.ctl.gen += 1; st.ctl.rqst_epoch += 1; st.ctl.resp_epoch += 1; diff --git a/src/dev/isp1040/tests.rs b/src/dev/isp1040/tests.rs index 8a96b9d66..0bb7e084c 100644 --- a/src/dev/isp1040/tests.rs +++ b/src/dev/isp1040/tests.rs @@ -2,7 +2,8 @@ //! and ring contract. Host memory is a flat byte array behind a fake `PciDma`; the disk //! is a small raw image. //! -//! Ported from tests/qtest/qlogic-isp-test.c in jamesbraid/qemu-sgi-origami. +//! Ported from tests/qtest/qlogic-isp-test.c and tests/qtest/sn0-isp12160-test.c +//! in jamesbraid/qemu-sgi-origami. use super::*; use crate::irq::LevelProbe; @@ -59,15 +60,16 @@ fn image(name: &str, blocks: usize) -> std::path::PathBuf { } impl Rig { - /// A chip on fake memory, no disks. - fn bare() -> Rig { + fn on(isp: Isp1040) -> Rig { let mem = FakeMem::new(); - let isp = Isp1040::new(); Device::start(&isp); isp.connect_dma(mem.clone()); Rig { isp, mem } } + /// A chip on fake memory, no disks. + fn bare() -> Rig { Rig::on(Isp1040::new()) } + /// A chip with a disk of `blocks` blocks at target `id`. fn with_disk(name: &str, id: u8, blocks: usize) -> Rig { let r = Rig::bare(); @@ -1616,6 +1618,403 @@ fn a_controller_built_on_a_filled_bus_answers_for_its_target() { let _ = std::fs::remove_file(img); } + +struct MarkerTarget(u8); + +impl crate::scsi::ScsiTarget for MarkerTarget { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: vec![self.0] }) + } +} + +/// Target 0 on channel 1 must stay distinct from target 0 on channel 0. +#[test] +fn the_isp12160_routes_ring_requests_to_separate_buses() { + let bus0 = Isp1040::new_bus(); + let bus1 = Isp1040::new_bus(); + bus0.attach(0, Box::new(MarkerTarget(0x10))).unwrap(); + bus1.attach(0, Box::new(MarkerTarget(0x20))).unwrap(); + let r = Rig::on(Isp1040::new_12160(bus0, bus1)); + r.init_rings(4, 4); + + r.put_entry(0, &request(0x1234, 0x80, &[0x12, 0, 0, 0, 36, 0], &[(BUF, 36)])); + r.doorbell(1); + assert_eq!(r.mem.get(BUF, 1), vec![0x20], "channel 1 reaches its target 0"); + + r.put_entry(1, &request(0x1235, 0, &[0x12, 0, 0, 0, 36, 0], &[(BUF + 64, 36)])); + r.doorbell(2); + assert_eq!(r.mem.get(BUF + 64, 1), vec![0x10], "channel 0 keeps its own target 0"); +} + +struct ResetProbe(Arc); + +impl crate::scsi::ScsiTarget for ResetProbe { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: Vec::new() }) + } + + fn bus_reset(&mut self) { + self.0.fetch_add(1, Ordering::Relaxed); + } +} + +/// A snapshot retains both the chip-wide reset and the selected target reset, +/// and the resumed worker delivers both through the original channel buses. +#[test] +fn the_isp12160_preserves_queued_bus_and_target_resets_across_save_load() { + let src = Isp1040::new_12160(Isp1040::new_bus(), Isp1040::new_bus()); + { + let mut state = src.st.lock(); + state.reset_targets = true; + state.pending_target_resets.push_back((1, Some(2))); + } + let saved = src.save_state(); + + let resets0 = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let resets1 = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let selected_reset = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let bus0 = Isp1040::new_bus(); + let bus1 = Isp1040::new_bus(); + bus0.attach(1, Box::new(ResetProbe(resets0.clone()))).unwrap(); + bus1.attach(1, Box::new(ResetProbe(resets1.clone()))).unwrap(); + bus1.attach(2, Box::new(ResetProbe(selected_reset.clone()))).unwrap(); + let dst = Isp1040::new_12160(bus0, bus1); + dst.load_state(&saved).unwrap(); + Device::start(&dst); + dst.wait_idle(); + Device::stop(&dst); + + assert_eq!(resets0.load(Ordering::Relaxed), 1); + assert_eq!(resets1.load(Ordering::Relaxed), 1); + assert_eq!(selected_reset.load(Ordering::Relaxed), 2, "the full reset and queued target reset both run"); + assert_eq!(saved.get("reset_targets").and_then(toml::Value::as_bool), Some(true)); + assert_eq!(saved.get("target_resets").and_then(toml::Value::as_array).map(Vec::len), Some(1)); + let after = dst.save_state(); + assert_eq!(after.get("reset_targets").and_then(toml::Value::as_bool), Some(false)); + assert_eq!(after.get("target_resets").and_then(toml::Value::as_array).map(Vec::len), Some(0)); +} + +// ---- ISP12160 second channel and recovery commands ----------------------- + +fn rig12160() -> Rig { Rig::on(Isp1040::new_12160(Isp1040::new_bus(), Isp1040::new_bus())) } + +/// A 64-block image whose first block is `fill`, so a read says which disk answered. +fn filled_image(name: &str, fill: u8) -> std::path::PathBuf { + let p = image(name, 64); + let mut data = vec![0u8; 64 * BLOCK_SIZE]; + data[..BLOCK_SIZE].fill(fill); + std::fs::write(&p, data).expect("test image"); + p +} + +/// A 12160 with a different disk at target 1 on each channel. +fn disks_on_both_channels(name: &str) -> Rig { + let r = rig12160(); + for channel in 0..2u8 { + let img = filled_image(&format!("{name}{channel}"), 0xa0 + channel); + r.isp.bus_for_channel(channel).unwrap().attach_image(1, &img, false).expect("attach"); + } + r +} + +/// Issues entries one at a time on a 16-entry ring and returns each completion. +struct Seq<'a> { + r: &'a Rig, + n: u16, +} + +impl<'a> Seq<'a> { + fn new(r: &'a Rig) -> Self { + r.init_rings(16, 16); + Seq { r, n: 0 } + } + fn run(&mut self, handle: u32, target: u8, cdb: &[u8], segs: &[(u32, u32)]) -> [u8; ENTRY_SIZE] { + let slot = self.n; + self.r.put_entry(slot as u32, &request(handle, target, cdb, segs)); + self.n += 1; + self.r.doorbell(self.n); + self.r.status(slot as u32) + } +} + +const TUR: [u8; 6] = [0; 6]; + +/// Counts bus resets and answers any command with a full block pair, so a read +/// whose second buffer segment is unreachable stays outstanding. +struct CountingDisk(Arc); + +impl crate::scsi::ScsiTarget for CountingDisk { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: vec![0; 2 * BLOCK_SIZE] }) + } + fn bus_reset(&mut self) { self.0.fetch_add(1, Ordering::Relaxed); } +} + +struct Held { + r: Rig, + /// Bus resets seen by the counting disks at target 1 on channel 0, target 1 on channel 1, + /// and target 2 on channel 1. + ch0: Arc, + ch1: Arc, + ch1_other: Arc, +} + +impl Held { + fn new() -> Held { + let counters: Vec<_> = (0..3).map(|_| Arc::new(std::sync::atomic::AtomicUsize::new(0))).collect(); + let (bus0, bus1) = (Isp1040::new_bus(), Isp1040::new_bus()); + bus0.attach(1, Box::new(CountingDisk(counters[0].clone()))).unwrap(); + bus1.attach(1, Box::new(CountingDisk(counters[1].clone()))).unwrap(); + bus1.attach(2, Box::new(CountingDisk(counters[2].clone()))).unwrap(); + let r = Rig::on(Isp1040::new_12160(bus0, bus1)); + r.init_rings(16, 16); + Held { r, ch0: counters[0].clone(), ch1: counters[1].clone(), ch1_other: counters[2].clone() } + } + /// A read whose payload store the fabric refuses: the adapter keeps it outstanding, which + /// is what the abort and reset commands act on. + fn hold(&self, slot: u32, handle: u32, target: u8) { + self.r.put_entry(slot, &request(handle, target, &cdb10(scsi_cmd::READ_10, 0, 2), + &[(BUF, BLOCK_SIZE as u32), (MEM as u32, BLOCK_SIZE as u32)])); + self.r.doorbell(slot as u16 + 1); + assert_eq!(g32(&self.r.status(slot), 4), 0, "handle {handle:#x} is held, not completed"); + } + fn command(&self, regs: &[u16]) -> u16 { + let status = self.r.cmd(regs); + self.r.isp.wait_idle(); + status + } + /// The completion posted in response slot `slot`: (handle, completion code). + fn answer(&self, slot: u32) -> (u32, u16) { + let s = self.r.status(slot); + (g32(&s, 4), g16(&s, 10)) + } +} + +const CH1: u16 = 0x8000; + +/// A driver sizing its queues and recognising the part reads these before any ring exists. +#[test] +fn the_isp12160_reports_its_own_identity_firmware_and_unmapped_registers() { + let r = rig12160(); + assert_eq!(r.r(reg::ID_HIGH), 0x1216); + assert_eq!(r.r(reg::BUS_CFG0), 0, "no revision field on this part"); + r.w(reg::DMA_CMD_CFG, 0xffff); + r.w(reg::DMA_DATA_CFG, 0xffff); + assert_eq!((r.r(reg::DMA_CMD_CFG), r.r(reg::DMA_DATA_CFG)), (0, 0), "the flat ISP1040 DMA blocks are not here"); + + // Mailbox 6 is seeded because an untouched output would also read zero. + r.w(r.mb(6), 0x1234); + r.w(r.mb(0), mbox::ABOUT_FIRMWARE); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!([r.r(r.mb(1)), r.r(r.mb(2))], [10, 4]); + assert_eq!(r.r(r.mb(6)), 0, "the attribute word is cleared, not echoed"); + r.ack(); + r.w(r.mb(0), mbox::GET_FIRMWARE_STATUS); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!([r.r(r.mb(1)), r.r(r.mb(2))], [0, MAX_REQUESTS]); + r.ack(); +} + +/// The driver switches register banks through bus configuration 1 bits 9:8; each of the +/// four selections must show its own words, and no other bit may stick. +#[test] +fn the_isp12160_selects_four_distinct_banks_and_keeps_only_the_bank_field() { + let r = rig12160(); + let selections = [0x0000, 0x0100, 0x0200, 0x0300]; + for sel in selections { + r.w(reg::BUS_CFG1, sel); + r.w(reg::BANK_BASE, 0x1000 + sel); + r.w(reg::BANK_BASE + 0x20, 0x2000 + sel); + } + for sel in selections { + r.w(reg::BUS_CFG1, sel); + assert_eq!(r.r(reg::BANK_BASE), 0x1000 + sel, "bank {sel:#x}"); + assert_eq!(r.r(reg::BANK_BASE + 0x20), 0x2000 + sel, "bank {sel:#x}"); + } + r.w(reg::BUS_CFG1, 0xb2c5); + assert_eq!(r.r(reg::BUS_CFG1), 0x0200); + + // A soft reset hands a re-initialising driver empty banks, not the previous run's words. + r.w(reg::BUS_CTRL, CTRL_SOFT_RESET); + assert_eq!(r.r(reg::BUS_CFG1), 0); + for sel in selections { + r.w(reg::BUS_CFG1, sel); + assert_eq!(r.r(reg::BANK_BASE), 0, "bank {sel:#x} after reset"); + } +} + +/// A second command submitted before the driver acknowledged the first result must not +/// run, or it would overwrite the answer the driver has not read. +#[test] +fn the_isp12160_ignores_a_mailbox_submission_while_the_result_is_unacknowledged() { + let r = rig12160(); + r.w(r.mb(0), mbox::NOP); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!(r.r(reg::SEMAPHORE) & SEMAPHORE_LOCK, SEMAPHORE_LOCK); + + r.w(r.mb(1), 0x9999); + r.w(r.mb(0), mbox::ABOUT_FIRMWARE); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::ABOUT_FIRMWARE, "the second submission did not run"); + assert_eq!(r.r(r.mb(1)), 0x9999); + + r.ack(); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!(r.r(r.mb(1)), 10, "acknowledged, the same submission runs"); + r.ack(); +} + +/// An empty slot on either channel times out on its own bus; a driver probing channel 1 +/// must see an absent target, not a hang or the channel 0 answer. +#[test] +fn a_missing_target_times_out_on_each_channel() { + let r = rig12160(); + r.init_rings(16, 16); + r.clock(0); + // An absent target answers once its selection timeout has run out on + // the adapter's clock. + let mut now = 0; + for (slot, (handle, target)) in [(0x31, 5), (0x32, 0x80 | 5)].into_iter().enumerate() { + r.put_entry(slot as u32, &request(handle, target, &TUR, &[])); + r.doorbell(slot as u16 + 1); + assert_ne!(g32(&r.status(slot as u32), 4), handle, "nothing before the selection timeout"); + now += DEFAULT_SELECTION_TIMEOUT as i64 * MS; + r.clock(now); + let s = r.status(slot as u32); + assert_eq!(g32(&s, 4), handle); + assert_eq!(g16(&s, 10), IOCB_INCOMPLETE); + assert_eq!(g16(&s, 12), STATE_GOT_BUS); + assert_eq!(g16(&s, 14), STATUS_FLAG_TIMEOUT); + } +} + +/// A command refused on channel 1 must not disturb its neighbour on channel 0, and +/// a write must land on the disk of the channel it named. +#[test] +fn a_refused_command_on_channel_1_leaves_channel_0_and_the_media_alone() { + let r = disks_on_both_channels("refuse"); + let mut seq = Seq::new(&r); + // Collect each disk's power-on unit attention so the reads below see real answers. + seq.run(0xa0, 1, &TUR, &[]); + seq.run(0xa1, 0x81, &TUR, &[]); + + r.mem.put(BUF, &[0x5c; BLOCK_SIZE]); + let beyond_disk = seq.run(0xb1, 0x81, &cdb10(scsi_cmd::READ_10, 1000, 1), &[(BUF, BLOCK_SIZE as u32)]); + assert_eq!(g16(&beyond_disk, 8) & 0xff, CHECK_CONDITION as u16); + assert_eq!(r.mem.get(BUF, BLOCK_SIZE), vec![0x5c; BLOCK_SIZE], "the refused read stored nothing"); + + let good = seq.run(0xb0, 1, &cdb10(scsi_cmd::READ_10, 0, 1), &[(BUF + 0x1000, BLOCK_SIZE as u32)]); + assert_eq!(g16(&good, 10), IOCB_COMPLETE); + assert_eq!(r.mem.get(BUF + 0x1000, BLOCK_SIZE), vec![0xa0; BLOCK_SIZE], "channel 0 still reads its own disk"); + + r.mem.put(BUF + 0x2000, &[0x77; BLOCK_SIZE]); + seq.run(0xc0, 0x81, &cdb10(scsi_cmd::WRITE_10, 3, 1), &[(BUF + 0x2000, BLOCK_SIZE as u32)]); + seq.run(0xc1, 0x81, &cdb10(scsi_cmd::READ_10, 3, 1), &[(BUF + 0x3000, BLOCK_SIZE as u32)]); + seq.run(0xc2, 1, &cdb10(scsi_cmd::READ_10, 3, 1), &[(BUF + 0x4000, BLOCK_SIZE as u32)]); + assert_eq!(r.mem.get(BUF + 0x3000, BLOCK_SIZE), vec![0x77; BLOCK_SIZE], "channel 1 holds what was written"); + assert_eq!(r.mem.get(BUF + 0x4000, BLOCK_SIZE), vec![0; BLOCK_SIZE], "channel 0's block 3 is untouched"); +} + +/// Abort device names a channel in the top bit of its selector; the same target number +/// held on the other bus must stay outstanding. +#[test] +fn abort_device_retires_only_the_named_channels_commands() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::ABORT_DEVICE, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_ABORTED)); + assert_eq!(h.answer(1), (0, 0), "channel 0's command is still held"); + // The survivor is still the adapter's to abort. + assert_eq!(h.command(&[mbox::ABORT_DEVICE, 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(1), (0x41, IOCB_ABORTED)); +} + +/// Abort command names one handle: two commands to the same channel, target and unit differ +/// only by it, and a selector that matches a held handle on the wrong channel is an error. +#[test] +fn abort_command_matches_the_handle_and_the_whole_selector() { + let h = Held::new(); + h.hold(0, 0x51, 0x81); + h.hold(1, 0x52, 0x81); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x52]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x52, IOCB_ABORTED)); + assert_eq!(h.answer(1), (0, 0), "its neighbour on the same target is untouched"); + + assert_eq!(h.command(&[mbox::ABORT_COMMAND, 0x0100, 0, 0x51]), mbox::COMMAND_ERROR, + "a held handle named on the wrong channel is not a match"); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x51]), mbox::COMPLETE); + assert_eq!(h.answer(1), (0x51, IOCB_ABORTED)); +} + +/// A driver clearing a device it already drained must read success, while naming a handle +/// nothing is using must read as an error rather than claim a retirement. +#[test] +fn aborting_nothing_is_complete_but_aborting_an_unknown_handle_is_refused() { + let h = Held::new(); + assert_eq!(h.command(&[mbox::ABORT_DEVICE, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x77]), mbox::COMMAND_ERROR); + assert_eq!(h.answer(0), (0, 0), "neither posted a completion"); +} + +/// Target reset reaches one target on one channel; the same number on the other channel and +/// the neighbour on the same channel must not see it, and the reset target keeps serving. +#[test] +fn target_reset_is_scoped_to_one_target_on_one_channel() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::ABORT_TARGET, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_RESET)); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!([h.ch0.load(Ordering::Relaxed), h.ch1.load(Ordering::Relaxed), h.ch1_other.load(Ordering::Relaxed)], + [0, 1, 0]); + + h.r.put_entry(2, &request(0x43, 0x81, &TUR, &[])); + h.r.doorbell(3); + assert_eq!(h.answer(1), (0x43, IOCB_COMPLETE)); +} + +/// Bus reset takes its channel from mailbox 2; mailbox 1 is the reset delay and here holds a value +/// that is not even a channel. The whole named bus resets, the other one does not. +#[test] +fn bus_reset_takes_its_channel_from_mailbox_two_and_resets_every_target_on_it() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::BUS_RESET, 3, 1]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_RESET)); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!([h.ch0.load(Ordering::Relaxed), h.ch1.load(Ordering::Relaxed), h.ch1_other.load(Ordering::Relaxed)], + [0, 1, 1]); + assert_eq!(h.command(&[mbox::BUS_RESET, 0, 2]), mbox::PARAM_ERROR, "there is no third bus"); +} + +/// A bus reset leaves a unit attention on the disks of that channel only; the driver's +/// first command afterwards is expected to collect it. +#[test] +fn a_bus_reset_leaves_a_unit_attention_on_the_reset_channel_only() { + let r = disks_on_both_channels("uareset"); + let mut seq = Seq::new(&r); + seq.run(0x10, 1, &TUR, &[]); + seq.run(0x11, 0x81, &TUR, &[]); + assert_eq!(g16(&seq.run(0x12, 0x81, &TUR, &[]), 8) & 0xff, 0, "attention was collected"); + + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 1]), mbox::COMPLETE); + r.isp.wait_idle(); + let after = seq.run(0x13, 0x81, &TUR, &[]); + assert_eq!(g16(&after, 8) & 0xff, CHECK_CONDITION as u16); + assert_eq!(after[34] & 0x0f, 0x06, "unit attention"); + let other = seq.run(0x14, 1, &TUR, &[]); + assert_eq!(g16(&other, 8) & 0xff, 0, "channel 0 was not reset"); + assert_eq!(g16(&seq.run(0x15, 0x81, &TUR, &[]), 8) & 0xff, 0, "reported once"); +} + // ---- the hold-off after a bus reset --------------------------------------- // // Datasheet, "Bus-reset hold-off (0x0018)": after the reset the chip starts no @@ -1651,6 +2050,87 @@ fn a_bus_reset_puts_a_disks_write_cache_back() { assert_eq!(r.mem.get(BUF + 6, 1)[0] & 0x04, 0x04, "WCE is back"); } +/// A 12160 with a cached disk at target 1 on each channel whose guests cleared WCE on both. +fn cleared_write_cache_on_both_channels(name: &str) -> Rig { + let r = rig12160(); + for channel in 0..2u8 { + let bus = r.isp.bus_for_channel(channel).unwrap(); + bus.attach_cached_image(1, &image(&format!("{name}{channel}"), 64)).expect("attach"); + bus.select_write_cache(1, false); + assert!(!bus.write_cache_reported(1), "channel {channel}: the guest cleared it"); + } + r +} + +/// The write cache bit MODE SENSE(6) of the caching page reports for target 1 on `channel`. A TUR +/// goes first: it collects the reset's unit attention, and consumes the reset flag as a guest's +/// first command does, so the mode sense that follows returns its page. +fn wce_after_next_command(seq: &mut Seq, channel: u8, handle: u32) -> bool { + let target = channel << 7 | 1; + seq.run(handle, target, &TUR, &[]); + let done = seq.run(handle + 0x100, target, &[0x1a, 0x08, 0x08, 0, 24, 0], &[(BUF, 24)]); + assert_eq!(g16(&done, 10), IOCB_COMPLETE); + assert_eq!(seq.r.mem.get(BUF + 4, 1)[0], 0x08, "caching page"); + seq.r.mem.get(BUF + 6, 1)[0] & 0x04 != 0 +} + +/// Each channel's disks put their write cache back on that channel's reset and not on the +/// other's, so the flag has to be set for the channel the mailbox names. +#[test] +fn a_12160_bus_reset_of_channel_1_restores_write_cache_on_channel_1_only() { + let r = cleared_write_cache_on_both_channels("wce12160-ch1"); + let mut seq = Seq::new(&r); + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 1]), mbox::COMPLETE); + r.isp.wait_idle(); + assert!(!wce_after_next_command(&mut seq, 0, 0x31), "channel 0 was not reset"); + assert!(wce_after_next_command(&mut seq, 1, 0x32), "channel 1 was reset"); +} + +#[test] +fn a_12160_bus_reset_of_channel_0_restores_write_cache_on_channel_0_only() { + let r = cleared_write_cache_on_both_channels("wce12160-ch0"); + let mut seq = Seq::new(&r); + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 0]), mbox::COMPLETE); + r.isp.wait_idle(); + assert!(!wce_after_next_command(&mut seq, 1, 0x33), "channel 1 was not reset"); + assert!(wce_after_next_command(&mut seq, 0, 0x34), "channel 0 was reset"); +} + +/// Both reset the chip's SCSI buses, which the disks see as a bus reset on each channel. +#[test] +fn a_12160_chip_reset_restores_write_cache_on_both_channels() { + let r = cleared_write_cache_on_both_channels("wce12160-chip"); + r.isp.reset(); + r.isp.wait_idle(); + let mut seq = Seq::new(&r); + assert!(wce_after_next_command(&mut seq, 0, 0x35), "channel 0"); + assert!(wce_after_next_command(&mut seq, 1, 0x36), "channel 1"); +} + +#[test] +fn a_12160_soft_reset_restores_write_cache_on_both_channels() { + let r = cleared_write_cache_on_both_channels("wce12160-soft"); + r.w(reg::BUS_CTRL, CTRL_SOFT_RESET); + r.isp.wait_idle(); + let mut seq = Seq::new(&r); + assert!(wce_after_next_command(&mut seq, 0, 0x37), "channel 0"); + assert!(wce_after_next_command(&mut seq, 1, 0x38), "channel 1"); +} + +/// The single-bus part has no bus 1: a mailbox that names it is ignored, and the one bus it has +/// keeps its disks' write cache as the guest set it. +#[test] +fn an_isp1040_bus_reset_naming_bus_1_leaves_the_reset_flag_unset() { + let r = Rig::bare(); + r.isp.bus().attach_cached_image(1, &image("wce-bus1", 64)).expect("attach"); + r.init_rings(4, 4); + r.isp.bus().select_write_cache(1, false); + bus_reset(&r, 0, 1); + r.isp.wait_idle(); + assert!(!r.isp.targets_reset[0].load(Ordering::Acquire)); + assert!(!r.isp.targets_reset[1].load(Ordering::Acquire)); +} + /// A TUR for the disk at target 1. fn tur1(handle: u32) -> [u8; ENTRY_SIZE] { request(handle, 1, &[0, 0, 0, 0, 0, 0], &[]) diff --git a/src/dev/isp12160.rs b/src/dev/isp12160.rs new file mode 100644 index 000000000..9fcf55ab1 --- /dev/null +++ b/src/dev/isp12160.rs @@ -0,0 +1,338 @@ +//! The QLogic ISP12160: two SCSI buses behind one PCI function. +//! +//! The ISP12160 shares the ISP1040 ring, DMA, and mailbox mechanism, while +//! selecting targets from either channel. Each channel remains an explicit +//! [`ScsiBus`], including for monitor and run-time disc operations. +//! +//! Ported from hw/scsi/qlogic-isp.c and include/hw/scsi/qlogic-isp.h +//! in jamesbraid/qemu-sgi-origami. + +use std::collections::VecDeque; +use std::ops::Deref; + +use parking_lot::MutexGuard; + +use crate::dev::isp1040::{ + mbox, reg, Bank, Core, Isp1040, IspPart, State, CFG1_BANK_SELECT, FW_VERSION, IOCB_ABORTED, IOCB_RESET, MAILBOX_COUNT, + MAX_REQUESTS, MAX_TARGET, PCI_CLASS_SCSI, PCI_VENDOR_QLOGIC, SEMAPHORE_LOCK, STATE_ALL_PHASES, +}; +use crate::irq::IrqLine; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciRegisterPolicy, PCI_INTA}; +use crate::scsi::{ScsiBus, ScsiIdentities, ScsiIdentity}; +use crate::snapshot::{get_field, hex_u16, hex_u32, load_u16_slice, toml_u16, toml_u32, toml_u8, u16_slice_to_toml}; +use crate::traits::{Device, Resettable, Saveable}; +use crate::workstation::DiscChanger; + +const ISP_DISK: ScsiIdentity = ScsiIdentity::new(b"SGI ", b"IRIS EMULATED ", b"1.0 "); +const ISP_CDROM: ScsiIdentity = ISP_DISK; + +pub(crate) struct Isp12160Part; + +impl IspPart for Isp12160Part { + fn name(&self) -> &'static str { "ISP12160" } + fn channels(&self) -> usize { 2 } + fn pci_config(&self, bars: [Bar; 6]) -> PciConfig { + let registers = [ + PciRegisterPolicy { reg_dword: 13, initial: 0x44, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: 17, initial: 0x0001_0001, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: 18, initial: 0, writable_mask: 0x100 }, + ]; + PciConfig::new_with_policy( + PCI_VENDOR_QLOGIC, + 0x1216, + PCI_CLASS_SCSI, + 6, + bars, + PCI_INTA, + PciConfigPolicy { + status: 0x02b0, + status_w1c_mask: 0, + command_mask: 0x0156, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: ®isters, + }, + ) + } + fn firmware_version(&self) -> [u16; 3] { [10, 4, FW_VERSION[2]] } + fn active_bank(&self, cfg1: u16) -> Bank { + match (cfg1 >> 8) & 3 { + 1 => Bank::Sxp, + 2 => Bank::Sxp1, + 3 => Bank::Dma, + _ => Bank::Risc, + } + } + fn checks_ram_bounds(&self) -> bool { true } + fn has_abort_commands(&self) -> bool { true } + fn target_on(&self, channel: u8, target: u8) -> u8 { target | (channel << 4) } + fn decode_target(&self, target: u8) -> (usize, u8) { ((target >> 4) as usize, target & 0x0f) } + fn id_high(&self) -> u16 { 0x1216 } + fn bus_cfg0(&self) -> u16 { 0 } + fn cfg1_write_mask(&self) -> u16 { 0x0300 } + fn unmodelled_dma_register(&self, off: u32) -> bool { + matches!(off, reg::DMA_CMD_CFG | reg::DMA_CMD_CTRL | reg::DMA_DATA_CFG | reg::DMA_DATA_CTRL) + } + fn completion_phases(&self, moved: u64, total: u64) -> u16 { + let mut flags = 0x1700; + if moved != 0 { + flags |= 0x0800; + if moved == total { flags |= 0x4000; } + } + flags + } + fn mailbox_ready(&self, semaphore: u16) -> bool { semaphore & SEMAPHORE_LOCK == 0 } + fn clear_target_parameters(&self, mailbox: &mut [u16; MAILBOX_COUNT]) { mailbox[6] = 0; } + fn set_reset_delay(&self, st: &mut State) { + st.reset_delay = st.mailbox[1]; + st.reset_delay1 = st.mailbox[2]; + } + fn reset_targets(&self, st: &mut State) -> bool { + st.reset_targets = true; + true + } + fn execute_mailbox(&self, core: &Core, st: &mut MutexGuard<'_, State>) -> bool { + let cmd = st.mailbox[0]; + match cmd { + mbox::LOAD_RAM_A64 => st.mailbox[0] = core.ram_transfer(st, true), + mbox::ABORT_COMMAND | mbox::ABORT_DEVICE | mbox::ABORT_TARGET => { + let device = st.mailbox[1]; + let valid_target = (device >> 8) & 0x7f <= MAX_TARGET as u16; + let target = ((device >> 8) & 0x7f) as u8 | (((device >> 15) as u8) << 4); + let lun = device as u8; + let handle = ((st.mailbox[2] as u32) << 16) | st.mailbox[3] as u32; + let hit = valid_target && st.active.is_some_and(|(t, l, h)| t == target + && (cmd == mbox::ABORT_TARGET || l == lun) && (cmd != mbox::ABORT_COMMAND || h == handle)); + let completion = if cmd == mbox::ABORT_TARGET { IOCB_RESET } else { IOCB_ABORTED }; + if hit { core.cancel_active(st, completion); } + let retired = core.retire_outstanding(st, |(t, l, h)| valid_target && t == target + && (cmd == mbox::ABORT_TARGET || l == lun) && (cmd != mbox::ABORT_COMMAND || h == handle), completion); + if cmd == mbox::ABORT_TARGET && valid_target { + st.pending_target_resets.push_back((target >> 4, Some(target & 15))); + } + st.mailbox[0] = if cmd == mbox::ABORT_COMMAND && !hit && retired == 0 { mbox::COMMAND_ERROR } else { mbox::COMPLETE }; + } + // Mailbox 1 is the reset delay; mailbox 2 selects the bus. + mbox::BUS_RESET => { + let channel = st.mailbox[2]; + if channel > 1 { st.mailbox[0] = mbox::PARAM_ERROR; } + else { + if st.active.is_some_and(|(target, _, _)| target >> 4 == channel as u8) { + core.cancel_active(st, IOCB_RESET); + } + core.retire_outstanding(st, |(target, _, _)| target >> 4 == channel as u8, IOCB_RESET); + st.pending_target_resets.push_back((channel as u8, None)); + core.note_bus_reset(usize::from(channel)); + core.kick(st); + st.mailbox[0] = mbox::COMPLETE; + } + } + _ => return false, + } + true + } + fn save_extra(&self, st: &State, t: &mut toml::map::Map) { + t.insert("reset_targets".into(), toml::Value::Boolean(st.reset_targets)); + t.insert("target_resets".into(), toml::Value::Array(st.pending_target_resets.iter().map(|(channel, target)| { + toml::Value::Array(vec![hex_u16(*channel as u16), hex_u16(target.unwrap_or(0xff) as u16)]) + }).collect())); + t.insert("sxp1_bank".into(), u16_slice_to_toml(&st.sxp1_bank)); + t.insert("dma_bank".into(), u16_slice_to_toml(&st.dma_bank)); + t.insert("reset_delay1".into(), hex_u16(st.reset_delay1)); + t.insert("outstanding".into(), toml::Value::Array(st.outstanding.iter().map(|&(target, lun, handle)| { + toml::Value::Array(vec![hex_u16(target as u16), hex_u16(lun as u16), hex_u32(handle)]) + }).collect())); + } + fn load_extra(&self, st: &mut State, v: &toml::Value) { + st.reset_targets = get_field(v, "reset_targets").and_then(|x| x.as_bool()).unwrap_or(false); + if let Some(toml::Value::Array(items)) = get_field(v, "target_resets") { + for item in items.iter().take(64) { + if let toml::Value::Array(fields) = item { + if fields.len() == 2 { + if let (Some(channel), Some(target)) = (toml_u8(&fields[0]), toml_u8(&fields[1])) { + if channel < 2 && (target < 16 || target == 0xff) { + st.pending_target_resets.push_back((channel, (target < 16).then_some(target))); + } + } + } + } + } + } + let u16f = |k: &str| get_field(v, k).and_then(toml_u16).unwrap_or(0); + st.reset_delay1 = u16f("reset_delay1"); + if let Some(m) = get_field(v, "sxp1_bank") { load_u16_slice(m, &mut st.sxp1_bank); } + if let Some(m) = get_field(v, "dma_bank") { load_u16_slice(m, &mut st.dma_bank); } + if let Some(toml::Value::Array(items)) = get_field(v, "outstanding") { + for item in items.iter().take(MAX_REQUESTS as usize) { + if let toml::Value::Array(fields) = item { + if fields.len() == 3 { + if let (Some(target), Some(lun), Some(handle)) = + (toml_u8(&fields[0]), toml_u8(&fields[1]), toml_u32(&fields[2])) { + if target < 32 && lun < 8 { st.outstanding.push((target, lun, handle)); } + } + } + } + } + } + } +} + +/// The two-channel controller. The shared ISP engine owns both buses. +pub struct Isp12160(Isp1040); + +impl Default for Isp12160 { + fn default() -> Self { Self::new(Self::new_bus(), Self::new_bus()) } +} + +impl Isp12160 { + /// Light the status bar's SCSI activity bits in `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: std::sync::Arc) { + self.0.set_heartbeat(heartbeat); + } + + /// A 16-id bus with the ISP controller's disk and CD-ROM identities. + pub fn new_bus() -> ScsiBus { + ScsiBus::new(16, ScsiIdentities { disk: ISP_DISK, cdrom: ISP_CDROM }) + } + + /// The controller with its two channel buses already populated. + pub fn new(bus0: ScsiBus, bus1: ScsiBus) -> Self { + Self(Isp1040::new_12160(bus0, bus1)) + } + + /// Borrow one channel for the shared monitor commands. + pub fn channel(&self, channel: u8) -> Option> { + (channel < 2).then_some(Channel { adapter: self, channel }) + } + + pub fn register_locks(&self) { self.0.register_locks(); } +} + +impl Deref for Isp12160 { + type Target = Core; + fn deref(&self) -> &Core { &self.0 } +} + +/// One channel, with SCSI ids 0–15 as the monitor presents them. +pub struct Channel<'a> { + adapter: &'a Isp12160, + channel: u8, +} + +impl crate::scsi_monitor::ScsiTargets for Channel<'_> { + fn scsi_bus(&self) -> &ScsiBus { + self.adapter.bus_for_channel(self.channel).expect("valid ISP12160 channel") + } +} + +/// The machine API flattens the ISP12160's channels to ids 0–31. +/// Monitor commands use a `Channel` instead, so their ids remain 0–15. +impl DiscChanger for Isp12160 { + fn disc_status(&self) -> Vec<(usize, String, Vec, u64, u64)> { + (0..2).flat_map(|channel| { + crate::scsi_monitor::disc_status(&self.channel(channel).expect("valid ISP12160 channel")) + .into_iter() + .map(move |(id, current, discs, physical, logical)| (id + channel as usize * 16, current, discs, physical, logical)) + }).collect() + } + + fn load_disc(&self, id: usize, path: String) -> Result { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::load_disc(&self.channel(channel).unwrap(), target, path) + } + + fn eject_disc(&self, id: usize) -> Result { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::eject_disc(&self.channel(channel).unwrap(), target) + } + + fn eject_to_empty(&self, id: usize) -> Result<(), String> { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::eject_to_empty(&self.channel(channel).unwrap(), target) + } +} + +impl Isp12160 { + fn split_id(id: usize) -> Result<(u8, usize), String> { + if id >= 32 { return Err(format!("SCSI ID {id} is out of range")); } + Ok(((id / 16) as u8, id % 16)) + } +} + +impl Device for Isp12160 { + fn step(&self, cycles: u64) { Device::step(&self.0, cycles) } + fn start(&self) { Device::start(&self.0) } + fn stop(&self) { Device::stop(&self.0) } + fn is_running(&self) -> bool { Device::is_running(&self.0) } + fn get_clock(&self) -> u64 { Device::get_clock(&self.0) } + + fn register_commands(&self) -> Vec<(String, String)> { + let usage = crate::scsi_monitor::SCSI_USAGE; + vec![ + ("scsi".into(), format!("SCSI (QLogic ISP12160, channel 0): {usage} | scsi notes")), + ("scsi0".into(), "Alias for `scsi` (ISP12160 channel 0)".into()), + ("cow".into(), "COW overlays on ISP12160 channel 0: cow status | cow commit [id] | cow reset [id]".into()), + ("scsi1".into(), format!("SCSI (QLogic ISP12160, channel 1): {usage}")), + ("cow1".into(), "COW overlays on ISP12160 channel 1: same subcommands as `cow`".into()), + ] + } + + fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { + use std::io::Write as _; + let (channel, cow) = match cmd { + "scsi" | "scsi0" | "cow" => (0, cmd.starts_with("cow")), + "scsi1" | "cow1" => (1, cmd.starts_with("cow")), + _ => return Err("Command not found".into()), + }; + let ctl = self.channel(channel).expect("valid ISP12160 channel"); + if cow { + return crate::scsi_monitor::cow_command(&ctl, args, &mut *w); + } + if let Some(done) = crate::scsi_monitor::scsi_subcommand(&ctl, args, &mut *w) { + return done; + } + if channel == 0 && args.first().copied() == Some("notes") { + let _ = writeln!(w, "QLogic ISP12160: {} commands", self.total_commands()); + for note in self.notes() { let _ = writeln!(w, " note: {note}"); } + return Ok(()); + } + Err(format!("Usage: {}", crate::scsi_monitor::SCSI_USAGE)) + } +} + +impl Resettable for Isp12160 { + fn power_on(&self) { + self.0.reset(); + PciBusDevice::config(&self.0).reset(); + } +} + +impl Saveable for Isp12160 { + fn save_state(&self) -> toml::Value { Saveable::save_state(&self.0) } + fn load_state(&self, v: &toml::Value) -> Result<(), String> { Saveable::load_state(&self.0, v) } +} + +impl PciBusDevice for Isp12160 { + fn config(&self) -> &PciConfig { PciBusDevice::config(&self.0) } + fn bar_read(&self, bar: usize, off: u32, width: usize) -> u32 { PciBusDevice::bar_read(&self.0, bar, off, width) } + fn bar_write(&self, bar: usize, off: u32, val: u32, width: usize) { PciBusDevice::bar_write(&self.0, bar, off, val, width) } + fn connect_interrupt(&self, line: IrqLine) { PciBusDevice::connect_interrupt(&self.0, line) } + fn connect_dma(&self, dma: std::sync::Arc) { PciBusDevice::connect_dma(&self.0, dma) } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The machine disc API uses target 17 for channel 1, target 1. + #[test] + fn disc_status_flattens_the_second_channels_target_ids() { + let bus0 = Isp12160::new_bus(); + let bus1 = Isp12160::new_bus(); + bus1.attach_device(1, "", true, Vec::new(), false, None, false).unwrap(); + let isp = Isp12160::new(bus0, bus1); + + let status = DiscChanger::disc_status(&isp); + assert_eq!(status.iter().map(|drive| drive.0).collect::>(), vec![17]); + } +} diff --git a/src/dev/mace_pci.rs b/src/dev/mace_pci.rs index ff1f41c91..227bbe03e 100644 --- a/src/dev/mace_pci.rs +++ b/src/dev/mace_pci.rs @@ -8,10 +8,11 @@ use std::sync::{Arc, Mutex, OnceLock, RwLock}; use crate::irq::{InterruptController, IrqLine}; use crate::dev::crime_mem::O2Memory; -use crate::pci::{bar_runs, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; +use crate::pci::{bar_runs, plug, BarAt, BarRun, IntxTable, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; use crate::snapshot::{get_field, hex_u32, load_u32_slice, toml_u32, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead32, BusRead64, Saveable, BUS_OK}; use crate::ip32::*; +use crate::pci_addin::{BusDesc, BusSlot, Evidence, SlotDesc}; pub mod macepci_reg { pub const ERROR_ADDR: u32 = 0x0000; @@ -32,6 +33,36 @@ pub const MACEPCI_REVISION_VALUE: u32 = 1; pub const IP32_SCSI_SLOT: u8 = 1; pub const IP32_SCSI_SLOT_B: u8 = 2; +/// The one add-in slot: the PCI board position in the O2's tray, device 3 +/// (slot 0 in the MACE specification's tables). Its INTA is CRIME input 10 +/// and INTB to INTD the shared inputs 13 to 15; Linux, OpenBSD and NetBSD +/// route them the same way. +pub const IP32_PCI_SLOT: u8 = 3; +pub const IP32_PCI_CRIME_INT: u32 = 10; +/// CRIME's inputs for a slot's INTB, INTC and INTD. They are shared: any +/// device's pin can be routed to one, so each is the OR of its pins. +pub const IP32_PCI_CRIME_SHARED: u32 = 13; + +/// The O2's PCI bus, for the configuration and the `pci slots` command. The +/// slot's four pins are routed as the MACE specification has them; what +/// the board's connector wires INTB to INTD to is not known. +pub const BUSES: &[BusDesc] = &[BusDesc { + id: "pci", + title: "the O2's PCI bus", + slots: &[ + BusSlot::Onboard { device: IP32_SCSI_SLOT, name: "internal SCSI controller (AIC-7880)" }, + BusSlot::Onboard { device: IP32_SCSI_SLOT_B, name: "external SCSI controller (AIC-7880)" }, + BusSlot::AddIn(SlotDesc { + device: IP32_PCI_SLOT, + label: "", + evidence: Evidence::Established, + source: "MACE specification, PCI interrupt input tables (Tables 85 and 87, pp. 164 and 166; Table 90, p. 179), which put device 3 (slot 0) on CRIME inputs 10, 13, 14 and 15; Linux arch/mips/pci/fixup-ip32.c (irq_tab_mace), OpenBSD macepcibridge.c (mace_pcibr_intr_map) and NetBSD macepci_intr_map agree; the board connector's wiring of INTB to INTD is unknown; O2 Workstation Hardware Reference Guide 007-3275-003, installing a PCI board", + settled_by: "", + }), + ], + no_add_in: "", +}]; + /// Which of CRIME's interrupt inputs the pin of the function in that slot /// drives. NetBSD works this out for itself and prints it: "interrupting at /// crime interrupt 8". @@ -43,53 +74,17 @@ fn crime_input(slot: u8, pin: u8) -> Option { match (slot, pin) { (IP32_SCSI_SLOT, 1) => Some(IP32_SCSI_CRIME_INT), (IP32_SCSI_SLOT_B, 1) => Some(IP32_SCSI_CRIME_INT_B), + (IP32_PCI_SLOT, 1) => Some(IP32_PCI_CRIME_INT), + // INTB to INTD, on the shared inputs 13 to 15, where the kernels + // look for them. Whether the board's connector wires them is unknown. + (IP32_PCI_SLOT, pin @ 2..=4) => Some(IP32_PCI_CRIME_SHARED + pin as u32 - 2), _ => None, } } /// Bus 0 has 32 device numbers, each with four interrupt pins. -const PCI_PINS: usize = 32 * 4; - -/// The bridge's interrupt router. Each slot's INTA..INTD is a line -/// (`slot * 4 + pin - 1`); the router keeps every pin's level and drives -/// each system input it routes to with the OR of the pins routed there, so -/// on a shared input one function cannot clear another's interrupt. -struct PciIrqRouter { - /// Where the system inputs are: CRIME. - intc: Arc, - /// Slot and pin (1 = INTA) to system input. - route: fn(u8, u8) -> Option, - levels: Mutex<[bool; PCI_PINS]>, -} - -impl PciIrqRouter { - fn line(slot: u8, pin: u8) -> u32 { - slot as u32 * 4 + (pin as u32 - 1) - } - - fn slot_pin(line: u32) -> (u8, u8) { - ((line / 4) as u8, (line % 4) as u8 + 1) - } -} - -impl InterruptController for PciIrqRouter { - fn set_line(&self, line: u32, level: bool) { - if line as usize >= PCI_PINS { - return; - } - let (slot, pin) = Self::slot_pin(line); - let Some(input) = (self.route)(slot, pin) else { return }; - // The lock is held while CRIME is driven, so two pins sharing an - // input cannot reach it out of order. - let mut levels = self.levels.lock().unwrap(); - levels[line as usize] = level; - let any = (0..PCI_PINS as u32).any(|l| { - let (s, p) = Self::slot_pin(l); - levels[l as usize] && (self.route)(s, p) == Some(input) - }); - self.intc.set_line(input, any); - } -} +const PCI_SLOTS: usize = 32; +const PCI_PINS: usize = PCI_SLOTS * 4; /// The CPU-side window onto PCI memory, and the PCI address it starts at. /// @@ -261,7 +256,7 @@ pub struct MacePci { windows: [AtomicU64; MAX_WINDOWS], nwindows: AtomicUsize, /// The slots' interrupt pins, routed to CRIME. - irq: Arc, + irq: Arc, /// Host memory, for the functions that master the bus. dma: Arc, /// Where the runs of the windows a BAR decodes whole go (the machine @@ -278,6 +273,12 @@ impl MacePci { /// A bridge with its own routing of slot pins (1 = INTA) to `intc`'s /// inputs. fn with_routing(intc: Arc, route: fn(u8, u8) -> Option, ram: Arc) -> Self { + let irq = IntxTable::new(intc, PCI_SLOTS); + for slot in 0..PCI_SLOTS as u8 { + for pin in 1..=4 { + irq.route(slot, pin, route(slot, pin)); + } + } Self { config_addr: Mutex::new(0), regs: Mutex::new([0u32; (MACEPCI_SIZE / 4) as usize]), @@ -286,7 +287,7 @@ impl MacePci { funcs: std::array::from_fn(|_| OnceLock::new()), windows: std::array::from_fn(|_| AtomicU64::new(0)), nwindows: AtomicUsize::new(0), - irq: Arc::new(PciIrqRouter { intc, route, levels: Mutex::new([false; PCI_PINS]) }), + irq: Arc::new(irq), dma: Arc::new(HostMemory::new(ram)), bar_sink: RwLock::new(None), bar_entries: Mutex::new(Default::default()), @@ -296,11 +297,7 @@ impl MacePci { /// Put a function in a slot on bus 0, wire its interrupt pin to the /// bridge, which routes it on, and give it host memory to master. pub fn attach(&self, slot: u8, name: &'static str, dev: Arc) { - dev.connect_dma(self.dma.clone()); - let pin = dev.config().interrupt_pin(); - if (1..=4).contains(&pin) { - dev.connect_interrupt(IrqLine::new(self.irq.clone(), PciIrqRouter::line(slot, pin))); - } + plug(&*dev, self.dma.clone(), |pin| IrqLine::new(self.irq.clone(), IntxTable::line(slot, pin))); assert!(self.funcs[slot as usize].set(dev.clone()).is_ok(), "pci: slot {slot} is taken"); let mut slots = self.slots.lock().unwrap(); slots.insert(slot, Arc::new(Slot { name, dev, touched: Mutex::new(Vec::new()) })); @@ -363,6 +360,42 @@ impl MacePci { Some((s.name, s.dev.clone(), touched)) } + /// The bus's slots from [`BUSES`], with what is in each and where its + /// interrupt pins go: one line per device number the table knows. + pub fn slots_report(&self) -> Vec { + let bus = &BUSES[0]; + let mut out = vec![format!("{}:", bus.title)]; + for row in bus.slots { + let device = row.device(); + let fitted = self.slot(device).map(|(name, f, _)| { + let id = f.config().read(0); + format!("{name} {:04x}:{:04x}", id & 0xffff, id >> 16) + }); + let line = match row { + BusSlot::Onboard { name, .. } => { + format!(" device {device}: onboard, {name}") + } + BusSlot::AddIn(s) => { + let pins: Vec = (1..=4u8) + .map(|p| match self.irq.input(device, p) { + Some(i) => format!("INT{} CRIME {i}", (b'A' + p - 1) as char), + None => format!("INT{} unrouted", (b'A' + p - 1) as char), + }) + .collect(); + let label = if s.label.is_empty() { String::new() } else { format!(" \"{}\"", s.label) }; + format!( + " device {device}{label}: add-in slot ({}), {}; {}", + s.evidence.name(), + fitted.as_deref().map_or("empty".to_string(), |f| format!("holds {f}")), + pins.join(", ") + ) + } + }; + out.push(line); + } + out + } + /// Whether `addr` is in one of the CPU's windows onto PCI memory. pub fn in_window(addr: u32) -> bool { (PCI_MEM_BASE..PCI_MEM_BASE + PCI_MEM_SIZE).contains(&addr) @@ -579,7 +612,7 @@ impl Saveable for MacePci { let mut t = toml::map::Map::new(); t.insert("config_addr".into(), hex_u32(*self.config_addr.lock().unwrap())); t.insert("regs".into(), u32_slice_to_toml(&*self.regs.lock().unwrap())); - let levels = *self.irq.levels.lock().unwrap(); + let levels = self.irq.levels(); t.insert("pins".into(), toml::Value::Array(levels.iter().map(|&b| toml::Value::Boolean(b)).collect())); let mut slots = toml::map::Map::new(); for (n, s) in self.slots.lock().unwrap().iter() { @@ -620,7 +653,7 @@ impl crate::traits::Device for MacePci { fn get_clock(&self) -> u64 { 0 } fn register_commands(&self) -> Vec<(String, String)> { - vec![("pci".to_string(), "MACE PCI bridge: pci status | pci config ".to_string())] + vec![("pci".to_string(), "MACE PCI bridge: pci status | pci slots | pci config ".to_string())] } fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { @@ -641,7 +674,7 @@ impl crate::traits::Device for MacePci { let bars: Vec = (0..6).map(|i| c.bar_base(i)).filter(|&b| b != 0).map(|b| format!("{b:#x}")).collect(); let irq = match c.interrupt_pin() { 0 => "no interrupt".to_string(), - p => match (self.irq.route)(*slot, p) { + p => match self.irq.input(*slot, p) { Some(l) => format!("INT{} to CRIME {l}", (b'A' + p - 1) as char), None => format!("INT{} unrouted", (b'A' + p - 1) as char), }, @@ -650,6 +683,11 @@ impl crate::traits::Device for MacePci { s.name, id & 0xffff, id >> 16, c.command(), bars.join(", ")).ok(); } } + "slots" => { + for line in self.slots_report() { + writeln!(w, "{line}").ok(); + } + } "config" => { let slot: u8 = args.get(1).and_then(|v| v.parse().ok()).ok_or("Usage: pci config ")?; let (_, f, _) = self.slot(slot).ok_or(format!("pci: nothing in slot {slot}"))?; @@ -658,7 +696,7 @@ impl crate::traits::Device for MacePci { writeln!(w, " {:02x}: {}", row * 16, v.join(" ")).ok(); } } - _ => return Err("Usage: pci status | config ".to_string()), + _ => return Err("Usage: pci status | slots | config ".to_string()), } Ok(()) } @@ -860,4 +898,105 @@ mod tests { lb.set(false); assert_eq!(last(), (12, false)); } + + /// Both onboard SCSI controllers' INTA reach their own CRIME inputs, 8 + /// and 9, and nothing else does. + #[test] + fn the_onboard_scsi_pins_reach_crime_8_and_9() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime.clone(), test_ram()); + let fake = |pin| Arc::new(Fake { + cfg: PciConfig::new(0x9004, 0x8078, 0x0100_00, 1, [Bar::Unused; 6], pin), + last: Mutex::new(None), + irq: std::sync::OnceLock::new(), + }); + let (a, b, c) = (fake(PCI_INTA), fake(PCI_INTA), fake(PCI_INTA + 1)); + pci.attach(IP32_SCSI_SLOT, "a", a.clone()); + pci.attach(IP32_SCSI_SLOT_B, "b", b.clone()); + pci.attach(7, "c", c.clone()); + a.irq.get().unwrap().set(true); + b.irq.get().unwrap().set(true); + c.irq.get().unwrap().set(true); + assert_eq!(*crime.0.lock().unwrap(), vec![(IP32_SCSI_CRIME_INT, true), (IP32_SCSI_CRIME_INT_B, true)]); + } + + /// The table's onboard rows are the slots the machine attaches, and its + /// one add-in slot is where nothing is attached. + #[test] + fn the_bus_table_matches_the_onboard_slots() { + let onboard: Vec = BUSES[0].slots.iter() + .filter_map(|s| match s { BusSlot::Onboard { device, .. } => Some(*device), _ => None }) + .collect(); + assert_eq!(onboard, vec![IP32_SCSI_SLOT, IP32_SCSI_SLOT_B]); + let add_in: Vec = BUSES[0].add_ins().map(|s| s.device).collect(); + assert_eq!(add_in, vec![IP32_PCI_SLOT]); + } + + #[test] + fn pci_slots_lists_every_device_the_table_knows() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime, test_ram()); + let report = pci.slots_report(); + assert_eq!(report.len(), 1 + BUSES[0].slots.len()); + assert!(report[1].contains("device 1: onboard"), "{report:?}"); + assert!(report[3].contains("device 3: add-in slot (established), empty"), "{report:?}"); + } + + fn pin_fake(pin: u8) -> Arc { + Arc::new(Fake { + cfg: PciConfig::new(0x1077, 0x1216, 0x01_0000, 1, [Bar::Unused; 6], pin), + last: Mutex::new(None), + irq: std::sync::OnceLock::new(), + }) + } + + /// Each pin of a card in the add-in slot reaches its own CRIME input: + /// INTA 10, INTB to INTD the shared 13 to 15. + #[test] + fn each_pin_of_the_add_in_slot_reaches_its_crime_input() { + for (pin, input) in [(PCI_INTA, 10), (PCI_INTA + 1, 13), (PCI_INTA + 2, 14), (PCI_INTA + 3, 15)] { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime.clone(), test_ram()); + let card = pin_fake(pin); + pci.attach(IP32_PCI_SLOT, "card", card.clone()); + card.irq.get().unwrap().set(true); + assert_eq!(*crime.0.lock().unwrap(), vec![(input, true)], "pin {pin}"); + card.irq.get().unwrap().set(false); + assert_eq!(crime.0.lock().unwrap().last(), Some(&(input, false)), "pin {pin}"); + } + assert_eq!(IP32_PCI_CRIME_INT, 10); + assert_eq!(IP32_PCI_CRIME_SHARED, 13); + } + + /// A shared input stays up until every pin routed to it has dropped: + /// the slot's INTB and another device's pin on CRIME 13. + #[test] + fn two_pins_on_a_shared_crime_input_or_together() { + fn route(slot: u8, pin: u8) -> Option { + crime_input(slot, pin).or((slot == 5 && pin == 1).then_some(IP32_PCI_CRIME_SHARED)) + } + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::with_routing(crime.clone(), route, test_ram()); + let (slot, other) = (pin_fake(PCI_INTA + 1), pin_fake(PCI_INTA)); + pci.attach(IP32_PCI_SLOT, "slot", slot.clone()); + pci.attach(5, "other", other.clone()); + let last = || *crime.0.lock().unwrap().last().unwrap(); + slot.irq.get().unwrap().set(true); + assert_eq!(last(), (13, true)); + other.irq.get().unwrap().set(true); + slot.irq.get().unwrap().set(false); + assert_eq!(last(), (13, true), "the other device's pin still holds it"); + other.irq.get().unwrap().set(false); + assert_eq!(last(), (13, false)); + } + + #[test] + fn pci_slots_shows_a_fitted_cards_routing() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime, test_ram()); + pci.attach(IP32_PCI_SLOT, "card", pin_fake(PCI_INTA)); + let report = pci.slots_report(); + assert!(report[3].contains("holds card 1077:1216"), "{report:?}"); + assert!(report[3].contains("INTA CRIME 10, INTB CRIME 13, INTC CRIME 14, INTD CRIME 15"), "{report:?}"); + } } diff --git a/src/dev/mod.rs b/src/dev/mod.rs index fdd54e4b1..c8e3177b3 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,3 +60,4 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod isp12160; diff --git a/src/exit_persistence_tests.rs b/src/exit_persistence_tests.rs index f2d507cb1..3b585cd3c 100644 --- a/src/exit_persistence_tests.rs +++ b/src/exit_persistence_tests.rs @@ -678,6 +678,38 @@ fn o2_stop_flushes_its_disks() { }); } +/// A snapshot taken with other PCI cards is refused before anything else is +/// read. The manifest is the only file written, so any other refusal would +/// name something else. +#[test] +fn o2_restore_refuses_a_snapshot_taken_with_other_cards() { + use crate::snapshot::{Manifest, PciRef, Snapshot}; + let _serial = serial(); + on_big_stack(|| { + let dir = Dir::new("o2-cards"); + make_images(&dir); + let mut cfg = config(Machine::O2, &dir, false); + cfg.pci.push(crate::pci_addin::PciEntry::parse_cli("3=isp12160").unwrap()); + let mut m = build(cfg); + let o2 = m.as_any_mut().downcast_mut::().unwrap(); + let snap = Snapshot::new(dir.file("snap")); + snap.ensure_dir().unwrap(); + let mut manifest = Manifest::for_current_save(); + manifest.machine = Some("o2_ip32".into()); + snap.write_manifest(&manifest).unwrap(); + let err = o2.read_snapshot_in(Snapshot::new(dir.file("snap")), "snap").err().expect("refused"); + assert!(err.contains("snapshot PCI cards differ"), "{err}"); + assert!(err.contains("this config has pci bus \"pci\" slot 3 = isp12160, the snapshot has no card there"), "{err}"); + + // The same card in the same slot passes this check; the restore then + // stops at the first file this test did not write. + manifest.pci = vec![PciRef { bus: "pci".into(), slot: 3, card: "isp12160".into(), options: String::new() }]; + snap.write_manifest(&manifest).unwrap(); + let err = o2.read_snapshot_in(Snapshot::new(dir.file("snap")), "snap").err().expect("no state files"); + assert!(!err.contains("PCI cards"), "{err}"); + }); +} + /// The O2 has a second way to switch off, for a machine nothing is /// listening to: its service loop flushes the buses and exits itself. #[test] diff --git a/src/ip30.rs b/src/ip30.rs index 6fd83fdbd..38858aa94 100644 --- a/src/ip30.rs +++ b/src/ip30.rs @@ -31,6 +31,7 @@ use parking_lot::Mutex; use crate::dev::heart_mem::HeartRam; use crate::pci::BarRun; +use crate::pci_addin::{BusDesc, BusSlot}; use crate::physical::Physical; use crate::ppmem::devmap::{entry_eq, SLOT_SHIFT}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_ERR, BUS_OK}; @@ -40,6 +41,22 @@ pub const WIDGET_XBOW: u8 = 0x0; pub const WIDGET_HEART: u8 = 0x8; pub const WIDGET_BRIDGE: u8 = 0xf; +/// The PCI bus on the Bridge, for the configuration. All four devices are +/// onboard; the Octane's PCI boards sit behind their own Bridge on an XIO +/// port, which this table does not describe until its port and device +/// numbers are established. +pub const BUSES: &[BusDesc] = &[BusDesc { + id: "bridge", + title: "the Octane's Bridge", + slots: &[ + BusSlot::Onboard { device: 0, name: "internal SCSI controller" }, + BusSlot::Onboard { device: 1, name: "external SCSI controller" }, + BusSlot::Onboard { device: 2, name: "IOC3 (Ethernet, serial, real-time clock)" }, + BusSlot::Onboard { device: 3, name: "RAD1 audio" }, + ], + no_add_in: "the Octane's PCI boards sit on XIO ports behind a Bridge of their own, and their port and device numbers are not established", +}]; + /// Widget `w`'s small window: 16 MiB at `0x1000_0000 + (w << 24)`. pub const fn widget_small_base(w: u8) -> u64 { 0x1000_0000 | (w as u64) << 24 diff --git a/src/ip32.rs b/src/ip32.rs index 1069ae454..4247440e5 100644 --- a/src/ip32.rs +++ b/src/ip32.rs @@ -175,6 +175,8 @@ pub struct Ip32Options { pub keyboard: Option, /// The Ethernet address in the 1-Wire ID chip, if not the default. pub mac: Option<[u8; 6]>, + /// Cards fitted in the add-in slot, with their disks attached. + pub cards: Vec, } /// Whether the per-register access counters (MACE's, GBE's, the rendering @@ -430,13 +432,25 @@ pub struct Ip32Bus { /// The SCSI controller, when one is attached, so the trace can report /// what the driver asked it for. pub scsi: Mutex>>, + /// The cards in the add-in slot. Fixed at power-on: a card is fitted + /// with the machine, not hot-plugged. + pub cards: Vec, } impl Ip32Bus { /// Flush the disks on every SCSI controller. pub fn flush_disks(&self) -> std::io::Result<()> { let controllers = self.scsi.lock().unwrap().clone(); - controllers.iter().try_for_each(|c| c.bus().flush()) + controllers.iter().try_for_each(|c| c.bus().flush())?; + self.cards.iter().flat_map(|f| f.buses()).try_for_each(|(_, b)| b.flush()) + } + + /// Every SCSI bus by controller number: the two onboard controllers' + /// first, then each card's. + pub fn scsi_buses<'a>(&'a self, onboard: &'a [Arc]) -> Vec<(usize, &'a crate::scsi::ScsiBus)> { + let mut all: Vec<_> = onboard.iter().enumerate().map(|(i, c)| (i, c.bus())).collect(); + all.extend(self.cards.iter().flat_map(|f| f.buses()).map(|(n, b)| (n as usize, b))); + all } /// `ram_bytes` of memory in the usual fitting for that size, or, for a @@ -474,6 +488,7 @@ impl Ip32Bus { mace: Mace::new(ram.clone(), crime, ust), misses: Arc::new(Misses::default()), scsi: Mutex::new(Vec::new()), + cards: opts.cards, }; bus.gbe.tr.start_from_env(); if let Some(mac) = opts.mac { @@ -504,6 +519,12 @@ impl Ip32Bus { list.push(scsi); list.push(scsi_b); } + // Cards in the add-in slot, on the same bridge the controllers are. + for f in &bus.cards { + f.card.set_heartbeat(bus.mace.id.heartbeat.clone()); + f.card.register_locks(); + bus.mace.pci.attach(f.device, f.card.name(), f.card.function()); + } // A saved flash carries the guest's environment -- boot paths, console // choice, everything the PROM's `setenv` writes -- from one run to // the next, which is what makes an install worth doing once. @@ -667,6 +688,9 @@ impl crate::traits::Saveable for Ip32Bus { t.insert("mace".into(), self.mace.save_state()); let scsi = self.scsi.lock().unwrap().iter().map(|c| c.save_state()).collect(); t.insert("scsi".into(), toml::Value::Array(scsi)); + if !self.cards.is_empty() { + t.insert("cards".into(), toml::Value::Array(self.cards.iter().map(|f| f.card.save_state()).collect())); + } if let Some(words) = self.ram.stack_page_words() { t.insert("stack_page".into(), toml::Value::Array(words.into_iter().map(crate::snapshot::hex_u64).collect())); } @@ -696,6 +720,16 @@ impl crate::traits::Saveable for Ip32Bus { c.load_state(s)?; } } + let saved_cards = match get_field(v, "cards") { + Some(toml::Value::Array(a)) => a.as_slice(), + _ => &[], + }; + if saved_cards.len() != self.cards.len() { + return Err(format!("saved state has {} PCI cards, this machine {}", saved_cards.len(), self.cards.len())); + } + for (f, s) in self.cards.iter().zip(saved_cards) { + f.card.load_state(s)?; + } if let Some(toml::Value::Array(a)) = get_field(v, "stack_page") { let words: Option> = a.iter().map(crate::snapshot::toml_u64).collect(); self.ram.restore_stack_page(&words.ok_or("stack_page: not a list of dwords")?)?; @@ -718,6 +752,9 @@ impl Ip32Bus { for d in devices { d.power_on(); } + for f in &self.cards { + f.card.power_on(); + } } /// RAM and the flash, as `ram.bin` and `flash.bin` in `dir`. diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 88b725934..dc3e5b500 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -958,3 +958,74 @@ fn every_device_answers_the_monitor_through_the_bus() { } assert!(bus.execute_command("nonesuch", &[], Box::new(std::io::sink())).is_err()); } + +/// A configuration read of `reg` in `device`'s header on bus 0. +fn pci_config_read(bus: &Ip32Bus, device: u32, reg: u32) -> u32 { + use crate::traits::BusDevice; + let pci = &bus.mace.pci; + pci.write32((MACEPCI_BASE + macepci_reg::CONFIG_ADDR) as u64, 1 << 31 | device << 11 | reg << 2); + pci.read32((MACEPCI_BASE + macepci_reg::CONFIG_DATA) as u64).data +} + +fn o2_with_cards(entries: &[&str]) -> Arc { + let entries: Vec<_> = entries.iter().map(|e| crate::pci_addin::PciEntry::parse_cli(e).unwrap()).collect(); + let placed = crate::pci_addin::validate_pci(crate::config::MachineProfile::O2Ip32, &entries).unwrap(); + let opts = Ip32Options { cards: crate::pci_addin::build_cards(placed).unwrap(), ..Default::default() }; + Ip32Bus::with_options(O2Memory::single(1 << 20), vec![0u8; PROM_SIZE as usize], opts) +} + +#[test] +fn a_card_in_the_add_in_slot_enumerates_at_device_3_and_the_slot_is_empty_without_one() { + let empty = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + assert_eq!(pci_config_read(&empty, 3, 0), 0xffff_ffff, "nothing answers in the slot"); + let bus = o2_with_cards(&["3=isp12160"]); + assert_eq!(pci_config_read(&bus, 3, 0), 0x1216_1077, "QLogic ISP12160 at device 3"); + assert_eq!(pci_config_read(&bus, 0, 0), 0xffff_ffff, "device 0 is still empty"); + assert_eq!(pci_config_read(&bus, 4, 0), 0xffff_ffff, "and so is device 4"); + let report = bus.mace.pci.slots_report(); + assert!(report[3].contains("holds isp12160 1077:1216"), "{report:?}"); + assert_eq!(bus.scsi_buses(&[]).iter().map(|&(n, _)| n).collect::>(), vec![2, 3], "after the two onboard controllers"); +} + +#[test] +fn each_pin_of_a_card_sets_its_crime_input_and_no_other() { + use crate::pci::{Bar, PciBusDevice, PciConfig, PCI_INTA}; + struct Pin(PciConfig, std::sync::OnceLock); + impl PciBusDevice for Pin { + fn config(&self) -> &PciConfig { &self.0 } + fn bar_read(&self, _: usize, _: u32, _: usize) -> u32 { 0 } + fn bar_write(&self, _: usize, _: u32, _: u32, _: usize) {} + fn connect_interrupt(&self, line: crate::irq::IrqLine) { let _ = self.1.set(line); } + } + for (pin, input) in [(PCI_INTA, 10), (PCI_INTA + 1, 13), (PCI_INTA + 2, 14), (PCI_INTA + 3, 15)] { + let bus = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + let card = Arc::new(Pin(PciConfig::new(0x1077, 0x1216, 0x01_0000, 1, [Bar::Unused; 6], pin), Default::default())); + bus.mace.pci.attach(IP32_PCI_SLOT, "pin", card.clone()); + let before = bus.crime.regs.intstat(); + card.1.get().expect("wired at attach").set(true); + assert_eq!(bus.crime.regs.intstat() & !before, 1 << input, "pin {pin}: CRIME bit {input}, nothing else"); + card.1.get().unwrap().set(false); + assert_eq!(bus.crime.regs.intstat(), before, "pin {pin}"); + } +} + +#[test] +fn a_card_changes_the_saved_state_and_a_machine_without_it_refuses_it() { + use crate::traits::Saveable; + let with = o2_with_cards(&["3=isp12160"]); + let without = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + let saved = with.save_state(); + with.load_state(&saved).expect("the same machine loads its own state"); + let err = without.load_state(&saved).unwrap_err(); + assert!(err.contains("slot 3, which is empty here"), "{err}"); + let err = with.load_state(&without.save_state()).unwrap_err(); + assert!(err.contains("0 PCI cards, this machine 1"), "{err}"); +} + +#[test] +fn an_aic7880_card_is_the_onboard_chip_in_the_add_in_slot_with_the_next_controller_number() { + let bus = o2_with_cards(&["3=aic7880"]); + assert_eq!(pci_config_read(&bus, 3, 0), 0x8078_9004, "Adaptec AIC-7880 at device 3"); + let onboard = bus.scsi.lock().unwrap().clone(); + assert_eq!(bus.scsi_buses(&onboard).iter().map(|&(n, _)| n).collect::>(), vec![2], "no onboard controllers here, one card bus"); +} diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index a7a42a6d4..75a64de95 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -180,11 +180,18 @@ impl O2Machine { // machines; `controller` is the bus (0 internal, 1 external). The // buses are filled before the controllers exist. let scsi = [crate::dev::aic7880::Aic7880::new_bus(), crate::dev::aic7880::Aic7880::new_bus()]; + // Cards in the add-in slot bring buses of their own, numbered after + // the two onboard ones. + let cards = crate::pci_addin::build_cards(crate::pci_addin::validate_pci(cfg.machine.profile, &cfg.pci)?)?; let attached = crate::scsi::attach_from_config(cfg, Some("ip32"), &|c| { - scsi.get(c as usize) - .ok_or_else(|| format!("controller {c} (an O2 has controllers 0 and 1)")) + if let Some(bus) = scsi.get(c as usize) { + return Ok(bus); + } + cards.iter().flat_map(|f| f.buses()).find(|&(n, _)| n == c).map(|(_, b)| b) + .ok_or_else(|| format!("controller {c} (an O2 has controllers 0 and 1, and each add-in card its own)")) })?; let opts = Ip32Options { + cards, scsi: Some(scsi), flash_file: Some(cfg.nvram.clone().into()), keyboard: (!cfg.headless).then_some(o2.mouse), @@ -542,6 +549,9 @@ impl O2Machine { for ctl in self.bus.scsi.lock().unwrap().iter() { ctl.stop(); } + for f in &self.bus.cards { + f.card.stop(); + } if !self.bus.crime.re.drain(Duration::from_secs(2)) { eprintln!("iris: O2 stopped with rendering engine writes still queued"); } @@ -569,6 +579,9 @@ impl O2Machine { for ctl in self.bus.scsi.lock().unwrap().iter() { ctl.start(); } + for f in &self.bus.cards { + f.card.start(); + } } /// The hardware as at power-on, with the CPU stopped: what a CRIME cold @@ -608,6 +621,7 @@ impl O2Machine { manifest.nvram = self.flash_path.clone(); manifest.cpu_model = Some(self.cpu.model_name().to_string()); manifest.cpu_cache = Some(self.cpu.cache_kind().to_string()); + manifest.pci = self.bus.cards.iter().map(|f| f.pci_ref()).collect(); snap.write_manifest(&manifest).map_err(|e| e.to_string())?; let sv = manifest.schema_version; snap.write_state("cpu", &self.cpu.save_state(), sv).map_err(|e| e.to_string())?; @@ -619,10 +633,11 @@ impl O2Machine { chunks.bank_chunks[0] = put_bank_words(&store, &self.bus.ram) .map_err(|e| format!("CAS memory put: {e}"))?; snap.write_chunks_manifest(&chunks).map_err(|e| e.to_string())?; - for (i, ctl) in self.bus.scsi.lock().unwrap().iter().enumerate() { + let onboard = self.bus.scsi.lock().unwrap().clone(); + for (i, bus) in self.bus.scsi_buses(&onboard) { let dir = snap.dir.join(format!("ctrl{i}")); std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - let overlays = ctl.export_overlays(&dir).map_err(|e| format!("COW overlay export (controller {i}): {e}"))?; + let overlays = bus.export_overlays(&dir).map_err(|e| format!("COW overlay export (controller {i}): {e}"))?; snap.write_toml(&format!("cow{i}.toml"), &crate::machine::build_cow_table(overlays)) .map_err(|e| e.to_string())?; } @@ -645,9 +660,13 @@ impl O2Machine { } fn read_snapshot(&self, name: &str) -> Result { + self.read_snapshot_in(crate::snapshot::Snapshot::new(std::path::PathBuf::from("saves").join(name)), name) + } + + /// [`Self::read_snapshot`] for the snapshot in `snap`'s directory. + pub(crate) fn read_snapshot_in(&self, snap: crate::snapshot::Snapshot, name: &str) -> Result { use crate::chunk_store::{get_chunks_as_words, ChunkStore}; - use crate::snapshot::{Snapshot, SCHEMA_VERSION}; - let snap = Snapshot::new(std::path::PathBuf::from("saves").join(name)); + use crate::snapshot::SCHEMA_VERSION; let m = snap.read_manifest()?.ok_or_else(|| format!("saves/{name}: no snapshot.toml"))?; if m.machine.as_deref() != Some(SNAPSHOT_MACHINE) { return Err(format!("snapshot {name} is not of an O2")); @@ -664,6 +683,10 @@ impl O2Machine { m.schema_version )); } + // Before anything else is compared: a snapshot of a machine with + // other cards is a different machine. + let fitted: Vec<_> = self.bus.cards.iter().map(|f| f.pci_ref()).collect(); + crate::snapshot::check_pci(&m.pci, &fitted)?; // As the Indy: build, CPU and disks must match. crate::snapshot::check_provenance(&m, self.cpu.model_name(), self.cpu.cache_kind(), &self.disks)?; let sv = m.schema_version; @@ -675,7 +698,8 @@ impl O2Machine { return Err(format!("snapshot memory is {} MB, this O2 has {} MB", memory.len() >> 18, self.bus.ram.len() >> 20)); } - let ctrls = self.bus.scsi.lock().unwrap().len(); + let onboard = self.bus.scsi.lock().unwrap().clone(); + let ctrls = self.bus.scsi_buses(&onboard).iter().map(|&(i, _)| i + 1).max().unwrap_or(0); let overlays = (0..ctrls) .map(|i| snap.read_toml(&format!("cow{i}.toml")).ok().map(|t| crate::machine::parse_cow_table(&t))) .collect(); @@ -701,9 +725,10 @@ impl O2Machine { self.boot.load(&s.boot); self.bus.set_flash(&s.flash)?; self.bus.ram.restore_words(&s.memory)?; - for (i, ctl) in self.bus.scsi.lock().unwrap().iter().enumerate() { + let onboard = self.bus.scsi.lock().unwrap().clone(); + for (i, bus) in self.bus.scsi_buses(&onboard) { if let Some(Some(sets)) = s.overlays.get(i) { - ctl.import_overlays(&s.dir.join(format!("ctrl{i}")), sets) + bus.import_overlays(&s.dir.join(format!("ctrl{i}")), sets) .map_err(|e| format!("COW overlay import (controller {i}): {e}"))?; } } @@ -738,7 +763,7 @@ impl O2Machine { } /// A snapshot read and checked, ready to put into the machine. -struct O2Snapshot { +pub(crate) struct O2Snapshot { cpu: toml::Value, bus: toml::Value, boot: toml::Value, @@ -814,7 +839,8 @@ impl Workstation for O2Machine { } fn pending_chd_sync_count(&self) -> usize { - self.bus.scsi.lock().unwrap().iter().map(|c| c.bus().pending_chd_sync_count()).sum() + let onboard = self.bus.scsi.lock().unwrap().clone(); + self.bus.scsi_buses(&onboard).iter().map(|(_, b)| b.pending_chd_sync_count()).sum() } fn flush_disks(&self) -> std::io::Result<()> { self.bus.flush_disks() @@ -826,7 +852,7 @@ impl Workstation for O2Machine { cancel: &dyn Fn() -> bool, ) -> std::io::Result { let controllers = self.bus.scsi.lock().unwrap().clone(); - let buses: Vec<_> = controllers.iter().map(|c| c.bus()).collect(); + let buses: Vec<_> = self.bus.scsi_buses(&controllers).into_iter().map(|(_, b)| b).collect(); crate::scsi::ScsiBus::sync_chd_disks(&buses, only, progress, cancel) } diff --git a/src/lib.rs b/src/lib.rs index c4b68b397..fc4f4d215 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,6 +199,8 @@ pub mod scsi_monitor; pub mod irq; pub mod host_audio; pub mod pci; +pub mod pci_addin; +pub mod pci_cards; pub mod ui; pub mod cpu; pub mod dev; diff --git a/src/pci.rs b/src/pci.rs index f81627099..d17d8a194 100644 --- a/src/pci.rs +++ b/src/pci.rs @@ -9,7 +9,7 @@ use std::sync::{Arc, Mutex}; -use crate::irq::IrqLine; +use crate::irq::{InterruptController, IrqLine}; use crate::snapshot::{get_field, load_u32_slice, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_OK}; @@ -46,6 +46,50 @@ pub enum Space { Mem, } +/// Initial value and writable bits for a function-specific configuration dword. +#[derive(Clone, Copy, Debug)] +pub struct PciRegisterPolicy { + /// Dword index. Header, BAR, subsystem identity, and interrupt dwords are managed separately. + pub reg_dword: usize, + /// Value restored by [`PciConfig::reset`]. + pub initial: u32, + /// Bits that configuration writes may change. + pub writable_mask: u32, +} + +/// Function-specific write behavior for a type 0 PCI configuration header. +/// +/// Register policies are copied into the header during construction; the +/// borrowed slice is not retained. +#[derive(Clone, Copy, Debug)] +pub struct PciConfigPolicy<'a> { + /// Fixed Status bits that read as one. + pub status: u16, + /// Device-latched Status bits that writes acknowledge with one. + pub status_w1c_mask: u16, + /// Writable Command bits. + pub command_mask: u16, + /// Writable bits in the latency timer byte. + pub latency_mask: u8, + /// Writable bits in the cache-line-size byte. + pub cache_line_mask: u8, + /// Initial values and write masks for function-specific dwords. + pub registers: &'a [PciRegisterPolicy], +} + +impl<'a> Default for PciConfigPolicy<'a> { + fn default() -> Self { + Self { + status: 0x0200, + status_w1c_mask: 0, + command_mask: 0xffff, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[], + } + } +} + /// One function's type 0 configuration header. /// /// Enough of it to be discovered and configured: identity, class, command @@ -61,6 +105,12 @@ pub struct PciConfig { /// Dwords `16..caps_end` hold a capability list, which firmware cannot /// overwrite. 0 when the function has none. caps_end: usize, + status: u16, + status_w1c_mask: u16, + command_mask: u16, + latency_mask: u8, + cache_line_mask: u8, + writable_masks: [u32; 64], } impl PciConfig { @@ -68,6 +118,27 @@ impl PciConfig { /// three bytes of register 0x08. `pin` is 0 for none, or [`PCI_INTA`] /// and on; the line register is left for firmware to assign. pub fn new(vendor: u16, device: u16, class: u32, revision: u8, bars: [Bar; 6], pin: u8) -> Self { + Self::new_with_policy( + vendor, + device, + class, + revision, + bars, + pin, + PciConfigPolicy::default(), + ) + } + + /// Construct a type 0 header with function-specific register behavior. + pub fn new_with_policy( + vendor: u16, + device: u16, + class: u32, + revision: u8, + bars: [Bar; 6], + pin: u8, + policy: PciConfigPolicy<'_>, + ) -> Self { let mut r = [0u32; 64]; r[0] = ((device as u32) << 16) | vendor as u32; r[2] = (class << 8) | revision as u32; @@ -77,7 +148,26 @@ impl PciConfig { r[4 + i] = 1; } } - Self { regs: Mutex::new(r), bars, power_on: r, caps_end: 0 } + let mut writable_masks = [u32::MAX; 64]; + for register in policy.registers { + assert!( + matches!(register.reg_dword, 10 | 12..=14 | 16..=63), + "pci: register policy requires a function-specific dword in 0..64"); + r[register.reg_dword] = register.initial; + writable_masks[register.reg_dword] = register.writable_mask; + } + Self { + regs: Mutex::new(r), + bars, + power_on: r, + status: policy.status, + status_w1c_mask: policy.status_w1c_mask, + command_mask: policy.command_mask, + latency_mask: policy.latency_mask, + cache_line_mask: policy.cache_line_mask, + writable_masks, + caps_end: 0, + } } /// Give the function a capability list: `caps` are the raw dwords of the @@ -113,24 +203,40 @@ impl PciConfig { pub fn read(&self, reg: usize) -> u32 { let r = self.regs.lock().unwrap(); match reg { - // Status: a medium-speed device with no errors to report. // A function with a capability list also sets status bit 4. - 1 => (r[1] & 0xffff) | 0x0200_0000 | if r[13] != 0 { 0x0010_0000 } else { 0 }, + 1 => (r[1] & (0xffff | ((self.status_w1c_mask as u32) << 16))) + | ((self.status as u32) << 16) + | if r[13] != 0 { 0x0010_0000 } else { 0 }, _ => r[reg], } } pub fn write(&self, reg: usize, val: u32) { + self.write_masked(reg, val, u32::MAX); + } + + /// Write selected bits of dword `reg`. Both `val` and `mask` use the + /// register's bit positions. Unselected Status events are not acknowledged. + pub fn write_masked(&self, reg: usize, val: u32, mask: u32) { + if mask == 0 { + return; + } let mut r = self.regs.lock().unwrap(); match reg { - // Identity and class are read-only. + // Identity, class and header type are read-only. 0 | 2 => {} - // Cache line size and latency timer are stored; the header type - // and BIST bytes above them are not. - 3 => r[3] = (r[3] & 0xffff_0000) | (val & 0xffff), - // Command; status bits are write-one-to-clear and none are set. - 1 => r[1] = val & 0xffff, + 3 => { + let mask = mask & (((self.latency_mask as u32) << 8) | self.cache_line_mask as u32); + r[3] = (r[3] & !mask) | (val & mask); + } + // Command writes replace selected bits; Status events clear on ones. + 1 => { + let clear = val & mask & ((self.status_w1c_mask as u32) << 16); + r[1] = (r[1] & !(mask & 0xffff) & !clear) + | (val & mask & self.command_mask as u32); + } 4..=9 => { + let val = (r[reg] & !mask) | (val & mask); let bar = self.bars[reg - 4]; r[reg] = match bar { Bar::Unused => 0, @@ -144,11 +250,27 @@ impl PciConfig { // The capability list is read-only. 16.. if reg < self.caps_end => {} // Interrupt pin is read-only; the line is firmware's scratch. - 15 => r[15] = (r[15] & 0xff00) | (val & !0xff00), - _ => r[reg] = val, + 15 => { + let mask = mask & !0xff00; + r[15] = (r[15] & !mask) | (val & mask); + } + _ => { + let mask = mask & self.writable_masks[reg]; + r[reg] = (r[reg] & !mask) | (val & mask); + } } } + /// Latch selected device Status events without replacing earlier events. + pub fn set_status_bits(&self, bits: u16) { + self.regs.lock().unwrap()[1] |= ((bits & self.status_w1c_mask) as u32) << 16; + } + + /// Set the board-assigned Subsystem ID, which survives function reset. + pub fn set_subsystem_identity(&self, value: u32) { + self.regs.lock().unwrap()[11] = value; + } + /// The base address firmware programmed into BAR `i`, type bits masked. pub fn bar_base(&self, i: usize) -> u32 { let v = self.regs.lock().unwrap()[4 + i]; @@ -300,6 +422,82 @@ pub trait PciDma: Send + Sync { fn dma_write(&self, addr: u64, data: &[u8]) -> PciDmaStatus; } +/// Seat a function on a bus: give it its way to memory and, when its header +/// names an interrupt pin (INTA..INTD), the line for that pin. +/// +/// Every host bridge does this by hand when a function is attached. They +/// differ only in the DMA object they hand out and in what a pin is wired +/// to, which is what the two arguments are. A function with no pin is +/// left unconnected. +pub fn plug(dev: &dyn PciBusDevice, dma: Arc, irq_for_pin: impl FnOnce(u8) -> IrqLine) { + dev.connect_dma(dma); + let pin = dev.config().interrupt_pin(); + if (1..=4).contains(&pin) { + dev.connect_interrupt(irq_for_pin(pin)); + } +} + +/// A host bridge's PCI interrupt pins and where they go. +/// +/// Each device number's INTA..INTD is a line ([`IntxTable::line`]). The +/// table keeps every pin's level and drives each system input it routes to +/// with the OR of the pins routed there, so on a shared input one function +/// cannot clear another's interrupt. A pin with no route is dropped. +/// +/// The routes are data, set when the bus is wired ([`IntxTable::route`]), +/// so wiring a function at another device number takes a call, not a new +/// bridge. +pub struct IntxTable { + /// Where the system inputs are. + intc: Arc, + st: Mutex, +} + +struct IntxState { + levels: Box<[bool]>, + input: Box<[Option]>, +} + +impl IntxTable { + /// A table for device numbers `0..devices`, every pin unrouted. + pub fn new(intc: Arc, devices: usize) -> Self { + let st = IntxState { levels: vec![false; devices * 4].into(), input: vec![None; devices * 4].into() }; + Self { intc, st: Mutex::new(st) } + } + + /// The line of `device`'s pin (1 = INTA). + pub fn line(device: u8, pin: u8) -> u32 { + device as u32 * 4 + (pin as u32 - 1) + } + + /// Wire `device`'s pin (1 = INTA) to system input `input`, or to none. + pub fn route(&self, device: u8, pin: u8, input: Option) { + self.st.lock().unwrap().input[Self::line(device, pin) as usize] = input; + } + + /// The system input `device`'s pin drives. + pub fn input(&self, device: u8, pin: u8) -> Option { + self.st.lock().unwrap().input.get(Self::line(device, pin) as usize).copied().flatten() + } + + /// Every pin's level, by line, for a snapshot. + pub fn levels(&self) -> Vec { + self.st.lock().unwrap().levels.to_vec() + } +} + +impl InterruptController for IntxTable { + fn set_line(&self, line: u32, level: bool) { + // The lock is held while the system input is driven, so two pins + // sharing an input cannot reach it out of order. + let mut st = self.st.lock().unwrap(); + let Some(&Some(input)) = st.input.get(line as usize) else { return }; + st.levels[line as usize] = level; + let any = st.levels.iter().zip(st.input.iter()).any(|(&l, &i)| l && i == Some(input)); + self.intc.set_line(input, any); + } +} + // ---- BARs on the physical bus ---- /// What a host bridge does to a CPU access on its way to a function: which @@ -436,6 +634,14 @@ mod tests { [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA) } + fn scsi_like_with_policy(policy: PciConfigPolicy<'_>) -> PciConfig { + PciConfig::new_with_policy( + 0x9004, 0x8078, 0x0100_00, 1, + [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA, + policy, + ) + } + /// Firmware learns how much space a device wants by writing all ones to /// a BAR and reading back which address bits stuck. #[test] @@ -502,7 +708,7 @@ mod tests { #[test] fn reset_keeps_the_subsystem_identity() { let c = scsi_like(); - c.write(11, 0xc304_10a9); + c.set_subsystem_identity(0xc304_10a9); c.write(4, 0x0000_1000); c.reset(); assert_eq!(c.read(11), 0xc304_10a9); @@ -529,6 +735,121 @@ mod tests { assert_eq!(c.interrupt_pin(), PCI_INTA); } + #[test] + fn header_policies_mask_writes_and_restore_reset_values() { + for (status, command, latency, cache) in [(0x0200, 0xffff, 0, 0), (0x02b0, 0x0156, 0xf0, 0x0f)] { + let c = scsi_like_with_policy(PciConfigPolicy { + status, + status_w1c_mask: 0, + command_mask: command, + latency_mask: latency, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0020, + writable_mask: 0xff00, + }], + }); + c.write(1, u32::MAX); + c.write(3, u32::MAX); + c.write(18, u32::MAX); + assert_eq!(c.read(1), (u32::from(status) << 16) | u32::from(command)); + assert_eq!(c.read(3), (u32::from(latency) << 8) | u32::from(cache)); + assert_eq!(c.read(18), 0xaabb_ff20); + c.reset(); + assert_eq!(c.read(1), u32::from(status) << 16); + assert_eq!(c.read(3), 0); + assert_eq!(c.read(18), 0xaabb_0020); + } + } + + #[test] + fn masked_writes_preserve_unselected_fields_and_status_events() { + let c = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: 0x8040, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0000, + writable_mask: 0xffff, + }], + }); + c.set_status_bits(0x8040); + c.write(1, 0x0157); + c.write(3, 0x1234); + c.write(5, 0x1234_5000); + c.write(4, 0x1201); + for (reg, value, mask, expected) in [ + (1, 0xffff_0002, 0xff, 0x8240_0102), + (1, 0xffff_0000, 0x8000_0000, 0x0240_0102), + (1, 0x0040_0000, 0x00ff_0000, 0x0200_0102), + (1, u32::MAX, 0, 0x0200_0102), + (3, 0xffff_00ab, 0xff, 0x12ab), + (5, 0xabcd_ffff, 0xffff_0000, 0xabcd_5000), + (4, 0x3400, 0xff00, 0x3401), + (15, u32::MAX, 0xff00, 0x0100), + (18, u32::MAX, 0xff, 0xaabb_00ff), + ] { + c.write_masked(reg, value, mask); + assert_eq!(c.read(reg), expected, "register {reg}"); + } + c.set_status_bits(u16::MAX); + c.reset(); + assert_eq!(c.read(1), 0x0200_0000); + let default = scsi_like(); + default.set_status_bits(u16::MAX); + assert_eq!(default.read(1), 0x0200_0000); + } + + #[test] + fn snapshots_restore_registers_with_destination_write_policies() { + use crate::traits::Saveable; + let src = scsi_like_with_policy(PciConfigPolicy { + status: 0x02b0, + status_w1c_mask: 0xc000, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0, + writable_mask: 0x100, + }], + }); + src.set_status_bits(0x8000); + src.write(3, 0x1234); + src.write(18, 0x100); + let saved = src.save_state(); + for (events, command, cache) in [(0, 7, 0x0f), (0xc000, 0x0157, 0xff)] { + let dst = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: events, + command_mask: command, + latency_mask: 0, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0x20, + writable_mask: 0x200, + }], + }); + dst.load_state(&saved).unwrap(); + assert_eq!(dst.save_state(), saved); + assert_eq!(dst.read(1), 0x0200_0000 | ((0x8000 & u32::from(events)) << 16)); + dst.write_masked(1, u32::MAX, u32::MAX); + dst.write(3, u32::MAX); + dst.write(18, u32::MAX); + assert_eq!(dst.read(1), 0x0200_0000 | u32::from(command)); + assert_eq!(dst.read(3), (0x1234 & !u32::from(cache)) | u32::from(cache)); + assert_eq!(dst.read(18), 0x300); + dst.reset(); + assert_eq!(dst.read(18), 0x20); + } + } + struct Nothing; impl BusDevice for Nothing {} @@ -566,4 +887,103 @@ mod tests { assert!(bar_runs(&funcs, Space::Mem, 0x1a00_0000, 0x9000_0000, 0x200_0000, |_| unreachable!()).is_empty(), "outside the window"); } + /// A function that records what the bridge connected. + struct Wired { + cfg: PciConfig, + dma: Mutex, + irq: Mutex>, + } + + impl Wired { + fn new(pin: u8) -> Arc { + Arc::new(Self { cfg: PciConfig::new(0x1234, 0x5678, 0, 0, [Bar::Unused; 6], pin), dma: Mutex::new(false), irq: Mutex::new(None) }) + } + } + + impl PciBusDevice for Wired { + fn config(&self) -> &PciConfig { &self.cfg } + fn bar_read(&self, _: usize, _: u32, _: usize) -> u32 { 0 } + fn bar_write(&self, _: usize, _: u32, _: u32, _: usize) {} + fn connect_interrupt(&self, line: IrqLine) { *self.irq.lock().unwrap() = Some(line); } + fn connect_dma(&self, _: Arc) { *self.dma.lock().unwrap() = true; } + } + + struct NoMemory; + impl PciDma for NoMemory { + fn dma_read(&self, _: u64, _: &mut [u8]) -> PciDmaStatus { PciDmaStatus::MasterAbort } + fn dma_write(&self, _: u64, _: &[u8]) -> PciDmaStatus { PciDmaStatus::MasterAbort } + } + + struct Inputs(Mutex>); + impl InterruptController for Inputs { + fn set_line(&self, line: u32, level: bool) { self.0.lock().unwrap().push((line, level)); } + } + + /// Memory always; a line only for a pin INTA..INTD, and the line is the + /// one the bridge builds for that pin. + #[test] + fn plug_gives_memory_and_a_line_for_a_real_pin() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + for pin in 0..=5u8 { + let f = Wired::new(pin); + let mut asked = Vec::new(); + plug(&*f, Arc::new(NoMemory), |p| { asked.push(p); IrqLine::new(inputs.clone(), p as u32) }); + assert!(*f.dma.lock().unwrap(), "pin {pin} still masters the bus"); + let wired = f.irq.lock().unwrap().is_some(); + assert_eq!(wired, (1..=4).contains(&pin), "pin {pin}"); + assert_eq!(asked, if wired { vec![pin] } else { vec![] }, "pin {pin}: the bridge is asked only for a real pin"); + } + let f = Wired::new(PCI_INTA + 2); + plug(&*f, Arc::new(NoMemory), |p| IrqLine::new(inputs.clone(), 40 + p as u32)); + f.irq.lock().unwrap().as_ref().unwrap().set(true); + assert_eq!(*inputs.0.lock().unwrap(), vec![(43, true)]); + } + + #[test] + fn a_pin_drives_the_input_it_is_routed_to() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.route(3, 1, Some(10)); + assert_eq!(t.input(3, 1), Some(10)); + assert_eq!(t.input(3, 2), None); + t.set_line(IntxTable::line(3, 1), true); + t.set_line(IntxTable::line(3, 1), false); + assert_eq!(*inputs.0.lock().unwrap(), vec![(10, true), (10, false)]); + assert_eq!(IntxTable::line(3, 4), 15); + } + + /// A pin with no route, or beyond the table, is dropped: no input is + /// driven and its level is not kept. + #[test] + fn an_unrouted_pin_goes_nowhere() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.set_line(IntxTable::line(2, 1), true); + t.set_line(8 * 4, true); + assert!(inputs.0.lock().unwrap().is_empty()); + assert!(t.levels().iter().all(|&l| !l)); + } + + /// Two pins on one input: it stays up until both have dropped, and a + /// pin on another input does not hold it. + #[test] + fn a_shared_input_stays_up_until_every_pin_drops() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.route(3, 1, Some(12)); + t.route(4, 2, Some(12)); + t.route(5, 1, Some(13)); + t.set_line(IntxTable::line(3, 1), true); + t.set_line(IntxTable::line(4, 2), true); + t.set_line(IntxTable::line(5, 1), true); + t.set_line(IntxTable::line(3, 1), false); + t.set_line(IntxTable::line(4, 2), false); + assert_eq!(*inputs.0.lock().unwrap(), + vec![(12, true), (12, true), (13, true), (12, true), (12, false)], + "input 12 drops only with its last pin"); + let mut levels = vec![false; 32]; + levels[IntxTable::line(5, 1) as usize] = true; + assert_eq!(t.levels(), levels, "slot 5's pin is still up"); + } + } diff --git a/src/pci_addin.rs b/src/pci_addin.rs new file mode 100644 index 000000000..36a746430 --- /dev/null +++ b/src/pci_addin.rs @@ -0,0 +1,508 @@ +//! Which PCI slots a machine has, and which of them take an add-in card. +//! +//! A machine's PCI buses are described by `const` tables: a bus id, and for +//! each device number on it either an onboard function (hard-wired by the +//! machine, never configurable) or an add-in slot. A row says how well its +//! device number is known and where that comes from. Only a slot whose +//! number is established or observed can be filled; the others are listed +//! so an error can say what is missing instead of "no such slot". +//! +//! Slots are named the way a machine's PCI bus numbers them, by device +//! number. A slot can also carry the label printed on the chassis; labels +//! are only a way to write the same device number in the configuration. + +use serde::{Deserialize, Serialize}; + +use crate::config::MachineProfile; +use crate::pci_cards::{card_def, card_names, CardDef, Options, PciCard}; +use crate::scsi::ScsiBus; + +/// How well a fact is known, from strongest to weakest. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Evidence { + /// A manual, or software that must agree with the hardware. + Established, + /// Seen on a real machine. + Observed, + /// Follows from a neighbouring fact, not seen. + Inferred, + /// A modelling choice that behaves like the hardware where tested. + Approximation, + /// No source says. + Unknown, +} + +impl Evidence { + /// A slot can be configured only when its device number is known. + pub fn selectable(self) -> bool { + matches!(self, Self::Established | Self::Observed) + } + + pub fn name(self) -> &'static str { + match self { + Self::Established => "established", + Self::Observed => "observed", + Self::Inferred => "inferred", + Self::Approximation => "approximation", + Self::Unknown => "unknown", + } + } +} + +/// A slot that takes an add-in card. +#[derive(Debug)] +pub struct SlotDesc { + /// The PCI device number (the IDSEL line) on the bus. + pub device: u8, + /// What the chassis calls the slot, or "" when it carries no label. + pub label: &'static str, + /// How well `device` is known. + pub evidence: Evidence, + /// The public manual or driver the number comes from. + pub source: &'static str, + /// What would make an unselectable slot selectable. + pub settled_by: &'static str, +} + +/// One device number on a bus. +#[derive(Debug)] +pub enum BusSlot { + /// A function the machine always has, wired by the machine. + Onboard { device: u8, name: &'static str }, + AddIn(SlotDesc), +} + +impl BusSlot { + pub fn device(&self) -> u8 { + match self { + Self::Onboard { device, .. } => *device, + Self::AddIn(s) => s.device, + } + } +} + +/// A PCI bus and the device numbers that are known on it. +#[derive(Debug)] +pub struct BusDesc { + /// The id a configuration names the bus by. + pub id: &'static str, + /// The bus as an error message names it, "the O2's PCI bus". + pub title: &'static str, + /// Ascending by device number. + pub slots: &'static [BusSlot], + /// Why the bus has no add-in slot, when it has none. + pub no_add_in: &'static str, +} + +impl BusDesc { + pub fn add_ins(&self) -> impl Iterator { + self.slots.iter().filter_map(|s| match s { + BusSlot::AddIn(d) => Some(d), + BusSlot::Onboard { .. } => None, + }) + } + + /// The add-in slots as an error lists them: `3`, or `5 (label)`. + pub fn add_in_list(&self) -> String { + let all: Vec = self + .add_ins() + .map(|s| if s.label.is_empty() { s.device.to_string() } else { format!("{} ({})", s.device, s.label) }) + .collect(); + if all.is_empty() { "none".into() } else { all.join(", ") } + } +} + +/// The PCI buses of a machine, empty for one with no PCI. +pub fn buses(profile: MachineProfile) -> &'static [BusDesc] { + match profile { + MachineProfile::O2Ip32 => crate::ip32::BUSES, + MachineProfile::OctaneIp30 => crate::ip30::BUSES, + MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 => &[], + } +} + +/// How a message names the machine. +pub fn machine_noun(profile: MachineProfile) -> &'static str { + match profile { + MachineProfile::IndyIp24 => "the Indy", + MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 => "the Indigo2", + MachineProfile::O2Ip32 => "the O2", + MachineProfile::OctaneIp30 => "the Octane", + } +} + +/// A slot as a configuration names it: the PCI device number, or the label +/// on the chassis. Labels are only a spelling of a device number; they are +/// resolved here and appear nowhere else. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SlotRef { + Device(u8), + Label(String), +} + +/// One `[[pci]]` entry: a card in a slot, with the card's own options inline. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PciEntry { + pub slot: SlotRef, + /// The bus id, left out on a machine with one PCI bus. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bus: Option, + pub card: String, + /// Every other key, for the card to read. + #[serde(flatten)] + pub options: Options, +} + +impl PciEntry { + /// Parse the command-line form `[BUS:]SLOT=CARD[,KEY=VALUE]...`. SLOT is + /// a PCI device number: chassis labels are for the configuration file. + /// Option values are booleans + /// and integers where they read as one, text otherwise. + pub fn parse_cli(text: &str) -> Result { + let usage = "expected [BUS:]SLOT=CARD[,KEY=VALUE]..."; + let (place, rest) = text.split_once('=').ok_or_else(|| format!("pci \"{text}\": {usage}"))?; + let (bus, slot) = match place.split_once(':') { + Some((b, s)) if !b.is_empty() => (Some(b.to_string()), s), + Some((_, s)) => (None, s), + None => (None, place), + }; + if slot.is_empty() { + return Err(format!("pci \"{text}\": the slot is empty ({usage})")); + } + let slot = slot.parse::().map(SlotRef::Device).map_err(|_| { + format!("pci \"{text}\": slot \"{slot}\" is not a PCI device number (0 to 255); chassis labels work only in the configuration file") + })?; + let mut parts = rest.split(','); + let card = parts.next().filter(|c| !c.is_empty()).ok_or_else(|| format!("pci \"{text}\": the card is empty ({usage})"))?; + let mut options = Options::new(); + for kv in parts { + let (k, v) = kv.split_once('=').filter(|(k, _)| !k.is_empty()).ok_or_else(|| format!("pci \"{text}\": option \"{kv}\" is not KEY=VALUE"))?; + let value = match v { + "true" => toml::Value::Boolean(true), + "false" => toml::Value::Boolean(false), + _ => v.parse::().map_or_else(|_| toml::Value::String(v.to_string()), toml::Value::Integer), + }; + if options.insert(k.to_string(), value).is_some() { + return Err(format!("pci \"{text}\": option \"{k}\" is given twice")); + } + } + Ok(Self { slot, bus, card: card.to_string(), options }) + } +} + +/// An entry that passed [`validate_pci`], with everything it resolved to. +#[derive(Debug)] +pub struct Placement { + pub bus: &'static BusDesc, + pub slot: &'static SlotDesc, + pub card: &'static CardDef, + pub options: Options, + /// The controller number of the card's first SCSI bus: after the + /// machine's own, and after the cards before it. + pub scsi_base: u8, +} + +/// A card built for its slot. +pub struct Fitted { + pub bus: &'static str, + pub device: u8, + /// The controller number of the card's first SCSI bus. + pub scsi_base: u8, + /// The entry's options as `key=value,...`, sorted, for a snapshot. + pub options: String, + pub card: Box, +} + +/// Build the card of every placement. +pub fn build_cards(placements: Vec) -> Result, String> { + placements + .into_iter() + .map(|p| { + let card = (p.card.build)(&p.options, p.scsi_base).map_err(|e| format!("card \"{}\" in {} slot {}: {e}", p.card.name, p.bus.id, p.slot.device))?; + let options = p.options.iter().map(|(k, v)| format!("{k}={}", v.as_str().map_or_else(|| v.to_string(), String::from))).collect::>().join(","); + Ok(Fitted { bus: p.bus.id, device: p.slot.device, scsi_base: p.scsi_base, options, card }) + }) + .collect() +} + +impl Fitted { + /// Where the card is and what it is, as a snapshot records it. + pub fn pci_ref(&self) -> crate::snapshot::PciRef { + crate::snapshot::PciRef { bus: self.bus.into(), slot: self.device, card: self.card.name().into(), options: self.options.clone() } + } + + /// The card's buses with their controller numbers. + pub fn buses(&self) -> impl Iterator { + self.card.scsi_buses().into_iter().enumerate().map(|(i, b)| (self.scsi_base + i as u8, b)) + } +} + +/// The bus an entry names, or the machine's only one. +fn find_bus<'a>(all: &'a [BusDesc], noun: &str, bus: Option<&str>) -> Result<&'a BusDesc, String> { + let ids = || all.iter().map(|b| b.id).collect::>().join(", "); + match (bus, all) { + (_, []) => Err(format!("{noun} has no PCI bus.")), + (Some(id), _) => all.iter().find(|b| b.id == id).ok_or_else(|| format!("{noun} has no PCI bus \"{id}\" (buses: {}).", ids())), + (None, [only]) => Ok(only), + (None, _) => Err(format!("{noun} has several PCI buses ({}); set bus.", ids())), + } +} + +/// The row a slot names on `bus`: a device number, or an add-in slot's label. +fn find_row<'a>(bus: &'a BusDesc, slot: &SlotRef) -> Option<&'a BusSlot> { + match slot { + SlotRef::Device(n) => bus.slots.iter().find(|s| s.device() == *n), + SlotRef::Label(l) => bus.slots.iter().find(|s| matches!(s, BusSlot::AddIn(d) if !d.label.is_empty() && d.label == l)), + } +} + +/// Check every `[[pci]]` entry against the machine's slot tables and the +/// card registry, in the order a reader would fix them: bus, slot, whether +/// the slot is free, card, options. The first problem ends the check. +pub fn validate_pci(profile: MachineProfile, entries: &[PciEntry]) -> Result, String> { + place(buses(profile), machine_noun(profile), profile.scsi_controllers(), entries) +} + +/// [`validate_pci`] for any table of buses, `scsi_base` being the first +/// controller number free for a card's SCSI buses. +fn place(all: &'static [BusDesc], noun: &str, mut scsi_base: u8, entries: &[PciEntry]) -> Result, String> { + let mut out: Vec = Vec::new(); + let mut used: Vec<(&'static str, u8, usize)> = Vec::new(); + for (i, e) in entries.iter().enumerate() { + let at = format!("pci[{i}]"); + let bus = find_bus(all, noun, e.bus.as_deref()).map_err(|m| format!("{at}: {m}"))?; + if bus.add_ins().count() == 0 { + return Err(format!("{at}: {} has no add-in slot: {}.", bus.title, bus.no_add_in)); + } + let slot = match find_row(bus, &e.slot) { + Some(BusSlot::AddIn(s)) => s, + Some(BusSlot::Onboard { device, name }) => { + return Err(format!( + "{at}: device {device} on {} is the onboard {name}, not an add-in slot. Add-in slots: {}.", + bus.title, + bus.add_in_list() + )); + } + None => { + let named = match &e.slot { + SlotRef::Device(n) => format!("device {n} on {} is not a slot", bus.title), + SlotRef::Label(l) if l.parse::().is_ok() => { + return Err(format!( + "{at}: slot \"{l}\" is text, so it names a chassis label, and {} has no such label. For device {l} write slot = {l} without quotes.", + bus.title + )); + } + SlotRef::Label(l) => format!("{} has no slot labelled \"{l}\"", bus.title), + }; + return Err(format!("{at}: {named}. Add-in slots: {}. `pci slots` in the monitor lists them.", bus.add_in_list())); + } + }; + if !slot.evidence.selectable() { + return Err(format!( + "{at}: slot {} on {} cannot be configured: its device number is {}. {}", + slot.device, + bus.title, + slot.evidence.name(), + slot.settled_by + )); + } + if let Some(&(_, _, j)) = used.iter().find(|&&(b, d, _)| b == bus.id && d == slot.device) { + return Err(format!("{at}: {} slot {} is already used by pci[{j}] (card \"{}\").", bus.id, slot.device, entries[j].card)); + } + let card = card_def(&e.card).ok_or_else(|| format!("{at}: unknown card \"{}\" (cards: {}).", e.card, card_names()))?; + if let Some(k) = e.options.keys().find(|k| !card.options.contains(&k.as_str())) { + let known = if card.options.is_empty() { "this card takes no options".to_string() } else { format!("known: {}", card.options.join(", ")) }; + return Err(format!("{at} card \"{}\": unknown option \"{k}\" ({known}).", card.name)); + } + used.push((bus.id, slot.device, i)); + out.push(Placement { bus, slot, card, options: e.options.clone(), scsi_base }); + scsi_base = scsi_base.saturating_add(card.scsi_controllers); + } + Ok(out) +} + +/// Add the command line's entries to the configuration's: an entry for a +/// slot already filled there replaces it in place, any other is appended. +/// Entries that do not resolve are appended and left for [`validate_pci`] +/// to report. +pub fn merge_cli(profile: MachineProfile, base: &mut Vec, cli: &[PciEntry]) { + let identity = |e: &PciEntry| -> Option<(&'static str, u8)> { + let bus = find_bus(buses(profile), machine_noun(profile), e.bus.as_deref()).ok()?; + Some((bus.id, find_row(bus, &e.slot)?.device())) + }; + for e in cli { + let id = identity(e); + match base.iter_mut().find(|b| id.is_some() && identity(b) == id) { + Some(slot) => *slot = e.clone(), + None => base.push(e.clone()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_table_is_ascending_and_every_slot_is_sourced() { + for profile in MachineProfile::EVERY { + for bus in buses(profile) { + let devices: Vec = bus.slots.iter().map(BusSlot::device).collect(); + let mut sorted = devices.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(devices, sorted, "{}: devices ascending and unique", bus.id); + assert!(devices.iter().all(|&d| d < 32), "{}: a bus has 32 devices", bus.id); + for s in bus.add_ins() { + assert!(!s.source.is_empty(), "{} slot {}: no source", bus.id, s.device); + if !s.evidence.selectable() { + assert!(!s.settled_by.is_empty(), "{} slot {}: say what settles it", bus.id, s.device); + } + } + assert!(bus.add_ins().count() > 0 || !bus.no_add_in.is_empty(), "{}: a bus with no slot says why", bus.id); + } + } + } + + #[test] + fn only_the_o2_has_an_add_in_slot_today() { + for profile in MachineProfile::EVERY { + let slots: usize = buses(profile).iter().map(|b| b.add_ins().count()).sum(); + assert_eq!(slots, usize::from(profile == MachineProfile::O2Ip32), "{}", profile.name()); + } + } + fn entry(text: &str) -> PciEntry { + PciEntry::parse_cli(text).unwrap() + } + + /// An entry that names its slot by label, as only a config file can. + fn labelled(bus: &str, label: &str, card: &str) -> PciEntry { + PciEntry { slot: SlotRef::Label(label.into()), bus: Some(bus.into()), card: card.into(), options: Options::new() } + } + + fn o2(texts: &[&str]) -> Result, String> { + validate_pci(MachineProfile::O2Ip32, &texts.iter().map(|t| entry(t)).collect::>()) + } + + /// A bus with labelled slots in each state, for what the real tables + /// cannot show yet: more than one slot, labels, an unsettled number. + static TEST_BUSES: [BusDesc; 2] = [ + BusDesc { + id: "a", + title: "bus a", + slots: &[ + BusSlot::Onboard { device: 1, name: "disk controller" }, + BusSlot::AddIn(SlotDesc { device: 5, label: "slot 1", evidence: Evidence::Established, source: "test", settled_by: "" }), + BusSlot::AddIn(SlotDesc { device: 6, label: "slot 2", evidence: Evidence::Observed, source: "test", settled_by: "" }), + BusSlot::AddIn(SlotDesc { device: 7, label: "slot 3", evidence: Evidence::Unknown, source: "test", settled_by: "a capture with a card in it" }), + ], + no_add_in: "", + }, + // Device 5 again, on another bus: a slot is a bus and a device number. + BusDesc { id: "b", title: "bus b", slots: &[BusSlot::AddIn(SlotDesc { device: 5, label: "", evidence: Evidence::Established, source: "test", settled_by: "" })], no_add_in: "" }, + ]; + + fn test_bus(texts: &[&str]) -> Result, String> { + place(&TEST_BUSES, "the test machine", 2, &texts.iter().map(|t| entry(t)).collect::>()) + } + + #[test] + fn the_o2s_slot_takes_a_card_and_numbers_its_scsi_buses_after_the_onboard_two() { + let p = o2(&["3=isp12160"]).unwrap(); + assert_eq!((p[0].bus.id, p[0].slot.device, p[0].card.name, p[0].scsi_base), ("pci", 3, "isp12160", 2)); + } + + #[test] + fn cli_form_parses_bus_slot_card_and_options() { + let e = entry("e:2=isp12160,depth=4,fast=true,mode=wide"); + assert_eq!((e.bus.as_deref(), &e.slot, e.card.as_str()), (Some("e"), &SlotRef::Device(2), "isp12160")); + assert_eq!(e.options["depth"], toml::Value::Integer(4)); + assert_eq!(e.options["fast"], toml::Value::Boolean(true)); + assert_eq!(e.options["mode"], toml::Value::String("wide".into())); + for bad in ["isp12160", "3=", "=isp12160", "3=isp12160,x", "3=isp12160,a=1,a=2", ":=x"] { + assert!(PciEntry::parse_cli(bad).is_err(), "{bad}"); + } + } + + #[test] + fn the_command_line_takes_device_numbers_only() { + for bad in ["left=isp12160", "300=isp12160", "a:slot 1=isp12160"] { + let err = PciEntry::parse_cli(bad).unwrap_err(); + assert!(err.contains("is not a PCI device number") && err.contains("only in the configuration file"), "{bad}: {err}"); + } + } + + #[test] + fn a_label_names_the_same_slot_as_its_device_number() { + let by_label = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "slot 2", "isp12160")]).unwrap(); + let by_device = test_bus(&["a:6=isp12160"]).unwrap(); + assert_eq!(by_label[0].slot.device, 6); + assert_eq!(by_label[0].slot.device, by_device[0].slot.device); + let err = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "slot 9", "isp12160")]).unwrap_err(); + assert_eq!(err, "pci[0]: bus a has no slot labelled \"slot 9\". Add-in slots: 5 (slot 1), 6 (slot 2), 7 (slot 3). `pci slots` in the monitor lists them."); + } + + #[test] + fn two_cards_of_one_kind_get_consecutive_scsi_controllers() { + let p = test_bus(&["a:5=isp12160", "a:6=isp12160", "b:5=isp12160"]).unwrap(); + assert_eq!(p.iter().map(|p| p.scsi_base).collect::>(), vec![2, 4, 6]); + } + + #[test] + fn a_slot_cannot_be_used_twice_but_another_bus_is_free() { + let by_label = [entry("a:5=isp12160"), labelled("a", "slot 1", "isp12160")]; + let err = place(&TEST_BUSES, "the test machine", 2, &by_label).unwrap_err(); + assert_eq!(err, "pci[1]: a slot 5 is already used by pci[0] (card \"isp12160\")."); + let err = test_bus(&["a:5=isp12160", "a:5=aic7880"]).unwrap_err(); + assert!(err.contains("already used by pci[0]"), "{err}"); + test_bus(&["a:5=isp12160", "b:5=isp12160"]).expect("device 5 on the other bus is another slot"); + } + + #[test] + fn errors_say_what_is_wrong_and_what_to_do() { + assert_eq!( + o2(&["7=isp12160"]).err().unwrap(), + "pci[0]: device 7 on the O2's PCI bus is not a slot. Add-in slots: 3. `pci slots` in the monitor lists them." + ); + assert_eq!( + o2(&["1=isp12160"]).err().unwrap(), + "pci[0]: device 1 on the O2's PCI bus is the onboard internal SCSI controller (AIC-7880), not an add-in slot. Add-in slots: 3." + ); + assert_eq!(o2(&["3=rad9"]).err().unwrap(), "pci[0]: unknown card \"rad9\" (cards: isp12160, aic7880)."); + assert_eq!( + o2(&["3=isp12160,volume=3"]).err().unwrap(), + "pci[0] card \"isp12160\": unknown option \"volume\" (this card takes no options)." + ); + assert_eq!( + test_bus(&["a:7=isp12160"]).err().unwrap(), + "pci[0]: slot 7 on bus a cannot be configured: its device number is unknown. a capture with a card in it" + ); + assert_eq!(test_bus(&["5=isp12160"]).err().unwrap(), "pci[0]: the test machine has several PCI buses (a, b); set bus."); + assert_eq!(test_bus(&["c:5=isp12160"]).err().unwrap(), "pci[0]: the test machine has no PCI bus \"c\" (buses: a, b)."); + assert_eq!(o2(&["pci:3=isp12160"]).unwrap().len(), 1); + let quoted = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "6", "isp12160")]).unwrap_err(); + assert_eq!( + quoted, + "pci[0]: slot \"6\" is text, so it names a chassis label, and bus a has no such label. For device 6 write slot = 6 without quotes." + ); + } + + #[test] + fn a_machine_without_an_add_in_slot_says_why() { + let indy = validate_pci(MachineProfile::IndyIp24, &[entry("3=isp12160")]).err().unwrap(); + assert_eq!(indy, "pci[0]: the Indy has no PCI bus."); + let octane = validate_pci(MachineProfile::OctaneIp30, &[entry("4=isp12160")]).err().unwrap(); + assert!(octane.starts_with("pci[0]: the Octane's Bridge has no add-in slot: "), "{octane}"); + } + + #[test] + fn a_command_line_entry_replaces_the_config_entry_for_its_slot() { + let mut base = vec![entry("3=isp12160")]; + merge_cli(MachineProfile::O2Ip32, &mut base, &[entry("pci:3=isp12160"), entry("9=isp12160")]); + assert_eq!(base.len(), 2); + assert_eq!(base[0].bus.as_deref(), Some("pci"), "replaced in place"); + assert_eq!(base[1].slot, SlotRef::Device(9), "an entry that names no slot is kept for validate to report"); + } + +} diff --git a/src/pci_cards.rs b/src/pci_cards.rs new file mode 100644 index 000000000..23fceed0c --- /dev/null +++ b/src/pci_cards.rs @@ -0,0 +1,206 @@ +//! The add-in cards a PCI slot can be fitted with. +//! +//! A card is a PCI function plus whatever the machine has to do for it +//! beyond giving it a slot: the SCSI buses it brings, a thread to start +//! and stop, state to save. The list of cards is a static table; each +//! entry builds a fresh card on every use, so a machine can fit several +//! of the same kind. + +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +use crate::dev::aic7880::Aic7880; +use crate::dev::isp12160::Isp12160; +use crate::pci::PciBusDevice; +use crate::scsi::ScsiBus; +use crate::traits::{Device, Resettable, Saveable}; + +/// The `key = value` pairs of a `[[pci]]` entry beyond `slot`, `bus` and +/// `card`. +pub type Options = BTreeMap; + +/// A card fitted in a slot. +pub trait PciCard: Send + Sync { + fn name(&self) -> &'static str; + + /// The card's PCI function, which the bus attaches. + fn function(&self) -> Arc; + + /// The SCSI buses the card brings, in channel order. The machine numbers + /// them as controllers after its own. + fn scsi_buses(&self) -> Vec<&ScsiBus> { + Vec::new() + } + + /// Light the status bar's activity bits in `heartbeat`. + fn set_heartbeat(&self, _heartbeat: Arc) {} + + /// Name the card's locks in the lock registry. + fn register_locks(&self) {} + + /// Start the card's worker, if it has one. Stop finishes the command it + /// is running, so a snapshot finds none half done. + fn start(&self) {} + fn stop(&self) {} + + fn power_on(&self) {} + fn save_state(&self) -> toml::Value; + fn load_state(&self, v: &toml::Value) -> Result<(), String>; +} + +/// Build a card from its options, given the controller number its first SCSI +/// bus will have. +pub type BuildCard = fn(&Options, u8) -> Result, String>; + +/// One kind of card. +#[derive(Debug)] +pub struct CardDef { + /// The name a `[[pci]]` entry gives as `card`. + pub name: &'static str, + pub summary: &'static str, + /// The option keys the card takes. + pub options: &'static [&'static str], + /// How many SCSI controllers (buses) it adds to the machine's. + pub scsi_controllers: u8, + pub build: BuildCard, +} + +pub static CARDS: &[CardDef] = &[CardDef { + name: "isp12160", + summary: "QLogic ISP12160, two Ultra160 SCSI buses on one PCI function", + options: &[], + scsi_controllers: 2, + build: |_, _| Ok(Box::new(IspCard(Arc::new(Isp12160::default())))), +}, CardDef { + name: "aic7880", + summary: "Adaptec AIC-7880, one Ultra SCSI bus on a PCI card", + options: &[], + scsi_controllers: 1, + build: |_, controller| Ok(Box::new(AhcCard(Aic7880::new_at(controller, Aic7880::new_bus())))), +}, +]; + +pub fn card_def(name: &str) -> Option<&'static CardDef> { + CARDS.iter().find(|c| c.name == name) +} + +/// The card names, for an error message. +pub fn card_names() -> String { + CARDS.iter().map(|c| c.name).collect::>().join(", ") +} + +/// The same chip as the O2's onboard controllers, on a card: the Adaptec +/// 2940UW and its kin carry it, and IRIX's driver for it binds by PCI id. +struct AhcCard(Arc); + +impl PciCard for AhcCard { + fn name(&self) -> &'static str { + "aic7880" + } + + fn function(&self) -> Arc { + self.0.clone() + } + + fn scsi_buses(&self) -> Vec<&ScsiBus> { + vec![self.0.bus()] + } + + fn set_heartbeat(&self, heartbeat: Arc) { + self.0.set_heartbeat(heartbeat); + } + + fn start(&self) { + Device::start(&*self.0); + } + + fn stop(&self) { + Device::stop(&*self.0); + } + + fn save_state(&self) -> toml::Value { + Saveable::save_state(&*self.0) + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + Saveable::load_state(&*self.0, v) + } +} + +struct IspCard(Arc); + +impl PciCard for IspCard { + fn name(&self) -> &'static str { + "isp12160" + } + + fn function(&self) -> Arc { + self.0.clone() + } + + fn scsi_buses(&self) -> Vec<&ScsiBus> { + (0..2).filter_map(|channel| self.0.bus_for_channel(channel)).collect() + } + + fn set_heartbeat(&self, heartbeat: Arc) { + self.0.set_heartbeat(heartbeat); + } + + fn register_locks(&self) { + self.0.register_locks(); + } + + fn start(&self) { + Device::start(&*self.0); + } + + fn stop(&self) { + Device::stop(&*self.0); + } + + fn power_on(&self) { + Resettable::power_on(&*self.0); + } + + fn save_state(&self) -> toml::Value { + Saveable::save_state(&*self.0) + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + Saveable::load_state(&*self.0, v) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_isp12160_brings_two_buses_and_a_qlogic_function() { + let def = card_def("isp12160").expect("registered"); + let card = (def.build)(&Options::new(), 2).unwrap(); + assert_eq!(card.scsi_buses().len(), def.scsi_controllers as usize); + let id = card.function().config().read(0); + assert_eq!((id & 0xffff, id >> 16), (0x1077, 0x1216), "QLogic ISP12160"); + assert!(card_def("rad9").is_none()); + } + + #[test] + fn the_aic7880_brings_one_bus_and_an_adaptec_function() { + let def = card_def("aic7880").expect("registered"); + let card = (def.build)(&Options::new(), 2).unwrap(); + assert_eq!(card.scsi_buses().len(), def.scsi_controllers as usize); + let id = card.function().config().read(0); + assert_eq!((id & 0xffff, id >> 16), (0x9004, 0x8078), "Adaptec AIC-7880"); + } + + #[test] + fn two_cards_do_not_share_buses_or_state() { + let def = card_def("isp12160").unwrap(); + let (a, b) = ((def.build)(&Options::new(), 2).unwrap(), (def.build)(&Options::new(), 2).unwrap()); + let (ba, bb) = (a.scsi_buses()[0] as *const ScsiBus, b.scsi_buses()[0] as *const ScsiBus); + assert_ne!(ba, bb); + b.load_state(&a.save_state()).expect("the same card's state loads"); + } +} diff --git a/src/scsi.rs b/src/scsi.rs index bfb2ffda8..eb82e3347 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -162,6 +162,7 @@ pub struct ScsiDevice { kind: DeviceKind, buffer: Vec, // Internal buffer for WRITE_BUFFER/READ_BUFFER commands pending_sense: [u8; 18], // Stored sense data, served on REQUEST_SENSE + pending_attention: Option, /// Physical sector size: always 2048 for CD-ROM (Yellow Book), 512 for HDD. /// Used for TOC lead-out LBA (CD spec requires TOC in physical sectors). phys_block_size: u64, @@ -222,6 +223,7 @@ impl ScsiDevice { kind: if is_cdrom { DeviceKind::Cdrom } else { DeviceKind::Disk }, buffer: vec![0u8; SCSI_BUFFER_SIZE], pending_sense: [0u8; 18], + pending_attention: None, phys_block_size: if is_cdrom { CDROM_SECTOR } else { 512 }, // CD-ROM drives default to 2048-byte logical blocks (Sony CDU-76S behaviour). // HDD defaults to 512. dksc switches CD-ROM to 512 for EFS, back to 2048 for ISO. @@ -242,6 +244,7 @@ impl ScsiDevice { kind: DeviceKind::Cdrom, buffer: vec![0u8; SCSI_BUFFER_SIZE], pending_sense: [0u8; 18], + pending_attention: None, phys_block_size: CDROM_SECTOR, logical_block_size: 2048, identity, @@ -326,6 +329,10 @@ impl ScsiDevice { ScsiResponse { status: 0x02, data: vec![] } } + fn take_attention(&mut self) -> Option { + self.pending_attention.take().or_else(|| self.media.take_changed().then_some(0x28)) + } + fn exec_test_unit_ready(&mut self, _cdb: &[u8]) -> Result { if self.media.take_changed() { // Sense key 0x06 UNIT_ATTENTION, ASC 0x28 "Not Ready to Ready Transition / Medium Changed" @@ -1118,11 +1125,10 @@ impl ScsiTarget for ScsiDevice { } // Unit attention fires before any other command (except REQUEST_SENSE/INQUIRY). - if req.cdb[0] != scsi_cmd::REQUEST_SENSE - && req.cdb[0] != scsi_cmd::INQUIRY - && self.media.take_changed() - { - return Ok(self.check_condition(0x06, 0x28, 0x00)); + if req.cdb[0] != scsi_cmd::REQUEST_SENSE && req.cdb[0] != scsi_cmd::INQUIRY { + if let Some(asc) = self.take_attention() { + return Ok(self.check_condition(0x06, asc, 0x00)); + } } let mut response = match req.cdb[0] { @@ -1268,6 +1274,12 @@ impl ScsiTarget for ScsiDevice { fn block_size(&self) -> Option { Some(self.logical_block_size) } + fn bus_reset(&mut self) { + self.pending_sense = [0; 18]; + self.pending_sense[0] = 0x70; + self.pending_attention = Some(0x29); + } + fn reset_block_size(&mut self, cdrom_512: bool) { if cdrom_512 && self.is_cdrom() { self.logical_block_size = 512; @@ -1867,12 +1879,38 @@ mod tests { cd.changer().unwrap().load(&chd.display().to_string()).unwrap(); assert!(cd.has_media()); assert_eq!(cd.size(), 16 * 2048, "the CHD's size, not the file's"); - // The medium change is reported once, then the disc reads. + // A reset is reported before a media change that was already pending. + crate::scsi::ScsiTarget::bus_reset(&mut cd); + let reset = cd.request(&request(&[0, 0, 0, 0, 0, 0], None)).unwrap(); + assert_eq!(reset.status, 0x02, "unit attention after the reset"); + let reset_sense = cd.request(&request(&[0x03, 0, 0, 0, 18, 0], None)).unwrap(); + assert_eq!(reset_sense.data[12], 0x29, "reset attention comes first"); + let tur = cd.request(&request(&[0, 0, 0, 0, 0, 0], None)).unwrap(); - assert_eq!(tur.status, 0x02, "unit attention after the change"); + assert_eq!(tur.status, 0x02, "the pending media change follows"); + let sense = cd.request(&request(&[0x03, 0, 0, 0, 18, 0], None)).unwrap(); + assert_eq!(sense.data[12], 0x28, "media change keeps its ASC"); let read = cd.request(&request(&[0x28, 0, 0, 0, 0, 3, 0, 0, 1, 0], None)).unwrap(); assert_eq!(read.status, 0x00); assert_eq!(read.data, vec![0x42u8; 2048]); let _ = std::fs::remove_dir_all(dir); } + #[test] + fn bus_reset_reports_unit_attention_once_and_keeps_block_size() { + let mut dev = cdrom(); + dev.logical_block_size = 512; + dev.set_sense(0x05, 0x20, 0x00); + crate::scsi::ScsiTarget::bus_reset(&mut dev); + + let old_sense = run(&mut dev, &[0x03, 0, 0, 0, 18, 0]); + assert_eq!((old_sense.data[2], old_sense.data[12]), (0, 0), "reset clears previous sense"); + let response = run(&mut dev, &[0x00, 0, 0, 0, 0, 0]); + assert_eq!(response.status, 0x02, "a bus reset arms unit attention"); + let sense = run(&mut dev, &[0x03, 0, 0, 0, 18, 0]); + assert_eq!(sense.data[12], 0x29, "the reset attention has the reset ASC"); + + let response = run(&mut dev, &[0x00, 0, 0, 0, 0, 0]); + assert_eq!(response.status, 0x00, "the attention is reported once"); + assert_eq!(dev.block_sizes().1, 512, "bus reset leaves the selected block size alone"); + } } diff --git a/src/scsi/bus.rs b/src/scsi/bus.rs index 4283d763f..cb978d19a 100644 --- a/src/scsi/bus.rs +++ b/src/scsi/bus.rs @@ -51,8 +51,15 @@ pub trait ScsiTarget: Send { /// The logical block size, for a target that has one. fn block_size(&self) -> Option { None } - /// A controller's bus reset: block size back to what the controller - /// leaves, with a CD-ROM at 512 bytes if `cdrom_512`. + /// A SCSI bus reset reached this target, which owes the initiator a + /// unit attention on its next command. The ISP12160 calls this when the + /// guest resets either of its buses; the WD33C93A, AIC-7880 and ISP1040 + /// models never told their targets about a reset and still do not. + fn bus_reset(&mut self) {} + + /// The AIC-7880's bus reset: block size back to what that controller + /// leaves, with a CD-ROM at 512 bytes if `cdrom_512`. Only the AIC-7880 + /// calls this. fn reset_block_size(&mut self, _cdrom_512: bool) {} /// Put the mode parameters a guest can change back to what the target diff --git a/src/snapshot.rs b/src/snapshot.rs index f38835381..6ae400b3c 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -70,6 +70,50 @@ pub struct DiskRef { pub size_bytes: u64, } +/// A PCI add-in card fitted when a snapshot was taken, by where it was. +/// `slot` is the PCI device number, never a chassis label, so a snapshot +/// outlives a relabelling. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PciRef { + pub bus: String, + pub slot: u8, + pub card: String, + /// The card's options as `key=value,...`, sorted. + pub options: String, +} + +impl PciRef { + fn describe(&self) -> String { + let options = if self.options.is_empty() { String::new() } else { format!(" ({})", self.options) }; + format!("pci bus \"{}\" slot {} = {}{options}", self.bus, self.slot, self.card) + } +} + +/// Refuse a restore into a machine whose fitted cards are not the ones the +/// snapshot was taken with. A card's state restored without the card, or a +/// card left at power-on under a running kernel, is not a state anything +/// was ever in. Not overridable: there is nothing to restore it into. +pub fn check_pci(snapshot: &[PciRef], current: &[PciRef]) -> Result<(), String> { + let mut problems = Vec::new(); + for s in snapshot { + match current.iter().find(|c| c.bus == s.bus && c.slot == s.slot) { + None => problems.push(format!("snapshot has {}, this config has no card there", s.describe())), + Some(c) if c != s => problems.push(format!("snapshot has {}, this config has {}", s.describe(), c.describe())), + Some(_) => {} + } + } + for c in current { + if !snapshot.iter().any(|s| s.bus == c.bus && s.slot == c.slot) { + problems.push(format!("this config has {}, the snapshot has no card there", c.describe())); + } + } + if problems.is_empty() { + Ok(()) + } else { + Err(format!("snapshot PCI cards differ:\n - {}", problems.join("\n - "))) + } +} + /// Reject a restore whose CPU model differs from the running machine's. /// `None` from the snapshot means a legacy manifest — the model was a build /// flag then, so there is nothing to compare and the restore is allowed. @@ -192,6 +236,9 @@ pub struct Manifest { /// for the O2. None for the Indy and Indigo2 and every older manifest. A /// snapshot loads only into the machine it names. pub machine: Option, + /// The PCI add-in cards fitted at capture time. Empty when none were, + /// and for every manifest written before cards could be fitted. + pub pci: Vec, } impl Manifest { @@ -218,6 +265,7 @@ impl Manifest { cpu_model: None, cpu_cache: None, machine: None, + pci: Vec::new(), } } @@ -266,6 +314,17 @@ impl Manifest { if let Some(m) = &self.machine { tbl.insert("machine".into(), Value::String(m.clone())); } + if !self.pci.is_empty() { + let cards: Vec = self.pci.iter().map(|c| { + let mut t = toml::map::Map::new(); + t.insert("bus".into(), Value::String(c.bus.clone())); + t.insert("slot".into(), Value::Integer(c.slot as i64)); + t.insert("card".into(), Value::String(c.card.clone())); + t.insert("options".into(), Value::String(c.options.clone())); + Value::Table(t) + }).collect(); + tbl.insert("pci".into(), Value::Array(cards)); + } Value::Table(tbl) } @@ -310,6 +369,22 @@ impl Manifest { let cpu_model = tbl.get("cpu_model").and_then(|x| x.as_str()).map(String::from); let cpu_cache = tbl.get("cpu_cache").and_then(|x| x.as_str()).map(String::from); let machine = tbl.get("machine").and_then(|x| x.as_str()).map(String::from); + // Cards: absent in a manifest from before they existed, which had + // none. A card entry that does not parse is an error, not a + // card-less snapshot, since the restore would then run without it. + let pci = match tbl.get("pci") { + None => Vec::new(), + Some(v) => v.as_array().ok_or("manifest: pci is not a list")?.iter().map(|x| { + let t = x.as_table().ok_or("manifest: a pci entry is not a table")?; + let text = |k: &str| t.get(k).and_then(|v| v.as_str()).map(String::from).ok_or(format!("manifest: pci entry has no {k}")); + Ok(PciRef { + bus: text("bus")?, + slot: t.get("slot").and_then(|v| v.as_integer()).and_then(|n| u8::try_from(n).ok()).ok_or("manifest: pci entry has no slot")?, + card: text("card")?, + options: t.get("options").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + }) + }).collect::, String>>()?, + }; Ok(Self { schema_version, iris_git_rev, @@ -324,6 +399,7 @@ impl Manifest { cpu_model, cpu_cache, machine, + pci, }) } } @@ -782,6 +858,7 @@ mod tests { cpu_model: Some("R5000".into()), cpu_cache: Some("shadow".into()), machine: Some("o2_ip32".into()), + pci: vec![PciRef { bus: "pci".into(), slot: 3, card: "isp12160".into(), options: String::new() }], }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -797,6 +874,7 @@ mod tests { assert_eq!(m2.nvram, m.nvram); assert_eq!(m2.cpu_model, m.cpu_model); assert_eq!(m2.machine, m.machine); + assert_eq!(m2.pci, m.pci); } #[test] @@ -815,6 +893,7 @@ mod tests { cpu_model: None, cpu_cache: None, machine: None, + pci: vec![], }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -827,6 +906,43 @@ mod tests { assert!(m2.nvram.is_none()); assert!(m2.cpu_model.is_none(), "legacy manifest has no cpu_model"); assert!(m2.machine.is_none(), "an Indy/Indigo2 manifest names no machine"); + assert!(m2.pci.is_empty(), "a manifest without cards names none, and writes no key"); + assert!(v.get("pci").is_none()); + } + + fn card(bus: &str, slot: u8, name: &str) -> PciRef { + PciRef { bus: bus.into(), slot, card: name.into(), options: String::new() } + } + + #[test] + fn a_restore_needs_the_same_cards_in_the_same_slots() { + let fitted = [card("pci", 3, "isp12160")]; + check_pci(&fitted, &fitted).expect("same card, same slot"); + check_pci(&[], &[]).expect("no cards on either side"); + assert_eq!( + check_pci(&fitted, &[]).unwrap_err(), + "snapshot PCI cards differ:\n - snapshot has pci bus \"pci\" slot 3 = isp12160, this config has no card there" + ); + assert_eq!( + check_pci(&[], &fitted).unwrap_err(), + "snapshot PCI cards differ:\n - this config has pci bus \"pci\" slot 3 = isp12160, the snapshot has no card there" + ); + let other = [card("pci", 3, "other")]; + assert!(check_pci(&fitted, &other).unwrap_err().contains("this config has pci bus \"pci\" slot 3 = other")); + let tuned = [PciRef { options: "depth=4".into(), ..card("pci", 3, "isp12160") }]; + assert!(check_pci(&fitted, &tuned).unwrap_err().contains("(depth=4)"), "options are part of the card"); + let two = [card("pci", 3, "isp12160"), card("pci", 4, "isp12160")]; + assert!(check_pci(&fitted, &two).unwrap_err().contains("slot 4")); + } + + #[test] + fn a_manifest_with_a_broken_card_entry_is_an_error() { + let mut m = Manifest::for_current_save(); + m.pci = vec![card("pci", 3, "isp12160")]; + let mut v = m.to_toml(); + assert_eq!(Manifest::from_toml(&v).unwrap().pci, m.pci); + v.as_table_mut().unwrap().insert("pci".into(), Value::Array(vec![Value::String("x".into())])); + assert!(Manifest::from_toml(&v).is_err(), "not a card-less snapshot"); } #[test]