feat(cartridge): implement transactional Flash programming and erase

This commit is contained in:
0xrsydn 2026-10-07 08:25:12 +07:00
commit 8ded9bc90e
7 changed files with 452 additions and 20 deletions

View file

@ -7,7 +7,10 @@ mod flash;
mod rtc; mod rtc;
pub use calendar::{RtcDateTime, RtcError}; pub use calendar::{RtcDateTime, RtcError};
use flash::Flash; use flash::Flash;
pub use flash::{SaveDevice, SaveError, SAVE_END, SAVE_START}; pub(crate) use flash::SaveMutation;
pub use flash::{
SaveDevice, SaveError, FLASH_ERASE_CYCLES, FLASH_PROGRAM_CYCLES, SAVE_END, SAVE_START,
};
use rtc::Rtc; use rtc::Rtc;
pub const GPIO_DATA: u32 = 0x0800_00c4; pub const GPIO_DATA: u32 = 0x0800_00c4;
@ -46,6 +49,20 @@ impl Cartridge {
self.flash.is_some() && (SAVE_START..=SAVE_END).contains(&address) self.flash.is_some() && (SAVE_START..=SAVE_END).contains(&address)
} }
pub(crate) fn advance(&mut self, cycles: u32) {
if let Some(flash) = self.flash.as_mut() {
flash.advance(cycles);
}
}
pub(crate) fn save_unfinished(&self) -> bool {
self.flash.is_some_and(Flash::unfinished)
}
pub(crate) fn take_save_mutation(&mut self) -> Option<SaveMutation> {
self.flash.as_mut().and_then(Flash::take_mutation)
}
pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> { pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
self.flash.expect("mapped save device").read(address, image) self.flash.expect("mapped save device").read(address, image)
} }
@ -94,7 +111,12 @@ impl Cartridge {
} }
/// Apply one access to a copy first. A rejected second halfword changes nothing. /// Apply one access to a copy first. A rejected second halfword changes nothing.
pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) -> Result<(), MemoryError> { pub(crate) fn write(
&mut self,
address: u32,
bytes: &[u8],
image: &[u8],
) -> Result<(), MemoryError> {
if !self.mapped(address) { if !self.mapped(address) {
return Ok(()); // Non-cartridge accesses are validated by the bus. return Ok(()); // Non-cartridge accesses are validated by the bus.
} }
@ -105,7 +127,7 @@ impl Cartridge {
operation: "Flash non-byte write", operation: "Flash non-byte write",
}); });
} }
return self.flash.as_mut().unwrap().write(address, bytes[0]); return self.flash.as_mut().unwrap().write(address, bytes[0], image);
} }
let fail = |at, value, operation| MemoryError::UnsupportedIo { let fail = |at, value, operation| MemoryError::UnsupportedIo {
address: at, address: at,

View file

@ -1,18 +1,20 @@
//! Bounded Macronix Flash identification, array reads, and 128 KiB banking. //! Bounded Macronix Flash commands with nominal delayed program/erase completion.
//! Program/erase commands remain diagnostic: this module cannot claim a completed save.
use crate::memory::MemoryError; use crate::memory::MemoryError;
use std::{error::Error, fmt}; use std::{error::Error, fmt};
pub const SAVE_START: u32 = 0x0e00_0000; pub const SAVE_START: u32 = 0x0e00_0000;
pub const SAVE_END: u32 = 0x0e00_ffff; pub const SAVE_END: u32 = 0x0e00_ffff;
/// Nominal simulation delays, not measured Macronix operation times.
pub const FLASH_PROGRAM_CYCLES: u32 = 650;
pub const FLASH_ERASE_CYCLES: u32 = 30000;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum SaveDevice { pub enum SaveDevice {
#[default] #[default]
None, None,
/// Macronix ID 1CC2h. Array reads and identification only. /// Macronix ID 1CC2h, 64 KiB array.
Flash64, Flash64,
/// Macronix ID 09C2h. Adds two 64 KiB banks; no programming or erase yet. /// Macronix ID 09C2h, two 64 KiB banks.
Flash128, Flash128,
} }
@ -50,6 +52,21 @@ pub(super) struct Flash {
phase: Phase, phase: Phase,
identify: bool, identify: bool,
bank: u8, bank: u8,
pending: Option<Pending>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SaveMutation {
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) value: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Pending {
mutation: SaveMutation,
remaining: u32,
poll: u32,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -58,6 +75,10 @@ enum Phase {
Unlock, Unlock,
Command, Command,
Bank, Bank,
Program,
EraseUnlock,
EraseCommand,
EraseConfirm,
} }
impl Flash { impl Flash {
@ -67,6 +88,7 @@ impl Flash {
phase: Phase::Ready, phase: Phase::Ready,
identify: false, identify: false,
bank: 0, bank: 0,
pending: None,
}) })
} }
@ -76,6 +98,22 @@ impl Flash {
pub(super) fn read(self, address: u32, image: &[u8]) -> Result<u8, MemoryError> { pub(super) fn read(self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
let offset = (address - SAVE_START) as usize; let offset = (address - SAVE_START) as usize;
let index = usize::from(self.bank) * 0x10000 + offset;
if let Some(pending) = self.pending {
if pending.remaining != 0 {
if address != pending.poll {
return Err(MemoryError::UnsupportedCartridgeAccess {
address,
operation: "Flash busy read outside polling address",
});
}
// Bounded DQ7-only data polling. Toggle/error bits are not modeled.
return Ok((pending.mutation.value ^ 0x80) & 0x80);
}
if (pending.mutation.start..pending.mutation.end).contains(&index) {
return Ok(pending.mutation.value);
}
}
if self.identify { if self.identify {
return match offset { return match offset {
0 => Ok(0xc2), 0 => Ok(0xc2),
@ -90,10 +128,33 @@ impl Flash {
}), }),
}; };
} }
Ok(image[usize::from(self.bank) * 0x10000 + offset]) Ok(image[index])
} }
pub(super) fn write(&mut self, address: u32, value: u8) -> Result<(), MemoryError> { pub(super) fn advance(&mut self, cycles: u32) {
if let Some(pending) = self.pending.as_mut() {
pending.remaining = pending.remaining.saturating_sub(cycles);
}
}
pub(super) fn unfinished(self) -> bool {
self.pending.is_some() || self.phase != Phase::Ready
}
pub(super) fn take_mutation(&mut self) -> Option<SaveMutation> {
if self.pending.is_some_and(|p| p.remaining == 0) {
self.pending.take().map(|p| p.mutation)
} else {
None
}
}
pub(super) fn write(
&mut self,
address: u32,
value: u8,
image: &[u8],
) -> Result<(), MemoryError> {
let offset = address - SAVE_START; let offset = address - SAVE_START;
let error = |operation| MemoryError::UnsupportedIo { let error = |operation| MemoryError::UnsupportedIo {
address, address,
@ -101,9 +162,19 @@ impl Flash {
operation, operation,
}; };
let mut next = *self; let mut next = *self;
if self.pending.is_some_and(|p| p.remaining != 0) {
if self.device == SaveDevice::Flash64 && offset == 0x5555 && value == 0xf0 {
next.pending = None;
next.phase = Phase::Ready;
*self = next;
return Ok(());
}
return Err(error("Flash write during busy operation"));
}
// GBATEK documents direct reset for the 64 KiB chip. For 128 KiB, accept // 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. // only a redundant reset while already idle in array mode. No busy/ID exit is inferred.
if offset == 0x5555 if self.phase != Phase::Program
&& offset == 0x5555
&& value == 0xf0 && value == 0xf0
&& (self.device == SaveDevice::Flash64 && (self.device == SaveDevice::Flash64
|| (self.phase == Phase::Ready && !self.identify)) || (self.phase == Phase::Ready && !self.identify))
@ -129,8 +200,8 @@ impl Flash {
} }
0xb0 if self.device == SaveDevice::Flash128 => Phase::Bank, 0xb0 if self.device == SaveDevice::Flash128 => Phase::Bank,
0xb0 => return Err(error("Flash bank selection on 64 KiB device")), 0xb0 => return Err(error("Flash bank selection on 64 KiB device")),
0xa0 => return Err(error("Flash byte programming")), 0xa0 => Phase::Program,
0x80 => return Err(error("Flash erase setup")), 0x80 => Phase::EraseUnlock,
_ => return Err(error("Flash command")), _ => return Err(error("Flash command")),
} }
} }
@ -139,6 +210,52 @@ impl Flash {
Phase::Ready Phase::Ready
} }
Phase::Bank => return Err(error("Flash bank address/value")), Phase::Bank => return Err(error("Flash bank address/value")),
Phase::Program => {
if self.pending.is_some() {
return Err(error("Flash completion awaiting commit"));
}
let index = usize::from(self.bank) * 0x10000 + offset as usize;
if value & !image[index] != 0 {
return Err(error(
"Flash programming requires erase for zero-to-one bits",
));
}
next.pending = Some(Pending {
mutation: SaveMutation {
start: index,
end: index + 1,
value,
},
remaining: FLASH_PROGRAM_CYCLES,
poll: address,
});
Phase::Ready
}
Phase::EraseUnlock if offset == 0x5555 && value == 0xaa => Phase::EraseCommand,
Phase::EraseCommand if offset == 0x2aaa && value == 0x55 => Phase::EraseConfirm,
Phase::EraseConfirm => {
if self.pending.is_some() {
return Err(error("Flash completion awaiting commit"));
}
let (start, end, poll) = match value {
0x10 if offset == 0x5555 => (0, self.device.capacity(), SAVE_START),
0x30 if offset & 0xfff == 0 => {
let start = usize::from(self.bank) * 0x10000 + offset as usize;
(start, start + 0x1000, address)
}
_ => return Err(error("Flash erase confirmation/address")),
};
next.pending = Some(Pending {
mutation: SaveMutation {
start,
end,
value: 0xff,
},
remaining: FLASH_ERASE_CYCLES,
poll,
});
Phase::Ready
}
_ => return Err(error("Flash unlock/reset sequence")), _ => return Err(error("Flash unlock/reset sequence")),
}; };
} }

View file

@ -12,6 +12,8 @@ mod fetch_tests;
#[cfg(test)] #[cfg(test)]
mod flash_step_tests; mod flash_step_tests;
#[cfg(test)] #[cfg(test)]
mod flash_write_tests;
#[cfg(test)]
mod irq_fetch_tests; mod irq_fetch_tests;
mod iwram_bus; mod iwram_bus;
#[cfg(test)] #[cfg(test)]
@ -138,6 +140,8 @@ pub struct Memory {
oam: Vec<u8>, oam: Vec<u8>,
rom: Vec<u8>, rom: Vec<u8>,
save_image: Vec<u8>, save_image: Vec<u8>,
save_modified: bool,
cartridge_elapsed: Cell<u32>,
cartridge: Cartridge, cartridge: Cartridge,
cartridge_step: Cell<Option<Cartridge>>, cartridge_step: Cell<Option<Cartridge>>,
bios: Option<Vec<u8>>, bios: Option<Vec<u8>>,
@ -170,6 +174,8 @@ impl Memory {
oam: vec![0; 1024], oam: vec![0; 1024],
rom, rom,
save_image: Vec::new(), save_image: Vec::new(),
save_modified: false,
cartridge_elapsed: Cell::new(0),
cartridge: Cartridge::default(), cartridge: Cartridge::default(),
cartridge_step: Cell::new(None), cartridge_step: Cell::new(None),
bios: None, bios: None,
@ -218,6 +224,7 @@ impl Memory {
"active cartridge transaction" "active cartridge transaction"
); );
self.save_image = vec![0xff; device.capacity()]; self.save_image = vec![0xff; device.capacity()];
self.save_modified = false;
self.cartridge.set_save_device(device); self.cartridge.set_save_device(device);
} }
@ -244,6 +251,7 @@ impl Memory {
}); });
} }
self.save_image.copy_from_slice(bytes); self.save_image.copy_from_slice(bytes);
self.save_modified = false;
self.cartridge.set_save_device(device); self.cartridge.set_save_device(device);
Ok(()) Ok(())
} }
@ -272,6 +280,24 @@ impl Memory {
self.cartridge.advance_rtc_seconds(seconds) self.cartridge.advance_rtc_seconds(seconds)
} }
/// True after a completed Flash operation changed stored bytes since setup/load.
pub fn save_modified(&self) -> bool {
self.save_modified
}
/// A command or operation is incomplete. Hosts should not persist on exit in this state.
pub fn save_write_pending(&self) -> bool {
self.cartridge.save_unfinished()
}
fn commit_save_mutation(&mut self) {
if let Some(effect) = self.cartridge.take_save_mutation() {
let target = &mut self.save_image[effect.start..effect.end];
self.save_modified |= target.iter().any(|&b| b != effect.value);
target.fill(effect.value);
}
}
fn cartridge_state(&self) -> Cartridge { fn cartridge_state(&self) -> Cartridge {
self.cartridge_step.get().unwrap_or(self.cartridge) self.cartridge_step.get().unwrap_or(self.cartridge)
} }
@ -345,14 +371,21 @@ impl Memory {
debug_assert!(self.timer_step.get().is_none()); debug_assert!(self.timer_step.get().is_none());
self.timer_step.set(Some(self.io.timer_step())); self.timer_step.set(Some(self.io.timer_step()));
self.cartridge_step.set(Some(self.cartridge)); self.cartridge_step.set(Some(self.cartridge));
self.cartridge_elapsed.set(0);
} }
pub(crate) fn discard_timer_step(&self) { pub(crate) fn discard_timer_step(&self) {
self.timer_step.set(None); self.timer_step.set(None);
self.cartridge_step.set(None); self.cartridge_step.set(None);
self.cartridge_elapsed.set(0);
} }
fn advance_timer_step(&self, cycles: u32) { fn advance_timer_step(&self, cycles: u32) {
if let Some(mut cartridge) = self.cartridge_step.get() {
cartridge.advance(cycles - self.cartridge_elapsed.get());
self.cartridge_elapsed.set(cycles);
self.cartridge_step.set(Some(cartridge));
}
if let Some(mut step) = self.timer_step.get() { if let Some(mut step) = self.timer_step.get() {
step.advance_to(cycles); step.advance_to(cycles);
self.timer_step.set(Some(step)); self.timer_step.set(Some(step));
@ -370,11 +403,15 @@ impl Memory {
.cartridge_step .cartridge_step
.take() .take()
.expect("active cartridge transaction"); .expect("active cartridge transaction");
self.commit_save_mutation();
self.cartridge_elapsed.set(0);
// Staged devices already reached this boundary. Advance remaining devices exactly once. // Staged devices already reached this boundary. Advance remaining devices exactly once.
self.advance_running_cycles_with_timers(cycles, false); self.advance_running_cycles_with_timers(cycles, false);
} }
fn advance_running_cycles(&mut self, cycles: u32) { fn advance_running_cycles(&mut self, cycles: u32) {
self.cartridge.advance(cycles);
self.commit_save_mutation();
self.advance_running_cycles_with_timers(cycles, true); self.advance_running_cycles_with_timers(cycles, true);
} }
@ -930,8 +967,6 @@ impl Memory {
) -> Result<(), MemoryError> { ) -> Result<(), MemoryError> {
let index = self.write_index::<N>(address, access)?; let index = self.write_index::<N>(address, access)?;
Self::validate_io_values(&mut self.audio_state(), address, &bytes)?; Self::validate_io_values(&mut self.audio_state(), address, &bytes)?;
let mut cartridge = self.cartridge_state();
cartridge.write(address, &bytes)?;
self.record_access( self.record_access(
address, address,
match N { match N {
@ -941,6 +976,8 @@ impl Memory {
_ => unreachable!("unsupported bus width"), _ => unreachable!("unsupported bus width"),
}, },
); );
let mut cartridge = self.cartridge_state();
cartridge.write(address, &bytes, &self.save_image)?;
if cartridge.mapped(address) { if cartridge.mapped(address) {
if self.cartridge_step.get().is_some() { if self.cartridge_step.get().is_some() {
self.cartridge_step.set(Some(cartridge)); self.cartridge_step.set(Some(cartridge));
@ -1150,6 +1187,8 @@ impl Memory {
let mut cartridge = self.cartridge_state(); let mut cartridge = self.cartridge_state();
let timing = self.cpu_timing.get(); let timing = self.cpu_timing.get();
let devices = self.timer_step.get(); let devices = self.timer_step.get();
let cartridge_step = self.cartridge_step.get();
let cartridge_elapsed = self.cartridge_elapsed.get();
// Preflight ordered data phases on temporary device/timing copies. // Preflight ordered data phases on temporary device/timing copies.
// A serial request can finish before a later control write reaches the bus. // A serial request can finish before a later control write reaches the bus.
let validation = (|| { let validation = (|| {
@ -1157,7 +1196,7 @@ impl Memory {
self.write_index::<4>(address, self.write_access())?; self.write_index::<4>(address, self.write_access())?;
let bytes = value.to_le_bytes(); let bytes = value.to_le_bytes();
Self::validate_io_values(&mut audio, address, &bytes)?; Self::validate_io_values(&mut audio, address, &bytes)?;
cartridge.write(address, &bytes)?; cartridge.write(address, &bytes, &self.save_image)?;
self.record_access(address, AccessWidth::Word); self.record_access(address, AccessWidth::Word);
if Serial::mapped(address) { if Serial::mapped(address) {
if let Some(mut step) = self.timer_step.get() { if let Some(mut step) = self.timer_step.get() {
@ -1174,6 +1213,8 @@ impl Memory {
})(); })();
self.cpu_timing.set(timing); self.cpu_timing.set(timing);
self.timer_step.set(devices); self.timer_step.set(devices);
self.cartridge_step.set(cartridge_step);
self.cartridge_elapsed.set(cartridge_elapsed);
validation?; validation?;
for &(address, value) in writes { for &(address, value) in writes {
// The map cannot change between validation and these writes. // The map cannot change between validation and these writes.

View file

@ -73,7 +73,7 @@ fn work_ram_byte_loads_observe_selected_id_with_save_bus_costs() {
} }
#[test] #[test]
fn rejected_programming_and_rom_execution_reads_roll_back_cpu_and_command_state() { fn rejected_commands_and_rom_execution_reads_roll_back_cpu_and_command_state() {
for thumb in [false, true] { for thumb in [false, true] {
for read in [false, true] { for read in [false, true] {
let (cpu, mut memory) = prepared( let (cpu, mut memory) = prepared(
@ -84,7 +84,7 @@ fn rejected_programming_and_rom_execution_reads_roll_back_cpu_and_command_state(
(false, true) => 0xe5d10000, (false, true) => 0xe5d10000,
(true, true) => 0x7808, (true, true) => 0x7808,
}], }],
&[(0, 0xa0), (1, if read { SAVE_START } else { HI })], &[(0, 0x42), (1, if read { SAVE_START } else { HI })],
); );
unlocked(&mut memory); unlocked(&mut memory);
let before = memory.cartridge; let before = memory.cartridge;
@ -94,7 +94,7 @@ fn rejected_programming_and_rom_execution_reads_roll_back_cpu_and_command_state(
assert!(error.to_string().contains(if read { assert!(error.to_string().contains(if read {
"outside work RAM" "outside work RAM"
} else { } else {
"programming" "Flash command"
})); }));
assert_eq!(machine.step(), Err(error)); assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cpu(), &cpu); assert_eq!(machine.cpu(), &cpu);

View file

@ -0,0 +1,95 @@
//! Original completion-at-bus-phase and mutation rollback checks.
use super::{timing_event_tests::prepared, *};
use crate::{
cartridge::{SaveDevice, FLASH_PROGRAM_CYCLES as PROGRAM, SAVE_START as BASE},
io::SOUNDBIAS,
machine::Machine,
};
fn setup(m: &mut Memory) {
m.set_save_device(SaveDevice::Flash128);
m.write8(BASE + 0x5555, 0xaa).unwrap();
m.write8(BASE + 0x2aaa, 0x55).unwrap();
m.write8(BASE + 0x5555, 0xa0).unwrap();
}
#[test]
fn arm_and_thumb_program_start_excludes_preceding_bus_cycles() {
for thumb in [false, true] {
let (cpu, mut m) = prepared(
thumb,
&[if thumb { 0x7008 } else { 0xe5c10000 }],
&[(0, 0x12), (1, BASE)],
);
setup(&mut m);
let mut machine = Machine::new(cpu, m);
machine.step().unwrap();
assert_eq!(machine.memory().save_image().unwrap()[0], 255);
machine.memory_mut().advance_cycles(PROGRAM - 1);
assert_eq!(machine.memory().save_image().unwrap()[0], 255);
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().save_image().unwrap()[0], 0x12);
assert!(machine.memory().save_modified());
}
}
#[test]
fn failed_block_load_restores_completion_and_dirty_state() {
let (cpu, mut m) = prepared(false, &[0xe8910005], &[(1, SOUNDBIAS)]);
setup(&mut m);
m.write8(BASE, 0x12).unwrap();
m.advance_cycles(PROGRAM - 7);
let controller = m.cartridge;
let mut machine = Machine::new(cpu, m);
for _ in 0..2 {
assert!(machine.step().is_err());
assert_eq!(machine.memory().cartridge, controller);
assert_eq!(machine.memory().save_image().unwrap()[0], 255);
assert!(!machine.memory().save_modified());
assert_eq!(machine.cycles(), u64::from(PROGRAM - 7));
}
machine.memory_mut().advance_cycles(7);
assert_eq!(machine.memory().save_image().unwrap()[0], 0x12);
}
#[test]
fn read_at_completion_uses_staged_overlay_and_read_before_completion_sees_busy() {
for elapsed in [PROGRAM - 7, PROGRAM - 6] {
let (mut cpu, mut m) = prepared(false, &[0xe12fff12], &[(1, BASE), (2, 0x03001000)]);
for i in 0..8 {
m.write32(
0x03001000 + i * 4,
if i == 0 { 0xe5d10000 } else { 0xe1a00000 },
)
.unwrap();
}
cpu.step(&mut m).unwrap();
setup(&mut m);
m.write8(BASE, 0).unwrap();
m.advance_cycles(elapsed);
let mut machine = Machine::new(cpu, m);
machine.step().unwrap();
assert_eq!(
machine.cpu().registers()[0],
if elapsed == PROGRAM - 6 { 0 } else { 0x80 }
);
assert_eq!(machine.memory().save_image().unwrap()[0], 0);
}
}
#[test]
fn preflight_and_discard_cannot_apply_save_mutations() {
let (_, mut m) = prepared(false, &[0xe1a00000], &[]);
setup(&mut m);
m.write8(BASE, 0x12).unwrap();
m.advance_cycles(PROGRAM - 1);
let before = m.cartridge;
m.begin_timer_step();
m.advance_timer_step(1);
assert_eq!(m.read8(BASE).unwrap(), 0x12);
assert_eq!(m.save_image().unwrap()[0], 255);
let shadow = m.cartridge_step.get();
let elapsed = m.cartridge_elapsed.get();
assert!(m.write_words(&[(0x02000000, 42), (BASE, 0)]).is_err());
assert_eq!(m.cartridge_step.get(), shadow);
assert_eq!(m.cartridge_elapsed.get(), elapsed);
assert_eq!(m.read32(0x02000000).unwrap(), 0);
m.discard_timer_step();
assert_eq!(m.cartridge, before);
assert!(!m.save_modified());
}

View file

@ -122,8 +122,8 @@ fn bank_selection_covers_every_byte_and_survives_id_mode() {
fn unsupported_commands_and_bad_bank_values_are_retryable_without_data_changes() { fn unsupported_commands_and_bad_bank_values_are_retryable_without_data_changes() {
for device in [SaveDevice::Flash64, SaveDevice::Flash128] { for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
for (byte, message) in [ for (byte, message) in [
(0xa0, "programming"), (0x42, "Flash command"),
(0x80, "erase setup"), (0x30, "Flash command"),
(0x10, "Flash command"), (0x10, "Flash command"),
] { ] {
let mut bus = memory(device); let mut bus = memory(device);

View file

@ -0,0 +1,157 @@
//! Original byte-program/erase probes. Nominal delays are not physical chip timings.
use gba_core::{
cartridge::{
SaveDevice, FLASH_ERASE_CYCLES as ERASE, FLASH_PROGRAM_CYCLES as PROGRAM,
SAVE_START as BASE,
},
io::{HALTCNT, IF},
memory::Memory,
};
const HI: u32 = BASE + 0x5555;
const LO: u32 = BASE + 0x2aaa;
fn bus(device: SaveDevice) -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_save_device(device);
m
}
fn command(m: &mut Memory, command: u8) {
m.write8(HI, 0xaa).unwrap();
m.write8(LO, 0x55).unwrap();
m.write8(HI, command).unwrap();
}
fn program(m: &mut Memory, address: u32, value: u8) {
command(m, 0xa0);
m.write8(address, value).unwrap();
}
fn erase(m: &mut Memory, address: u32, confirm: u8) {
command(m, 0x80);
m.write8(HI, 0xaa).unwrap();
m.write8(LO, 0x55).unwrap();
m.write8(address, confirm).unwrap();
}
fn bank(m: &mut Memory, index: u8) {
command(m, 0xb0);
m.write8(BASE, index).unwrap();
}
#[test]
fn every_byte_value_programs_after_delay_without_touching_neighbors_or_other_bank() {
let mut m = bus(SaveDevice::Flash128);
for b in 0..2 {
bank(&mut m, b);
for value in 0..=255 {
let address = BASE + u32::from(value);
program(&mut m, address, value);
assert!(m.save_write_pending());
assert_eq!(m.read8(address).unwrap(), (value ^ 0x80) & 0x80);
assert_eq!(
m.save_image().unwrap()[usize::from(b) * 65536 + usize::from(value)],
255
);
m.advance_cycles(PROGRAM - 1);
assert_eq!(m.read8(address).unwrap(), (value ^ 0x80) & 0x80);
m.advance_cycles(1);
assert_eq!(m.read8(address).unwrap(), value);
assert!(!m.save_write_pending());
}
}
let image = m.save_image().unwrap();
for b in 0..2 {
for (i, &v) in image[b * 65536..(b + 1) * 65536].iter().enumerate() {
assert_eq!(v, if i < 256 { i as u8 } else { 255 });
}
}
assert!(m.save_modified());
assert_eq!(m.read16(IF).unwrap(), 0);
}
#[test]
fn sector_and_chip_erases_have_exact_scope_and_wait_for_completion() {
for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
let mut m = bus(device);
m.load_save_image(&vec![0; device.capacity()]).unwrap();
if device == SaveDevice::Flash128 {
bank(&mut m, 1);
}
erase(&mut m, BASE + 0xf000, 0x30);
assert_eq!(m.read8(BASE + 0xf000).unwrap(), 0);
m.advance_cycles(ERASE - 1);
assert!(m.save_image().unwrap().iter().all(|&b| b == 0));
m.advance_cycles(1);
let start = device.capacity() - 4096;
assert!(m.save_image().unwrap()[..start].iter().all(|&b| b == 0));
assert!(m.save_image().unwrap()[start..].iter().all(|&b| b == 255));
erase(&mut m, HI, 0x10);
m.advance_cycles(ERASE);
assert!(m.save_image().unwrap().iter().all(|&b| b == 255));
}
}
#[test]
fn data_at_unlock_address_is_not_mistaken_for_reset_and_zero_to_one_requires_erase() {
let mut m = bus(SaveDevice::Flash64);
program(&mut m, HI, 0xf0);
m.advance_cycles(PROGRAM);
assert_eq!(m.read8(HI).unwrap(), 0xf0);
program(&mut m, HI, 0x80);
m.advance_cycles(PROGRAM);
assert_eq!(m.read8(HI).unwrap(), 0x80);
command(&mut m, 0xa0);
let error = m.write8(HI, 0xff).unwrap_err();
assert!(error.to_string().contains("requires erase"));
assert_eq!(m.write8(HI, 0xff), Err(error));
m.write8(HI, 0).unwrap();
m.advance_cycles(PROGRAM);
assert_eq!(m.read8(HI).unwrap(), 0);
}
#[test]
fn busy_access_limits_and_64_reset_are_atomic_without_fabricated_completion() {
for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
let mut m = bus(device);
program(&mut m, BASE + 123, 0x12);
assert!(m.read8(BASE + 124).is_err());
let error = m.write8(HI, 0xaa).unwrap_err();
assert_eq!(m.write8(HI, 0xaa), Err(error));
if device == SaveDevice::Flash64 {
m.write8(HI, 0xf0).unwrap();
m.advance_cycles(PROGRAM);
assert_eq!(m.read8(BASE + 123).unwrap(), 255);
assert!(!m.save_modified());
} else {
assert!(m.write8(HI, 0xf0).is_err());
m.advance_cycles(PROGRAM);
assert_eq!(m.read8(BASE + 123).unwrap(), 0x12);
}
}
}
#[test]
fn invalid_erase_sequences_preserve_data_and_can_be_retried() {
let mut m = bus(SaveDevice::Flash128);
m.load_save_image(&vec![0; 131072]).unwrap();
command(&mut m, 0x80);
assert!(m.write8(LO, 0x55).is_err());
m.write8(HI, 0xaa).unwrap();
m.write8(LO, 0x55).unwrap();
for (at, val) in [(BASE + 1, 0x30), (BASE, 0x10), (HI, 0x90)] {
let err = m.write8(at, val).unwrap_err();
assert_eq!(m.write8(at, val), Err(err));
assert!(m.save_image().unwrap().iter().all(|&b| b == 0));
}
m.write8(BASE, 0x30).unwrap();
m.advance_cycles(ERASE);
assert!(m.save_image().unwrap()[..4096].iter().all(|&b| b == 255));
}
#[test]
fn halt_clocks_nominal_operations_stop_freezes_them_and_setup_resets_dirty_state() {
let mut m = bus(SaveDevice::Flash128);
program(&mut m, BASE, 0);
m.write8(HALTCNT, 0).unwrap();
m.advance_cycles(PROGRAM);
assert!(m.save_modified());
assert_eq!(m.read8(BASE).unwrap(), 0);
m.load_save_image(&vec![255; 131072]).unwrap();
assert!(!m.save_modified());
program(&mut m, BASE, 1);
m.write8(HALTCNT, 0x80).unwrap();
m.advance_cycles(PROGRAM);
assert!(m.save_write_pending());
assert!(!m.save_modified());
assert_eq!(m.read8(BASE).unwrap(), 0x80);
}