feat(cartridge): add explicit Flash identification and banked reads
This commit is contained in:
parent
fce222eea2
commit
8b3b7ae62d
23 changed files with 1042 additions and 52 deletions
|
|
@ -14,14 +14,14 @@ use std::{
|
|||
|
||||
use gba_core::{
|
||||
bios,
|
||||
cartridge::{CartridgeHardware, RtcError},
|
||||
cartridge::{CartridgeHardware, RtcError, SaveDevice},
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
memory::ROM_CAPACITY,
|
||||
};
|
||||
|
||||
const MAX_STEPS: u64 = 100_000_000;
|
||||
const MAX_FRAMES: u64 = 100_000;
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT]); steps: 1..=100000000, frames: 1..=100000; do not combine with demo or help options";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
|
|
@ -34,6 +34,7 @@ pub struct Options {
|
|||
path: PathBuf,
|
||||
mode: Mode,
|
||||
hardware: CartridgeHardware,
|
||||
save_device: SaveDevice,
|
||||
}
|
||||
|
||||
pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
||||
|
|
@ -43,6 +44,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
let mut frames = None;
|
||||
let mut window = false;
|
||||
let mut hardware = CartridgeHardware::None;
|
||||
let mut save_device = SaveDevice::None;
|
||||
let mut args = args.iter();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--rom" && path.is_none() {
|
||||
|
|
@ -53,6 +55,12 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
path = Some(PathBuf::from(value));
|
||||
} else if arg == "--rtc" && hardware == CartridgeHardware::None {
|
||||
hardware = CartridgeHardware::Rtc;
|
||||
} else if arg == "--save-type" && save_device == SaveDevice::None {
|
||||
save_device = match args.next().and_then(|value| value.to_str()) {
|
||||
Some("flash64") => SaveDevice::Flash64,
|
||||
Some("flash128") => SaveDevice::Flash128,
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
} else if arg == "--window" && !window {
|
||||
window = true;
|
||||
} else if (arg == "--steps" && steps.is_none()) || (arg == "--frames" && frames.is_none()) {
|
||||
|
|
@ -90,6 +98,7 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
path: path.ok_or_else(invalid)?,
|
||||
mode,
|
||||
hardware,
|
||||
save_device,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -264,10 +273,19 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
if options.hardware == CartridgeHardware::Rtc {
|
||||
writeln!(writer, "RTC selected: UTC at startup, then host elapsed time; no persistence or RTC interrupts.")?;
|
||||
}
|
||||
if options.save_device != SaveDevice::None {
|
||||
writeln!(writer, "Flash selected: {} KiB Macronix, erased array; identification/bank reads only. No programming, erase, or save files.", options.save_device.capacity() / 1024)?;
|
||||
}
|
||||
let steps = match options.mode {
|
||||
Mode::Terminal { steps } => steps,
|
||||
Mode::Window { frames } => {
|
||||
return crate::desktop::run_rom(bytes, frames, options.hardware, writer)
|
||||
return crate::desktop::run_rom(
|
||||
bytes,
|
||||
frames,
|
||||
options.hardware,
|
||||
options.save_device,
|
||||
writer,
|
||||
)
|
||||
}
|
||||
};
|
||||
writeln!(writer, "Machine-step limit: {steps} (includes boot)")?;
|
||||
|
|
@ -275,6 +293,7 @@ pub fn execute(options: Options, writer: &mut impl Write) -> Result<(), Box<dyn
|
|||
machine
|
||||
.memory_mut()
|
||||
.set_cartridge_hardware(options.hardware);
|
||||
machine.memory_mut().set_save_device(options.save_device);
|
||||
let mut clock = if options.hardware == CartridgeHardware::Rtc {
|
||||
Some(rtc_clock::RtcHostClock::new(machine.memory_mut())?)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ fn options_require_both_flags_in_either_order() {
|
|||
Options {
|
||||
path: "original program.gba".into(),
|
||||
mode: Mode::Terminal { steps: count },
|
||||
hardware: CartridgeHardware::None
|
||||
hardware: CartridgeHardware::None,
|
||||
save_device: SaveDevice::None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ pub fn run(
|
|||
bytes: Vec<u8>,
|
||||
frame_limit: Option<u64>,
|
||||
hardware: gba_core::cartridge::CartridgeHardware,
|
||||
save_device: gba_core::cartridge::SaveDevice,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut session = Session::new(bytes)?;
|
||||
session.set_cartridge_hardware(hardware);
|
||||
session.memory_mut().set_save_device(save_device);
|
||||
let mut rtc_clock = if hardware == gba_core::cartridge::CartridgeHardware::Rtc {
|
||||
Some(crate::cartridge::rtc_clock::RtcHostClock::new(
|
||||
session.memory_mut(),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,14 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
return cartridge::suite::parse_args(args).map(RunMode::TestSuite);
|
||||
}
|
||||
if args.iter().any(|arg| {
|
||||
["--rom", "--steps", "--window", "--frames", "--rtc"]
|
||||
[
|
||||
"--rom",
|
||||
"--steps",
|
||||
"--window",
|
||||
"--frames",
|
||||
"--rtc",
|
||||
"--save-type",
|
||||
]
|
||||
.iter()
|
||||
.any(|flag| arg == flag)
|
||||
}) {
|
||||
|
|
@ -79,7 +86,7 @@ fn parse_args(args: &[OsString]) -> Result<RunMode, io::Error> {
|
|||
[arg] if arg == "--help" || arg == "-h" => Ok(RunMode::Help),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
"usage: gameboy-rust [--cpu-demo | --timer-demo | --graphics-demo | --smoke-test | --graphics-smoke-test | --raster-demo | --raster-smoke-test | --mosaic-demo | --mosaic-smoke-test | --effects-demo | --effects-smoke-test | --tile-demo | --tile-smoke-test | --affine-demo | --affine-smoke-test | --affine-raster-demo | --affine-raster-smoke-test | --bitmap4-demo | --bitmap5-demo | --bitmap4-smoke-test | --bitmap5-smoke-test | --help]; or gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128] (--steps COUNT | --window [--frames COUNT]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +133,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--rom PATH --window Open a raw ROM with keyboard input\n\
|
||||
--frames COUNT Exit ROM window after 1..=100000 captured frames\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--save-type flash64|flash128 Select Flash identification/read subset (no saving yet)\n\
|
||||
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||
--cpu-demo Run the terminal-only ARM/Thumb instruction demo\n\
|
||||
--timer-demo Run the timer IRQ demo with nominal cycle costs\n\
|
||||
|
|
|
|||
|
|
@ -142,6 +142,75 @@ fn rtc_calendar_commands_run_through_original_arm_code_with_host_clock_enabled()
|
|||
);
|
||||
}
|
||||
|
||||
fn flash_setup() -> Vec<u32> {
|
||||
// Original ARM unlock/identify, then install a byte reader in IWRAM and execute it.
|
||||
let mut code = Vec::new();
|
||||
let mut literals = Vec::new();
|
||||
fn load(code: &mut Vec<u32>, literals: &mut Vec<(usize, u32, u32)>, register: u32, value: u32) {
|
||||
literals.push((code.len(), register, value));
|
||||
code.push(0);
|
||||
}
|
||||
load(&mut code, &mut literals, 1, 0x0e005555);
|
||||
code.extend([0xe3a000aa, 0xe5c10000]);
|
||||
load(&mut code, &mut literals, 2, 0x0e002aaa);
|
||||
code.extend([0xe3a00055, 0xe5c20000, 0xe3a00090, 0xe5c10000]);
|
||||
load(&mut code, &mut literals, 3, 0x0e000000);
|
||||
load(&mut code, &mut literals, 4, 0x03000000);
|
||||
for (offset, instruction) in [(0, 0xe5d35000), (4, 0xe5d36001), (8, 0xeafffffe)] {
|
||||
load(&mut code, &mut literals, 0, instruction);
|
||||
code.push(0xe5840000 | offset);
|
||||
}
|
||||
code.push(0xe12fff14); // BX r4
|
||||
for (at, register, value) in literals {
|
||||
let offset = ((code.len() - at) * 4 - 8) as u32;
|
||||
code[at] = 0xe59f0000 | (register << 12) | offset;
|
||||
code.push(value);
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_flash_selection_runs_ram_byte_reader_and_leaves_files_unchanged() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&flash_setup());
|
||||
let bytes = fs::read(&path).unwrap();
|
||||
assert!(stderr(&run(&path, "1000")).contains("unmapped memory at 0x0e005555"));
|
||||
for (device, id) in [("flash64", "r6=0x0000001c"), ("flash128", "r6=0x00000009")] {
|
||||
let output = command()
|
||||
.args(["--save-type", device, "--rtc", "--rom"])
|
||||
.arg(&path)
|
||||
.args(["--steps", "1000"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
let text = stdout(&output);
|
||||
assert!(text.contains("identification/bank reads only"));
|
||||
assert!(text.contains("r5=0x000000c2"), "{text}");
|
||||
assert!(text.contains(id), "{text}");
|
||||
assert_eq!(fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
assert_eq!(fs::read_dir(&fixture.0).unwrap().count(), 1); // No save file is invented.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_save_type_options_fail_before_file_access() {
|
||||
for extra in [
|
||||
vec!["--save-type"],
|
||||
vec!["--save-type", "sram"],
|
||||
vec!["--save-type", ""],
|
||||
vec!["--save-type", "flash128", "--save-type", "flash64"],
|
||||
] {
|
||||
let output = command()
|
||||
.args(["--rom", "missing.gba", "--steps", "1"])
|
||||
.args(extra)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(stderr(&output).contains("usage:"));
|
||||
assert!(!stderr(&output).contains("cannot load"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -311,11 +380,14 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
fn window(path: &Path, rtc: bool) -> Output {
|
||||
fn window(path: &Path, rtc: bool, save: Option<&str>) -> Output {
|
||||
let mut command = command();
|
||||
if rtc {
|
||||
command.arg("--rtc");
|
||||
}
|
||||
if let Some(device) = save {
|
||||
command.args(["--save-type", device]);
|
||||
}
|
||||
let mut child = command
|
||||
.arg("--rom")
|
||||
.arg(path)
|
||||
|
|
@ -342,7 +414,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
let fixture = Fixture::new();
|
||||
let path = fixture.0.join("original-input.gba");
|
||||
fs::write(&path, gba_demos::input_rom()).unwrap();
|
||||
let output = window(&path, false);
|
||||
let output = window(&path, false, None);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
|
|
@ -352,12 +424,12 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
assert!(stdout(&output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&output).contains("Result: window frame limit reached"));
|
||||
let rtc_path = fixture.rom(&rtc_setup());
|
||||
let rtc_output = window(&rtc_path, true);
|
||||
let rtc_output = window(&rtc_path, true, None);
|
||||
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
||||
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
||||
let calendar_path = fixture.rom(&rtc_calendar_setup());
|
||||
let calendar_output = window(&calendar_path, true);
|
||||
let calendar_output = window(&calendar_path, true, None);
|
||||
assert!(
|
||||
calendar_output.status.success(),
|
||||
"{}",
|
||||
|
|
@ -365,6 +437,12 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
);
|
||||
assert!(stdout(&calendar_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&calendar_output).contains("r2=0x00000092"));
|
||||
let flash_path = fixture.rom(&flash_setup());
|
||||
let flash_output = window(&flash_path, true, Some("flash128"));
|
||||
assert!(flash_output.status.success(), "{}", stderr(&flash_output));
|
||||
assert!(stdout(&flash_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&flash_output).contains("r5=0x000000c2"));
|
||||
assert!(stdout(&flash_output).contains("r6=0x00000009"));
|
||||
for (code, error) in [
|
||||
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
||||
(vec![0xee00_0000], "instruction"),
|
||||
|
|
@ -380,7 +458,7 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
let output = window(&path, false);
|
||||
let output = window(&path, false, None);
|
||||
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
|
||||
assert!(stdout(&output).contains("Result: ROM window diagnostic"));
|
||||
assert!(stdout(&output).contains("Captured ROM frames: 0"));
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
use crate::memory::MemoryError;
|
||||
|
||||
mod calendar;
|
||||
mod flash;
|
||||
mod rtc;
|
||||
pub use calendar::{RtcDateTime, RtcError};
|
||||
use flash::Flash;
|
||||
pub use flash::{SaveDevice, SaveError, SAVE_END, SAVE_START};
|
||||
use rtc::Rtc;
|
||||
|
||||
pub const GPIO_DATA: u32 = 0x0800_00c4;
|
||||
|
|
@ -23,13 +26,28 @@ pub enum CartridgeHardware {
|
|||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Cartridge {
|
||||
gpio: Option<Gpio>,
|
||||
flash: Option<Flash>,
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
pub(crate) fn new(hardware: CartridgeHardware) -> Self {
|
||||
Self {
|
||||
gpio: (hardware == CartridgeHardware::Rtc).then(Gpio::default),
|
||||
pub(crate) fn set_hardware(&mut self, hardware: CartridgeHardware) {
|
||||
self.gpio = (hardware == CartridgeHardware::Rtc).then(Gpio::default);
|
||||
}
|
||||
|
||||
pub(crate) fn set_save_device(&mut self, device: SaveDevice) {
|
||||
self.flash = Flash::new(device);
|
||||
}
|
||||
|
||||
pub(crate) fn save_device(&self) -> SaveDevice {
|
||||
self.flash.map_or(SaveDevice::None, Flash::device)
|
||||
}
|
||||
|
||||
pub(crate) fn save_mapped(&self, address: u32) -> bool {
|
||||
self.flash.is_some() && (SAVE_START..=SAVE_END).contains(&address)
|
||||
}
|
||||
|
||||
pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
self.flash.expect("mapped save device").read(address, image)
|
||||
}
|
||||
|
||||
pub(crate) fn rtc_datetime(&self) -> Option<RtcDateTime> {
|
||||
|
|
@ -56,13 +74,14 @@ impl Cartridge {
|
|||
}
|
||||
|
||||
pub(crate) fn mapped(&self, address: u32) -> bool {
|
||||
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
|
||||
self.save_mapped(address)
|
||||
|| (self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address))
|
||||
}
|
||||
|
||||
/// Disabled reads expose the supplied ROM bytes, not a zero-filled replacement.
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
let gpio = self.gpio?;
|
||||
if !self.mapped(address) || !gpio.readable {
|
||||
if !(GPIO_DATA..GPIO_CONTROL + 2).contains(&address) || !gpio.readable {
|
||||
return None;
|
||||
}
|
||||
let value = match address & !1 {
|
||||
|
|
@ -79,6 +98,15 @@ impl Cartridge {
|
|||
if !self.mapped(address) {
|
||||
return Ok(()); // Non-cartridge accesses are validated by the bus.
|
||||
}
|
||||
if self.save_mapped(address) {
|
||||
if bytes.len() != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
});
|
||||
}
|
||||
return self.flash.as_mut().unwrap().write(address, bytes[0]);
|
||||
}
|
||||
let fail = |at, value, operation| MemoryError::UnsupportedIo {
|
||||
address: at,
|
||||
value,
|
||||
|
|
|
|||
148
crates/gba-core/src/cartridge/flash.rs
Normal file
148
crates/gba-core/src/cartridge/flash.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! Bounded Macronix Flash identification, array reads, and 128 KiB banking.
|
||||
//! Program/erase commands remain diagnostic: this module cannot claim a completed save.
|
||||
use crate::memory::MemoryError;
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub const SAVE_START: u32 = 0x0e00_0000;
|
||||
pub const SAVE_END: u32 = 0x0e00_ffff;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SaveDevice {
|
||||
#[default]
|
||||
None,
|
||||
/// Macronix ID 1CC2h. Array reads and identification only.
|
||||
Flash64,
|
||||
/// Macronix ID 09C2h. Adds two 64 KiB banks; no programming or erase yet.
|
||||
Flash128,
|
||||
}
|
||||
|
||||
impl SaveDevice {
|
||||
pub fn capacity(self) -> usize {
|
||||
match self {
|
||||
Self::None => 0,
|
||||
Self::Flash64 => 64 * 1024,
|
||||
Self::Flash128 => 128 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SaveError {
|
||||
NoDevice,
|
||||
InvalidSize { expected: usize, actual: usize },
|
||||
}
|
||||
impl fmt::Display for SaveError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NoDevice => f.write_str("no cartridge save device selected"),
|
||||
Self::InvalidSize { expected, actual } => write!(
|
||||
f,
|
||||
"save image size {actual}; expected exactly {expected} bytes"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error for SaveError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Flash {
|
||||
device: SaveDevice,
|
||||
phase: Phase,
|
||||
identify: bool,
|
||||
bank: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Phase {
|
||||
Ready,
|
||||
Unlock,
|
||||
Command,
|
||||
Bank,
|
||||
}
|
||||
|
||||
impl Flash {
|
||||
pub(super) fn new(device: SaveDevice) -> Option<Self> {
|
||||
(device != SaveDevice::None).then_some(Self {
|
||||
device,
|
||||
phase: Phase::Ready,
|
||||
identify: false,
|
||||
bank: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn device(self) -> SaveDevice {
|
||||
self.device
|
||||
}
|
||||
|
||||
pub(super) fn read(self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
let offset = (address - SAVE_START) as usize;
|
||||
if self.identify {
|
||||
return match offset {
|
||||
0 => Ok(0xc2),
|
||||
1 => Ok(if self.device == SaveDevice::Flash64 {
|
||||
0x1c
|
||||
} else {
|
||||
9
|
||||
}),
|
||||
_ => Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash ID read outside manufacturer/device bytes",
|
||||
}),
|
||||
};
|
||||
}
|
||||
Ok(image[usize::from(self.bank) * 0x10000 + offset])
|
||||
}
|
||||
|
||||
pub(super) fn write(&mut self, address: u32, value: u8) -> Result<(), MemoryError> {
|
||||
let offset = address - SAVE_START;
|
||||
let error = |operation| MemoryError::UnsupportedIo {
|
||||
address,
|
||||
value,
|
||||
operation,
|
||||
};
|
||||
let mut next = *self;
|
||||
// GBATEK documents direct reset for the 64 KiB chip. For 128 KiB, accept
|
||||
// only a redundant reset while already idle in array mode. No busy/ID exit is inferred.
|
||||
if offset == 0x5555
|
||||
&& value == 0xf0
|
||||
&& (self.device == SaveDevice::Flash64
|
||||
|| (self.phase == Phase::Ready && !self.identify))
|
||||
{
|
||||
next.identify = false;
|
||||
next.phase = Phase::Ready;
|
||||
} else {
|
||||
next.phase = match self.phase {
|
||||
Phase::Ready if offset == 0x5555 && value == 0xaa => Phase::Unlock,
|
||||
Phase::Unlock if offset == 0x2aaa && value == 0x55 => Phase::Command,
|
||||
Phase::Command if offset == 0x5555 => {
|
||||
if self.identify && value != 0xf0 {
|
||||
return Err(error("Flash command while in ID mode"));
|
||||
}
|
||||
match value {
|
||||
0x90 => {
|
||||
next.identify = true;
|
||||
Phase::Ready
|
||||
}
|
||||
0xf0 => {
|
||||
next.identify = false;
|
||||
Phase::Ready
|
||||
}
|
||||
0xb0 if self.device == SaveDevice::Flash128 => Phase::Bank,
|
||||
0xb0 => return Err(error("Flash bank selection on 64 KiB device")),
|
||||
0xa0 => return Err(error("Flash byte programming")),
|
||||
0x80 => return Err(error("Flash erase setup")),
|
||||
_ => return Err(error("Flash command")),
|
||||
}
|
||||
}
|
||||
Phase::Bank if offset == 0 && value < 2 => {
|
||||
next.bank = value;
|
||||
Phase::Ready
|
||||
}
|
||||
Phase::Bank => return Err(error("Flash bank address/value")),
|
||||
_ => return Err(error("Flash unlock/reset sequence")),
|
||||
};
|
||||
}
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ mod fetch;
|
|||
#[cfg(test)]
|
||||
mod fetch_tests;
|
||||
#[cfg(test)]
|
||||
mod flash_step_tests;
|
||||
#[cfg(test)]
|
||||
mod irq_fetch_tests;
|
||||
mod iwram_bus;
|
||||
#[cfg(test)]
|
||||
|
|
@ -27,7 +29,7 @@ use iwram_bus::IwramBus;
|
|||
|
||||
use crate::{
|
||||
audio::{Audio, StereoLevel},
|
||||
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError},
|
||||
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError, SaveDevice, SaveError},
|
||||
cpu::InstructionSet,
|
||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
|
|
@ -53,6 +55,10 @@ pub enum MemoryError {
|
|||
Unaligned(u32),
|
||||
RomTooLarge(usize),
|
||||
InvalidBiosSize(usize),
|
||||
UnsupportedCartridgeAccess {
|
||||
address: u32,
|
||||
operation: &'static str,
|
||||
},
|
||||
UnsupportedIo {
|
||||
address: u32,
|
||||
value: u8,
|
||||
|
|
@ -63,6 +69,9 @@ pub enum MemoryError {
|
|||
impl fmt::Display for MemoryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnsupportedCartridgeAccess { address, operation } => {
|
||||
write!(f, "unsupported {operation} at {address:#010x}")
|
||||
}
|
||||
Self::UnsupportedIo {
|
||||
address,
|
||||
value,
|
||||
|
|
@ -122,6 +131,7 @@ pub struct Memory {
|
|||
video_ram: Vec<u8>,
|
||||
oam: Vec<u8>,
|
||||
rom: Vec<u8>,
|
||||
save_image: Vec<u8>,
|
||||
cartridge: Cartridge,
|
||||
cartridge_step: Cell<Option<Cartridge>>,
|
||||
bios: Option<Vec<u8>>,
|
||||
|
|
@ -153,6 +163,7 @@ impl Memory {
|
|||
video_ram: vec![0; VRAM_SIZE],
|
||||
oam: vec![0; 1024],
|
||||
rom,
|
||||
save_image: Vec::new(),
|
||||
cartridge: Cartridge::default(),
|
||||
cartridge_step: Cell::new(None),
|
||||
bios: None,
|
||||
|
|
@ -183,14 +194,52 @@ impl Memory {
|
|||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Replace optional cartridge hardware with its initial state. Call between machine steps.
|
||||
/// This never modifies the supplied ROM bytes or enables a host clock.
|
||||
/// Replace GPIO/RTC hardware with its initial state. Call between machine steps.
|
||||
/// Save hardware/data are preserved. This never modifies ROM bytes or enables a host clock.
|
||||
pub fn set_cartridge_hardware(&mut self, hardware: CartridgeHardware) {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.cartridge = Cartridge::new(hardware);
|
||||
self.cartridge.set_hardware(hardware);
|
||||
}
|
||||
|
||||
/// Replace save hardware and its array with erased bytes. GPIO/RTC state is unchanged.
|
||||
/// Call between machine steps; no file is loaded or written by the core.
|
||||
pub fn set_save_device(&mut self, device: SaveDevice) {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.save_image = vec![0xff; device.capacity()];
|
||||
self.cartridge.set_save_device(device);
|
||||
}
|
||||
|
||||
/// Inspect the complete save array, independently of ID mode and selected bank.
|
||||
pub fn save_image(&self) -> Option<&[u8]> {
|
||||
(self.cartridge.save_device() != SaveDevice::None).then_some(self.save_image.as_slice())
|
||||
}
|
||||
|
||||
/// Replace the entire array and reset Flash command/bank state. RTC state is unchanged.
|
||||
/// A wrong length changes nothing. This setup API does not emulate programming.
|
||||
pub fn load_save_image(&mut self, bytes: &[u8]) -> Result<(), SaveError> {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
let device = self.cartridge.save_device();
|
||||
if device == SaveDevice::None {
|
||||
return Err(SaveError::NoDevice);
|
||||
}
|
||||
if bytes.len() != device.capacity() {
|
||||
return Err(SaveError::InvalidSize {
|
||||
expected: device.capacity(),
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
self.save_image.copy_from_slice(bytes);
|
||||
self.cartridge.set_save_device(device);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inspect the live calendar, not an in-progress serial read snapshot.
|
||||
|
|
@ -749,6 +798,16 @@ impl Memory {
|
|||
|
||||
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
||||
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if self.cartridge_state().save_mapped(address)
|
||||
&& self
|
||||
.cpu_access
|
||||
.is_some_and(|access| !matches!(access.pc >> 24, 0x02 | 0x03))
|
||||
{
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash CPU read outside work RAM",
|
||||
});
|
||||
}
|
||||
if address >> 24 == 0x04 {
|
||||
if let Some(step) = self.timer_step.get() {
|
||||
if Serial::mapped(address) {
|
||||
|
|
@ -784,7 +843,11 @@ impl Memory {
|
|||
|
||||
// Strict lookup for host access, instruction fetches, and prefetch snapshots.
|
||||
fn read_mapped_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if let Some(value) = self.cartridge_state().read8(address) {
|
||||
let cartridge = self.cartridge_state();
|
||||
if cartridge.save_mapped(address) {
|
||||
return cartridge.read_save8(address, &self.save_image);
|
||||
}
|
||||
if let Some(value) = cartridge.read8(address) {
|
||||
return Ok(value);
|
||||
}
|
||||
match address >> 24 {
|
||||
|
|
@ -1001,6 +1064,15 @@ impl Memory {
|
|||
}
|
||||
Ok(0)
|
||||
}
|
||||
0x0e if self.cartridge_state().save_mapped(address) => {
|
||||
if N != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
});
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
_ => Err(MemoryError::Unmapped(address)),
|
||||
}
|
||||
}
|
||||
|
|
@ -1086,6 +1158,7 @@ impl Memory {
|
|||
if address & 1 != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
self.validate_save_read_width(address)?;
|
||||
self.record_access(address, AccessWidth::Halfword);
|
||||
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
||||
self.iwram_bus
|
||||
|
|
@ -1093,11 +1166,22 @@ impl Memory {
|
|||
Ok(value)
|
||||
}
|
||||
|
||||
fn validate_save_read_width(&self, address: u32) -> Result<(), MemoryError> {
|
||||
if self.cartridge_state().save_mapped(address) {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte read",
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The bus requires alignment; the CPU applies ARM7TDMI load rotation.
|
||||
pub fn read32(&self, address: u32) -> Result<u32, MemoryError> {
|
||||
if address & 3 != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
self.validate_save_read_width(address)?;
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
let value = u32::from_le_bytes([
|
||||
self.read_byte(address)?,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ impl Memory {
|
|||
if address & (width as u32 - 1) != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
if self.cartridge_state().save_mapped(address) {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash instruction fetch",
|
||||
});
|
||||
}
|
||||
let mut bytes = [0; 4];
|
||||
for (offset, byte) in bytes[..width].iter_mut().enumerate() {
|
||||
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
|
||||
|
|
|
|||
148
crates/gba-core/src/memory/flash_step_tests.rs
Normal file
148
crates/gba-core/src/memory/flash_step_tests.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! Original Flash CPU bus phases, failed writes, fetch restrictions, and preflight checks.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
cartridge::{SaveDevice, SAVE_START},
|
||||
cpu::Cpu,
|
||||
io::{IF, WAITCNT},
|
||||
machine::Machine,
|
||||
};
|
||||
|
||||
const HI: u32 = SAVE_START + 0x5555;
|
||||
const LO: u32 = SAVE_START + 0x2aaa;
|
||||
fn unlocked(memory: &mut Memory) {
|
||||
memory.set_save_device(SaveDevice::Flash128);
|
||||
memory.write8(HI, 0xaa).unwrap();
|
||||
memory.write8(LO, 0x55).unwrap();
|
||||
}
|
||||
fn ram_reader(thumb: bool, base: u32) -> (Cpu, Memory) {
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe12fff12],
|
||||
&[(1, SAVE_START), (2, base | u32::from(thumb))],
|
||||
);
|
||||
for at in (0..32).step_by(4) {
|
||||
memory
|
||||
.write32(base + at, if thumb { 0x46c046c0 } else { 0xe1a00000 })
|
||||
.unwrap();
|
||||
}
|
||||
if thumb {
|
||||
memory.write16(base, 0x7808).unwrap();
|
||||
} else {
|
||||
memory.write32(base, 0xe5d10000).unwrap();
|
||||
}
|
||||
cpu.step(&mut memory).unwrap(); // BX from setup ROM into original RAM reader.
|
||||
(cpu, memory)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_flash_commands_commit_once_at_all_save_wait_settings() {
|
||||
for thumb in [false, true] {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x7008 } else { 0xe5c10000 }],
|
||||
&[(0, 0x90), (1, HI)],
|
||||
);
|
||||
unlocked(&mut memory);
|
||||
memory.write16(WAITCNT, wait as u16).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(machine.last_timing().data_cycles, cycles);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_ram_byte_loads_observe_selected_id_with_save_bus_costs() {
|
||||
for thumb in [false, true] {
|
||||
for base in [0x02001000, 0x03001000] {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let (cpu, mut memory) = ram_reader(thumb, base);
|
||||
unlocked(&mut memory);
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
memory.write16(WAITCNT, wait as u16).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0xc2);
|
||||
assert_eq!(machine.last_timing().data_cycles, cycles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_programming_and_rom_execution_reads_roll_back_cpu_and_command_state() {
|
||||
for thumb in [false, true] {
|
||||
for read in [false, true] {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[match (thumb, read) {
|
||||
(false, false) => 0xe5c10000,
|
||||
(true, false) => 0x7008,
|
||||
(false, true) => 0xe5d10000,
|
||||
(true, true) => 0x7808,
|
||||
}],
|
||||
&[(0, 0xa0), (1, if read { SAVE_START } else { HI })],
|
||||
);
|
||||
unlocked(&mut memory);
|
||||
let before = memory.cartridge;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains(if read {
|
||||
"outside work RAM"
|
||||
} else {
|
||||
"programming"
|
||||
}));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, before);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
machine.memory_mut().write8(HI, 0x90).unwrap(); // Failure retained the accepted unlock prefix.
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_id_reads_and_discard_do_not_mutate_array_or_committed_controller() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
unlocked(&mut memory);
|
||||
let before = memory.cartridge;
|
||||
memory.begin_timer_step();
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(memory.cartridge, before);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 255);
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
memory.begin_timer_step();
|
||||
memory.write8(HI, 0xaa).unwrap();
|
||||
memory.write8(LO, 0x55).unwrap();
|
||||
memory.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 255);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert!(memory.save_image().unwrap().iter().all(|&b| b == 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_fetch_and_block_word_access_cannot_bypass_byte_only_device() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
unlocked(&mut memory);
|
||||
for state in [InstructionSet::Arm, InstructionSet::Thumb] {
|
||||
let error = memory
|
||||
.fetch_instruction(SAVE_START, state)
|
||||
.instruction
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("Flash instruction fetch"));
|
||||
}
|
||||
let before = memory.cartridge;
|
||||
assert!(memory
|
||||
.write_words(&[(0x02000000, 0x12345678), (SAVE_START, 0)])
|
||||
.is_err());
|
||||
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||
assert_eq!(memory.cartridge, before);
|
||||
}
|
||||
|
|
@ -87,6 +87,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
0
|
||||
}
|
||||
}
|
||||
// The supported save bus permits byte transfers only; N/S use the same SRAM waits.
|
||||
0x0e | 0x0f => 1 + [4, 3, 2, 8][usize::from(waitcnt & 3)],
|
||||
// BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped.
|
||||
_ => 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,30 @@ fn queue(waitcnt: u16) -> Prefetch {
|
|||
queue
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_wait_changes_preserve_the_queue_until_a_flash_data_access_cancels_it() {
|
||||
for (wait, raw) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let mut q = queue(ENABLE);
|
||||
assert_eq!(
|
||||
q.code(PC, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
q.advance(2); // One cycle remains in the nominal three-cycle ROM halfword.
|
||||
let stream = q.stream;
|
||||
q.configure(ENABLE | wait as u16);
|
||||
assert_eq!(q.stream, stream);
|
||||
assert_eq!(
|
||||
q.data(0x0e005555, AccessWidth::Byte, AccessKind::NonSequential),
|
||||
raw + 1
|
||||
);
|
||||
assert_eq!(q.stream, None);
|
||||
assert_eq!(
|
||||
q.code(PC + 2, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_and_in_progress_fetches_follow_all_rom_wait_settings_and_widths() {
|
||||
for window in 0..3 {
|
||||
|
|
|
|||
276
crates/gba-core/tests/flash.rs
Normal file
276
crates/gba-core/tests/flash.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
//! Original Flash command sequences and supplied save bytes; no game assets or save files.
|
||||
use gba_core::{
|
||||
cartridge::{CartridgeHardware, SaveDevice, SaveError, SAVE_END, SAVE_START},
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{HALTCNT, IE, IF, IME},
|
||||
machine::Machine,
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
const HI: u32 = SAVE_START + 0x5555;
|
||||
const LO: u32 = SAVE_START + 0x2aaa;
|
||||
fn memory(device: SaveDevice) -> Memory {
|
||||
let mut bus = Memory::new(0xeafffffeu32.to_le_bytes().to_vec()).unwrap();
|
||||
bus.set_save_device(device);
|
||||
bus
|
||||
}
|
||||
fn command(bus: &mut Memory, byte: u8) -> Result<(), MemoryError> {
|
||||
bus.write8(HI, 0xaa)?;
|
||||
bus.write8(LO, 0x55)?;
|
||||
bus.write8(HI, byte)
|
||||
}
|
||||
fn bank(bus: &mut Memory, number: u8) {
|
||||
command(bus, 0xb0).unwrap();
|
||||
bus.write8(SAVE_START, number).unwrap();
|
||||
}
|
||||
fn image(device: SaveDevice) -> Vec<u8> {
|
||||
(0..device.capacity())
|
||||
.map(|i| ((i >> 16) * 73 + i * 19 + (i >> 8)) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_erased_state_and_exact_image_loading_are_explicit() {
|
||||
let mut bus = memory(SaveDevice::None);
|
||||
assert_eq!(bus.save_image(), None);
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START),
|
||||
Err(MemoryError::Unmapped(SAVE_START))
|
||||
);
|
||||
assert_eq!(bus.write8(HI, 0xaa), Err(MemoryError::Unmapped(HI)));
|
||||
assert_eq!(bus.load_save_image(&[]), Err(SaveError::NoDevice));
|
||||
for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
|
||||
bus.set_save_device(device);
|
||||
assert_eq!(bus.save_image().unwrap(), vec![255; device.capacity()]);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 255);
|
||||
assert_eq!(bus.read8(SAVE_END).unwrap(), 255);
|
||||
let bytes = image(device);
|
||||
bus.load_save_image(&bytes).unwrap();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
for len in [0, device.capacity() - 1, device.capacity() + 1] {
|
||||
assert_eq!(
|
||||
bus.load_save_image(&vec![0; len]),
|
||||
Err(SaveError::InvalidSize {
|
||||
expected: device.capacity(),
|
||||
actual: len
|
||||
})
|
||||
);
|
||||
assert_eq!(bus.save_image().unwrap(), bytes);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2); // Invalid load did not reset ID state.
|
||||
}
|
||||
bus.load_save_image(&bytes).unwrap(); // Valid host load resets command/bank state.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0]);
|
||||
assert_eq!(bus.read32(ROM_START).unwrap(), 0xeafffffe);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manufacturer_and_device_id_are_separate_bytes_and_exit_preserves_data() {
|
||||
for (device, id) in [(SaveDevice::Flash64, 0x1c), (SaveDevice::Flash128, 9)] {
|
||||
let mut bus = memory(device);
|
||||
let data = image(device);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), id);
|
||||
assert!(bus
|
||||
.read8(SAVE_START + 2)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("ID read outside"));
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), data[1]);
|
||||
assert_eq!(bus.save_image().unwrap(), data);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bank_selection_covers_every_byte_and_survives_id_mode() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let data = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
for selected in [1, 0, 1] {
|
||||
bank(&mut bus, selected);
|
||||
for offset in 0..=0xffff {
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START + offset).unwrap(),
|
||||
data[selected as usize * 0x10000 + offset as usize]
|
||||
);
|
||||
}
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), 9);
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START).unwrap(),
|
||||
data[selected as usize * 0x10000]
|
||||
);
|
||||
}
|
||||
let mut small = memory(SaveDevice::Flash64);
|
||||
assert!(command(&mut small, 0xb0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("64 KiB"));
|
||||
small.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(small.save_image().unwrap().len(), 0x10000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_commands_and_bad_bank_values_are_retryable_without_data_changes() {
|
||||
for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
|
||||
for (byte, message) in [
|
||||
(0xa0, "programming"),
|
||||
(0x80, "erase setup"),
|
||||
(0x10, "Flash command"),
|
||||
] {
|
||||
let mut bus = memory(device);
|
||||
let data = image(device);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
let error = command(&mut bus, byte).unwrap_err();
|
||||
assert!(error.to_string().contains(message));
|
||||
assert_eq!(bus.write8(HI, byte), Err(error));
|
||||
assert_eq!(bus.save_image().unwrap(), data);
|
||||
bus.write8(HI, 0xf0).unwrap(); // Finish the already accepted unlock sequence.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let data = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
command(&mut bus, 0xb0).unwrap();
|
||||
for (address, value) in [(SAVE_START, 2), (SAVE_START, 255), (SAVE_START + 1, 1)] {
|
||||
let error = bus.write8(address, value).unwrap_err();
|
||||
assert_eq!(bus.write8(address, value), Err(error));
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
}
|
||||
bus.write8(SAVE_START, 1).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0x10000]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_aliases_and_invalid_unlocks_remain_diagnostic() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
for address in [SAVE_START - 1, SAVE_END + 1, 0x0f000000, 0x0eff5555] {
|
||||
assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address)));
|
||||
}
|
||||
assert!(bus
|
||||
.read16(SAVE_START)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte read"));
|
||||
assert!(bus
|
||||
.read32(SAVE_START)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte read"));
|
||||
assert!(bus
|
||||
.write16(HI & !1, 0xaa)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte write"));
|
||||
assert!(bus
|
||||
.write32(SAVE_START, 0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte write"));
|
||||
for (address, value) in [(HI, 0x55), (LO, 0xaa)] {
|
||||
let error = bus.write8(address, value).unwrap_err();
|
||||
assert_eq!(bus.write8(address, value), Err(error));
|
||||
}
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
assert!(bus.write8(LO + 1, 0x55).is_err());
|
||||
bus.write8(LO, 0x55).unwrap();
|
||||
assert!(bus.write8(HI + 1, 0x90).is_err());
|
||||
bus.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redundant_128_reset_preserves_array_bank_but_does_not_exit_id_or_partial_unlock() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let bytes = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&bytes).unwrap();
|
||||
bank(&mut bus, 1);
|
||||
bus.write8(HI, 0xf0).unwrap(); // Already idle in array mode.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0x10000]);
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert!(bus.write8(HI, 0xf0).is_err()); // Direct ID exit for this device is not established.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap(); // Redundant reset after documented unlocked ID exit.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0x10000]);
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
assert!(bus.write8(HI, 0xf0).is_err());
|
||||
bus.write8(LO, 0x55).unwrap();
|
||||
bus.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(bus.save_image().unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macronix_64_direct_reset_and_gpio_selection_are_independent() {
|
||||
let mut bus = memory(SaveDevice::Flash64);
|
||||
bus.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
bus.advance_rtc_seconds(42).unwrap();
|
||||
let rtc = bus.rtc_datetime();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 255);
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap(); // Abort partial unlock for this documented device.
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.set_cartridge_hardware(CartridgeHardware::None);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
bus.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
bus.set_rtc_datetime(rtc.unwrap()).unwrap();
|
||||
bus.set_save_device(SaveDevice::Flash128);
|
||||
assert_eq!(bus.rtc_datetime(), rtc);
|
||||
bus.set_save_device(SaveDevice::None);
|
||||
assert_eq!(bus.save_image(), None);
|
||||
assert_eq!(bus.rtc_datetime(), rtc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_stop_and_dma_do_not_create_a_flash_completion_or_change_data() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.write16(IE, 0x2000).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write8(HALTCNT, if stop { 128 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..20 {
|
||||
machine.step().unwrap();
|
||||
}
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.halted(), !stop);
|
||||
assert_eq!(machine.stopped(), stop);
|
||||
}
|
||||
for source in [false, true] {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let dma = DMA_BASE + 36;
|
||||
bus.write32(0x02000000, 0xaa).unwrap();
|
||||
bus.write32(dma, if source { SAVE_START } else { 0x02000000 })
|
||||
.unwrap();
|
||||
bus.write32(dma + 4, if source { 0x02000010 } else { SAVE_START })
|
||||
.unwrap();
|
||||
bus.write32(dma + 8, 0xc0000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("Flash non-byte"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read32(0x02000010).unwrap(), 0);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert!(machine
|
||||
.memory()
|
||||
.save_image()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|&x| x == 255));
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,20 @@ fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() {
|
|||
assert_eq!(memory.waitcnt(), 0x4317);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_byte_access_costs_follow_waitcnt_and_ignore_sequential_kind() {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
|
||||
for address in [0x0e000000, 0x0e005555, 0x0e00ffff] {
|
||||
assert_eq!(
|
||||
bus_cycles(wait as u16, address, AccessWidth::Byte, kind),
|
||||
cycles
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() {
|
||||
for window in 0..3 {
|
||||
|
|
@ -112,8 +126,8 @@ fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
|
||||
// This stateless helper excludes opcode prefetch. PHI output and SRAM remain unmapped.
|
||||
fn stored_prefetch_phi_and_save_wait_settings_do_not_change_rom_bus_costs() {
|
||||
// This stateless helper excludes opcode prefetch. Save waits do not affect ROM cycles.
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
|
||||
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/tests.rs` | Condition truth tables and immediate arithmetic tests |
|
||||
| `crates/gba-core/src/cpu/instruction_tests.rs` | Register operations, transfers, branches, and edge cases |
|
||||
| `crates/gba-core/src/cartridge.rs` | Explicit GPIO/RTC selection, ROM read overlay, pin directions, caller-clock routing, and bounded diagnostics |
|
||||
| `crates/gba-core/src/cartridge/flash.rs` | Explicit save devices, Macronix identification/read controller, bank selection, and unsupported-write diagnostics |
|
||||
| `crates/gba-core/tests/flash.rs` | Erased/supplied images, chip IDs, all bank addresses, invalid commands, reset limits, DMA rejection, and RTC independence |
|
||||
| `crates/gba-core/src/memory/flash_step_tests.rs` | ARM/Thumb Flash writes and RAM reads, wait costs, staged IDs, fetch restrictions, and failed-step/preflight isolation |
|
||||
| `crates/gba-core/src/cartridge/rtc.rs` | RTC framing, control/calendar commands, read snapshots, complete writes, and reset |
|
||||
| `crates/gba-core/src/cartridge/calendar.rs` | Validated decimal calendar, BCD representation, checked epoch conversion, and bounded elapsed-time arithmetic |
|
||||
| `crates/gba-core/src/cartridge/calendar/tests.rs` | Calendar field validation, century-wide rollovers, hour modes, and large/split advances |
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ Unsupported operations and invalid calendar payloads return diagnostics.
|
|||
## Selection
|
||||
|
||||
`Memory::set_cartridge_hardware(CartridgeHardware::Rtc)` attaches the bounded interface with initial state.
|
||||
`CartridgeHardware::None` is the default. Selecting either option replaces peripheral state, without changing ROM bytes.
|
||||
`CartridgeHardware::None` is the default. Selecting either option replaces GPIO/RTC state, without changing ROM bytes.
|
||||
The separately selected [Flash device](saves.md) and its array remain unchanged.
|
||||
Call this API between machine steps. The core does not inspect filenames, game headers, or library signatures to select hardware.
|
||||
|
||||
The desktop enables this interface with `--rtc`, in either ROM execution mode:
|
||||
|
|
|
|||
|
|
@ -33,14 +33,18 @@ It does not decode archives or ELF executables. Supply a raw binary with an ARM
|
|||
Headerless original programs are accepted.
|
||||
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values.
|
||||
All three Game Pak ROM windows use the same supplied bytes and existing wait-state rules.
|
||||
Ordinary cartridge bytes remain read-only. Save devices remain unsupported.
|
||||
Ordinary cartridge bytes remain read-only.
|
||||
Optional `--save-type flash64` or `--save-type flash128` enables [Flash identification and array reads](saves.md).
|
||||
The desktop starts an erased device and does not load or write save files. Programming and erase remain diagnostic.
|
||||
Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC calendar interface](cartridge-gpio.md).
|
||||
It supports date/time reads, complete writes, reset, and explicit elapsed-time updates. RTC interrupts and persistence remain unsupported.
|
||||
The desktop seeds UTC once, then supplies monotonic host elapsed seconds independently of GBA cycles and STOP.
|
||||
|
||||
Both modes call `bios::boot` with the loaded bytes.
|
||||
With `--rtc`, the host then selects the cartridge peripheral before the first machine step.
|
||||
Without that option, no cartridge peripheral is attached. Duplicate `--rtc` options are rejected.
|
||||
Without that option, no RTC is attached. Duplicate `--rtc` options are rejected.
|
||||
Save-device selection is independent of RTC selection. Omitting `--save-type` leaves save memory unmapped.
|
||||
Unknown save types and duplicate options fail before file access. No ROM-based hardware detection is used.
|
||||
The [original BIOS replacement](bios.md) executes its minimal boot before entering `0x08000000` in ARM System mode.
|
||||
There is no command-line option for external BIOS files.
|
||||
Unsupported services remain diagnostics. RegisterRamReset now accepts sound/serial reset flags, including `r0=0xff`, within the initialization subset.
|
||||
|
|
|
|||
|
|
@ -351,6 +351,8 @@ Palette and video RAM cost 1 cycle for byte/halfword accesses and 2 for words, w
|
|||
ROM costs include one cycle plus the configured wait states for each 16-bit transfer.
|
||||
A ROM word uses two halfword accesses; its second halfword always uses sequential timing.
|
||||
Accesses at 128 KiB ROM boundaries force non-sequential timing for the first halfword.
|
||||
Supported save-byte accesses use WAITCNT bits 0–1: one cycle plus 4, 3, 2, or 8 wait states.
|
||||
Sequential and non-sequential save requests use the same nominal cost. Flash data accesses cancel the opcode prefetch queue.
|
||||
|
||||
`WAITCNT` resets to zero and supports byte, halfword, and word access.
|
||||
The writable mask is `0x5fff`; the Game Pak type flag reads as GBA, and the upper halfword reads as zero.
|
||||
|
|
@ -421,6 +423,7 @@ Important timing limits:
|
|||
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
|
||||
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
|
||||
- Game Pak prefetch is nominal. Full-buffer restart, page boundaries, cancellation, and live WAITCNT changes need independent hardware validation.
|
||||
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
|
||||
- Save wait fields drive the supported Flash byte-access costs. PHI output and SRAM mapping remain unimplemented.
|
||||
See [Flash access restrictions](saves.md#bus-behavior); wider save transfers and physical bus timing remain unsupported or unverified.
|
||||
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
|
||||
- Exact DMA startup/resumption delays, display-bus contention, and timer startup/register-write delays remain unmodeled.
|
||||
|
|
|
|||
122
docs/hardware/saves.md
Normal file
122
docs/hardware/saves.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Cartridge Flash identification and read subset
|
||||
|
||||
The core supports explicit Macronix Flash selection, chip identification, array reads, and 128 KiB bank selection.
|
||||
Programming, erase, busy polling, and host save-file persistence remain unimplemented.
|
||||
This is not working game-save support. Unsupported writes fail instead of reporting a completed save.
|
||||
SRAM, EEPROM, and other manufacturers' protocols remain separate work.
|
||||
|
||||
## Selection and initial data
|
||||
|
||||
The desktop accepts either `--save-type flash64` or `--save-type flash128` in ROM terminal/window modes.
|
||||
The option is independent of `--rtc`. Omitting it leaves save memory unmapped.
|
||||
Unknown values and duplicate options fail before opening the ROM.
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --steps 3000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window --frames 60
|
||||
```
|
||||
|
||||
Selection is explicit, not inferred from a filename, ROM signature, or the first write.
|
||||
A 64 KiB device cannot silently become a 128 KiB device.
|
||||
The desktop starts each selected device with erased `0xff` bytes and reports the read-only subset's limits.
|
||||
It does not look for, read, create, or overwrite a save file. Existing game saves remain untouched.
|
||||
The ROM suite schema still uses no save device by default and has no save-selection field yet.
|
||||
|
||||
Core APIs on `Memory`:
|
||||
|
||||
- `set_save_device(SaveDevice)` replaces save hardware and initializes its array to erased bytes.
|
||||
`None`, `Flash64`, and `Flash128` are available. The call preserves GPIO/RTC state.
|
||||
- `load_save_image(&[u8])` supplies an exact-capacity image and resets command state, ID mode, and bank selection.
|
||||
A wrong length or missing device changes nothing. This is host setup, not an emulated program operation.
|
||||
- `save_image()` inspects the complete array, independently of ID mode or selected bank.
|
||||
It returns `None` when no save device is selected.
|
||||
|
||||
Configuration and image replacement occur between machine steps. No filesystem operations belong to the core.
|
||||
Selecting GPIO/RTC hardware does not replace an attached save device or its bytes.
|
||||
This feature can inspect caller-supplied images but cannot modify their data through Flash commands.
|
||||
|
||||
## Bus behavior
|
||||
|
||||
Only `0x0e000000` through `0x0e00ffff` are mapped when a device is selected.
|
||||
The Flash interface uses byte accesses. Halfword/word transfers, DMA, and instruction fetches remain diagnostic.
|
||||
Unverified aliases outside this window remain unmapped rather than returning mirrored data.
|
||||
Ordinary ROM writes remain read-only.
|
||||
|
||||
CPU Flash reads must execute from external or internal work RAM, as required by GBATEK's access description.
|
||||
A read from ROM/BIOS code reports a diagnostic rather than inventing a value for an unsupported bus sequence.
|
||||
Host byte inspection has no CPU execution restriction. CPU command writes may execute from ROM.
|
||||
|
||||
WAITCNT bits 0–1 select save wait states 4, 3, 2, or 8.
|
||||
The nominal byte-access costs are therefore 5, 4, 3, or 9 cycles, for both sequential and non-sequential requests.
|
||||
Save waits do not alter ROM wait fields. Flash data accesses cancel Game Pak opcode prefetch through the existing nominal model.
|
||||
Changing only save wait bits preserves the queue until a cartridge access cancels it.
|
||||
These costs are source-backed estimates, not physical timing measurements.
|
||||
|
||||
## Commands
|
||||
|
||||
Use these byte addresses and values for the unlock prefix:
|
||||
|
||||
1. Write `0xaa` to `0x0e005555`.
|
||||
2. Write `0x55` to `0x0e002aaa`.
|
||||
3. Write a command byte to `0x0e005555`.
|
||||
|
||||
Supported commands after the prefix:
|
||||
|
||||
| Command | Behavior |
|
||||
| --- | --- |
|
||||
| `0x90` | Enter chip-identification mode |
|
||||
| `0xf0` | Exit identification mode and return to array reads |
|
||||
| `0xb0` | On Flash128 only, accept a bank byte next at `0x0e000000` |
|
||||
|
||||
In ID mode, byte zero is the manufacturer and byte one is the device:
|
||||
|
||||
| Selection | Manufacturer | Device | Combined ID |
|
||||
| --- | --- | --- | --- |
|
||||
| Flash64 | `0xc2` | `0x1c` | `0x1cc2` |
|
||||
| Flash128 | `0xc2` | `0x09` | `0x09c2` |
|
||||
|
||||
Other ID-mode addresses remain diagnostic because their values are not established by this subset.
|
||||
Array mode reads bytes from the selected 64 KiB bank. Flash64 always uses bank zero.
|
||||
Flash128 accepts bank values zero and one only. Bank changes do not alter stored data.
|
||||
Entering and leaving ID mode preserves the selected bank.
|
||||
Reads during an incomplete unlock retain the previous read mode and bank.
|
||||
|
||||
For the documented 64 KiB Macronix device, a direct `0xf0` write at `0x0e005555` resets command/ID state.
|
||||
For Flash128, the same write is accepted only as a redundant reset when already idle in array mode.
|
||||
This bounded no-op matches the inspected emulators' idle behavior. It does not establish direct ID exit or busy-command cancellation.
|
||||
Flash128 still requires the full unlock prefix to exit ID mode. Direct reset during an incomplete unlock remains diagnostic.
|
||||
No matching manufacturer datasheet or physical measurement was used to claim broader 128 KiB reset behavior.
|
||||
|
||||
`0xa0` byte-program setup and `0x80` erase setup remain diagnostic at the command write.
|
||||
No operation starts, data changes, busy state, completion flag, or interrupt is fabricated.
|
||||
Unknown commands, invalid prefixes, and invalid bank writes are also diagnostic.
|
||||
Rejected accesses retain the previous command state and bytes, so retrying the same access gives the same result.
|
||||
This atomic diagnostic policy does not reproduce hardware handling of every malformed sequence.
|
||||
|
||||
## Transactions and validation
|
||||
|
||||
The small Flash controller state joins the existing cartridge machine-step transaction.
|
||||
The full save image stays outside the copied controller state; this read-only subset never mutates it during emulation.
|
||||
Failed CPU or DMA steps discard speculative controller changes and timing.
|
||||
Block-store preflight rejects a wide Flash destination before committing earlier RAM or device writes.
|
||||
HALT/STOP and elapsed cycles cannot create Flash completion events. There is no pending program or erase operation to finish.
|
||||
|
||||
Original tests cover both device IDs, every byte in both banks, exact image lengths, erased state, invalid sequences, and unchanged ROM/save data.
|
||||
They distinguish redundant idle reset from unsupported Flash128 ID/partial-command reset.
|
||||
ARM/Thumb probes verify ROM command writes, RAM byte reads, wait settings, restrictions, and retryable failed steps.
|
||||
Additional tests cover staged ID reads, block rollback, DMA rejection, GPIO/RTC independence, and prefetch cancellation.
|
||||
Desktop tests execute an original IWRAM byte reader with explicit device selection in terminal and native-window modes.
|
||||
No game code, save file, or copyrighted assets are included in these tests.
|
||||
|
||||
## Remaining work
|
||||
|
||||
Implement and validate program/erase commands, busy polling, cancellation, and byte-program bit transitions before claiming save functionality.
|
||||
Then add explicit host save loading and safe persistence, with exact-size validation and restart tests.
|
||||
Do not overwrite existing files or add immediate-success responses merely to pass a game check.
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK backup Flash](https://problemkaputt.de/gbatek-gba-cart-backup-flash-rom.htm): capacities, IDs, unlock sequences, bank selection, 64 KiB Macronix reset, byte-only access, and RAM-execution requirements.
|
||||
- [GBATEK](https://mgba-emu.github.io/gbatek/), WAITCNT section: save wait fields and one-cycle-plus-wait timing.
|
||||
- [mGBA savedata at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/savedata.c): comparison for controller phases, bank addressing, ID overlays, and idle reset handling. Its default chip IDs differ; its write/erase timing and busy approximation are not copied.
|
||||
- [ares Flash at 9408cb43](https://github.com/ares-emulator/ares/blob/9408cb43d4948fc3ea6e152a307a34348df3fe04/ares/gba/cartridge/flash.cpp): comparison for IDs, banking, unlocked ID exit, and unchanged idle state after an unrecognized direct reset. Its immediate program/erase behavior and permissive unlock handling are not adopted.
|
||||
|
|
@ -25,8 +25,8 @@ Byte, halfword, word, block-store, and DMA validation retain explicit errors wit
|
|||
Use a local ROM that you may lawfully use:
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --steps 2000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --window --frames 60
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --steps 3000000
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window --frames 60
|
||||
```
|
||||
|
||||
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
|
||||
|
|
@ -71,16 +71,22 @@ The calendar model now supports date/time reads, complete writes, reset, and cal
|
|||
Core tests inject deterministic time. Desktop `--rtc` uses UTC startup and monotonic elapsed time, including during GBA STOP.
|
||||
Select it explicitly; ordinary ROM bytes and other ROMs' default hardware remain unchanged.
|
||||
|
||||
Both terminal and native runs with `--rtc` now reach a Flash unlock write:
|
||||
RTC support then reached a Flash unlock write (`0xaa` at `0x0e005555`, PC `0x082e1892`).
|
||||
The explicit [Flash read subset](../hardware/saves.md) now handles identification, array reads, and bank selection.
|
||||
Select `--save-type flash128` for this bounded run; the device begins erased and no save file is opened.
|
||||
A later redundant `0xf0` write occurred after returning to idle array mode.
|
||||
The subset accepts that no-op without claiming direct Flash128 ID exit or busy-command cancellation.
|
||||
|
||||
Both terminal and native runs with these options now reach Joybus mode selection:
|
||||
|
||||
```text
|
||||
Steps: 964375; instructions: 961679; IRQ entries: 80; DMA units: 2616; HALT idle: 0
|
||||
Nominal cycles: 3546961
|
||||
PC=0x082e1892
|
||||
unmapped memory at 0x0e005555
|
||||
Steps: 1389249; instructions: 1386088; IRQ entries: 89; DMA units: 3072; HALT idle: 0
|
||||
Nominal cycles: 4688039
|
||||
PC=0x082deeca
|
||||
unsupported Joybus serial mode: write 0xc0 at 0x04000135
|
||||
```
|
||||
|
||||
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
||||
The native run captures sixteen startup frames. These do not establish a working title screen or gameplay.
|
||||
Audio-device behavior is validated by original tests, not by audible game output.
|
||||
The window run does not reach its requested frame limit and correctly exits with failure.
|
||||
Terminal execution is also diagnostic, not a compatibility pass.
|
||||
|
|
@ -92,9 +98,9 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
|||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next requirement is a cartridge Flash save device, beginning with command unlock and identification.
|
||||
The failing state contains value `0xaa` and address `0x0e005555`, matching the documented unlock sequence.
|
||||
Research device selection, IDs, banking, program/erase, busy behavior, and safe persistence before claiming save compatibility.
|
||||
The next startup requirement is disconnected Joybus configuration, beginning with `RCNT=0xc000`.
|
||||
Research idle registers, absent-device status, and mode transitions before implementing them. Do not fabricate a connected peer.
|
||||
Flash programming, erase, busy behavior, and safe persistence remain required before claiming save compatibility.
|
||||
RTC persistence and cartridge IRQ behavior remain separate missing features.
|
||||
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.
|
||||
This result does not establish gameplay, linked transfers, or cartridge/serial GPIO interrupt support.
|
||||
|
|
@ -108,4 +114,5 @@ Cartridge saves remain a separate required feature. SRAM alone will not support
|
|||
|
||||
Hardware and emulator references are recorded in the linked subsystem documents.
|
||||
[GBATEK backup Flash](https://problemkaputt.de/gbatek-gba-cart-backup-flash-rom.htm) identifies the `0xaa`/`0x5555` unlock and chip-identification sequence.
|
||||
The bounded failure establishes an access requirement, not the device's capacity or full save compatibility.
|
||||
The selected device is an explicit test configuration, not a cartridge-capacity determination from the filename or unlock write.
|
||||
The bounded run does not establish save compatibility.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Code costs use actual ARM PC+8/Thumb PC+4 source fetches and branch/exception target pairs, including region and ROM boundaries.
|
||||
- IRQ entry samples its discarded incoming-state fetch before the ARM vector pair, with local IWRAM history updates.
|
||||
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
||||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows and supported save-byte accesses.
|
||||
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
|
||||
- Staged timer/audio/serial clocks follow CPU and DMA bus-completion phases, including timer/serial IF reads, acknowledgements, and failed-step rollback.
|
||||
- The unchanged original ARM cancellation probe matches both published read interval totals and its unadjusted timer samples.
|
||||
|
|
@ -79,7 +79,9 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Explicit cartridge RTC selection with first-window GPIO read enable, direction/latch behavior, and serial calendar/control transactions.
|
||||
- Validated RTC date/time, BCD transfers, read snapshots, complete writes/reset, and caller-supplied battery-clock advancement.
|
||||
- Desktop `--rtc` seeds UTC once and supplies host elapsed seconds independently of CPU cycles and STOP.
|
||||
- Cartridge GPIO changes participate in CPU/DMA rollback and block-store preflight without modifying ROM bytes.
|
||||
- Cartridge GPIO and Flash controller changes participate in CPU/DMA rollback and block-store preflight without modifying ROM bytes.
|
||||
- Explicit Macronix Flash64/Flash128 identification, read-only arrays, and 128 KiB bank selection.
|
||||
- Exact-size core save-image initialization and inspection; desktop save selection starts erased and performs no save-file access.
|
||||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
||||
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
||||
|
|
@ -142,7 +144,9 @@ Internal affine origins, vertical window flags, and vertical mosaic counters tra
|
|||
Horizontal window comparators retain four-cycle event history.
|
||||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||
There is no host audio output or cartridge save support.
|
||||
There is no host audio output or save writing/persistence.
|
||||
The [Flash subset](hardware/saves.md) supports explicit device selection, identification, array reads, and banking.
|
||||
Flash programming, erase, busy polling, wider accesses, unverified aliases, and other save-device protocols remain diagnostic or unimplemented.
|
||||
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports calendar/control transfers and explicit clock updates, selected with `--rtc` in ROM modes.
|
||||
RTC persistence, interrupts, invalid-date correction, physical subsecond timing, GPIO aliases, and byte writes remain unsupported or unverified.
|
||||
The core uses deterministic caller time. Desktop RTC runs use host UTC/elapsed time and are not fully deterministic.
|
||||
|
|
@ -169,7 +173,8 @@ It also completes a CpuSet call to address zero through the corrected ignored BI
|
|||
It also accepts an external-clock serial request without inventing clock edges or completion.
|
||||
It now also completes internally clocked normal transfers and selects idle multiplayer mode.
|
||||
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and RTC calendar reads.
|
||||
It next stops at a Flash unlock write (`0x0e005555`, PC `0x082e1892`). Cartridge save devices remain unimplemented.
|
||||
With `--save-type flash128`, it also passes Flash identification and reaches a later startup check.
|
||||
It next stops at Joybus mode selection (`0x04000135`, PC `0x082deeca`). Flash writing and persistence remain unimplemented.
|
||||
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
|
||||
No title-screen, gameplay, or audible-game compatibility is established.
|
||||
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
||||
|
|
@ -201,7 +206,7 @@ Host inspection and instruction fetches remain strict. Other unmapped reads retu
|
|||
Reads beyond the supplied cartridge bytes also return errors.
|
||||
Direct memory-bus halfword and word accesses require alignment.
|
||||
CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus.
|
||||
Ordinary ROM writes remain read-only. Only the explicitly selected cartridge GPIO register subset accepts writes.
|
||||
Ordinary ROM writes remain read-only. Explicitly selected cartridge GPIO and supported Flash command writes are separate from ROM storage.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ ARM/Thumb and DMA3 checks retain Game Pak costs; staged fetches, rejected edges,
|
|||
Desktop tests execute original ARM calendar commands in terminal and native-window modes with explicit `--rtc` selection.
|
||||
Host-adapter tests inject UTC epoch values and elapsed durations without sleeping. Guest time changes survive subsequent elapsed updates.
|
||||
Physical pin timing, invalid-date hardware correction, GPIO aliases, and battery persistence remain unverified or unimplemented.
|
||||
Flash regressions use original command sequences and exact-size generated arrays, not game saves.
|
||||
They check device IDs, every byte in both 128 KiB banks, erased state, image validation, reset limits, and unsupported program/erase commands.
|
||||
ARM/Thumb probes verify RAM byte reads, ROM command writes, all save wait settings, staged ID state, and failed-step rollback.
|
||||
Wide accesses, DMA, instruction fetches, and CPU reads outside work RAM retain explicit diagnostics.
|
||||
An original desktop ROM installs its Flash byte reader in IWRAM; terminal/native tests check explicit selection and unchanged files.
|
||||
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
||||
The optional [local Emerald startup check](research/emerald-reset.md) now reaches sound activation but still fails deliberately there.
|
||||
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
||||
|
|
|
|||
|
|
@ -41,11 +41,12 @@ This exposed and corrected missing firmware affine-scale initialization, without
|
|||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
||||
It also passes normal serial transfers and idle multiplayer configuration.
|
||||
With `--rtc`, it passes Game Pak GPIO setup and RTC calendar reads, then stops at a Flash unlock write.
|
||||
With `--rtc --save-type flash128`, it passes Game Pak GPIO setup, RTC reads, and Flash identification.
|
||||
It next stops at Joybus serial-mode selection.
|
||||
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||
See [the runtime result](research/emerald-reset.md). Flash identification and its save protocol are the next demonstrated startup requirement.
|
||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||
See [the runtime result](research/emerald-reset.md). Disconnected Joybus configuration is the next demonstrated startup requirement.
|
||||
The separate save requirement includes Flash programming/erase and safe host persistence. Identification/read support is not successful saving.
|
||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||
|
||||
[Paperdomo101/2048-GBA at ae36800d](https://github.com/Paperdomo101/2048-GBA/tree/ae36800dfa1314f96c4c8785b5c2d077fb731aff) is the next independent candidate.
|
||||
|
|
@ -56,8 +57,9 @@ A reproducible source build or an accessible upstream ROM is needed before recor
|
|||
|
||||
Next implementation steps:
|
||||
|
||||
1. Verify SRAM bus widths, mirroring, erased state, and wait-state behavior against independent references.
|
||||
2. Add explicit cartridge save-device selection and original core regressions. Keep absent-device accesses diagnostic.
|
||||
1. Research the demonstrated Joybus configuration failure without inventing a connected device.
|
||||
2. Extend explicit Flash selection with verified programming, erase, and busy behavior. Keep absent devices and unsupported commands diagnostic.
|
||||
Add SRAM separately when the independent candidate requires it; verify its widths, mirroring, and wait behavior first.
|
||||
3. Add bounded save loading and safe persistence in the desktop crate. Do not put file operations in the core.
|
||||
4. Test a save/restart/load cycle across processes with original ROM instructions before external-game validation.
|
||||
5. Resume the second-game run and implement demonstrated audio requirements rather than accepting writes without behavior.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue