Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions copperline.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 10 additions & 4 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
121 changes: 107 additions & 14 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::ops::RangeInclusive<u32>>,
/// 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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -311,15 +311,28 @@ pub struct IdeConfig {
pub slave: Option<DriveImage>,
}

/// 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.
#[default]
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)]
Expand Down Expand Up @@ -1958,14 +1971,18 @@ impl TryFrom<RawConfig> 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
}
Expand All @@ -1985,17 +2002,33 @@ impl TryFrom<RawConfig> 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"
Expand Down Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::zorro_device::BoardDevice> = 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)?;
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/scsi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ------------------------------------------------

Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions src/sdmac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading