feat(cartridge): add 512-byte and 8 KiB serial EEPROM saves
Select them with --save-type eeprom512 or eeprom8k. The part follows GBATEK's protocol on Game Pak data bit 0: DMA3 sends one bit per halfword, '11' + address + '0' requests a 64-bit block that reads back after four dummy bits, and '10' + address + data + '0' writes one. A write reports busy for a nominal 108,368 cycles before the block is stored. 8 KiB parts decode the low 10 address bits. ROMs above 16 MiB map EEPROM only at 0x0dffff00..0x0dffffff. Bits shift out only inside machine steps, so failed steps roll back. CPU stores, byte/word accesses, CPU reads during a read-out, and malformed or overlapping requests are diagnostics. Blocks are stored first-bit-as-MSB, matching common .sav layouts, and work with save files and save states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU
This commit is contained in:
parent
4e4dde742d
commit
652ea397f2
15 changed files with 828 additions and 24 deletions
|
|
@ -23,7 +23,7 @@ use gba_core::{
|
|||
|
||||
const MAX_STEPS: u64 = 100_000_000;
|
||||
const MAX_FRAMES: u64 = 100_000;
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128|sram] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); speed: integer 1..=16 (default 1, audio muted above 1); steps: 1..=100000000, frames: 1..=100000; state file (window only): F5 saves and F9 loads a save state, default is the ROM path with a .state extension; do not combine with demo or help options";
|
||||
const USAGE: &str = "usage: gameboy-rust --rom PATH [--rtc] [--save-type flash64|flash128|sram|eeprom512|eeprom8k] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); speed: integer 1..=16 (default 1, audio muted above 1); steps: 1..=100000000, frames: 1..=100000; state file (window only): F5 saves and F9 loads a save state, default is the ROM path with a .state extension; do not combine with demo or help options";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
|
|
@ -70,6 +70,8 @@ pub fn parse_args(args: &[OsString]) -> io::Result<Options> {
|
|||
Some("flash64") => SaveDevice::Flash64,
|
||||
Some("flash128") => SaveDevice::Flash128,
|
||||
Some("sram") => SaveDevice::Sram,
|
||||
Some("eeprom512") => SaveDevice::Eeprom512,
|
||||
Some("eeprom8k") => SaveDevice::Eeprom8k,
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
} else if arg == "--save-file" && save_file.is_none() {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,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] [--save-type flash64|flash128|sram] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); 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|sram|eeprom512|eeprom8k] [--save-file PATH] (--steps COUNT | --window [--frames COUNT] [--audio] [--speed MULTIPLIER] [--state-file PATH]); or gameboy-rust --test-suite PATH.json",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
--speed MULTIPLIER ROM-window speed: integer 1..=16 (default: 1); mutes audio above 1x\n\
|
||||
--audio Enable macOS output at 1x (default: muted)\n\
|
||||
--rtc Select cartridge RTC (UTC startup, host elapsed time)\n\
|
||||
--save-type flash64|flash128|sram Select nominal Macronix Flash or 32 KiB SRAM\n\
|
||||
--save-type flash64|flash128|sram|eeprom512|eeprom8k Select nominal Flash, 32 KiB SRAM, or serial EEPROM\n\
|
||||
--save-file PATH Load raw save image; persist on clean exit or window diagnostic\n\
|
||||
--state-file PATH ROM window save state; F5 saves, F9 loads (default: ROM path + .state)\n\
|
||||
--test-suite PATH.json Run bounded ROM assertions; write a JSON report\n\
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ fn explicit_flash_selection_runs_ram_byte_reader_and_leaves_files_unchanged() {
|
|||
fn invalid_save_type_options_fail_before_file_access() {
|
||||
for extra in [
|
||||
vec!["--save-type"],
|
||||
vec!["--save-type", "eeprom"],
|
||||
vec!["--save-type", "fram"],
|
||||
vec!["--save-type", ""],
|
||||
vec!["--save-type", "flash128", "--save-type", "flash64"],
|
||||
] {
|
||||
|
|
|
|||
|
|
@ -254,3 +254,82 @@ fn sram_bytes_written_and_read_from_rom_code_persist_across_processes() {
|
|||
assert!(text(&output).contains("r6=0x0000003c"), "{}", text(&output));
|
||||
assert!(text(&output).contains("Save unchanged"));
|
||||
}
|
||||
|
||||
/// One halfword per bit, MSB first, packed into ROM words.
|
||||
fn bit_halfwords(bits: &[u8]) -> Vec<u32> {
|
||||
bits.chunks(2)
|
||||
.map(|pair| u32::from(pair[0]) | pair.get(1).map_or(0, |&b| u32::from(b) << 16))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bits(value: u64, width: u32) -> Vec<u8> {
|
||||
(0..width)
|
||||
.rev()
|
||||
.map(|shift| ((value >> shift) & 1) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Run `dma` transfers (source, destination, halfword count) from ROM code, then `tail`.
|
||||
/// Halfword bitstreams live at ROM offset 0x400.
|
||||
fn eeprom_rom(dma: &[(u32, u32, u32)], tail: &[u32], data: &[u8]) -> Vec<u32> {
|
||||
let mut code = Vec::new();
|
||||
for &(source, destination, count) in dma {
|
||||
load(&mut code, 0, 0x0400_00d4); // DMA3 source register
|
||||
load(&mut code, 1, source);
|
||||
load(&mut code, 2, destination);
|
||||
load(&mut code, 3, 0x8000_0000 | count);
|
||||
code.push(0xe880_000e); // STMIA r0,{r1-r3}
|
||||
}
|
||||
code.extend(tail);
|
||||
code.push(0xeafffffe);
|
||||
code.resize(0x100, 0);
|
||||
code.extend(bit_halfwords(data));
|
||||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eeprom_blocks_written_and_read_through_dma3_persist_across_processes() {
|
||||
let f = Fixture::new();
|
||||
let save = f.save();
|
||||
let request = [
|
||||
vec![1, 0],
|
||||
bits(1, 6),
|
||||
bits(0x8011_2233_4455_6677, 64),
|
||||
vec![0],
|
||||
]
|
||||
.concat();
|
||||
let write = f.rom(
|
||||
"eeprom-writer.gba",
|
||||
&eeprom_rom(&[(0x0800_0400, 0x0d00_0000, 73)], &[], &request),
|
||||
);
|
||||
// Enough steps for the nominal ~108k-cycle write to finish before exit.
|
||||
let output = command(&write, &save, "eeprom512")
|
||||
.args(["--steps", "100000"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{}", text(&output));
|
||||
assert!(text(&output).contains("Save persisted on clean exit"));
|
||||
let image = fs::read(&save).unwrap();
|
||||
assert_eq!(image.len(), 512);
|
||||
assert_eq!(image[8..16], 0x8011_2233_4455_6677_u64.to_be_bytes());
|
||||
assert!(image[..8].iter().chain(&image[16..]).all(|&v| v == 255));
|
||||
|
||||
// Request block 1, shift out 68 bits into IWRAM, then load data bits 0 and 1.
|
||||
let request = [vec![1, 1], bits(1, 6), vec![0]].concat();
|
||||
let read = f.rom(
|
||||
"eeprom-reader.gba",
|
||||
&eeprom_rom(
|
||||
&[
|
||||
(0x0800_0400, 0x0d00_0000, 9),
|
||||
(0x0d00_0000, 0x0300_0000, 68),
|
||||
],
|
||||
&[0xe1d2_50b8, 0xe1d2_60ba], // LDRH r5,[r2,#8]; LDRH r6,[r2,#10]
|
||||
&request,
|
||||
),
|
||||
);
|
||||
let output = run(&read, &save, "eeprom512");
|
||||
assert!(output.status.success(), "{}", text(&output));
|
||||
assert!(text(&output).contains("r5=0x00000001"), "{}", text(&output));
|
||||
assert!(text(&output).contains("r6=0x00000000"), "{}", text(&output));
|
||||
assert!(text(&output).contains("Save unchanged"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue