feat(cartridge): implement transactional Flash programming and erase
This commit is contained in:
parent
2ea7164d9a
commit
8ded9bc90e
7 changed files with 452 additions and 20 deletions
|
|
@ -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<SaveMutation> {
|
||||
self.flash.as_mut().and_then(Flash::take_mutation)
|
||||
}
|
||||
|
||||
pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<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)]
|
||||
|
|
@ -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<u8, MemoryError> {
|
||||
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<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 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")),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
rom: Vec<u8>,
|
||||
save_image: Vec<u8>,
|
||||
save_modified: bool,
|
||||
cartridge_elapsed: Cell<u32>,
|
||||
cartridge: Cartridge,
|
||||
cartridge_step: Cell<Option<Cartridge>>,
|
||||
bios: Option<Vec<u8>>,
|
||||
|
|
@ -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::<N>(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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
95
crates/gba-core/src/memory/flash_write_tests.rs
Normal file
95
crates/gba-core/src/memory/flash_write_tests.rs
Normal 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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
157
crates/gba-core/tests/flash_write.rs
Normal file
157
crates/gba-core/tests/flash_write.rs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue