diff --git a/copperline.example.toml b/copperline.example.toml index 2120b20..2fa6740 100644 --- a/copperline.example.toml +++ b/copperline.example.toml @@ -104,11 +104,11 @@ model = "68000" # A500 (ECS 8372A Agnus, OCS 8362 Denise, 512 KiB chip + 512 KiB trapdoor # slow RAM) -- the most common and most-targeted Amiga. # The big-box profiles (A3000, A4000) are new and incomplete, but both boot. -# The A3000 has its motherboard SCSI (Super DMAC + WD33C93 at $DD0000, driven by -# Kickstart's own scsi.device) with no drives attached yet; the A4000's IDE at -# $DD2020 is not emulated at all, so Kickstart spends several seconds probing for -# it on every boot. Give either machine a Zorro controller or a [[filesys]] mount -# to boot from. +# The A3000 boots off its motherboard SCSI (Super DMAC + WD33C93 at $DD0000, +# driven by Kickstart's own scsi.device): put drives on it with [scsi]. The +# A4000's IDE at $DD2020 is not emulated at all, so Kickstart spends several +# seconds probing for it on every boot; give that machine a Zorro controller or +# a [[filesys]] mount to boot from. # [machine] # profile = "A600" # A1000 | A500 | A500OCS | A500Plus | A600 | A1200 | A3000 | A4000 | CDTV | CD32 # rtc = true # add a $DC0000 battery RTC (default: only A500+/CDTV/A3000/A4000 have one) @@ -189,18 +189,22 @@ slow = "512K" # metadata = "boards/megaram.toml" -# SCSI controller: a Zorro board with up to seven drives, on any machine -# model. Preferred over [ide] for multiple disks: the board's own boot ROM -# carries scsi.device and autoboots on Kickstart 1.3+, so it does not depend -# on the Kickstart IDE driver (which only probes the master on stock 3.1). -# Drive paths accept the same images as [ide]: RDB HDFs, bare partition -# hardfiles, or host directories. +# SCSI bus with up to seven drives, on any machine model. Preferred over [ide] +# for multiple disks: a Zorro board's own boot ROM carries scsi.device and +# autoboots on Kickstart 1.3+, so it does not depend on the Kickstart IDE +# driver (which only probes the master on stock 3.1). Drive paths accept the +# same images as [ide]: RDB HDFs, bare partition hardfiles, or host +# directories. # -# controller picks the board: "a2091" (Zorro II, WD33C93; the default) or -# "a4091" (Zorro III, NCR 53C710). The A2091 needs an A590/A2091 boot ROM -# (6.6+, 16K/32K; rom_odd takes the odd half of a split even/odd EPROM dump); -# the A4091 needs a raw A4091 EPROM image (e.g. the open-source a4091.rom) and -# has a single ROM (no rom_odd). +# controller picks the host adapter: +# "a2091" - Zorro II, WD33C93. The default on machines without onboard SCSI. +# Needs an A590/A2091 boot ROM (6.6+, 16K/32K; rom_odd takes the +# odd half of a split even/odd EPROM dump). +# "a4091" - Zorro III, NCR 53C710. Needs a raw A4091 EPROM image (e.g. the +# open-source a4091.rom); single ROM, no rom_odd. +# "a3000" - the A3000's motherboard SCSI (Super DMAC + WD33C93 at $DD0000), +# and the default on that machine. No ROM: Kickstart's own +# scsi.device drives it and autoboots from an RDB drive. # [scsi] # controller = "a2091" # rom = "a2091-v6.6.rom" diff --git a/src/bus.rs b/src/bus.rs index 137dda7..ecbb69c 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -2890,11 +2890,17 @@ impl Bus { self.cdtv.is_some() || self.akiko.is_some() } - /// Whether a SCSI controller board is fitted (lights the HDD LED). + /// Whether the machine has a hard-disk controller, which is what gives it + /// an HDD LED: a Zorro board, or the A3000's motherboard SCSI. fn has_scsi_device(&self) -> bool { - self.devices - .iter() - .any(|d| matches!(d, crate::zorro_device::BoardDevice::A2091(_))) + self.sdmac.is_some() + || self.devices.iter().any(|d| { + matches!( + d, + crate::zorro_device::BoardDevice::A2091(_) + | crate::zorro_device::BoardDevice::A4091(_) + ) + }) } /// Whether a disc is mounted (or, on CDTV, waiting in the tray). diff --git a/src/config.rs b/src/config.rs index 038c2cd..00dc21d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,9 +93,9 @@ pub struct Config { /// ROM probes enough empty space to make this a firehose, so it is meant /// to be pointed at one window (e.g. the A4000 IDE at $DD2020). pub log_unmapped: Option>, - /// Super DMAC fitted (A3000 profile): the SCSI DMA controller at $DD0000. - /// No WD33C93 behind it yet, so the machine boots with an empty SCSI - /// socket -- but Kickstart hangs outright if nothing answers at all. + /// Super DMAC fitted (A3000 profile): the SCSI DMA controller at $DD0000 + /// and the WD33C93 behind it. Kickstart hangs outright if nothing answers + /// there. Drives go on its bus through `[scsi] controller = "a3000"`. pub sdmac: bool, /// Akiko gate array fitted (CD32 profile): ID + C2P port at $B80000. pub akiko: bool, @@ -127,10 +127,10 @@ pub struct Config { /// Gayle IDE drive images (raw flat HDF, RDB inside), opened /// read/write. Only valid on machines with a Gayle gate array. pub ide: IdeConfig, - /// SCSI controller (`[scsi]`): the `controller` selects an A2091 (Zorro II) - /// or A4091 (Zorro III), plus a boot ROM image and up to seven drive images - /// on SCSI IDs 0-6. The board autoconfigs on the Zorro chain and carries its - /// own scsi.device. + /// SCSI controller (`[scsi]`): the `controller` selects an A2091 (Zorro II), + /// an A4091 (Zorro III), or the A3000's motherboard SCSI, plus up to seven + /// drive images on SCSI IDs 0-6. The Zorro boards autoconfig on the chain + /// and carry their own boot ROM and scsi.device; the A3000's does not. pub scsi: ScsiConfig, /// A2065 Ethernet board (`[a2065]`): when set, an A2065 NIC autoconfigs on /// the Zorro chain using the named host network backend. Networking is @@ -311,8 +311,10 @@ pub struct IdeConfig { pub slave: Option, } -/// Which SCSI host adapter the `[scsi]` section fits. Both are Zorro -/// autoconfig boards carrying their own boot ROM and scsi.device. +/// Which SCSI host adapter the `[scsi]` section fits: one of the two Zorro +/// autoconfig boards, which carry their own boot ROM and scsi.device, or the +/// A3000's motherboard SCSI, which has neither (Kickstart's own scsi.device +/// drives it) and is only there on a machine with a Super DMAC. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum ScsiController { /// Commodore A2091/A590: Zorro II, WD33C93. The default. @@ -320,6 +322,17 @@ pub enum ScsiController { A2091, /// Commodore A4091: Zorro III, NCR 53C710. A4091, + /// A3000 motherboard SCSI: Super DMAC + WD33C93 at $DD0000. The default on + /// a machine that has one. + A3000, +} + +impl ScsiController { + /// Whether the controller is a Zorro board (it autoconfigs and needs a boot + /// ROM) rather than motherboard silicon. + pub fn is_zorro_board(self) -> bool { + !matches!(self, ScsiController::A3000) + } } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -1958,14 +1971,18 @@ impl TryFrom for Config { } let scsi_controller = match raw.scsi.controller.as_deref() { + // A machine with a Super DMAC already has a SCSI bus, so drives go + // on it unless the config asks for a Zorro board instead. + None if defaults.sdmac => ScsiController::A3000, None => ScsiController::A2091, Some(raw_ctrl) => match raw_ctrl.trim().to_ascii_lowercase().as_str() { "a2091" => ScsiController::A2091, "a4091" => ScsiController::A4091, + "a3000" => ScsiController::A3000, _ => { errors.push(anyhow!( "[scsi] controller = {raw_ctrl:?} is not known \ - (expected \"a2091\" or \"a4091\")" + (expected \"a2091\", \"a4091\", or \"a3000\")" )); ScsiController::A2091 } @@ -1985,17 +2002,33 @@ impl TryFrom for Config { raw.scsi.unit6.map(drive_image).transpose()?, ], }; - if scsi.enabled() && scsi.rom.is_none() { + if scsi.enabled() && scsi.rom.is_none() && scsi.controller.is_zorro_board() { let hint = match scsi.controller { - ScsiController::A2091 => { - "an A590/A2091 6.x ROM image; its scsi.device drives the disks" - } ScsiController::A4091 => "a raw A4091 EPROM image, e.g. the open-source a4091.rom", + _ => "an A590/A2091 6.x ROM image; its scsi.device drives the disks", }; errors.push(anyhow!( "[scsi] drives need the boot ROM: set [scsi] rom = \"...\" ({hint})" )); } + // The motherboard SCSI is silicon, not a card: it has no boot ROM (the + // Kickstart carries its driver), and it only exists where the Super + // DMAC does. + if scsi.controller == ScsiController::A3000 { + if !defaults.sdmac { + errors.push(anyhow!( + "[scsi] controller = \"a3000\" is the motherboard SCSI: set \ + [machine] profile = \"A3000\", or fit a Zorro board with \ + controller = \"a2091\" (or \"a4091\")" + )); + } + if scsi.rom.is_some() { + errors.push(anyhow!( + "[scsi] rom does not apply to the A3000 motherboard SCSI: it has no \ + boot ROM, Kickstart's own scsi.device drives it" + )); + } + } if scsi.rom_odd.is_some() && scsi.controller != ScsiController::A2091 { errors.push(anyhow!( "[scsi] rom_odd is an A2091 split-EPROM option; the A4091 has a single rom" @@ -3823,6 +3856,66 @@ mod tests { Ok(()) } + /// The A3000's SCSI is motherboard silicon: its drives need no boot ROM, + /// they are the default on that machine, and they fit nowhere else. + #[test] + fn the_a3000_scsi_bus_takes_drives_without_a_boot_rom() -> Result<()> { + let cfg = parse_config( + r#" + [machine] + profile = "A3000" + [scsi] + unit0 = "workbench.hdf" + "#, + )?; + assert!(cfg.sdmac); + assert_eq!(cfg.scsi.controller, ScsiController::A3000); + assert_eq!( + cfg.scsi.units[0].as_ref().map(|d| d.path.as_path()), + Some(Path::new("workbench.hdf")) + ); + + // A Zorro board still fits an A3000, and there it does need its ROM. + let cfg = parse_config( + r#" + [machine] + profile = "A3000" + [scsi] + controller = "a2091" + rom = "a2091.rom" + unit0 = "workbench.hdf" + "#, + )?; + assert_eq!(cfg.scsi.controller, ScsiController::A2091); + + // No Super DMAC, no motherboard SCSI. + let err = parse_config( + r#" + [machine] + profile = "A1200" + [scsi] + controller = "a3000" + unit0 = "workbench.hdf" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("motherboard SCSI"), "{err:#}"); + + // And there is no ROM to give it. + let err = parse_config( + r#" + [machine] + profile = "A3000" + [scsi] + rom = "a2091.rom" + unit0 = "workbench.hdf" + "#, + ) + .unwrap_err(); + assert!(err.to_string().contains("no boot ROM"), "{err:#}"); + Ok(()) + } + #[test] fn drive_entries_accept_a_volume_name_override() -> Result<()> { // IDE and SCSI drives take either a bare path or a table carrying an diff --git a/src/emulator.rs b/src/emulator.rs index 32a4ff6..7011736 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1785,12 +1785,15 @@ pub fn build_machine( // the chain (mapping its window to a device slot) while the device object // is attached to the bus after it is built; the slot index ties them. let mut devices: Vec = Vec::new(); - if cfg.scsi.enabled() { + // The A3000's motherboard SCSI is not a Zorro board: its drives are fitted + // to the Super DMAC further down, once the bus exists. + if cfg.scsi.enabled() && cfg.scsi.controller.is_zorro_board() { use crate::config::ScsiController; let rom_path = cfg.scsi.rom.as_ref().expect("config validated [scsi] rom"); let slot = devices.len(); // The controller picks the board; the drive plumbing is identical. let device = match cfg.scsi.controller { + ScsiController::A3000 => unreachable!("not a Zorro board"), ScsiController::A2091 => { let rom = crate::a2091::A2091::load_rom(rom_path, cfg.scsi.rom_odd.as_deref())?; let mut board = crate::a2091::A2091::new(rom)?; @@ -1962,8 +1965,24 @@ pub fn build_machine( bus.log_unmapped = Some(range); } if cfg.sdmac { - bus.attach_sdmac(crate::sdmac::Sdmac::new()); - info!("sdmac: Super DMAC + WD33C93 at $DD0000 (no drives)"); + let mut sdmac = crate::sdmac::Sdmac::new(); + let mut drives = 0; + if cfg.scsi.controller == crate::config::ScsiController::A3000 { + for (unit, drive) in cfg.scsi.units.iter().enumerate() { + let Some(drive) = drive else { continue }; + sdmac.attach_drive( + unit, + crate::scsi::ScsiDisk::open(&drive.path, unit, drive.volume_name.as_deref())?, + ); + info!("scsi: unit {unit} {}", drive.path.display()); + drives += 1; + } + } + bus.attach_sdmac(sdmac); + match drives { + 0 => info!("sdmac: Super DMAC + WD33C93 at $DD0000 (no drives)"), + n => info!("sdmac: Super DMAC + WD33C93 at $DD0000, {n} drive(s)"), + } } if let Some(revision) = cfg.mem_controller.ramsey_revision() { // TODO(codewiz): pass the real bank size once motherboard fast RAM diff --git a/src/scsi.rs b/src/scsi.rs index 6a8486f..6330850 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -38,6 +38,12 @@ pub const WD_DESTINATION_ID: u8 = 0x15; pub const WD_SCSI_STATUS: u8 = 0x17; pub const WD_COMMAND: u8 = 0x18; pub const WD_DATA: u8 = 0x19; +/// The register file ends at the data register: 0x1A-0x1E are not registers at +/// all and float, and 0x1F is the auxiliary status, which the chip also aliases +/// into the register file as read-only. cdhooper's sdmac tool identifies the +/// WD33C93 by exactly this: 0x1E must read $FF, and 0x1F must not be writable. +const WD_LAST_REG: u8 = WD_DATA; +pub const WD_AUX_STATUS: u8 = 0x1F; // ----- auxiliary status bits ------------------------------------------------ @@ -617,6 +623,12 @@ impl Wd33c93 { pub fn read_data_port(&mut self) -> u8 { match self.sasr { WD_DATA => self.pio_read_byte(), + WD_AUX_STATUS => self.read_aux_status(), + r if r > WD_LAST_REG => { + // Not a register: the chip does not drive the bus. + self.incr_sasr(); + 0xFF + } WD_SCSI_STATUS => { let v = self.regs[usize::from(WD_SCSI_STATUS)]; // Reading the status register acknowledges the interrupt; @@ -637,6 +649,9 @@ impl Wd33c93 { match self.sasr { WD_COMMAND => self.execute_command(v), WD_DATA => self.pio_write_byte(v), + // The auxiliary status is read-only, and past it there is nothing + // to write to. + r if r > WD_LAST_REG => self.incr_sasr(), r => { self.regs[usize::from(r)] = v; self.incr_sasr(); diff --git a/src/sdmac.rs b/src/sdmac.rs index 488534c..0f88a29 100644 --- a/src/sdmac.rs +++ b/src/sdmac.rs @@ -431,6 +431,32 @@ mod tests { assert_eq!(aux & (ASR_CIP | ASR_BSY | ASR_INT), 0, "aux {aux:#04X}"); } + /// The register file ends at the data register: $1F is the auxiliary status + /// aliased in read-only, and $1A-$1E are not registers and float. This is + /// how cdhooper's sdmac tool identifies the chip -- with both writable, it + /// reported "SCSI Controller: Not detected: INVALID" and failed its WDC + /// test. + #[test] + fn the_registers_past_the_data_register_are_read_only_or_float() { + use crate::scsi::WD_AUX_STATUS; + let mut s = sdmac(); + let aux = s.read_byte(SDMAC_BASE + SASR); + + for value in [0xFFu8, 0xA5, 0x5A] { + // The auxiliary status ignores writes and keeps reading the status. + s.write_byte(SDMAC_BASE + SASR, WD_AUX_STATUS); + s.write_byte(SDMAC_BASE + SCMD, value); + s.write_byte(SDMAC_BASE + SASR, WD_AUX_STATUS); + assert_eq!(s.read_byte(SDMAC_BASE + SCMD), aux, "wrote {value:#04X}"); + + // An undefined register never drives the bus. + s.write_byte(SDMAC_BASE + SASR, 0x1E); + s.write_byte(SDMAC_BASE + SCMD, value); + s.write_byte(SDMAC_BASE + SASR, 0x1E); + assert_eq!(s.read_byte(SDMAC_BASE + SCMD), 0xFF, "wrote {value:#04X}"); + } + } + /// The WD33C93's registers are reached by writing a register number to the /// select latch and reading or writing the data port. Both addresses have /// an alias four bytes below (the SDMAC decodes them loosely) -- SysInfo diff --git a/src/video/launcher.rs b/src/video/launcher.rs index 09f86c1..3bb6d99 100644 --- a/src/video/launcher.rs +++ b/src/video/launcher.rs @@ -554,10 +554,11 @@ const JOYSTICK_MODES: [JoystickInputMode; 2] = [JoystickInputMode::Gamepad, JoystickInputMode::Keyboard]; // `None` = no SCSI board fitted; the two boards are mutually exclusive here even // though the engine could run both, so a config round-trips through this picker. -const SCSI_CONTROLLERS: [Option; 3] = [ +const SCSI_CONTROLLERS: [Option; 4] = [ None, Some(ScsiController::A2091), Some(ScsiController::A4091), + Some(ScsiController::A3000), ]; #[cfg(feature = "midi")] const SERIAL_MODES: [SerialMode; 5] = [ @@ -911,14 +912,25 @@ impl MachineSetup { // Only emit `[scsi]` when a controller is fitted, so an unset board // leaves the section absent rather than writing dangling ROM/units. if let Some(controller) = self.scsi_controller { - // "a2091" is the default, so omit it; name only "a4091". - raw.scsi.controller = match controller { - ScsiController::A2091 => None, - ScsiController::A4091 => Some("a4091".to_string()), - }; - raw.scsi.rom = self.scsi_rom.as_deref().map(path_string); + // Name every controller: which one a bare [scsi] means depends on + // the machine (an A3000 defaults to its motherboard SCSI). + raw.scsi.controller = Some( + match controller { + ScsiController::A2091 => "a2091", + ScsiController::A4091 => "a4091", + ScsiController::A3000 => "a3000", + } + .to_string(), + ); + // The motherboard SCSI has no boot ROM of its own. + raw.scsi.rom = controller + .is_zorro_board() + .then(|| self.scsi_rom.as_deref().map(path_string)) + .flatten(); // rom_odd is an A2091 split-EPROM option; the A4091 has one ROM. - raw.scsi.rom_odd = (controller == ScsiController::A2091) + // It is the odd half OF rom, so without rom there is nothing for it + // to complete and the config would not validate. + raw.scsi.rom_odd = (controller == ScsiController::A2091 && raw.scsi.rom.is_some()) .then(|| self.scsi_rom_odd.as_deref().map(path_string)) .flatten(); raw.scsi.unit0 = drive_raw( @@ -1126,12 +1138,21 @@ impl MachineSetup { if model != Some(MachineModel::Cd32) { self.cd32_nvram = None; } + // The motherboard SCSI leaves with the motherboard; the drives stay and + // land on the default Zorro board instead. + if !self.has_sdmac() && self.scsi_controller == Some(ScsiController::A3000) { + self.scsi_controller = Some(ScsiController::A2091); + } } fn has_gayle(&self) -> bool { matches!(self.model, Some(MachineModel::A600 | MachineModel::A1200)) } + fn has_sdmac(&self) -> bool { + self.model == Some(MachineModel::A3000) + } + fn has_cd(&self) -> bool { matches!(self.model, Some(MachineModel::Cdtv | MachineModel::Cd32)) } @@ -1157,9 +1178,18 @@ impl MachineSetup { F::Z3Ram => reason(cpu_is_32bit(self.cpu), "needs 32-bit CPU"), F::IdeMaster | F::IdeSlave => reason(self.has_gayle(), "needs A600/A1200"), // The ROM and drives belong to the fitted controller; greyed with - // none. rom_odd is an A2091 split-EPROM option only. - F::ScsiRom - | F::ScsiUnit0 + // none. The A3000's motherboard SCSI has no ROM of its own, and + // rom_odd is an A2091 split-EPROM option only. + F::ScsiRom => reason( + self.scsi_controller + .is_some_and(ScsiController::is_zorro_board), + if self.scsi_controller.is_some() { + "Zorro boards only" + } else { + "no controller" + }, + ), + F::ScsiUnit0 | F::ScsiUnit1 | F::ScsiUnit2 | F::ScsiUnit3 @@ -1371,6 +1401,7 @@ impl MachineSetup { None => "None".to_string(), Some(ScsiController::A2091) => "A2091 (Z2)".to_string(), Some(ScsiController::A4091) => "A4091 (Z3)".to_string(), + Some(ScsiController::A3000) => "A3000 (onboard)".to_string(), }, #[cfg(feature = "midi")] F::SerialMode => match self.serial_mode { @@ -1476,7 +1507,12 @@ impl MachineSetup { cycle_slice(&JOYSTICK_MODES, self.joystick_input_mode, forward) } F::ScsiController => { - self.scsi_controller = cycle_slice(&SCSI_CONTROLLERS, self.scsi_controller, forward) + // The motherboard SCSI is only on offer where the silicon is. + let choices: Vec> = SCSI_CONTROLLERS + .into_iter() + .filter(|c| self.has_sdmac() || *c != Some(ScsiController::A3000)) + .collect(); + self.scsi_controller = cycle_slice(&choices, self.scsi_controller, forward) } #[cfg(feature = "midi")] F::SerialMode => {