From 8ded9bc90ea96f5d8ffe6ac2948b1d3e008d4814 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 7 Oct 2026 08:25:12 +0700 Subject: [PATCH] feat(cartridge): implement transactional Flash programming and erase --- crates/gba-core/src/cartridge.rs | 28 +++- crates/gba-core/src/cartridge/flash.rs | 135 ++++++++++++++- crates/gba-core/src/memory.rs | 47 +++++- .../gba-core/src/memory/flash_step_tests.rs | 6 +- .../gba-core/src/memory/flash_write_tests.rs | 95 +++++++++++ crates/gba-core/tests/flash.rs | 4 +- crates/gba-core/tests/flash_write.rs | 157 ++++++++++++++++++ 7 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 crates/gba-core/src/memory/flash_write_tests.rs create mode 100644 crates/gba-core/tests/flash_write.rs diff --git a/crates/gba-core/src/cartridge.rs b/crates/gba-core/src/cartridge.rs index 4b99797..c40e802 100644 --- a/crates/gba-core/src/cartridge.rs +++ b/crates/gba-core/src/cartridge.rs @@ -7,7 +7,10 @@ mod flash; mod rtc; pub use calendar::{RtcDateTime, RtcError}; 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; pub const GPIO_DATA: u32 = 0x0800_00c4; @@ -46,6 +49,20 @@ impl Cartridge { 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 { + self.flash.as_mut().and_then(Flash::take_mutation) + } + pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result { 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. - 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) { return Ok(()); // Non-cartridge accesses are validated by the bus. } @@ -105,7 +127,7 @@ impl Cartridge { 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 { address: at, diff --git a/crates/gba-core/src/cartridge/flash.rs b/crates/gba-core/src/cartridge/flash.rs index cfba317..d0b9adf 100644 --- a/crates/gba-core/src/cartridge/flash.rs +++ b/crates/gba-core/src/cartridge/flash.rs @@ -1,18 +1,20 @@ -//! Bounded Macronix Flash identification, array reads, and 128 KiB banking. -//! Program/erase commands remain diagnostic: this module cannot claim a completed save. +//! Bounded Macronix Flash commands with nominal delayed program/erase completion. use crate::memory::MemoryError; use std::{error::Error, fmt}; pub const SAVE_START: u32 = 0x0e00_0000; 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)] pub enum SaveDevice { #[default] None, - /// Macronix ID 1CC2h. Array reads and identification only. + /// Macronix ID 1CC2h, 64 KiB array. Flash64, - /// Macronix ID 09C2h. Adds two 64 KiB banks; no programming or erase yet. + /// Macronix ID 09C2h, two 64 KiB banks. Flash128, } @@ -50,6 +52,21 @@ pub(super) struct Flash { phase: Phase, identify: bool, bank: u8, + pending: Option, +} + +#[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)] @@ -58,6 +75,10 @@ enum Phase { Unlock, Command, Bank, + Program, + EraseUnlock, + EraseCommand, + EraseConfirm, } impl Flash { @@ -67,6 +88,7 @@ impl Flash { phase: Phase::Ready, identify: false, bank: 0, + pending: None, }) } @@ -76,6 +98,22 @@ impl Flash { pub(super) fn read(self, address: u32, image: &[u8]) -> Result { 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 { return match offset { 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 { + 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 error = |operation| MemoryError::UnsupportedIo { address, @@ -101,9 +162,19 @@ impl Flash { operation, }; 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 // 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 && (self.device == SaveDevice::Flash64 || (self.phase == Phase::Ready && !self.identify)) @@ -129,8 +200,8 @@ impl Flash { } 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")), + 0xa0 => Phase::Program, + 0x80 => Phase::EraseUnlock, _ => return Err(error("Flash command")), } } @@ -139,6 +210,52 @@ impl Flash { Phase::Ready } 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")), }; } diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index 5c9a1fa..da9b626 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -12,6 +12,8 @@ mod fetch_tests; #[cfg(test)] mod flash_step_tests; #[cfg(test)] +mod flash_write_tests; +#[cfg(test)] mod irq_fetch_tests; mod iwram_bus; #[cfg(test)] @@ -138,6 +140,8 @@ pub struct Memory { oam: Vec, rom: Vec, save_image: Vec, + save_modified: bool, + cartridge_elapsed: Cell, cartridge: Cartridge, cartridge_step: Cell>, bios: Option>, @@ -170,6 +174,8 @@ impl Memory { oam: vec![0; 1024], rom, save_image: Vec::new(), + save_modified: false, + cartridge_elapsed: Cell::new(0), cartridge: Cartridge::default(), cartridge_step: Cell::new(None), bios: None, @@ -218,6 +224,7 @@ impl Memory { "active cartridge transaction" ); self.save_image = vec![0xff; device.capacity()]; + self.save_modified = false; self.cartridge.set_save_device(device); } @@ -244,6 +251,7 @@ impl Memory { }); } self.save_image.copy_from_slice(bytes); + self.save_modified = false; self.cartridge.set_save_device(device); Ok(()) } @@ -272,6 +280,24 @@ impl Memory { 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 { self.cartridge_step.get().unwrap_or(self.cartridge) } @@ -345,14 +371,21 @@ impl Memory { debug_assert!(self.timer_step.get().is_none()); self.timer_step.set(Some(self.io.timer_step())); self.cartridge_step.set(Some(self.cartridge)); + self.cartridge_elapsed.set(0); } pub(crate) fn discard_timer_step(&self) { self.timer_step.set(None); self.cartridge_step.set(None); + self.cartridge_elapsed.set(0); } 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() { step.advance_to(cycles); self.timer_step.set(Some(step)); @@ -370,11 +403,15 @@ impl Memory { .cartridge_step .take() .expect("active cartridge transaction"); + self.commit_save_mutation(); + self.cartridge_elapsed.set(0); // Staged devices already reached this boundary. Advance remaining devices exactly once. self.advance_running_cycles_with_timers(cycles, false); } fn advance_running_cycles(&mut self, cycles: u32) { + self.cartridge.advance(cycles); + self.commit_save_mutation(); self.advance_running_cycles_with_timers(cycles, true); } @@ -930,8 +967,6 @@ impl Memory { ) -> Result<(), MemoryError> { let index = self.write_index::(address, access)?; Self::validate_io_values(&mut self.audio_state(), address, &bytes)?; - let mut cartridge = self.cartridge_state(); - cartridge.write(address, &bytes)?; self.record_access( address, match N { @@ -941,6 +976,8 @@ impl Memory { _ => unreachable!("unsupported bus width"), }, ); + let mut cartridge = self.cartridge_state(); + cartridge.write(address, &bytes, &self.save_image)?; if cartridge.mapped(address) { if self.cartridge_step.get().is_some() { self.cartridge_step.set(Some(cartridge)); @@ -1150,6 +1187,8 @@ impl Memory { let mut cartridge = self.cartridge_state(); let timing = self.cpu_timing.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. // A serial request can finish before a later control write reaches the bus. let validation = (|| { @@ -1157,7 +1196,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)?; + cartridge.write(address, &bytes, &self.save_image)?; self.record_access(address, AccessWidth::Word); if Serial::mapped(address) { if let Some(mut step) = self.timer_step.get() { @@ -1174,6 +1213,8 @@ impl Memory { })(); self.cpu_timing.set(timing); self.timer_step.set(devices); + self.cartridge_step.set(cartridge_step); + self.cartridge_elapsed.set(cartridge_elapsed); validation?; for &(address, value) in writes { // The map cannot change between validation and these writes. diff --git a/crates/gba-core/src/memory/flash_step_tests.rs b/crates/gba-core/src/memory/flash_step_tests.rs index 5699149..18fe602 100644 --- a/crates/gba-core/src/memory/flash_step_tests.rs +++ b/crates/gba-core/src/memory/flash_step_tests.rs @@ -73,7 +73,7 @@ fn work_ram_byte_loads_observe_selected_id_with_save_bus_costs() { } #[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 read in [false, true] { 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, (true, true) => 0x7808, }], - &[(0, 0xa0), (1, if read { SAVE_START } else { HI })], + &[(0, 0x42), (1, if read { SAVE_START } else { HI })], ); unlocked(&mut memory); 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 { "outside work RAM" } else { - "programming" + "Flash command" })); assert_eq!(machine.step(), Err(error)); assert_eq!(machine.cpu(), &cpu); diff --git a/crates/gba-core/src/memory/flash_write_tests.rs b/crates/gba-core/src/memory/flash_write_tests.rs new file mode 100644 index 0000000..33a7184 --- /dev/null +++ b/crates/gba-core/src/memory/flash_write_tests.rs @@ -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()); +} diff --git a/crates/gba-core/tests/flash.rs b/crates/gba-core/tests/flash.rs index 6708c86..0a688db 100644 --- a/crates/gba-core/tests/flash.rs +++ b/crates/gba-core/tests/flash.rs @@ -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() { for device in [SaveDevice::Flash64, SaveDevice::Flash128] { for (byte, message) in [ - (0xa0, "programming"), - (0x80, "erase setup"), + (0x42, "Flash command"), + (0x30, "Flash command"), (0x10, "Flash command"), ] { let mut bus = memory(device); diff --git a/crates/gba-core/tests/flash_write.rs b/crates/gba-core/tests/flash_write.rs new file mode 100644 index 0000000..4798427 --- /dev/null +++ b/crates/gba-core/tests/flash_write.rs @@ -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); +}