feat(cartridge): add explicit Flash identification and banked reads
This commit is contained in:
parent
fce222eea2
commit
8b3b7ae62d
23 changed files with 1042 additions and 52 deletions
|
|
@ -3,8 +3,11 @@
|
|||
use crate::memory::MemoryError;
|
||||
|
||||
mod calendar;
|
||||
mod flash;
|
||||
mod rtc;
|
||||
pub use calendar::{RtcDateTime, RtcError};
|
||||
use flash::Flash;
|
||||
pub use flash::{SaveDevice, SaveError, SAVE_END, SAVE_START};
|
||||
use rtc::Rtc;
|
||||
|
||||
pub const GPIO_DATA: u32 = 0x0800_00c4;
|
||||
|
|
@ -23,13 +26,28 @@ pub enum CartridgeHardware {
|
|||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Cartridge {
|
||||
gpio: Option<Gpio>,
|
||||
flash: Option<Flash>,
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
pub(crate) fn new(hardware: CartridgeHardware) -> Self {
|
||||
Self {
|
||||
gpio: (hardware == CartridgeHardware::Rtc).then(Gpio::default),
|
||||
}
|
||||
pub(crate) fn set_hardware(&mut self, hardware: CartridgeHardware) {
|
||||
self.gpio = (hardware == CartridgeHardware::Rtc).then(Gpio::default);
|
||||
}
|
||||
|
||||
pub(crate) fn set_save_device(&mut self, device: SaveDevice) {
|
||||
self.flash = Flash::new(device);
|
||||
}
|
||||
|
||||
pub(crate) fn save_device(&self) -> SaveDevice {
|
||||
self.flash.map_or(SaveDevice::None, Flash::device)
|
||||
}
|
||||
|
||||
pub(crate) fn save_mapped(&self, address: u32) -> bool {
|
||||
self.flash.is_some() && (SAVE_START..=SAVE_END).contains(&address)
|
||||
}
|
||||
|
||||
pub(crate) fn read_save8(&self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
self.flash.expect("mapped save device").read(address, image)
|
||||
}
|
||||
|
||||
pub(crate) fn rtc_datetime(&self) -> Option<RtcDateTime> {
|
||||
|
|
@ -56,13 +74,14 @@ impl Cartridge {
|
|||
}
|
||||
|
||||
pub(crate) fn mapped(&self, address: u32) -> bool {
|
||||
self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address)
|
||||
self.save_mapped(address)
|
||||
|| (self.gpio.is_some() && (GPIO_DATA..GPIO_CONTROL + 2).contains(&address))
|
||||
}
|
||||
|
||||
/// Disabled reads expose the supplied ROM bytes, not a zero-filled replacement.
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
let gpio = self.gpio?;
|
||||
if !self.mapped(address) || !gpio.readable {
|
||||
if !(GPIO_DATA..GPIO_CONTROL + 2).contains(&address) || !gpio.readable {
|
||||
return None;
|
||||
}
|
||||
let value = match address & !1 {
|
||||
|
|
@ -79,6 +98,15 @@ impl Cartridge {
|
|||
if !self.mapped(address) {
|
||||
return Ok(()); // Non-cartridge accesses are validated by the bus.
|
||||
}
|
||||
if self.save_mapped(address) {
|
||||
if bytes.len() != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
});
|
||||
}
|
||||
return self.flash.as_mut().unwrap().write(address, bytes[0]);
|
||||
}
|
||||
let fail = |at, value, operation| MemoryError::UnsupportedIo {
|
||||
address: at,
|
||||
value,
|
||||
|
|
|
|||
148
crates/gba-core/src/cartridge/flash.rs
Normal file
148
crates/gba-core/src/cartridge/flash.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! Bounded Macronix Flash identification, array reads, and 128 KiB banking.
|
||||
//! Program/erase commands remain diagnostic: this module cannot claim a completed save.
|
||||
use crate::memory::MemoryError;
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub const SAVE_START: u32 = 0x0e00_0000;
|
||||
pub const SAVE_END: u32 = 0x0e00_ffff;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SaveDevice {
|
||||
#[default]
|
||||
None,
|
||||
/// Macronix ID 1CC2h. Array reads and identification only.
|
||||
Flash64,
|
||||
/// Macronix ID 09C2h. Adds two 64 KiB banks; no programming or erase yet.
|
||||
Flash128,
|
||||
}
|
||||
|
||||
impl SaveDevice {
|
||||
pub fn capacity(self) -> usize {
|
||||
match self {
|
||||
Self::None => 0,
|
||||
Self::Flash64 => 64 * 1024,
|
||||
Self::Flash128 => 128 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SaveError {
|
||||
NoDevice,
|
||||
InvalidSize { expected: usize, actual: usize },
|
||||
}
|
||||
impl fmt::Display for SaveError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NoDevice => f.write_str("no cartridge save device selected"),
|
||||
Self::InvalidSize { expected, actual } => write!(
|
||||
f,
|
||||
"save image size {actual}; expected exactly {expected} bytes"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error for SaveError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Flash {
|
||||
device: SaveDevice,
|
||||
phase: Phase,
|
||||
identify: bool,
|
||||
bank: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Phase {
|
||||
Ready,
|
||||
Unlock,
|
||||
Command,
|
||||
Bank,
|
||||
}
|
||||
|
||||
impl Flash {
|
||||
pub(super) fn new(device: SaveDevice) -> Option<Self> {
|
||||
(device != SaveDevice::None).then_some(Self {
|
||||
device,
|
||||
phase: Phase::Ready,
|
||||
identify: false,
|
||||
bank: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn device(self) -> SaveDevice {
|
||||
self.device
|
||||
}
|
||||
|
||||
pub(super) fn read(self, address: u32, image: &[u8]) -> Result<u8, MemoryError> {
|
||||
let offset = (address - SAVE_START) as usize;
|
||||
if self.identify {
|
||||
return match offset {
|
||||
0 => Ok(0xc2),
|
||||
1 => Ok(if self.device == SaveDevice::Flash64 {
|
||||
0x1c
|
||||
} else {
|
||||
9
|
||||
}),
|
||||
_ => Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash ID read outside manufacturer/device bytes",
|
||||
}),
|
||||
};
|
||||
}
|
||||
Ok(image[usize::from(self.bank) * 0x10000 + offset])
|
||||
}
|
||||
|
||||
pub(super) fn write(&mut self, address: u32, value: u8) -> Result<(), MemoryError> {
|
||||
let offset = address - SAVE_START;
|
||||
let error = |operation| MemoryError::UnsupportedIo {
|
||||
address,
|
||||
value,
|
||||
operation,
|
||||
};
|
||||
let mut next = *self;
|
||||
// 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
|
||||
&& value == 0xf0
|
||||
&& (self.device == SaveDevice::Flash64
|
||||
|| (self.phase == Phase::Ready && !self.identify))
|
||||
{
|
||||
next.identify = false;
|
||||
next.phase = Phase::Ready;
|
||||
} else {
|
||||
next.phase = match self.phase {
|
||||
Phase::Ready if offset == 0x5555 && value == 0xaa => Phase::Unlock,
|
||||
Phase::Unlock if offset == 0x2aaa && value == 0x55 => Phase::Command,
|
||||
Phase::Command if offset == 0x5555 => {
|
||||
if self.identify && value != 0xf0 {
|
||||
return Err(error("Flash command while in ID mode"));
|
||||
}
|
||||
match value {
|
||||
0x90 => {
|
||||
next.identify = true;
|
||||
Phase::Ready
|
||||
}
|
||||
0xf0 => {
|
||||
next.identify = false;
|
||||
Phase::Ready
|
||||
}
|
||||
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")),
|
||||
_ => return Err(error("Flash command")),
|
||||
}
|
||||
}
|
||||
Phase::Bank if offset == 0 && value < 2 => {
|
||||
next.bank = value;
|
||||
Phase::Ready
|
||||
}
|
||||
Phase::Bank => return Err(error("Flash bank address/value")),
|
||||
_ => return Err(error("Flash unlock/reset sequence")),
|
||||
};
|
||||
}
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ mod fetch;
|
|||
#[cfg(test)]
|
||||
mod fetch_tests;
|
||||
#[cfg(test)]
|
||||
mod flash_step_tests;
|
||||
#[cfg(test)]
|
||||
mod irq_fetch_tests;
|
||||
mod iwram_bus;
|
||||
#[cfg(test)]
|
||||
|
|
@ -27,7 +29,7 @@ use iwram_bus::IwramBus;
|
|||
|
||||
use crate::{
|
||||
audio::{Audio, StereoLevel},
|
||||
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError},
|
||||
cartridge::{Cartridge, CartridgeHardware, RtcDateTime, RtcError, SaveDevice, SaveError},
|
||||
cpu::InstructionSet,
|
||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
|
|
@ -53,6 +55,10 @@ pub enum MemoryError {
|
|||
Unaligned(u32),
|
||||
RomTooLarge(usize),
|
||||
InvalidBiosSize(usize),
|
||||
UnsupportedCartridgeAccess {
|
||||
address: u32,
|
||||
operation: &'static str,
|
||||
},
|
||||
UnsupportedIo {
|
||||
address: u32,
|
||||
value: u8,
|
||||
|
|
@ -63,6 +69,9 @@ pub enum MemoryError {
|
|||
impl fmt::Display for MemoryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnsupportedCartridgeAccess { address, operation } => {
|
||||
write!(f, "unsupported {operation} at {address:#010x}")
|
||||
}
|
||||
Self::UnsupportedIo {
|
||||
address,
|
||||
value,
|
||||
|
|
@ -122,6 +131,7 @@ pub struct Memory {
|
|||
video_ram: Vec<u8>,
|
||||
oam: Vec<u8>,
|
||||
rom: Vec<u8>,
|
||||
save_image: Vec<u8>,
|
||||
cartridge: Cartridge,
|
||||
cartridge_step: Cell<Option<Cartridge>>,
|
||||
bios: Option<Vec<u8>>,
|
||||
|
|
@ -153,6 +163,7 @@ impl Memory {
|
|||
video_ram: vec![0; VRAM_SIZE],
|
||||
oam: vec![0; 1024],
|
||||
rom,
|
||||
save_image: Vec::new(),
|
||||
cartridge: Cartridge::default(),
|
||||
cartridge_step: Cell::new(None),
|
||||
bios: None,
|
||||
|
|
@ -183,14 +194,52 @@ impl Memory {
|
|||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Replace optional cartridge hardware with its initial state. Call between machine steps.
|
||||
/// This never modifies the supplied ROM bytes or enables a host clock.
|
||||
/// Replace GPIO/RTC hardware with its initial state. Call between machine steps.
|
||||
/// Save hardware/data are preserved. This never modifies ROM bytes or enables a host clock.
|
||||
pub fn set_cartridge_hardware(&mut self, hardware: CartridgeHardware) {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.cartridge = Cartridge::new(hardware);
|
||||
self.cartridge.set_hardware(hardware);
|
||||
}
|
||||
|
||||
/// Replace save hardware and its array with erased bytes. GPIO/RTC state is unchanged.
|
||||
/// Call between machine steps; no file is loaded or written by the core.
|
||||
pub fn set_save_device(&mut self, device: SaveDevice) {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
self.save_image = vec![0xff; device.capacity()];
|
||||
self.cartridge.set_save_device(device);
|
||||
}
|
||||
|
||||
/// Inspect the complete save array, independently of ID mode and selected bank.
|
||||
pub fn save_image(&self) -> Option<&[u8]> {
|
||||
(self.cartridge.save_device() != SaveDevice::None).then_some(self.save_image.as_slice())
|
||||
}
|
||||
|
||||
/// Replace the entire array and reset Flash command/bank state. RTC state is unchanged.
|
||||
/// A wrong length changes nothing. This setup API does not emulate programming.
|
||||
pub fn load_save_image(&mut self, bytes: &[u8]) -> Result<(), SaveError> {
|
||||
assert!(
|
||||
self.cartridge_step.get().is_none(),
|
||||
"active cartridge transaction"
|
||||
);
|
||||
let device = self.cartridge.save_device();
|
||||
if device == SaveDevice::None {
|
||||
return Err(SaveError::NoDevice);
|
||||
}
|
||||
if bytes.len() != device.capacity() {
|
||||
return Err(SaveError::InvalidSize {
|
||||
expected: device.capacity(),
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
self.save_image.copy_from_slice(bytes);
|
||||
self.cartridge.set_save_device(device);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inspect the live calendar, not an in-progress serial read snapshot.
|
||||
|
|
@ -749,6 +798,16 @@ 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().save_mapped(address)
|
||||
&& self
|
||||
.cpu_access
|
||||
.is_some_and(|access| !matches!(access.pc >> 24, 0x02 | 0x03))
|
||||
{
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash CPU read outside work RAM",
|
||||
});
|
||||
}
|
||||
if address >> 24 == 0x04 {
|
||||
if let Some(step) = self.timer_step.get() {
|
||||
if Serial::mapped(address) {
|
||||
|
|
@ -784,7 +843,11 @@ impl Memory {
|
|||
|
||||
// Strict lookup for host access, instruction fetches, and prefetch snapshots.
|
||||
fn read_mapped_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if let Some(value) = self.cartridge_state().read8(address) {
|
||||
let cartridge = self.cartridge_state();
|
||||
if cartridge.save_mapped(address) {
|
||||
return cartridge.read_save8(address, &self.save_image);
|
||||
}
|
||||
if let Some(value) = cartridge.read8(address) {
|
||||
return Ok(value);
|
||||
}
|
||||
match address >> 24 {
|
||||
|
|
@ -1001,6 +1064,15 @@ impl Memory {
|
|||
}
|
||||
Ok(0)
|
||||
}
|
||||
0x0e if self.cartridge_state().save_mapped(address) => {
|
||||
if N != 1 {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte write",
|
||||
});
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
_ => Err(MemoryError::Unmapped(address)),
|
||||
}
|
||||
}
|
||||
|
|
@ -1086,6 +1158,7 @@ impl Memory {
|
|||
if address & 1 != 0 {
|
||||
return Err(MemoryError::Unaligned(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)?]);
|
||||
self.iwram_bus
|
||||
|
|
@ -1093,11 +1166,22 @@ impl Memory {
|
|||
Ok(value)
|
||||
}
|
||||
|
||||
fn validate_save_read_width(&self, address: u32) -> Result<(), MemoryError> {
|
||||
if self.cartridge_state().save_mapped(address) {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash non-byte read",
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The bus requires alignment; the CPU applies ARM7TDMI load rotation.
|
||||
pub fn read32(&self, address: u32) -> Result<u32, MemoryError> {
|
||||
if address & 3 != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
self.validate_save_read_width(address)?;
|
||||
self.record_access(address, AccessWidth::Word);
|
||||
let value = u32::from_le_bytes([
|
||||
self.read_byte(address)?,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ impl Memory {
|
|||
if address & (width as u32 - 1) != 0 {
|
||||
return Err(MemoryError::Unaligned(address));
|
||||
}
|
||||
if self.cartridge_state().save_mapped(address) {
|
||||
return Err(MemoryError::UnsupportedCartridgeAccess {
|
||||
address,
|
||||
operation: "Flash instruction fetch",
|
||||
});
|
||||
}
|
||||
let mut bytes = [0; 4];
|
||||
for (offset, byte) in bytes[..width].iter_mut().enumerate() {
|
||||
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
|
||||
|
|
|
|||
148
crates/gba-core/src/memory/flash_step_tests.rs
Normal file
148
crates/gba-core/src/memory/flash_step_tests.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! Original Flash CPU bus phases, failed writes, fetch restrictions, and preflight checks.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
cartridge::{SaveDevice, SAVE_START},
|
||||
cpu::Cpu,
|
||||
io::{IF, WAITCNT},
|
||||
machine::Machine,
|
||||
};
|
||||
|
||||
const HI: u32 = SAVE_START + 0x5555;
|
||||
const LO: u32 = SAVE_START + 0x2aaa;
|
||||
fn unlocked(memory: &mut Memory) {
|
||||
memory.set_save_device(SaveDevice::Flash128);
|
||||
memory.write8(HI, 0xaa).unwrap();
|
||||
memory.write8(LO, 0x55).unwrap();
|
||||
}
|
||||
fn ram_reader(thumb: bool, base: u32) -> (Cpu, Memory) {
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe12fff12],
|
||||
&[(1, SAVE_START), (2, base | u32::from(thumb))],
|
||||
);
|
||||
for at in (0..32).step_by(4) {
|
||||
memory
|
||||
.write32(base + at, if thumb { 0x46c046c0 } else { 0xe1a00000 })
|
||||
.unwrap();
|
||||
}
|
||||
if thumb {
|
||||
memory.write16(base, 0x7808).unwrap();
|
||||
} else {
|
||||
memory.write32(base, 0xe5d10000).unwrap();
|
||||
}
|
||||
cpu.step(&mut memory).unwrap(); // BX from setup ROM into original RAM reader.
|
||||
(cpu, memory)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_and_thumb_flash_commands_commit_once_at_all_save_wait_settings() {
|
||||
for thumb in [false, true] {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
thumb,
|
||||
&[if thumb { 0x7008 } else { 0xe5c10000 }],
|
||||
&[(0, 0x90), (1, HI)],
|
||||
);
|
||||
unlocked(&mut memory);
|
||||
memory.write16(WAITCNT, wait as u16).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(machine.last_timing().data_cycles, cycles);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_ram_byte_loads_observe_selected_id_with_save_bus_costs() {
|
||||
for thumb in [false, true] {
|
||||
for base in [0x02001000, 0x03001000] {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let (cpu, mut memory) = ram_reader(thumb, base);
|
||||
unlocked(&mut memory);
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
memory.write16(WAITCNT, wait as u16).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0xc2);
|
||||
assert_eq!(machine.last_timing().data_cycles, cycles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_programming_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(
|
||||
thumb,
|
||||
&[match (thumb, read) {
|
||||
(false, false) => 0xe5c10000,
|
||||
(true, false) => 0x7008,
|
||||
(false, true) => 0xe5d10000,
|
||||
(true, true) => 0x7808,
|
||||
}],
|
||||
&[(0, 0xa0), (1, if read { SAVE_START } else { HI })],
|
||||
);
|
||||
unlocked(&mut memory);
|
||||
let before = memory.cartridge;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains(if read {
|
||||
"outside work RAM"
|
||||
} else {
|
||||
"programming"
|
||||
}));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.memory().cartridge, before);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
machine.memory_mut().write8(HI, 0x90).unwrap(); // Failure retained the accepted unlock prefix.
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_id_reads_and_discard_do_not_mutate_array_or_committed_controller() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
unlocked(&mut memory);
|
||||
let before = memory.cartridge;
|
||||
memory.begin_timer_step();
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(memory.cartridge, before);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 255);
|
||||
memory.write8(HI, 0x90).unwrap();
|
||||
memory.begin_timer_step();
|
||||
memory.write8(HI, 0xaa).unwrap();
|
||||
memory.write8(LO, 0x55).unwrap();
|
||||
memory.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 255);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert!(memory.save_image().unwrap().iter().all(|&b| b == 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_fetch_and_block_word_access_cannot_bypass_byte_only_device() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
unlocked(&mut memory);
|
||||
for state in [InstructionSet::Arm, InstructionSet::Thumb] {
|
||||
let error = memory
|
||||
.fetch_instruction(SAVE_START, state)
|
||||
.instruction
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("Flash instruction fetch"));
|
||||
}
|
||||
let before = memory.cartridge;
|
||||
assert!(memory
|
||||
.write_words(&[(0x02000000, 0x12345678), (SAVE_START, 0)])
|
||||
.is_err());
|
||||
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||
assert_eq!(memory.cartridge, before);
|
||||
}
|
||||
|
|
@ -87,6 +87,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
0
|
||||
}
|
||||
}
|
||||
// The supported save bus permits byte transfers only; N/S use the same SRAM waits.
|
||||
0x0e | 0x0f => 1 + [4, 3, 2, 8][usize::from(waitcnt & 3)],
|
||||
// BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped.
|
||||
_ => 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,30 @@ fn queue(waitcnt: u16) -> Prefetch {
|
|||
queue
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_wait_changes_preserve_the_queue_until_a_flash_data_access_cancels_it() {
|
||||
for (wait, raw) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
let mut q = queue(ENABLE);
|
||||
assert_eq!(
|
||||
q.code(PC, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
q.advance(2); // One cycle remains in the nominal three-cycle ROM halfword.
|
||||
let stream = q.stream;
|
||||
q.configure(ENABLE | wait as u16);
|
||||
assert_eq!(q.stream, stream);
|
||||
assert_eq!(
|
||||
q.data(0x0e005555, AccessWidth::Byte, AccessKind::NonSequential),
|
||||
raw + 1
|
||||
);
|
||||
assert_eq!(q.stream, None);
|
||||
assert_eq!(
|
||||
q.code(PC + 2, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_and_in_progress_fetches_follow_all_rom_wait_settings_and_widths() {
|
||||
for window in 0..3 {
|
||||
|
|
|
|||
276
crates/gba-core/tests/flash.rs
Normal file
276
crates/gba-core/tests/flash.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
//! Original Flash command sequences and supplied save bytes; no game assets or save files.
|
||||
use gba_core::{
|
||||
cartridge::{CartridgeHardware, SaveDevice, SaveError, SAVE_END, SAVE_START},
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{HALTCNT, IE, IF, IME},
|
||||
machine::Machine,
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
const HI: u32 = SAVE_START + 0x5555;
|
||||
const LO: u32 = SAVE_START + 0x2aaa;
|
||||
fn memory(device: SaveDevice) -> Memory {
|
||||
let mut bus = Memory::new(0xeafffffeu32.to_le_bytes().to_vec()).unwrap();
|
||||
bus.set_save_device(device);
|
||||
bus
|
||||
}
|
||||
fn command(bus: &mut Memory, byte: u8) -> Result<(), MemoryError> {
|
||||
bus.write8(HI, 0xaa)?;
|
||||
bus.write8(LO, 0x55)?;
|
||||
bus.write8(HI, byte)
|
||||
}
|
||||
fn bank(bus: &mut Memory, number: u8) {
|
||||
command(bus, 0xb0).unwrap();
|
||||
bus.write8(SAVE_START, number).unwrap();
|
||||
}
|
||||
fn image(device: SaveDevice) -> Vec<u8> {
|
||||
(0..device.capacity())
|
||||
.map(|i| ((i >> 16) * 73 + i * 19 + (i >> 8)) as u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_erased_state_and_exact_image_loading_are_explicit() {
|
||||
let mut bus = memory(SaveDevice::None);
|
||||
assert_eq!(bus.save_image(), None);
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START),
|
||||
Err(MemoryError::Unmapped(SAVE_START))
|
||||
);
|
||||
assert_eq!(bus.write8(HI, 0xaa), Err(MemoryError::Unmapped(HI)));
|
||||
assert_eq!(bus.load_save_image(&[]), Err(SaveError::NoDevice));
|
||||
for device in [SaveDevice::Flash64, SaveDevice::Flash128] {
|
||||
bus.set_save_device(device);
|
||||
assert_eq!(bus.save_image().unwrap(), vec![255; device.capacity()]);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 255);
|
||||
assert_eq!(bus.read8(SAVE_END).unwrap(), 255);
|
||||
let bytes = image(device);
|
||||
bus.load_save_image(&bytes).unwrap();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
for len in [0, device.capacity() - 1, device.capacity() + 1] {
|
||||
assert_eq!(
|
||||
bus.load_save_image(&vec![0; len]),
|
||||
Err(SaveError::InvalidSize {
|
||||
expected: device.capacity(),
|
||||
actual: len
|
||||
})
|
||||
);
|
||||
assert_eq!(bus.save_image().unwrap(), bytes);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2); // Invalid load did not reset ID state.
|
||||
}
|
||||
bus.load_save_image(&bytes).unwrap(); // Valid host load resets command/bank state.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0]);
|
||||
assert_eq!(bus.read32(ROM_START).unwrap(), 0xeafffffe);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manufacturer_and_device_id_are_separate_bytes_and_exit_preserves_data() {
|
||||
for (device, id) in [(SaveDevice::Flash64, 0x1c), (SaveDevice::Flash128, 9)] {
|
||||
let mut bus = memory(device);
|
||||
let data = image(device);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), id);
|
||||
assert!(bus
|
||||
.read8(SAVE_START + 2)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("ID read outside"));
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), data[1]);
|
||||
assert_eq!(bus.save_image().unwrap(), data);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bank_selection_covers_every_byte_and_survives_id_mode() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let data = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
for selected in [1, 0, 1] {
|
||||
bank(&mut bus, selected);
|
||||
for offset in 0..=0xffff {
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START + offset).unwrap(),
|
||||
data[selected as usize * 0x10000 + offset as usize]
|
||||
);
|
||||
}
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(bus.read8(SAVE_START + 1).unwrap(), 9);
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
assert_eq!(
|
||||
bus.read8(SAVE_START).unwrap(),
|
||||
data[selected as usize * 0x10000]
|
||||
);
|
||||
}
|
||||
let mut small = memory(SaveDevice::Flash64);
|
||||
assert!(command(&mut small, 0xb0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("64 KiB"));
|
||||
small.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(small.save_image().unwrap().len(), 0x10000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"),
|
||||
(0x10, "Flash command"),
|
||||
] {
|
||||
let mut bus = memory(device);
|
||||
let data = image(device);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
let error = command(&mut bus, byte).unwrap_err();
|
||||
assert!(error.to_string().contains(message));
|
||||
assert_eq!(bus.write8(HI, byte), Err(error));
|
||||
assert_eq!(bus.save_image().unwrap(), data);
|
||||
bus.write8(HI, 0xf0).unwrap(); // Finish the already accepted unlock sequence.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let data = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&data).unwrap();
|
||||
command(&mut bus, 0xb0).unwrap();
|
||||
for (address, value) in [(SAVE_START, 2), (SAVE_START, 255), (SAVE_START + 1, 1)] {
|
||||
let error = bus.write8(address, value).unwrap_err();
|
||||
assert_eq!(bus.write8(address, value), Err(error));
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0]);
|
||||
}
|
||||
bus.write8(SAVE_START, 1).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), data[0x10000]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widths_aliases_and_invalid_unlocks_remain_diagnostic() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
for address in [SAVE_START - 1, SAVE_END + 1, 0x0f000000, 0x0eff5555] {
|
||||
assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address)));
|
||||
}
|
||||
assert!(bus
|
||||
.read16(SAVE_START)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte read"));
|
||||
assert!(bus
|
||||
.read32(SAVE_START)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte read"));
|
||||
assert!(bus
|
||||
.write16(HI & !1, 0xaa)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte write"));
|
||||
assert!(bus
|
||||
.write32(SAVE_START, 0)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("non-byte write"));
|
||||
for (address, value) in [(HI, 0x55), (LO, 0xaa)] {
|
||||
let error = bus.write8(address, value).unwrap_err();
|
||||
assert_eq!(bus.write8(address, value), Err(error));
|
||||
}
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
assert!(bus.write8(LO + 1, 0x55).is_err());
|
||||
bus.write8(LO, 0x55).unwrap();
|
||||
assert!(bus.write8(HI + 1, 0x90).is_err());
|
||||
bus.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redundant_128_reset_preserves_array_bank_but_does_not_exit_id_or_partial_unlock() {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let bytes = image(SaveDevice::Flash128);
|
||||
bus.load_save_image(&bytes).unwrap();
|
||||
bank(&mut bus, 1);
|
||||
bus.write8(HI, 0xf0).unwrap(); // Already idle in array mode.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0x10000]);
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
assert!(bus.write8(HI, 0xf0).is_err()); // Direct ID exit for this device is not established.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
command(&mut bus, 0xf0).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap(); // Redundant reset after documented unlocked ID exit.
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), bytes[0x10000]);
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
assert!(bus.write8(HI, 0xf0).is_err());
|
||||
bus.write8(LO, 0x55).unwrap();
|
||||
bus.write8(HI, 0x90).unwrap();
|
||||
assert_eq!(bus.save_image().unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macronix_64_direct_reset_and_gpio_selection_are_independent() {
|
||||
let mut bus = memory(SaveDevice::Flash64);
|
||||
bus.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
bus.advance_rtc_seconds(42).unwrap();
|
||||
let rtc = bus.rtc_datetime();
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap();
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 255);
|
||||
bus.write8(HI, 0xaa).unwrap();
|
||||
bus.write8(HI, 0xf0).unwrap(); // Abort partial unlock for this documented device.
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.set_cartridge_hardware(CartridgeHardware::None);
|
||||
assert_eq!(bus.read8(SAVE_START).unwrap(), 0xc2);
|
||||
bus.set_cartridge_hardware(CartridgeHardware::Rtc);
|
||||
bus.set_rtc_datetime(rtc.unwrap()).unwrap();
|
||||
bus.set_save_device(SaveDevice::Flash128);
|
||||
assert_eq!(bus.rtc_datetime(), rtc);
|
||||
bus.set_save_device(SaveDevice::None);
|
||||
assert_eq!(bus.save_image(), None);
|
||||
assert_eq!(bus.rtc_datetime(), rtc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_stop_and_dma_do_not_create_a_flash_completion_or_change_data() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
command(&mut bus, 0x90).unwrap();
|
||||
bus.write16(IE, 0x2000).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write8(HALTCNT, if stop { 128 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..20 {
|
||||
machine.step().unwrap();
|
||||
}
|
||||
assert_eq!(machine.memory().read8(SAVE_START).unwrap(), 0xc2);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.halted(), !stop);
|
||||
assert_eq!(machine.stopped(), stop);
|
||||
}
|
||||
for source in [false, true] {
|
||||
let mut bus = memory(SaveDevice::Flash128);
|
||||
let dma = DMA_BASE + 36;
|
||||
bus.write32(0x02000000, 0xaa).unwrap();
|
||||
bus.write32(dma, if source { SAVE_START } else { 0x02000000 })
|
||||
.unwrap();
|
||||
bus.write32(dma + 4, if source { 0x02000010 } else { SAVE_START })
|
||||
.unwrap();
|
||||
bus.write32(dma + 8, 0xc0000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("Flash non-byte"));
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read32(0x02000010).unwrap(), 0);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert!(machine
|
||||
.memory()
|
||||
.save_image()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|&x| x == 255));
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,20 @@ fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() {
|
|||
assert_eq!(memory.waitcnt(), 0x4317);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_byte_access_costs_follow_waitcnt_and_ignore_sequential_kind() {
|
||||
for (wait, cycles) in [5, 4, 3, 9].into_iter().enumerate() {
|
||||
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
|
||||
for address in [0x0e000000, 0x0e005555, 0x0e00ffff] {
|
||||
assert_eq!(
|
||||
bus_cycles(wait as u16, address, AccessWidth::Byte, kind),
|
||||
cycles
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() {
|
||||
for window in 0..3 {
|
||||
|
|
@ -112,8 +126,8 @@ fn internal_regions_and_ewram_use_fixed_width_specific_costs() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
|
||||
// This stateless helper excludes opcode prefetch. PHI output and SRAM remain unmapped.
|
||||
fn stored_prefetch_phi_and_save_wait_settings_do_not_change_rom_bus_costs() {
|
||||
// This stateless helper excludes opcode prefetch. Save waits do not affect ROM cycles.
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] {
|
||||
for kind in [AccessKind::NonSequential, AccessKind::Sequential] {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue