From 8dd0f4768ab379b88debb6c2b18f19255e6b1078 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 04:27:42 -0700 Subject: [PATCH] Add support for flash save media. --- agb-tests/tests/save_test_common/mod.rs | 2 +- agb-tests/tests/test_save_flash_128k.rs | 16 + agb-tests/tests/test_save_flash_64k.rs | 16 + agb/src/save/flash.rs | 470 ++++++++++++++++++++++++ agb/src/save/mod.rs | 63 +++- agb/src/save/sram.rs | 9 +- agb/src/save/utils.rs | 72 ++-- 7 files changed, 591 insertions(+), 57 deletions(-) create mode 100644 agb-tests/tests/test_save_flash_128k.rs create mode 100644 agb-tests/tests/test_save_flash_64k.rs create mode 100644 agb/src/save/flash.rs diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb-tests/tests/save_test_common/mod.rs index 9e494424..d3f823c2 100644 --- a/agb-tests/tests/save_test_common/mod.rs +++ b/agb-tests/tests/save_test_common/mod.rs @@ -39,7 +39,7 @@ fn do_test( let mut access = gba.save.access_with_timer(timers.timer2)?; // writes data to the save media - let prepared = access.prepare_write(offset..offset + len)?; + let mut prepared = access.prepare_write(offset..offset + len)?; let mut rng = seed.clone(); let mut current = offset; let end = offset + len; diff --git a/agb-tests/tests/test_save_flash_128k.rs b/agb-tests/tests/test_save_flash_128k.rs new file mode 100644 index 00000000..7256ddfb --- /dev/null +++ b/agb-tests/tests/test_save_flash_128k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_128k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-tests/tests/test_save_flash_64k.rs b/agb-tests/tests/test_save_flash_64k.rs new file mode 100644 index 00000000..6c179ae3 --- /dev/null +++ b/agb-tests/tests/test_save_flash_64k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_64k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs new file mode 100644 index 00000000..a3b470e5 --- /dev/null +++ b/agb/src/save/flash.rs @@ -0,0 +1,470 @@ +//! Module for flash save media support. +//! +//! Flash may be read with ordinary read commands, but writing requires +//! sending structured commands to the flash chip. + +// TODO: Setup cartridge read timings for faster Flash access. + +use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray}; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::sync::{InitOnce, Static}; +use core::cmp; +use crate::save::utils::Timeout; + +// Volatile address ports for flash +const FLASH_PORT_BANK: MemoryMapped = unsafe { MemoryMapped::new(0x0E000000) }; +const FLASH_PORT_A: MemoryMapped = unsafe { MemoryMapped::new(0x0E005555) }; +const FLASH_PORT_B: MemoryMapped = unsafe { MemoryMapped::new(0x0E002AAA) }; +const FLASH_DATA: MemoryMapped1DArray = unsafe { MemoryMapped1DArray::new(0x0E000000) }; + +// Various constants related to sector sizes +const BANK_SHIFT: usize = 16; // 64 KiB +const BANK_LEN: usize = 1 << BANK_SHIFT; +const BANK_MASK: usize = BANK_LEN - 1; + +// Constants relating to flash commands. +const CMD_SET_BANK: u8 = 0xB0; +const CMD_READ_CHIP_ID: u8 = 0x90; +const CMD_READ_CONTENTS: u8 = 0xF0; +const CMD_WRITE: u8 = 0xA0; +const CMD_ERASE_SECTOR_BEGIN: u8 = 0x80; +const CMD_ERASE_SECTOR_CONFIRM: u8 = 0x30; +const CMD_ERASE_SECTOR_ALL: u8 = 0x10; + +/// Starts a command to the flash chip. +fn start_flash_command() { + FLASH_PORT_A.set(0xAA); + FLASH_PORT_B.set(0x55); +} + +/// Helper function for issuing commands to the flash chip. +fn issue_flash_command(c2: u8) { + start_flash_command(); + FLASH_PORT_A.set(c2); +} + +/// A simple thing to avoid excessive bank switches +static CURRENT_BANK: Static = Static::new(!0); +fn set_bank(bank: u8) -> Result<(), Error> { + if bank == 0xFF { + Err(Error::OutOfBounds) + } else if bank != CURRENT_BANK.read() { + issue_flash_command(CMD_SET_BANK); + FLASH_PORT_BANK.set(bank as u8); + CURRENT_BANK.write(bank); + Ok(()) + } else { + Ok(()) + } +} + +/// Identifies a particular f +/// lash chip in use by a Game Pak. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +#[repr(u8)] +pub enum FlashChipType { + /// 64KiB SST chip + Sst64K, + /// 64KiB Macronix chip + Macronix64K, + /// 64KiB Panasonic chip + Panasonic64K, + /// 64KiB Atmel chip + Atmel64K, + /// 128KiB Sanyo chip + Sanyo128K, + /// 128KiB Macronix chip + Macronix128K, + /// An unidentified chip + Unknown, +} +impl FlashChipType { + /// Returns the type of the flash chip currently in use. + pub fn detect() -> Result { + Ok(Self::from_id(detect_chip_id()?)) + } + + /// Determines the flash chip type from an ID. + pub fn from_id(id: u16) -> Self { + match id { + 0xD4BF => FlashChipType::Sst64K, + 0x1CC2 => FlashChipType::Macronix64K, + 0x1B32 => FlashChipType::Panasonic64K, + 0x3D1F => FlashChipType::Atmel64K, + 0x1362 => FlashChipType::Sanyo128K, + 0x09C2 => FlashChipType::Macronix128K, + _ => FlashChipType::Unknown, + } + } +} + +/// Determines the raw ID of the flash chip currently in use. +pub fn detect_chip_id() -> Result { + issue_flash_command(CMD_READ_CHIP_ID); + let high = unsafe { read_raw_byte(0x0E000001) }; + let low = unsafe { read_raw_byte(0x0E000000) }; + let id = (high as u16) << 8 | low as u16; + issue_flash_command(CMD_READ_CONTENTS); + Ok(id) +} + +/// Information relating to a particular flash chip that could be found in a +/// Game Pak. +#[allow(dead_code)] +struct ChipInfo { + /// The wait state required to read from the chip. + read_wait: u8, + /// The wait state required to write to the chip. + write_wait: u8, + + /// The timeout in milliseconds for writes to this chip. + write_timeout: u16, + /// The timeout in milliseconds for erasing a sector in this chip. + erase_sector_timeout: u16, + /// The timeout in milliseconds for erasing the entire chip. + erase_chip_timeout: u16, + + /// The number of 64KiB banks in this chip. + bank_count: u8, + /// Whether this is an Atmel chip, which has 128 byte sectors instead of 4K. + uses_atmel_api: bool, + /// Whether this is an Macronix chip, which requires an additional command + /// to cancel the current action after a timeout. + requires_cancel_command: bool, + + /// The [`MediaInfo`] to return for this chip type. + info: &'static MediaInfo, +} + +// Media info for the various chipsets. +static INFO_64K: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 12, // 4 KiB + sector_count: 16, // 4 KiB * 16 = 64 KiB + uses_prepare_write: true, +}; +static INFO_64K_ATMEL: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 7, // 128 bytes + sector_count: 512, // 128 bytes * 512 = 64 KiB + uses_prepare_write: false, +}; +static INFO_128K: MediaInfo = MediaInfo { + media_type: MediaType::Flash128K, + sector_shift: 12, + sector_count: 32, // 4 KiB * 32 = 128 KiB + uses_prepare_write: true, +}; + +// Chip info for the various chipsets. +static CHIP_INFO_SST_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 1, // 3 cycles + write_timeout: 10, + erase_sector_timeout: 40, + erase_chip_timeout: 200, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_MACRONIX_64K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_64K, +}; +static CHIP_INFO_PANASONIC_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 0, // 4 cycles + write_timeout: 10, + erase_sector_timeout: 500, + erase_chip_timeout: 500, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_ATMEL_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 40, + erase_chip_timeout: 40, + bank_count: 1, + uses_atmel_api: true, + requires_cancel_command: false, + info: &INFO_64K_ATMEL, +}; +static CHIP_INFO_GENERIC_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_128K, +}; +static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 2, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_128K, +}; + +impl FlashChipType { + /// Returns the internal info for this chip. + fn chip_info(&self) -> &'static ChipInfo { + match *self { + FlashChipType::Sst64K => &CHIP_INFO_SST_64K, + FlashChipType::Macronix64K => &CHIP_INFO_MACRONIX_64K, + FlashChipType::Panasonic64K => &CHIP_INFO_PANASONIC_64K, + FlashChipType::Atmel64K => &CHIP_INFO_ATMEL_64K, + FlashChipType::Sanyo128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Macronix128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Unknown => &CHIP_INFO_GENERIC_64K, + } + } +} +static CHIP_INFO: InitOnce<&'static ChipInfo> = InitOnce::new(); +fn cached_chip_info() -> Result<&'static ChipInfo, Error> { + CHIP_INFO + .try_get(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) }) + .map(Clone::clone) +} + +/// Actual implementation of the ChipInfo functions. +impl ChipInfo { + /// Returns the total length of this chip. + fn total_len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + // Checks whether a byte offset is in bounds. + fn check_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.total_len() { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + // Checks whether a sector offset is in bounds. + fn check_sector_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.info.sector_count { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + /// Sets the currently active bank. + fn set_bank(&self, bank: usize) -> Result<(), Error> { + if bank >= self.bank_count as usize { + Err(Error::OutOfBounds) + } else if self.bank_count > 1 { + set_bank(bank as u8) + } else { + Ok(()) + } + } + + /// Reads a buffer from save media into memory. + fn read_buffer(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + while buf.len() != 0 { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + unsafe { + read_raw_buf(&mut buf[..end_len], 0x0E000000 + start); + } + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Verifies that a buffer was properly stored into save media. + fn verify_buffer(&self, mut offset: usize, mut buf: &[u8]) -> Result { + while buf.len() != 0 { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + if !unsafe { verify_raw_buf(&buf[..end_len], 0x0E000000 + start) } { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Waits for a timeout, or an operation to complete. + fn wait_for_timeout( + &self, offset: usize, val: u8, ms: u16, timeout: &mut Timeout, + ) -> Result<(), Error> { + timeout.start(); + let offset = 0x0E000000 + offset; + + while unsafe { read_raw_byte(offset) != val } { + if timeout.check_timeout_met(ms) { + if self.requires_cancel_command { + FLASH_PORT_A.set(0xF0); + } + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Erases a sector to flash. + fn erase_sector(&self, sector: usize, timeout: &mut Timeout) -> Result<(), Error> { + let offset = sector << self.info.sector_shift; + self.set_bank(offset >> BANK_SHIFT)?; + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + start_flash_command(); + FLASH_DATA.set(offset & BANK_MASK, CMD_ERASE_SECTOR_CONFIRM); + self.wait_for_timeout(offset & BANK_MASK, 0xFF, self.erase_sector_timeout, timeout) + } + + /// Erases the entire chip. + fn erase_chip(&self, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + issue_flash_command(CMD_ERASE_SECTOR_ALL); + self.wait_for_timeout(0, 0xFF, 3000, timeout) + } + + /// Writes a byte to the save media. + fn write_byte(&self, offset: usize, byte: u8, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_WRITE); + FLASH_DATA.set(offset, byte); + self.wait_for_timeout(offset, byte, self.write_timeout, timeout) + } + + /// Writes an entire buffer to the save media. + fn write_buffer(&self, offset: usize, buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.set_bank(offset >> BANK_SHIFT)?; + for i in 0..buf.len() { + let byte_off = offset + i; + if (byte_off & BANK_MASK) == 0 { + self.set_bank(byte_off >> BANK_SHIFT)?; + } + self.write_byte(byte_off & BANK_MASK, buf[i], timeout)?; + } + Ok(()) + } + + /// Erases and writes an entire 128b sector on Atmel devices. + fn write_atmel_sector_raw( + &self, offset: usize, buf: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + crate::interrupt::free(|_| { + issue_flash_command(CMD_WRITE); + for i in 0..128 { + FLASH_DATA.set(offset + i, buf[i]); + } + self.wait_for_timeout(offset + 127, buf[127], self.erase_sector_timeout, timeout) + })?; + Ok(()) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + #[inline(never)] // avoid allocating the 128 byte buffer for no reason. + fn write_atmel_sector_safe( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut sector = [0u8; 128]; + self.read_buffer(offset, &mut sector[0..start])?; + sector[start..start + buf.len()].copy_from_slice(buf); + self.read_buffer(offset + start + buf.len(), &mut sector[start + buf.len()..128])?; + self.write_atmel_sector_raw(offset, §or, timeout) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + /// + /// This avoids allocating stack if there is no need to. + fn write_atmel_sector( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if start == 0 && buf.len() == 128 { + self.write_atmel_sector_raw(offset, buf, timeout) + } else { + self.write_atmel_sector_safe(offset, buf, start, timeout) + } + } +} + +/// The [`RawSaveAccess`] used for flash save media. +pub struct FlashAccess; +impl RawSaveAccess for FlashAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(cached_chip_info()?.info) + } + + fn read(&self, offset: usize, buf: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.read_buffer(offset, buf) + } + + fn verify(&self, offset: usize, buf: &[u8], _: &mut Timeout) -> Result { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.verify_buffer(offset, buf) + } + + fn prepare_write( + &self, sector: usize, count: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_sector_len(sector, count)?; + + if chip.uses_atmel_api { + Ok(()) + } else if count == chip.info.sector_count { + chip.erase_chip(timeout) + } else { + for i in sector..sector + count { + chip.erase_sector(i, timeout)?; + } + Ok(()) + } + } + + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + if chip.uses_atmel_api { + while buf.len() != 0 { + let start = offset & 127; + let end_len = cmp::min(128 - start, buf.len()); + chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } else { + // Write the bytes one by one. + chip.write_buffer(offset, buf, timeout)?; + Ok(()) + } + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 7de71242..34195676 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -103,12 +103,13 @@ //! small sector size. use core::ops::Range; +use crate::save::utils::Timeout; use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; //pub mod eeprom; -//pub mod flash; +mod flash; mod sram; mod utils; @@ -181,10 +182,10 @@ impl MediaInfo { /// A trait allowing low-level saving and writing to save media. trait RawSaveAccess: Sync { fn info(&self) -> Result<&'static MediaInfo, Error>; - fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; - fn verify(&self, offset: usize, buffer: &[u8]) -> Result; - fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; - fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; + fn read(&self, offset: usize, buffer: &mut [u8], timeout: &mut Timeout) -> Result<(), Error>; + fn verify(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result; + fn prepare_write(&self, sector: usize, count: usize, timeout: &mut Timeout) -> Result<(), Error>; + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>; } static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); @@ -201,7 +202,7 @@ fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { /// Allows reading and writing of save media. pub struct SaveData { - lock: RawMutexGuard<'static>, + _lock: RawMutexGuard<'static>, access: &'static dyn RawSaveAccess, info: &'static MediaInfo, timeout: utils::Timeout, @@ -211,7 +212,7 @@ impl SaveData { fn new(timer: Option) -> Result { match get_save_implementation() { Some(access) => Ok(SaveData { - lock: utils::lock_media()?, + _lock: utils::lock_media_access()?, access, info: access.info()?, timeout: utils::Timeout::new(timer), @@ -255,15 +256,15 @@ impl SaveData { /// Copies data from the save media to a buffer. /// /// If an error is returned, the contents of the buffer are unpredictable. - pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + pub fn read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { self.check_bounds_len(offset, buffer.len())?; - self.access.read(offset, buffer) + self.access.read(offset, buffer, &mut self.timeout) } /// Verifies that a given block of memory matches the save media. - pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + pub fn verify(&mut self, offset: usize, buffer: &[u8]) -> Result { self.check_bounds_len(offset, buffer.len())?; - self.access.verify(offset, buffer) + self.access.verify(offset, buffer, &mut self.timeout) } /// Returns a range that contains all sectors the input range overlaps. @@ -286,7 +287,9 @@ impl SaveData { if self.info.uses_prepare_write { let range = self.align_range(range.clone()); let shift = self.info.sector_shift; - self.access.prepare_write(range.start >> shift, range.len() >> shift)?; + self.access.prepare_write( + range.start >> shift, range.len() >> shift, &mut self.timeout, + )?; } Ok(SavePreparedBlock { parent: self, @@ -307,14 +310,14 @@ impl<'a> SavePreparedBlock<'a> { /// call to `prepare_write` will leave the save data in an unpredictable /// state. If an error is returned, the contents of the save media is /// unpredictable. - pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { if buffer.len() == 0 { Ok(()) } else if !self.range.contains(&offset) || !self.range.contains(&(offset + buffer.len() - 1)) { Err(Error::OutOfBounds) } else { - self.parent.access.write(offset, buffer) + self.parent.access.write(offset, buffer, &mut self.parent.timeout) } } @@ -327,7 +330,7 @@ impl<'a> SavePreparedBlock<'a> { /// call to `prepare_write` will leave the save data in an unpredictable /// state. If an error is returned, the contents of the save media is /// unpredictable. - pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + pub fn write_and_verify(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { self.write(offset, buffer)?; if !self.parent.verify(offset, buffer)? { Err(Error::WriteError) @@ -387,6 +390,36 @@ impl SaveManager { set_save_implementation(&sram::BatteryBackedAccess); } + /// Declares that the ROM uses 64KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_64k(&mut self) { + marker::emit_flash_512k_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 128KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_128k(&mut self) { + marker::emit_flash_1m_marker(); + set_save_implementation(&flash::FlashAccess); + } + /// Creates a new accessor to the save data. /// /// You must have initialized the save manager beforehand to use a specific diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs index b4e21eca..614b751e 100644 --- a/agb/src/save/sram.rs +++ b/agb/src/save/sram.rs @@ -5,6 +5,7 @@ use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; use crate::save::asm_utils::*; +use crate::save::utils::Timeout; const SRAM_SIZE: usize = 32 * 1024; // 32 KiB @@ -28,7 +29,7 @@ impl RawSaveAccess for BatteryBackedAccess { }) } - fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { check_bounds(offset, buffer.len())?; unsafe { read_raw_buf(buffer, 0x0E000000 + offset); @@ -36,17 +37,17 @@ impl RawSaveAccess for BatteryBackedAccess { Ok(()) } - fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { check_bounds(offset, buffer.len())?; let val = unsafe { verify_raw_buf(buffer, 0x0E000000 + offset) }; Ok(val) } - fn prepare_write(&self, _: usize, _: usize) -> Result<(), Error> { + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { Ok(()) } - fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + fn write(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result<(), Error> { check_bounds(offset, buffer.len())?; unsafe { write_raw_buf(0x0E000000 + offset, buffer); diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs index 2ada1eeb..a19879c2 100644 --- a/agb/src/save/utils.rs +++ b/agb/src/save/utils.rs @@ -7,48 +7,46 @@ use crate::timer::{Timer, Divider}; /// A timeout type used to prevent hardware errors in save media from hanging /// the game. pub struct Timeout { - timer: Option, + timer: Option, } impl Timeout { - /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. - /// - /// ## Errors - /// - /// If another timeout has already been created. - #[inline(never)] - pub fn new(timer: Option) -> Self { - Timeout { timer } - } - - /// Starts this timeout. - pub fn start(&mut self) { - if let Some(timer) = &mut self.timer { - timer.set_divider(Divider::Divider1024); - timer.set_interrupt(false); - timer.set_overflow_amount(0xFFFF); - timer.set_cascade(false); - timer.set_enabled(true); + /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. + /// + /// ## Errors + /// + /// If another timeout has already been created. + #[inline(never)] + pub fn new(timer: Option) -> Self { + Timeout { timer } } - } - /// Returns whether a number of milliseconds has passed since the last call - /// to [`Timeout::start()`]. - pub fn check_timeout_met(&self, check_ms: u16) -> bool { - if let Some(timer) = &self.timer { - check_ms * 17 < timer.value() - } else { - false + /// Starts this timeout. + pub fn start(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + timer.set_divider(Divider::Divider1024); + timer.set_interrupt(false); + timer.set_overflow_amount(0xFFFF); + timer.set_cascade(false); + timer.set_enabled(true); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`Timeout::start()`]. + pub fn check_timeout_met(&self, check_ms: u16) -> bool { + if let Some(timer) = &self.timer { + check_ms * 17 < timer.value() + } else { + false + } } - } } -/// Tries to obtain a lock on the global lock for save operations. -/// -/// This is used to prevent problems with stateful save media. -pub fn lock_media() -> Result, Error> { - static LOCK: RawMutex = RawMutex::new(); - match LOCK.try_lock() { - Some(x) => Ok(x), - None => Err(Error::MediaInUse), - } +pub fn lock_media_access() -> Result, Error> { + static LOCK: RawMutex = RawMutex::new(); + match LOCK.try_lock() { + Some(x) => Ok(x), + None => Err(Error::MediaInUse), + } }