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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@
!/assets/aros/aros-amiga-m68k-ext.bin
!/assets/services/services_rom.bin

# Local run artifacts (screenshots, recordings, save states, input scripts).
# Local run artifacts (screenshots, recordings, save states, input scripts,
# RTC battery-RAM backing files).
copperline-screenshot-*.png
copperline-video-*.avi
*.clstate
*.clscript
*.nvram
.DS_Store

# MyST documentation build output (see docs/README.md).
Expand Down
3 changes: 3 additions & 0 deletions copperline.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ model = "68000"
# # A seeded clock ticks in emulated time, so the guest-visible
# # time is deterministic across runs.
# rtc_frozen = true # stop the seeded clock at rtc_time exactly
# battmem = "battmem.nvram" # RP5C01 battery-RAM (battmem.resource) backing file, in the
# # WinUAE/Amiberry .nvram layout. Defaults on when an RP5C01
# # is fitted; "" keeps the battery registers session-only.
# mem_controller = "ramsey-07" # none | ramsey-04 (A3000) | ramsey-07 (A4000)
# rom_scsi_device_disable = true # skip the ROM's scsi.device (default: see above)

Expand Down
17 changes: 17 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ rtc = true # add a battery RTC (default: only A500+/CDTV/A3000/A4000 ship
# rtc_chip = "RP5C01" # MSM6242 (default) or RP5C01 (A3000/A4000 default)
# rtc_time = "2005-03-18 01:58:29" # seed the clock; it then ticks in emulated time
# rtc_frozen = true # stop the seeded clock at rtc_time exactly
# battmem = "battmem.nvram" # RP5C01 battery-RAM backing file (default when fitted)
mem_controller = "ramsey-07" # none, ramsey-04 (A3000), ramsey-07 (A4000)
rom_scsi_device_disable = true # skip the ROM's scsi.device (default: when its bus has no drives)
```
Expand Down Expand Up @@ -160,6 +161,22 @@ so the choice is mostly invisible to it, but Linux/m68k does not probe: it
drives the chip the machine model dictates, so an A3000/A4000 booting Linux
needs the RP5C01 answering for its clock to work.

`battmem` persists the RP5C01's battery-backed registers -- the 26 RAM
nibbles behind `battmem.resource` plus the alarm and 12/24 settings --
across runs, the way the real board's battery does. This is where
`scsi.device` keeps its per-unit SCSI host settings (an A3000's or A4091's
synchronous-transfer, disconnect, and last-drive options, including
remembering attached CD-ROM drives), so without it those revert every run.
The file uses the same `.nvram` layout as WinUAE and Amiberry, so backing
files interchange between emulators; only the battery payload loads back --
the time-of-save digits in the file never override the (host- or
`rtc_time`-driven) clock. It defaults to `battmem.nvram` in the working
directory whenever an RP5C01 is fitted; point it elsewhere with a path, or
set `battmem = ""` to keep the battery registers session-only. Note that a
persisted file carries guest-visible state from one run into the next by
design, so delete it (or disable it) where byte-for-byte reproducible
headless runs matter.

`rtc_time` seeds the clock instead of letting it mirror the host's: the value
is either an integer (Unix seconds, UTC) or a string
`"YYYY-MM-DD HH:MM[:SS]"` giving exactly the wall-clock time the guest reads
Expand Down
125 changes: 108 additions & 17 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ pub struct Config {
/// Stop the seeded RTC (`[machine] rtc_frozen`): every read returns
/// exactly `rtc_seed_unix`, for pinning a guest to one time window.
pub rtc_frozen: bool,
/// RP5C01 battery-RAM (battmem) backing file (`[machine] battmem`),
/// in the WinUAE/Amiberry `.nvram` layout; `None` keeps the battery
/// registers session-only. Defaults to `battmem.nvram` when an
/// RP5C01 is fitted.
pub battmem_path: Option<PathBuf>,
pub video_standard: VideoStandard,
pub audio: AudioConfig,
/// Gayle IDE drive images (raw flat HDF, RDB inside), opened
Expand Down Expand Up @@ -1159,6 +1164,7 @@ impl Default for Config {
rtc_chip: crate::rtc::RtcChip::Msm6242,
rtc_seed_unix: None,
rtc_frozen: false,
battmem_path: None,
video_standard: VideoStandard::Pal,
audio: AudioConfig::default(),
ide: IdeConfig::default(),
Expand Down Expand Up @@ -1973,6 +1979,13 @@ pub(crate) struct RawMachine {
/// Only meaningful together with `rtc_time`.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) rtc_frozen: Option<bool>,
/// Backing file for the RP5C01's battery RAM (the storage behind
/// AmigaOS `battmem.resource` on the A3000/A4000), in the
/// WinUAE/Amiberry `.nvram` file layout so files interchange between
/// emulators. Defaults to `battmem.nvram` whenever an RP5C01 is
/// fitted; an empty string keeps the battery registers session-only.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) battmem: Option<String>,
/// Memory controller fitted, defaulting per profile: `none`, `ramsey-04`
/// (A3000) or `ramsey-07` (A4000). Ramsey answers at $DE0000, which no
/// other chip decodes, so it can also be fitted to a wedge machine to
Expand Down Expand Up @@ -2602,6 +2615,38 @@ impl TryFrom<RawConfig> for Config {
}
}

let rtc_present = match raw.machine.rtc {
// A configured time on an explicitly unfitted clock would
// silently do nothing; make the contradiction loud.
Some(false) if rtc_seed_unix.is_some() => anyhow::bail!(
"[machine] rtc_time is set but rtc = false leaves the \
clock unfitted; drop one of them"
),
// Naming a chip for a socket declared empty is the same
// contradiction.
Some(false) if rtc_chip.is_some() => anyhow::bail!(
"[machine] rtc_chip is set but rtc = false leaves the \
clock unfitted; drop one of them"
),
Some(fitted) => fitted,
None => defaults.rtc_present || rtc_seed_unix.is_some() || rtc_chip.is_some(),
};
let rtc_chip = rtc_chip.unwrap_or(defaults.rtc_chip);
let rp5c01_fitted = rtc_present && rtc_chip == crate::rtc::RtcChip::Rp5c01;
let battmem_path = match raw.machine.battmem.as_deref() {
// An empty path keeps the battery registers session-only.
Some("") => None,
// A backing file for battery RAM the machine does not have
// would silently never fill; make the contradiction loud.
Some(path) if !rp5c01_fitted => anyhow::bail!(
"[machine] battmem ({path}) backs the RP5C01's battery RAM, \
but this machine has no RP5C01 fitted; set \
rtc_chip = \"RP5C01\" or drop battmem"
),
Some(path) => Some(PathBuf::from(path)),
None => rp5c01_fitted.then(|| PathBuf::from("battmem.nvram")),
};

Ok(Config {
rom_path: raw.rom.map(PathBuf::from).unwrap_or(defaults.rom_path),
cpu,
Expand Down Expand Up @@ -2668,25 +2713,11 @@ impl TryFrom<RawConfig> for Config {
.nvram
.map(PathBuf::from)
.or_else(|| defaults.akiko.then(|| PathBuf::from("cd32-nvram.bin"))),
rtc_present: match raw.machine.rtc {
// A configured time on an explicitly unfitted clock would
// silently do nothing; make the contradiction loud.
Some(false) if rtc_seed_unix.is_some() => anyhow::bail!(
"[machine] rtc_time is set but rtc = false leaves the \
clock unfitted; drop one of them"
),
// Naming a chip for a socket declared empty is the same
// contradiction.
Some(false) if rtc_chip.is_some() => anyhow::bail!(
"[machine] rtc_chip is set but rtc = false leaves the \
clock unfitted; drop one of them"
),
Some(fitted) => fitted,
None => defaults.rtc_present || rtc_seed_unix.is_some() || rtc_chip.is_some(),
},
rtc_chip: rtc_chip.unwrap_or(defaults.rtc_chip),
rtc_present,
rtc_chip,
rtc_seed_unix,
rtc_frozen,
battmem_path,
log_unmapped: raw
.debug
.log_unmapped
Expand Down Expand Up @@ -3751,6 +3782,65 @@ mod tests {
assert!(err.to_string().contains("rtc_chip"), "{err:#}");
}

#[test]
fn battmem_defaults_to_a_backing_file_only_where_an_rp5c01_sits() -> Result<()> {
// The big boxes get the default backing file with their Ricoh part.
for profile in ["A3000", "A4000"] {
let cfg = parse_config(&format!("[machine]\nprofile = \"{profile}\"\n"))?;
assert_eq!(
cfg.battmem_path.as_deref(),
Some(std::path::Path::new("battmem.nvram")),
"{profile}"
);
}
// The OKI part has no battery RAM: no file, even with a clock.
let cfg = parse_config("[machine]\nprofile = \"A500+\"\n")?;
assert_eq!(cfg.battmem_path, None);
// Fitting an RP5C01 anywhere brings the default with it.
let cfg = parse_config("[machine]\nrtc_chip = \"RP5C01\"\n")?;
assert!(cfg.battmem_path.is_some());

// An explicit path wins; an empty one keeps RAM session-only.
let cfg = parse_config(
r#"
[machine]
profile = "A4000"
battmem = "shared/A4000.nvram"
"#,
)?;
assert_eq!(
cfg.battmem_path.as_deref(),
Some(std::path::Path::new("shared/A4000.nvram"))
);
let cfg = parse_config(
r#"
[machine]
profile = "A4000"
battmem = ""
"#,
)?;
assert_eq!(cfg.battmem_path, None);
Ok(())
}

#[test]
fn battmem_without_an_rp5c01_is_rejected() {
// The default A500 has no RP5C01 for the file to back.
let err = parse_config("[machine]\nbattmem = \"battmem.nvram\"\n").unwrap_err();
assert!(err.to_string().contains("RP5C01"), "{err:#}");

// An MSM6242 clock is not enough: it carries no battery RAM.
let err = parse_config(
r#"
[machine]
rtc = true
battmem = "battmem.nvram"
"#,
)
.unwrap_err();
assert!(err.to_string().contains("RP5C01"), "{err:#}");
}

#[test]
fn rtc_time_cli_override_matches_the_config_field() -> Result<()> {
let cfg = load_overrides(&ConfigOverrides {
Expand Down Expand Up @@ -3822,6 +3912,7 @@ mod tests {
rtc_chip: Some("RP5C01".to_string()),
rtc_time: Some(RawRtcTime::Text("2005-03-18 01:58:29".to_string())),
rtc_frozen: Some(true),
battmem: Some("battmem.nvram".to_string()),
mem_controller: Some("ramsey-07".to_string()),
rom_scsi_device_disable: Some(true),
},
Expand Down
4 changes: 4 additions & 0 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,10 @@ pub fn build_machine(
bus.set_rtc_present(cfg.rtc_present);
bus.set_rtc_chip(cfg.rtc_chip);
bus.rtc.set_seed(cfg.rtc_seed_unix, cfg.rtc_frozen);
if let Some(path) = &cfg.battmem_path {
info!("rtc: battery RAM (battmem) persisted to {}", path.display());
bus.rtc.set_battmem_path(path.clone());
}
if let Some(id) = cfg.gate_array.gayle_id() {
let mut gayle = crate::gayle::Gayle::new(id);
if let Some(drive) = &cfg.ide.master {
Expand Down
Loading
Loading