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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue