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:
0xrsydn 2026-10-14 12:06:17 +07:00
commit 652ea397f2
15 changed files with 828 additions and 24 deletions

View file

@ -4,10 +4,13 @@ use crate::memory::MemoryError;
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
mod calendar;
mod eeprom;
mod flash;
mod rtc;
mod sram;
pub use calendar::{RtcDateTime, RtcError};
use eeprom::Eeprom;
pub use eeprom::EEPROM_WRITE_CYCLES;
use flash::Flash;
pub(crate) use flash::SaveMutation;
pub use flash::{
@ -34,6 +37,7 @@ pub(crate) struct Cartridge {
gpio: Option<Gpio>,
flash: Option<Flash>,
sram: Option<Sram>,
eeprom: Option<Eeprom>,
}
impl Cartridge {
@ -41,18 +45,35 @@ impl Cartridge {
self.gpio = (hardware == CartridgeHardware::Rtc).then(Gpio::default);
}
pub(crate) fn set_save_device(&mut self, device: SaveDevice) {
/// The ROM length selects the EEPROM window: carts above 16 MiB decode only its top 256 bytes.
pub(crate) fn set_save_device(&mut self, device: SaveDevice, rom_len: usize) {
self.flash = Flash::new(device);
self.sram = (device == SaveDevice::Sram).then(Sram::default);
self.eeprom = Eeprom::new(device, rom_len);
}
pub(crate) fn save_device(&self) -> SaveDevice {
if self.sram.is_some() {
return SaveDevice::Sram;
}
if let Some(eeprom) = self.eeprom {
return eeprom.device();
}
self.flash.map_or(SaveDevice::None, Flash::device)
}
pub(crate) fn eeprom_mapped(&self, address: u32) -> bool {
self.eeprom.is_some_and(|eeprom| eeprom.mapped(address))
}
/// One EEPROM halfword read. CPU reads poll status; other reads shift out data.
pub(crate) fn read_eeprom16(&mut self, address: u32, cpu: bool) -> Result<u16, MemoryError> {
self.eeprom
.as_mut()
.expect("mapped EEPROM")
.read(address, cpu)
}
pub(crate) fn save_mapped(&self, address: u32) -> bool {
(self.flash.is_some() || self.sram.is_some()) && (SAVE_START..=SAVE_END).contains(&address)
}
@ -66,10 +87,20 @@ impl Cartridge {
if let Some(flash) = self.flash.as_mut() {
flash.advance(cycles);
}
if let Some(eeprom) = self.eeprom.as_mut() {
eeprom.advance(cycles);
}
}
pub(crate) fn save_unfinished(&self) -> bool {
self.flash.is_some_and(Flash::unfinished) || self.sram.is_some_and(Sram::unfinished)
self.flash.is_some_and(Flash::unfinished)
|| self.sram.is_some_and(Sram::unfinished)
|| self.eeprom.is_some_and(Eeprom::unfinished)
}
/// A completed EEPROM write: its first byte offset and the eight block bytes.
pub(crate) fn take_save_block(&mut self) -> Option<(usize, [u8; 8])> {
self.eeprom.as_mut().and_then(Eeprom::take_block)
}
pub(crate) fn take_save_mutation(&mut self) -> Option<SaveMutation> {
@ -111,6 +142,7 @@ impl Cartridge {
pub(crate) fn mapped(&self, address: u32) -> bool {
self.save_mapped(address)
|| self.eeprom_mapped(address)
|| (self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address))
}
@ -130,15 +162,31 @@ impl Cartridge {
}
/// Apply one access to a copy first. A rejected second halfword changes nothing.
/// `cpu` marks CPU stores, which cannot carry EEPROM bitstreams.
pub(crate) fn write(
&mut self,
address: u32,
bytes: &[u8],
image: &[u8],
cpu: bool,
) -> Result<(), MemoryError> {
if !self.mapped(address) {
return Ok(()); // Non-cartridge accesses are validated by the bus.
}
if self.eeprom_mapped(address) {
if bytes.len() != 2 {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,
operation: "EEPROM non-halfword write",
});
}
let value = u16::from_le_bytes([bytes[0], bytes[1]]);
return self
.eeprom
.as_mut()
.unwrap()
.write(address, value, cpu, image);
}
if self.save_mapped(address) {
if bytes.len() != 1 {
return Err(MemoryError::UnsupportedCartridgeAccess {
@ -213,6 +261,8 @@ impl Cartridge {
SaveDevice::Flash64 => 1,
SaveDevice::Flash128 => 2,
SaveDevice::Sram => 3,
SaveDevice::Eeprom512 => 4,
SaveDevice::Eeprom8k => 5,
},
]
}
@ -220,7 +270,12 @@ impl Cartridge {
impl Snapshot for Cartridge {
fn save(&self, writer: &mut Writer) {
let Self { gpio, flash, sram } = self;
let Self {
gpio,
flash,
sram,
eeprom,
} = self;
writer.tag(b"CART");
writer.raw(&self.setup_identity());
if let Some(gpio) = gpio {
@ -232,11 +287,19 @@ impl Snapshot for Cartridge {
if let Some(sram) = sram {
sram.save(writer);
}
if let Some(eeprom) = eeprom {
eeprom.save(writer);
}
}
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
let target_device = self.save_device();
let Self { gpio, flash, sram } = self;
let Self {
gpio,
flash,
sram,
eeprom,
} = self;
reader.tag(b"CART")?;
let hardware = reader.u8()?;
@ -252,6 +315,8 @@ impl Snapshot for Cartridge {
1 => SaveDevice::Flash64,
2 => SaveDevice::Flash128,
3 => SaveDevice::Sram,
4 => SaveDevice::Eeprom512,
5 => SaveDevice::Eeprom8k,
_ => return Err(SnapshotError::Corrupt("save device")),
};
if save_device != target_device {
@ -267,6 +332,9 @@ impl Snapshot for Cartridge {
if let Some(sram) = sram {
sram.load(reader)?;
}
if let Some(eeprom) = eeprom {
eeprom.load(reader)?;
}
Ok(())
}
}
@ -321,7 +389,7 @@ mod snapshot_tests {
fn configured_cartridge(hardware: CartridgeHardware, device: SaveDevice) -> Cartridge {
let mut cartridge = Cartridge::default();
cartridge.set_hardware(hardware);
cartridge.set_save_device(device);
cartridge.set_save_device(device, 0);
cartridge
}

View file

@ -0,0 +1,365 @@
//! Serial EEPROM on Game Pak data bit 0, following GBATEK's bitstream description.
//! Requests arrive one bit per halfword: `11` + address + `0` reads a 64-bit block, and
//! `10` + address + 64 data bits + `0` writes one. A read returns 4 ignored bits, then the
//! block MSB first. A write completes after a nominal delay; until then status reads 0.
//! Block bytes are stored in transfer order, the first data bit as byte 0 bit 7.
use super::SaveDevice;
use crate::memory::MemoryError;
use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer};
/// GBATEK: "ca. 108368 clock cycles (ca. 6.5ms)" to erase and program one block.
pub const EEPROM_WRITE_CYCLES: u32 = 108_368;
const READ_BITS: u8 = 68;
/// Carts with ROM above 16 MiB decode EEPROM only in the top 256 bytes of the 0x0d window.
const LARGE_ROM: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Eeprom {
large: bool,
start: u32,
input: u128,
count: u8,
output: Option<Readout>,
busy: Option<Busy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Readout {
data: u64,
remaining: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Busy {
block: u16,
data: u64,
remaining: u32,
}
impl Eeprom {
pub(super) fn new(device: SaveDevice, rom_len: usize) -> Option<Self> {
let large = match device {
SaveDevice::Eeprom512 => false,
SaveDevice::Eeprom8k => true,
_ => return None,
};
Some(Self {
large,
start: if rom_len > LARGE_ROM {
0x0dff_ff00
} else {
0x0d00_0000
},
input: 0,
count: 0,
output: None,
busy: None,
})
}
pub(super) fn device(self) -> SaveDevice {
if self.large {
SaveDevice::Eeprom8k
} else {
SaveDevice::Eeprom512
}
}
pub(super) fn mapped(self, address: u32) -> bool {
(self.start..=0x0dff_ffff).contains(&address)
}
fn address_bits(self) -> u8 {
if self.large {
14
} else {
6
}
}
/// Block index from a request's address field. 8 KiB parts decode the low 10 bits.
fn block(self, address: u128) -> u16 {
(address as u16) & if self.large { 0x3ff } else { 0x3f }
}
/// One halfword read. CPU reads only poll status; DMA reads also shift out data.
pub(super) fn read(&mut self, address: u32, cpu: bool) -> Result<u16, MemoryError> {
if let Some(output) = self.output.as_mut() {
if cpu {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,
operation: "EEPROM CPU read during a data read-out",
});
}
output.remaining -= 1;
let bit = if output.remaining < 64 {
(output.data >> output.remaining) & 1
} else {
0 // Four leading bits carry no data.
};
if output.remaining == 0 {
self.output = None;
}
return Ok(bit as u16);
}
// Bits 1-15 read as zero here; hardware drives only bit 0.
Ok(u16::from(self.busy.is_none()))
}
/// One halfword of a request bitstream. Only bit 0 is used.
pub(super) fn write(
&mut self,
address: u32,
value: u16,
cpu: bool,
image: &[u8],
) -> Result<(), MemoryError> {
let fail = |operation| MemoryError::UnsupportedIo {
address,
value: value as u8,
operation,
};
if cpu {
// GBATEK: bitstreams need DMA3; manual STRH transfers do not work.
return Err(fail("EEPROM CPU bitstream write"));
}
if self.busy.is_some() {
return Err(fail("EEPROM write while busy"));
}
if self.output.is_some() {
return Err(fail("EEPROM request during a data read-out"));
}
let mut next = *self;
next.input = (next.input << 1) | u128::from(value & 1);
next.count += 1;
if next.count == 1 && value & 1 == 0 {
return Err(fail("EEPROM request without a leading 1 bit"));
}
let address_bits = self.address_bits();
let read_bits = 2 + address_bits + 1;
let write_bits = 2 + address_bits + 64 + 1;
let reading = next.count >= 2 && (next.input >> (next.count - 2)) & 1 == 1;
let complete = if reading { read_bits } else { write_bits };
if next.count == complete {
if next.input & 1 != 0 {
return Err(fail("EEPROM request without a trailing 0 bit"));
}
if reading {
let block = next.block(next.input >> 1);
let offset = usize::from(block) * 8;
let mut bytes = [0; 8];
bytes.copy_from_slice(&image[offset..offset + 8]);
next.output = Some(Readout {
data: u64::from_be_bytes(bytes),
remaining: READ_BITS,
});
} else {
next.busy = Some(Busy {
block: next.block(next.input >> 65),
data: (next.input >> 1) as u64,
remaining: EEPROM_WRITE_CYCLES,
});
}
next.input = 0;
next.count = 0;
}
*self = next;
Ok(())
}
pub(super) fn advance(&mut self, cycles: u32) {
if let Some(busy) = self.busy.as_mut() {
busy.remaining = busy.remaining.saturating_sub(cycles);
}
}
pub(super) fn unfinished(self) -> bool {
self.count != 0 || self.output.is_some() || self.busy.is_some()
}
/// A completed write: its first byte offset and the eight block bytes.
pub(super) fn take_block(&mut self) -> Option<(usize, [u8; 8])> {
let busy = self.busy.filter(|busy| busy.remaining == 0)?;
self.busy = None;
Some((usize::from(busy.block) * 8, busy.data.to_be_bytes()))
}
}
impl Snapshot for Eeprom {
fn save(&self, writer: &mut Writer) {
let Self {
large,
start,
input,
count,
output,
busy,
} = self;
writer.tag(b"EEPR");
writer.bool(*large);
writer.u32(*start);
writer.u64((*input >> 64) as u64);
writer.u64(*input as u64);
writer.u8(*count);
writer.option(output.as_ref(), |output, writer| {
writer.u64(output.data);
writer.u8(output.remaining);
});
writer.option(busy.as_ref(), |busy, writer| {
writer.u32(u32::from(busy.block));
writer.u64(busy.data);
writer.u32(busy.remaining);
});
}
fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> {
reader.tag(b"EEPR")?;
let large = reader.bool()?;
let start = reader.u32()?;
let input = (u128::from(reader.u64()?) << 64) | u128::from(reader.u64()?);
let count = reader.u8()?;
let output = reader.option(|reader| {
Ok(Readout {
data: reader.u64()?,
remaining: reader.u8()?,
})
})?;
let busy = reader.option(|reader| {
let block =
u16::try_from(reader.u32()?).map_err(|_| SnapshotError::Corrupt("EEPROM block"))?;
Ok(Busy {
block,
data: reader.u64()?,
remaining: reader.u32()?,
})
})?;
if large != self.large || start != self.start {
return Err(SnapshotError::ConfigMismatch("save device"));
}
let restored = Self {
large,
start,
input,
count,
output,
busy,
};
let write_bits = 2 + restored.address_bits() + 64 + 1;
if count >= write_bits
|| input >> count != 0
|| output.is_some_and(|o| o.remaining == 0 || o.remaining > READ_BITS)
|| busy.is_some_and(|b| b.block != restored.block(u128::from(b.block)))
|| (output.is_some() as u8 + busy.is_some() as u8 + (count != 0) as u8) > 1
{
return Err(SnapshotError::Corrupt("EEPROM state"));
}
*self = restored;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bits(eeprom: &mut Eeprom, value: u128, count: u8, image: &[u8]) {
for shift in (0..count).rev() {
let bit = ((value >> shift) & 1) as u16;
eeprom.write(0x0d00_0000, bit, false, image).unwrap();
}
}
#[test]
fn small_and_large_parts_read_four_dummy_bits_then_the_block_msb_first() {
for (device, address_bits, block) in [
(SaveDevice::Eeprom512, 6, 0x3f_u128),
(SaveDevice::Eeprom8k, 14, 0x3ff),
] {
let mut image = vec![0xff; device.capacity()];
let offset = block as usize * 8;
image[offset..offset + 8].copy_from_slice(&0x0123_4567_89ab_cdef_u64.to_be_bytes());
let mut eeprom = Eeprom::new(device, 0).unwrap();
bits(
&mut eeprom,
(0b11 << (address_bits + 1)) | (block << 1),
address_bits + 3,
&image,
);
let mut value = 0_u64;
for index in 0..68 {
let bit = eeprom.read(0x0d00_0000, false).unwrap();
if index < 4 {
assert_eq!(bit, 0);
} else {
value = (value << 1) | u64::from(bit);
}
}
assert_eq!(value, 0x0123_4567_89ab_cdef);
assert!(!eeprom.unfinished());
assert_eq!(eeprom.read(0x0d00_0000, true).unwrap(), 1); // Idle and ready.
}
}
#[test]
fn writes_report_busy_until_the_nominal_delay_then_yield_the_block() {
let image = vec![0xff; SaveDevice::Eeprom512.capacity()];
let mut eeprom = Eeprom::new(SaveDevice::Eeprom512, 0).unwrap();
let data = 0xfedc_ba98_7654_3210_u128;
bits(
&mut eeprom,
(0b10 << 71) | (5 << 65) | (data << 1),
73,
&image,
);
assert_eq!(eeprom.read(0x0d00_0000, true).unwrap(), 0);
eeprom.advance(EEPROM_WRITE_CYCLES - 1);
assert_eq!(eeprom.take_block(), None);
eeprom.advance(1);
assert_eq!(
eeprom.take_block(),
Some((40, 0xfedc_ba98_7654_3210_u64.to_be_bytes()))
);
assert_eq!(eeprom.read(0x0d00_0000, true).unwrap(), 1);
}
#[test]
fn malformed_requests_cpu_stores_and_busy_writes_are_diagnostics() {
let image = vec![0xff; SaveDevice::Eeprom512.capacity()];
let mut eeprom = Eeprom::new(SaveDevice::Eeprom512, 0).unwrap();
assert!(eeprom.write(0x0d00_0000, 0, false, &image).is_err());
assert!(eeprom.write(0x0d00_0000, 1, true, &image).is_err());
let mut trailing = Eeprom::new(SaveDevice::Eeprom512, 0).unwrap();
bits(&mut trailing, 0b11_000000, 8, &image);
assert!(trailing.write(0x0d00_0000, 1, false, &image).is_err());
bits(&mut eeprom, 0b10 << 71, 73, &image);
assert!(eeprom.write(0x0d00_0000, 1, false, &image).is_err());
}
#[test]
fn roms_above_16_mib_map_only_the_top_256_bytes() {
let small = Eeprom::new(SaveDevice::Eeprom8k, LARGE_ROM).unwrap();
assert!(small.mapped(0x0d00_0000));
let large = Eeprom::new(SaveDevice::Eeprom8k, LARGE_ROM + 1).unwrap();
assert!(!large.mapped(0x0dff_feff));
assert!(large.mapped(0x0dff_ff00));
}
#[test]
fn snapshots_round_trip_mid_request() {
let image = vec![0xff; SaveDevice::Eeprom8k.capacity()];
let mut eeprom = Eeprom::new(SaveDevice::Eeprom8k, 0).unwrap();
bits(&mut eeprom, 0b10_0000, 6, &image);
let mut writer = Writer::new();
eeprom.save(&mut writer);
let bytes = writer.finish().unwrap();
let mut restored = Eeprom::new(SaveDevice::Eeprom8k, 0).unwrap();
restored.load(&mut Reader::new(&bytes)).unwrap();
assert_eq!(restored, eeprom);
let mut other = Eeprom::new(SaveDevice::Eeprom512, 0).unwrap();
assert_eq!(
other.load(&mut Reader::new(&bytes)),
Err(SnapshotError::ConfigMismatch("save device"))
);
}
}

View file

@ -19,6 +19,10 @@ pub enum SaveDevice {
Flash128,
/// 32 KiB battery-backed SRAM, byte addressed and mirrored through the save window.
Sram,
/// 512-byte serial EEPROM with 6-bit block addresses, in the 0x0d window.
Eeprom512,
/// 8 KiB serial EEPROM with 14-bit block addresses, in the 0x0d window.
Eeprom8k,
}
impl SaveDevice {
@ -28,6 +32,8 @@ impl SaveDevice {
Self::Flash64 => 64 * 1024,
Self::Flash128 => 128 * 1024,
Self::Sram => 32 * 1024,
Self::Eeprom512 => 512,
Self::Eeprom8k => 8 * 1024,
}
}
}
@ -489,7 +495,9 @@ impl Snapshot for Flash {
SaveDevice::None => 0,
SaveDevice::Flash64 => 1,
SaveDevice::Flash128 => 2,
SaveDevice::Sram => unreachable!("SRAM has no Flash state"),
SaveDevice::Sram | SaveDevice::Eeprom512 | SaveDevice::Eeprom8k => {
unreachable!("only Flash devices have Flash state")
}
});
phase.save(writer);
writer.bool(*identify);

View file

@ -226,7 +226,7 @@ impl Memory {
);
self.save_image = vec![0xff; device.capacity()];
self.save_modified = false;
self.cartridge.set_save_device(device);
self.cartridge.set_save_device(device, self.rom.len());
}
/// Inspect the complete save array, independently of ID mode and selected bank.
@ -253,7 +253,7 @@ impl Memory {
}
self.save_image.copy_from_slice(bytes);
self.save_modified = false;
self.cartridge.set_save_device(device);
self.cartridge.set_save_device(device, self.rom.len());
Ok(())
}
@ -297,6 +297,11 @@ impl Memory {
self.save_modified |= target.iter().any(|&b| b != effect.value);
target.fill(effect.value);
}
if let Some((start, block)) = self.cartridge.take_save_block() {
let target = &mut self.save_image[start..start + block.len()];
self.save_modified |= *target != block;
target.copy_from_slice(&block);
}
}
fn cartridge_state(&self) -> Cartridge {
@ -864,6 +869,12 @@ 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().eeprom_mapped(address) {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,
operation: "EEPROM non-halfword read",
});
}
if self.cartridge_state().flash_mapped(address)
&& self
.cpu_access
@ -990,7 +1001,7 @@ impl Memory {
},
);
let mut cartridge = self.cartridge_state();
cartridge.write(address, &bytes, &self.save_image)?;
cartridge.write(address, &bytes, &self.save_image, self.cpu_access.is_some())?;
if cartridge.mapped(address) {
if self.cartridge_step.get().is_some() {
self.cartridge_step.set(Some(cartridge));
@ -1224,7 +1235,7 @@ impl Memory {
self.write_index::<4>(address, self.write_access())?;
let bytes = value.to_le_bytes();
Self::validate_io_values(&mut audio, address, &bytes)?;
cartridge.write(address, &bytes, &self.save_image)?;
cartridge.write(address, &bytes, &self.save_image, self.cpu_access.is_some())?;
self.record_access(address, AccessWidth::Word);
if Serial::mapped(address) {
if let Some(mut step) = self.timer_step.get() {
@ -1255,6 +1266,9 @@ impl Memory {
if address & 1 != 0 {
return Err(MemoryError::Unaligned(address));
}
if self.cartridge_state().eeprom_mapped(address) {
return self.read_eeprom16(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)?]);
@ -1263,8 +1277,26 @@ impl Memory {
Ok(value)
}
/// Shift out one EEPROM bit inside a machine step. Host reads outside a step only
/// inspect status or the next bit; nothing outside a step can stage the change.
fn read_eeprom16(&self, address: u32) -> Result<u16, MemoryError> {
self.record_access(address, AccessWidth::Halfword);
let mut cartridge = self.cartridge_state();
let value = cartridge.read_eeprom16(address, self.cpu_access.is_some())?;
if self.cartridge_step.get().is_some() {
self.cartridge_step.set(Some(cartridge));
}
Ok(value)
}
fn validate_save_read_width(&self, address: u32) -> Result<(), MemoryError> {
let cartridge = self.cartridge_state();
if cartridge.eeprom_mapped(address) {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,
operation: "EEPROM non-halfword read",
});
}
if cartridge.save_mapped(address) {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,

View file

@ -0,0 +1,226 @@
//! Original EEPROM bitstreams sent through DMA3; no game assets or save files.
use gba_core::{
cartridge::{SaveDevice, EEPROM_WRITE_CYCLES},
cpu::Cpu,
dma::DMA_BASE,
machine::{Machine, MachineError, StepKind},
memory::{Memory, MemoryError, ROM_START},
};
const EEPROM: u32 = 0x0d00_0000;
const BUFFER: u32 = 0x0200_0000;
const DMA3: u32 = DMA_BASE + 0x24;
fn words(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
/// A ROM that polls EEPROM status with LDRH r1,[r0] and then loops.
fn machine(device: SaveDevice) -> Machine {
let mut bus = Memory::new(words(&[0xe59f_0004, 0xe1d0_10b0, 0xeaff_fffe, EEPROM])).unwrap();
bus.set_save_device(device);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
Machine::new(cpu, bus)
}
fn address_bits(device: SaveDevice) -> u32 {
if device == SaveDevice::Eeprom8k {
14
} else {
6
}
}
/// Run one immediate halfword DMA3 transfer to completion.
fn dma(machine: &mut Machine, source: u32, destination: u32, count: u32) {
let bus = machine.memory_mut();
bus.write32(DMA3, source).unwrap();
bus.write32(DMA3 + 4, destination).unwrap();
bus.write32(DMA3 + 8, 0x8000_0000 | count).unwrap();
for _ in 0..count {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
}
assert_eq!(machine.memory().read16(DMA3 + 10).unwrap() & 0x8000, 0);
}
/// Store `bits` MSB first as one halfword per bit and send them to the EEPROM.
fn send(machine: &mut Machine, bits: &[u8]) {
for (index, &bit) in bits.iter().enumerate() {
let value = u16::from(bit) | 0xfffe; // Bits 1-15 are don't care.
machine
.memory_mut()
.write16(BUFFER + index as u32 * 2, value)
.unwrap();
}
dma(machine, BUFFER, EEPROM, bits.len() as u32);
}
fn field(value: u64, width: u32) -> Vec<u8> {
(0..width)
.rev()
.map(|shift| ((value >> shift) & 1) as u8)
.collect()
}
fn write_request(device: SaveDevice, block: u64, data: u64) -> Vec<u8> {
[
vec![1, 0],
field(block, address_bits(device)),
field(data, 64),
vec![0],
]
.concat()
}
fn read_block(machine: &mut Machine, device: SaveDevice, block: u64) -> u64 {
send(
machine,
&[vec![1, 1], field(block, address_bits(device)), vec![0]].concat(),
);
let output = BUFFER + 0x400;
dma(machine, EEPROM, output, 68);
let mut value = 0;
for index in 0..68 {
let bit = machine.memory().read16(output + index * 2).unwrap();
assert!(bit <= 1);
if index < 4 {
assert_eq!(bit, 0, "dummy bit {index}");
} else {
value = (value << 1) | u64::from(bit);
}
}
value
}
fn poll(machine: &mut Machine) -> u32 {
let mut probe = machine.cpu().clone();
probe.step(machine.memory_mut()).unwrap();
probe.registers()[1]
}
#[test]
fn dma_writes_program_after_the_nominal_delay_and_reads_return_the_block() {
for (device, block) in [(SaveDevice::Eeprom512, 0x3f), (SaveDevice::Eeprom8k, 0x3ff)] {
let mut machine = machine(device);
assert_eq!(poll(&mut machine), 1); // Idle reads as ready.
send(
&mut machine,
&write_request(device, block, 0x0123_4567_89ab_cdef),
);
assert_eq!(poll(&mut machine), 0); // Busy.
assert!(machine.memory().save_write_pending());
machine.memory_mut().advance_cycles(EEPROM_WRITE_CYCLES);
assert_eq!(poll(&mut machine), 1);
assert!(!machine.memory().save_write_pending());
assert!(machine.memory().save_modified());
let offset = block as usize * 8;
assert_eq!(
machine.memory().save_image().unwrap()[offset..offset + 8],
0x0123_4567_89ab_cdef_u64.to_be_bytes()
);
assert_eq!(
read_block(&mut machine, device, block),
0x0123_4567_89ab_cdef
);
assert_eq!(read_block(&mut machine, device, 0), u64::MAX); // Erased.
}
}
#[test]
fn large_parts_decode_only_the_low_ten_address_bits() {
let device = SaveDevice::Eeprom8k;
let mut machine = machine(device);
send(&mut machine, &write_request(device, 0x3c01, 7));
machine.memory_mut().advance_cycles(EEPROM_WRITE_CYCLES);
assert_eq!(read_block(&mut machine, device, 1), 7);
}
#[test]
fn cpu_stores_and_byte_or_word_accesses_are_diagnostics() {
let mut machine = machine(SaveDevice::Eeprom512);
let bus = machine.memory_mut();
assert!(matches!(
bus.read8(EEPROM),
Err(MemoryError::UnsupportedCartridgeAccess { .. })
));
assert!(matches!(
bus.read32(EEPROM),
Err(MemoryError::UnsupportedCartridgeAccess { .. })
));
assert!(matches!(
bus.write32(EEPROM, 1),
Err(MemoryError::UnsupportedCartridgeAccess { .. })
));
// STRH r0,[r0] from ROM code.
let mut bus = Memory::new(words(&[0xe59f_0004, 0xe1c0_00b0, 0xeaff_fffe, EEPROM])).unwrap();
bus.set_save_device(SaveDevice::Eeprom512);
let mut cpu = Cpu::new(ROM_START);
cpu.step(&mut bus).unwrap();
let mut machine = Machine::new(cpu, bus);
let before = machine.cpu().clone();
assert!(matches!(
machine.step(),
Err(MachineError::Cpu(gba_core::cpu::CpuError::Memory(
MemoryError::UnsupportedIo { .. }
)))
));
assert_eq!(machine.cpu(), &before);
assert!(!machine.memory().save_write_pending());
}
#[test]
fn malformed_streams_fail_without_partial_state() {
let mut machine = machine(SaveDevice::Eeprom512);
machine.memory_mut().write16(BUFFER, 0).unwrap(); // Missing leading 1 bit.
let bus = machine.memory_mut();
bus.write32(DMA3, BUFFER).unwrap();
bus.write32(DMA3 + 4, EEPROM).unwrap();
bus.write32(DMA3 + 8, 0x8000_0001).unwrap();
assert!(matches!(machine.step(), Err(MachineError::Dma(_))));
assert!(!machine.memory().save_write_pending());
}
#[test]
fn roms_above_16_mib_map_eeprom_only_in_the_top_256_bytes() {
let mut rom = vec![0; 16 * 1024 * 1024 + 4];
rom[..4].copy_from_slice(&0xeaff_fffe_u32.to_le_bytes());
let mut bus = Memory::new(rom).unwrap();
bus.set_save_device(SaveDevice::Eeprom512);
assert_eq!(bus.read16(0x0dff_ff00).unwrap(), 1);
assert_eq!(bus.read16(0x0d00_0000).unwrap(), 0); // Ordinary ROM bytes.
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
machine.memory_mut().write16(BUFFER, 1).unwrap();
let bus = machine.memory_mut();
bus.write32(DMA3, BUFFER).unwrap();
bus.write32(DMA3 + 4, 0x0d00_0000).unwrap();
bus.write32(DMA3 + 8, 0x8000_0001).unwrap();
assert!(matches!(machine.step(), Err(MachineError::Dma(_))));
}
#[test]
fn save_states_restore_a_read_out_in_progress() {
let device = SaveDevice::Eeprom512;
let mut machine = machine(device);
send(
&mut machine,
&write_request(device, 2, 0xa5a5_a5a5_a5a5_a5a5),
);
machine.memory_mut().advance_cycles(EEPROM_WRITE_CYCLES);
send(&mut machine, &[vec![1, 1], field(2, 6), vec![0]].concat());
dma(&mut machine, EEPROM, BUFFER + 0x400, 5); // Four dummy bits and the first data bit.
let state = machine.save_state().unwrap();
dma(&mut machine, EEPROM, BUFFER + 0x400, 63);
let finished = machine.memory().read16(BUFFER + 0x400 + 62 * 2).unwrap();
machine.load_state(&state).unwrap();
assert!(machine.memory().save_write_pending());
dma(&mut machine, EEPROM, BUFFER + 0x400, 63);
assert_eq!(
machine.memory().read16(BUFFER + 0x400 + 62 * 2).unwrap(),
finished
);
assert!(!machine.memory().save_write_pending());
}