From 217f42a635dfd3b0099cdfac12daeacfdc81ba08 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Tue, 16 Aug 2022 17:16:34 -0700 Subject: [PATCH 01/11] Initial work on porting gba save code to agb codebase. --- agb/build.rs | 1 + agb/src/lib.rs | 2 + agb/src/save/asm_routines.s | 89 ++++++++++ agb/src/save/asm_utils.rs | 63 +++++++ agb/src/save/mod.rs | 345 ++++++++++++++++++++++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 agb/src/save/asm_routines.s create mode 100644 agb/src/save/asm_utils.rs create mode 100644 agb/src/save/mod.rs diff --git a/agb/build.rs b/agb/build.rs index 2bb2f4bb..d4245b4a 100644 --- a/agb/build.rs +++ b/agb/build.rs @@ -7,6 +7,7 @@ fn main() { "src/sound/mixer/mixer.s", "src/agbabi/memset.s", "src/agbabi/memcpy.s", + "src/save/asm_routines.s", ]; println!("cargo:rerun-if-changed=gba.ld"); diff --git a/agb/src/lib.rs b/agb/src/lib.rs index fa3dd6f9..08ba848f 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,6 +168,8 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; +/// Implements save games. +pub mod save; mod single; /// Implements sound output. pub mod sound; diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s new file mode 100644 index 00000000..41e8df74 --- /dev/null +++ b/agb/src/save/asm_routines.s @@ -0,0 +1,89 @@ +@ +@ char WramReadByte(const char* offset); +@ +@ A routine that reads a byte from a given memory offset. +@ + .thumb + .global WramReadByte + .thumb_func + .align 2 +WramReadByte: + ldr r1, =WramReadByteInner + bx r1 + + .section .data + + .thumb + .thumb_func + .align 2 +WramReadByteInner: + ldrb r0, [r0] + mov pc, lr + + .section .text + +@ +@ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); +@ +@ A routine that compares two memory offsets. +@ + .thumb + .global WramVerifyBuf + .thumb_func + .align 2 +WramVerifyBuf: + push {r4-r5, lr} + movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result + movs r0, #0 @ set up r0 so the default return result is false + ldr r4, =WramVerifyBufInner + bx r4 @ jump to the part in WRAM + + .section .data + + .thumb + .thumb_func + .align 2 +WramVerifyBufInner: + @ At this point, buf1 is actually in r5, so r0 can be used as a status return + ldrb r3, [r5,r2] + ldrb r4, [r1,r2] + cmp r3, r4 + bne 0f + sub r2, #1 + bpl WramVerifyBufInner + + @ Returns from the function successfully + movs r0, #1 +0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point + pop {r4-r5, pc} + + .section .text + +@ +@ void WramXferBuf(const char* source, char* dest, int count); +@ +@ A routine that copies one buffer into another. +@ + .thumb + .global WramXferBuf + .thumb_func + .align 2 +WramXferBuf: + ldr r3, =WramXferBufInner + bx r3 + + .pool + .section .data + + .thumb + .thumb_func + .align 2 +WramXferBufInner: + sub r2, #1 + ldrb r3, [r0,r2] + strb r3, [r1,r2] + bne WramXferBufInner + mov pc, lr + + .pool + .section .text diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs new file mode 100644 index 00000000..f9ca1db0 --- /dev/null +++ b/agb/src/save/asm_utils.rs @@ -0,0 +1,63 @@ +//! A module containing low-level assembly functions that can be loaded into +//! WRAM. Both flash media and battery-backed SRAM require reads to be +//! performed via code in WRAM and cannot be accessed by DMA. + +extern "C" { + fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn WramReadByte(src: *const u8) -> u8; + fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; +} + +/// Copies data from a given memory address into a buffer. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { + if dst.len() != 0 { + WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + } +} + +/// Copies data from a buffer into a given memory address. +/// +/// This is not strictly needed to write into save media, but reuses the +/// optimized loop used in `read_raw_buf`, and will often be faster. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { + if src.len() != 0 { + WramXferBuf(src.as_ptr(), dst as _, src.len()); + } +} + +/// Verifies that the data in a buffer matches that in a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { + if buf1.len() != 0 { + WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + } else { + true + } +} + +/// Reads a byte from a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those from code found in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_byte(src: usize) -> u8 { + WramReadByte(src as _) +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs new file mode 100644 index 00000000..c4fad1a2 --- /dev/null +++ b/agb/src/save/mod.rs @@ -0,0 +1,345 @@ +//! Module for reading and writing to save media. +//! +//! This module provides both specific interfaces that directly access particular +//! types of save media, and an abstraction layer that allows access to all kinds +//! of save media using a shared interface. +//! +//! ## Save media types +//! +//! There are, broadly speaking, three different kinds of save media that can be +//! found in official Game Carts: +//! +//! * Battery-Backed SRAM: The simplest kind of save media, which can be accessed +//! like normal memory. You can have SRAM up to 32KiB, and while there exist a +//! few variants this does not matter much for a game developer. +//! * EEPROM: A kind of save media based on very cheap chips and slow chips. +//! These are accessed using a serial interface based on reading/writing bit +//! streams into IO registers. This memory comes in 8KiB and 512 byte versions, +//! which unfortunately cannot be distinguished at runtime. +//! * Flash: A kind of save media based on flash memory. Flash memory can be read +//! like ordinary memory, but writing requires sending commands using multiple +//! IO register spread across the address space. This memory comes in 64KiB +//! and 128KiB variants, which can thankfully be distinguished using a chip ID. +//! +//! As these various types of save media cannot be easily distinguished at +//! runtime, the kind of media in use should be set manually. +//! +//! ## Setting save media type +//! +//! To use save media in your game, you must set which type to use. This is done +//! by calling one of the following functions at startup: +//! +//! * For 32 KiB battery-backed SRAM, call [`use_sram`]. +//! * For 64 KiB flash memory, call [`use_flash_64k`]. +//! * For 128 KiB flash memory, call [`use_flash_128k`]. +//! * For 512 byte EEPROM, call [`use_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. +//! +//! Then, call [`set_timer_for_timeout`] to set the timer you intend to use to +//! track the timeout that prevents errors with the save media from hanging your +//! game. For more information on GBA timers, see the [`timer`](`crate::timer`) +//! module's documentation. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::save; +//! save::use_flash_128k(); +//! save::set_timer_for_timeout(3); // Uses timer 3 for save media timeouts. +//! ``` +//! +//! ## Using save media +//! +//! To access save media, use the [`SaveAccess::new`] method to create a new +//! [`SaveAccess`] object. Its methods are used to read or write save media. +//! +//! Reading data from the savegame is simple. Use [`read`](`SaveAccess::read`) +//! to copy data from an offset in the savegame into a buffer in memory. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::{info, save::SaveAccess}; +//! let mut buf = [0; 1000]; +//! SaveAccess::new()?.read(1000, &mut buf)?; +//! info!("Memory result: {:?}", buf); +//! ``` +//! +//! Writing to save media requires you to prepare the area for writing by calling +//! the [`prepare_write`](`SaveAccess::prepare_write`) method before doing the +//! actual write commands with the [`write`](`SaveAccess::write`) method. +//! +//! TODO Update example +//! ```rust,norun +//! # use gba::{info, save::SaveAccess}; +//! let access = SaveAccess::new()?; +//! access.prepare_write(500..600)?; +//! access.write(500, &[10; 25])?; +//! access.write(525, &[20; 25])?; +//! access.write(550, &[30; 25])?; +//! access.write(575, &[40; 25])?; +//! ``` +//! +//! The `prepare_write` method leaves everything in a sector that overlaps the +//! range passed to it in an implementation defined state. On some devices it may +//! do nothing, and on others, it may clear the entire range to `0xFF`. +//! +//! Because writes can only be prepared on a per-sector basis, a clear on a range +//! of `4000..5000` on a device with 4096 byte sectors will actually clear a range +//! of `0..8192`. Use [`sector_size`](`SaveAccess::sector_size`) to find the +//! sector size, or [`align_range`](`SaveAccess::align_range`) to directly +//! calculate the range of memory that will be affected by the clear. +//! +//! ## Performance and Other Details +//! +//! Because `prepare_write` does nothing on non-flash chips, it would not cause +//! correctness issues to ignore it. Even so, it is recommend to write code to +//! use the `prepare_write` function regardless of the save media, as it has +//! minimal runtime cost on other save media types. If needed, you can check if +//! `prepare_write` is required by calling the +//! (`requires_prepare_write`)(`SaveAccess::requires_prepare_write`) method. +//! +//! Some memory types have a `sector_size` above `1`, but do not use +//! `prepare_write`. This indicates that the media type has sectors that must +//! be rewritten all at once, instead of supporting the separate erase/write +//! cycles that flash media does. Writing non-sector aligned memory will be +//! slower on such save media, as the implementation needs to read the old +//! contents into a buffer before writing to avoid data loss. +//! +//! To summarize, for all supported media types: +//! +//! * SRAM does not require `prepare_write` and has no sectors to align to. Reads +//! and writes at any alignment are efficient. Furthermore, it does not require +//! a timer to be set with [`set_timer_for_timeout`]. +//! * Non-Atmel flash chips requires `prepare_write`, and have sectors of 4096 +//! bytes. Atmel flash chips instead do not require `prepare_write`, and instead +//! have sectors of 128 bytes. You should generally try to use `prepare_write` +//! regardless, and write in blocks of 128 bytes if at all possible. +//! * EEPROM does not require `prepare_write` and has sectors of 8 bytes. + +use core::cell::Cell; +use core::ops::Range; +use bare_metal::Mutex; + +mod asm_utils; +//mod setup; +//mod utils; + +//pub use asm_utils::*; +//pub use setup::*; +//pub use utils::*; + +//pub mod eeprom; +//pub mod flash; +//pub mod sram; + +/// A list of save media types. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub enum MediaType { + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, + /// A user-defined save media type + Custom, +} + +/// The type used for errors encountered while reading or writing save media. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Error { + /// There is no save media attached to this game cart. + NoMedia, + /// Failed to write the data to save media. + WriteError, + /// An operation on save media timed out. + OperationTimedOut, + /// An attempt was made to access save media at an invalid offset. + OutOfBounds, + /// The media is already in use. + /// + /// This can generally only happen in an IRQ that happens during an ongoing + /// save media operation. + MediaInUse, + /// This command cannot be used with the save media in use. + IncompatibleCommand, +} + +/// Information about the save media used. +#[derive(Clone, Debug)] +pub struct MediaInfo { + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires the use of the + /// [`prepare_write`](`SaveAccess::prepare_write`) function before a block of + /// memory can be overwritten. + pub requires_prepare_write: bool, +} + +/// A trait allowing low-level saving and writing to save media. +/// +/// It exposes an interface mostly based around the requirements of reading and +/// writing flash memory, as those are the most restrictive. +/// +/// This interface treats memory as a continuous block of bytes for purposes of +/// reading, and as an array of sectors . +pub trait RawSaveAccess: Sync { + /// Returns information about the save media used. + fn info(&self) -> Result<&'static MediaInfo, Error>; + + /// Reads a slice of memory from save media. + /// + /// This will attempt to fill `buffer` entirely, and will error if this is + /// not possible. The contents of `buffer` are unpredictable if an error is + /// returned. + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; + + /// Verifies that the save media has been successfully written, comparing + /// it against the given buffer. + fn verify(&self, offset: usize, buffer: &[u8]) -> Result; + + /// Prepares a given span of sectors for writing. This may permanently erase + /// the current contents of the sector on some save media. + fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; + + /// Writes a buffer to the save media. + /// + /// The sectors you are writing to must be prepared with a call to the + /// `prepare_write` function beforehand, or else the contents of the save + /// media may be unpredictable after writing. + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; +} + +/// Contains the current save media implementation. +static CURRENT_SAVE_ACCESS: Mutex>> = + Mutex::new(Cell::new(None)); + +/// Sets the save media implementation in use. +pub fn set_save_implementation(access: Option<&'static dyn RawSaveAccess>) { + crate::interrupt::free(|c| { + CURRENT_SAVE_ACCESS.borrow(c).set(access) + }) +} + +/// Gets the save media implementation in use. +pub fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + crate::interrupt::free(|c| { + CURRENT_SAVE_ACCESS.borrow(c).get() + }) +} + +/// Allows reading and writing of save media. +#[derive(Copy, Clone)] +pub struct SaveAccess { + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, +} +impl SaveAccess { + /// Creates a new save accessor around the current save implementaiton. + pub fn new() -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveAccess { access, info: access.info()? }), + None => Err(Error::NoMedia), + } + } + + /// Returns the media info underlying this accessor. + pub fn media_info(&self) -> &'static MediaInfo { + self.info + } + + /// Returns the save media type being used. + pub fn media_type(&self) -> MediaType { + self.info.media_type + } + + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + pub fn sector_size(&self) -> usize { + 1 << self.info.sector_shift + } + + /// Returns the total length of this save media. + pub fn len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + /// Copies data from the save media to a buffer. + pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.access.read(offset, buffer) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + self.access.verify(offset, buffer) + } + + /// Returns whether this save media requires the use of [`SaveAccess::prepare_write`]. + pub fn requires_prepare_write(&self) -> bool { + self.info.requires_prepare_write + } + + /// Returns a range that contains all sectors the input range overlaps. + /// + /// This can be used to calculate which blocks would be erased by a call + /// to [`prepare_write`](`SaveAccess::prepare_write`) + pub fn align_range(&self, range: Range) -> Range { + let shift = self.info.sector_shift; + let mask = (1 << shift) - 1; + (range.start & !mask)..((range.end + mask) & !mask) + } + + /// Prepares a given span of offsets for writing. + /// + /// This will erase any data in any sector overlapping the input range. To + /// calculate which offset ranges would be affected, use the + /// [`align_range`](`SaveAccess::align_range`) function. + pub fn prepare_write(&self, range: Range) -> Result<(), Error> { + if self.info.requires_prepare_write { + let range = self.align_range(range); + let shift = self.info.sector_shift; + self.access.prepare_write(range.start >> shift, range.len() >> shift) + } else { + Ok(()) + } + } + + /// Writes a given buffer into the save media. + /// + /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns + /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the + /// range you intend to write for this to function correctly. The contents of + /// the save media are unpredictable if you do not. + pub fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.access.write(offset, buffer) + } + + /// Writes and validates a given buffer into the save media. + /// + /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns + /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the + /// range you intend to write for this to function correctly. The contents of + /// the save media will be unpredictable if you do not. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + pub fn write_and_verify(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.write(offset, buffer)?; + if !self.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} From d50413a3cccffb716845e696ff2076ab0d15dcbe Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:20:35 -0700 Subject: [PATCH 02/11] Update the code style of the save module to better match agb's philosophy. --- agb/src/lib.rs | 1 - agb/src/save/mod.rs | 227 +++++++++++++++++++++--------------------- agb/src/save/utils.rs | 54 ++++++++++ 3 files changed, 167 insertions(+), 115 deletions(-) create mode 100644 agb/src/save/utils.rs diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 08ba848f..59174b71 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,7 +168,6 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; -/// Implements save games. pub mod save; mod single; /// Implements sound output. diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index c4fad1a2..bcc0e6c0 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -1,9 +1,5 @@ //! Module for reading and writing to save media. //! -//! This module provides both specific interfaces that directly access particular -//! types of save media, and an abstraction layer that allows access to all kinds -//! of save media using a shared interface. -//! //! ## Save media types //! //! There are, broadly speaking, three different kinds of save media that can be @@ -35,11 +31,6 @@ //! * For 512 byte EEPROM, call [`use_eeprom_512b`]. //! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. //! -//! Then, call [`set_timer_for_timeout`] to set the timer you intend to use to -//! track the timeout that prevents errors with the save media from hanging your -//! game. For more information on GBA timers, see the [`timer`](`crate::timer`) -//! module's documentation. -//! //! TODO Update example //! ```rust,norun //! # use gba::save; @@ -49,11 +40,11 @@ //! //! ## Using save media //! -//! To access save media, use the [`SaveAccess::new`] method to create a new -//! [`SaveAccess`] object. Its methods are used to read or write save media. +//! To access save media, use the [`SaveData::new`] method to create a new +//! [`SaveData`] object. Its methods are used to read or write save media. //! -//! Reading data from the savegame is simple. Use [`read`](`SaveAccess::read`) -//! to copy data from an offset in the savegame into a buffer in memory. +//! Reading data from the savegame is simple. Use [`read`] to copy data from an +//! offset in the savegame into a buffer in memory. //! //! TODO Update example //! ```rust,norun @@ -64,8 +55,8 @@ //! ``` //! //! Writing to save media requires you to prepare the area for writing by calling -//! the [`prepare_write`](`SaveAccess::prepare_write`) method before doing the -//! actual write commands with the [`write`](`SaveAccess::write`) method. +//! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains +//! the actual [`write`] method. //! //! TODO Update example //! ```rust,norun @@ -84,48 +75,43 @@ //! //! Because writes can only be prepared on a per-sector basis, a clear on a range //! of `4000..5000` on a device with 4096 byte sectors will actually clear a range -//! of `0..8192`. Use [`sector_size`](`SaveAccess::sector_size`) to find the -//! sector size, or [`align_range`](`SaveAccess::align_range`) to directly -//! calculate the range of memory that will be affected by the clear. +//! of `0..8192`. Use [`sector_size`] to find the sector size, or [`align_range`] +//! to directly calculate the range of memory that will be affected by the clear. +//! +//! [`read`]: SaveData::read +//! [`prepare_write`]: SaveData::prepare_write +//! [`write`]: SavePreparedBlock::write +//! [`sector_size`]: SaveAccess::sector_size +//! [`align_range`]: SaveAccess::align_range //! //! ## Performance and Other Details //! -//! Because `prepare_write` does nothing on non-flash chips, it would not cause -//! correctness issues to ignore it. Even so, it is recommend to write code to -//! use the `prepare_write` function regardless of the save media, as it has -//! minimal runtime cost on other save media types. If needed, you can check if -//! `prepare_write` is required by calling the -//! (`requires_prepare_write`)(`SaveAccess::requires_prepare_write`) method. +//! The performance characteristics of the media types are as follows: //! -//! Some memory types have a `sector_size` above `1`, but do not use -//! `prepare_write`. This indicates that the media type has sectors that must -//! be rewritten all at once, instead of supporting the separate erase/write -//! cycles that flash media does. Writing non-sector aligned memory will be -//! slower on such save media, as the implementation needs to read the old -//! contents into a buffer before writing to avoid data loss. -//! -//! To summarize, for all supported media types: -//! -//! * SRAM does not require `prepare_write` and has no sectors to align to. Reads -//! and writes at any alignment are efficient. Furthermore, it does not require -//! a timer to be set with [`set_timer_for_timeout`]. -//! * Non-Atmel flash chips requires `prepare_write`, and have sectors of 4096 -//! bytes. Atmel flash chips instead do not require `prepare_write`, and instead -//! have sectors of 128 bytes. You should generally try to use `prepare_write` -//! regardless, and write in blocks of 128 bytes if at all possible. -//! * EEPROM does not require `prepare_write` and has sectors of 8 bytes. +//! * SRAM is simply a form of battery backed memory, and has no particular +//! performance characteristics. Reads and writes at any alignment are +//! efficient. Furthermore, no timer is needed for accesses to this type of +//! media. `prepare_write` does not immediately erase any data. +//! * Non-Atmel flash chips have a sector size of 4096 bytes. Reads and writes +//! to any alignment are efficient, however, `prepare_write` will erase all +//! data in an entire sector before writing. +//! * Atmel flash chips have a sector size of 128 bytes. Reads to any alignment +//! are efficient, however, unaligned writes are extremely slow. +//! `prepare_write` does not immediately erase any data. +//! * EEPROM has a sector size of 8 bytes. Unaligned reads and writes are +//! slower than aligned writes, however, this is easily mitigated by the +//! small sector size. -use core::cell::Cell; use core::ops::Range; -use bare_metal::Mutex; +use crate::sync::{Mutex, RawMutexGuard}; +use crate::timer::Timer; mod asm_utils; //mod setup; -//mod utils; +mod utils; //pub use asm_utils::*; //pub use setup::*; -//pub use utils::*; //pub mod eeprom; //pub mod flash; @@ -171,6 +157,7 @@ pub enum Error { /// Information about the save media used. #[derive(Clone, Debug)] +#[non_exhaustive] pub struct MediaInfo { /// The type of save media installed. pub media_type: MediaType, @@ -181,75 +168,49 @@ pub struct MediaInfo { pub sector_shift: usize, /// The size of the save media, in sectors. pub sector_count: usize, - /// Whether the save media type requires the use of the - /// [`prepare_write`](`SaveAccess::prepare_write`) function before a block of - /// memory can be overwritten. - pub requires_prepare_write: bool, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, } /// A trait allowing low-level saving and writing to save media. -/// -/// It exposes an interface mostly based around the requirements of reading and -/// writing flash memory, as those are the most restrictive. -/// -/// This interface treats memory as a continuous block of bytes for purposes of -/// reading, and as an array of sectors . -pub trait RawSaveAccess: Sync { - /// Returns information about the save media used. +trait RawSaveAccess: Sync { fn info(&self) -> Result<&'static MediaInfo, Error>; - - /// Reads a slice of memory from save media. - /// - /// This will attempt to fill `buffer` entirely, and will error if this is - /// not possible. The contents of `buffer` are unpredictable if an error is - /// returned. fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error>; - - /// Verifies that the save media has been successfully written, comparing - /// it against the given buffer. fn verify(&self, offset: usize, buffer: &[u8]) -> Result; - - /// Prepares a given span of sectors for writing. This may permanently erase - /// the current contents of the sector on some save media. fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>; - - /// Writes a buffer to the save media. - /// - /// The sectors you are writing to must be prepared with a call to the - /// `prepare_write` function beforehand, or else the contents of the save - /// media may be unpredictable after writing. fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>; } -/// Contains the current save media implementation. -static CURRENT_SAVE_ACCESS: Mutex>> = - Mutex::new(Cell::new(None)); +static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); -/// Sets the save media implementation in use. -pub fn set_save_implementation(access: Option<&'static dyn RawSaveAccess>) { - crate::interrupt::free(|c| { - CURRENT_SAVE_ACCESS.borrow(c).set(access) - }) +fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); } -/// Gets the save media implementation in use. -pub fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { - crate::interrupt::free(|c| { - CURRENT_SAVE_ACCESS.borrow(c).get() - }) +fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + *CURRENT_SAVE_ACCESS.lock() } /// Allows reading and writing of save media. #[derive(Copy, Clone)] -pub struct SaveAccess { +pub struct SaveData { + lock: RawMutexGuard<'static>, access: &'static dyn RawSaveAccess, info: &'static MediaInfo, + timeout: utils::Timeout, } -impl SaveAccess { +impl SaveData { /// Creates a new save accessor around the current save implementaiton. - pub fn new() -> Result { + fn new(timer: Option) -> Result { match get_save_implementation() { - Some(access) => Ok(SaveAccess { access, info: access.info()? }), + Some(access) => Ok(SaveData { + lock: utils::lock_media()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), None => Err(Error::NoMedia), } } @@ -275,21 +236,31 @@ impl SaveAccess { self.info.sector_count << self.info.sector_shift } + fn check_bounds(&self, range: Range) -> Result<(), Error> { + if range.start >= self.len() || range.end >= self.len() { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> { + self.check_bounds(offset..(offset + len)) + } + /// 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> { + self.check_bounds_len(offset, buffer.len())?; self.access.read(offset, buffer) } /// Verifies that a given block of memory matches the save media. pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + self.check_bounds_len(offset, buffer.len())?; self.access.verify(offset, buffer) } - /// Returns whether this save media requires the use of [`SaveAccess::prepare_write`]. - pub fn requires_prepare_write(&self) -> bool { - self.info.requires_prepare_write - } - /// Returns a range that contains all sectors the input range overlaps. /// /// This can be used to calculate which blocks would be erased by a call @@ -305,41 +276,69 @@ impl SaveAccess { /// This will erase any data in any sector overlapping the input range. To /// calculate which offset ranges would be affected, use the /// [`align_range`](`SaveAccess::align_range`) function. - pub fn prepare_write(&self, range: Range) -> Result<(), Error> { - if self.info.requires_prepare_write { - let range = self.align_range(range); + pub fn prepare_write(&mut self, range: Range) -> Result { + self.check_bounds(range.clone())?; + 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) - } else { - Ok(()) + self.access.prepare_write(range.start >> shift, range.len() >> shift)?; } + Ok(SavePreparedBlock { + parent: self, + range + }) } +} +/// A block of save memory that has been prepared for writing. +pub struct SavePreparedBlock<'a> { + parent: &'a mut SaveData, + range: Range, +} +impl<'a> SavePreparedBlock<'a> { /// Writes a given buffer into the save media. /// - /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns - /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the - /// range you intend to write for this to function correctly. The contents of - /// the save media are unpredictable if you do not. + /// Multiple overlapping writes to the same memory range without a separate + /// 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> { - self.access.write(offset, buffer) + 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) + } } /// Writes and validates a given buffer into the save media. /// - /// If [`requires_prepare_write`](`SaveAccess::requires_prepare_write`) returns - /// `true`, you must call [`prepare_write`](`SaveAccess::prepare_write`) on the - /// range you intend to write for this to function correctly. The contents of - /// the save media will be unpredictable if you do not. - /// /// This function will verify that the write has completed successfully, and /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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> { self.write(offset, buffer)?; - if !self.verify(offset, buffer)? { + if !self.parent.verify(offset, buffer)? { Err(Error::WriteError) } else { Ok(()) } } } + +/// Allows access to the cartridge's save data. +pub struct SaveManager; +impl SaveManager { + pub fn access() -> Result { + SaveData::new(None) + } + pub fn access_with_timer(timer: Timer) -> Result { + SaveData::new(Some(timer)) + } +} \ No newline at end of file diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs new file mode 100644 index 00000000..2ada1eeb --- /dev/null +++ b/agb/src/save/utils.rs @@ -0,0 +1,54 @@ +//! A package containing useful utilities for writing save accessors. + +use super::Error; +use crate::sync::{RawMutex, RawMutexGuard}; +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, +} +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); + } + } + + /// 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), + } +} From ec41db2fc925719ebe4abd1ffd3ed5463cf33737 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 00:42:34 -0700 Subject: [PATCH 03/11] Reintegrate SRAM save media reader. --- agb/src/lib.rs | 3 + agb/src/save/asm_utils.rs | 30 +-- agb/src/save/mod.rs | 408 +++++++++++++++++++++----------------- agb/src/save/sram.rs | 56 ++++++ 4 files changed, 302 insertions(+), 195 deletions(-) create mode 100644 agb/src/save/sram.rs diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 59174b71..764cdbfa 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -224,6 +224,8 @@ pub struct Gba { pub sound: sound::dmg::Sound, /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. pub mixer: sound::mixer::MixerController, + /// Manages access to the Game Boy Advance cartridge's save chip. + pub save: save::SaveManager, /// Manages access to the Game Boy Advance's 4 timers. pub timers: timer::TimerController, } @@ -240,6 +242,7 @@ impl Gba { display: display::Display::new(), sound: sound::dmg::Sound::new(), mixer: sound::mixer::MixerController::new(), + save: save::SaveManager::new(), timers: timer::TimerController::new(), } } diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index f9ca1db0..030fe0df 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -3,9 +3,9 @@ //! performed via code in WRAM and cannot be accessed by DMA. extern "C" { - fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); - fn WramReadByte(src: *const u8) -> u8; - fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; + fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn WramReadByte(src: *const u8) -> u8; + fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; } /// Copies data from a given memory address into a buffer. @@ -17,9 +17,9 @@ extern "C" { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { - if dst.len() != 0 { - WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); - } + if dst.len() != 0 { + WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + } } /// Copies data from a buffer into a given memory address. @@ -30,9 +30,9 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { - if src.len() != 0 { - WramXferBuf(src.as_ptr(), dst as _, src.len()); - } + if src.len() != 0 { + WramXferBuf(src.as_ptr(), dst as _, src.len()); + } } /// Verifies that the data in a buffer matches that in a given memory address. @@ -44,11 +44,11 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { - if buf1.len() != 0 { - WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) - } else { - true - } + if buf1.len() != 0 { + WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + } else { + true + } } /// Reads a byte from a given memory address. @@ -59,5 +59,5 @@ pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn read_raw_byte(src: usize) -> u8 { - WramReadByte(src as _) + WramReadByte(src as _) } diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index bcc0e6c0..eef39947 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -107,238 +107,286 @@ use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; -//mod setup; -mod utils; - -//pub use asm_utils::*; -//pub use setup::*; - //pub mod eeprom; //pub mod flash; -//pub mod sram; +mod sram; +mod utils; /// A list of save media types. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[non_exhaustive] pub enum MediaType { - /// 32KiB Battery-Backed SRAM or FRAM - Sram32K, - /// 8KiB EEPROM - Eeprom8K, - /// 512B EEPROM - Eeprom512B, - /// 64KiB flash chip - Flash64K, - /// 128KiB flash chip - Flash128K, - /// A user-defined save media type - Custom, + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, } /// The type used for errors encountered while reading or writing save media. #[derive(Clone, Debug)] #[non_exhaustive] pub enum Error { - /// There is no save media attached to this game cart. - NoMedia, - /// Failed to write the data to save media. - WriteError, - /// An operation on save media timed out. - OperationTimedOut, - /// An attempt was made to access save media at an invalid offset. - OutOfBounds, - /// The media is already in use. - /// - /// This can generally only happen in an IRQ that happens during an ongoing - /// save media operation. - MediaInUse, - /// This command cannot be used with the save media in use. - IncompatibleCommand, + /// There is no save media attached to this game cart. + NoMedia, + /// Failed to write the data to save media. + WriteError, + /// An operation on save media timed out. + OperationTimedOut, + /// An attempt was made to access save media at an invalid offset. + OutOfBounds, + /// The media is already in use. + /// + /// This can generally only happen in an IRQ that happens during an ongoing + /// save media operation. + MediaInUse, + /// This command cannot be used with the save media in use. + IncompatibleCommand, } /// Information about the save media used. #[derive(Clone, Debug)] #[non_exhaustive] pub struct MediaInfo { - /// The type of save media installed. - pub media_type: MediaType, - /// The power-of-two size of each sector. Zero represents a sector size of - /// 0, implying sectors are not in use. - /// - /// (For example, 512 byte sectors would return 9 here.) - pub sector_shift: usize, - /// The size of the save media, in sectors. - pub sector_count: usize, - /// Whether the save media type requires media be prepared before writing. - pub uses_prepare_write: bool, + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, } /// 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 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>; } static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { - let mut access = CURRENT_SAVE_ACCESS.lock(); - assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); - *access = Some(access_impl); + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); } fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { - *CURRENT_SAVE_ACCESS.lock() + *CURRENT_SAVE_ACCESS.lock() } /// Allows reading and writing of save media. -#[derive(Copy, Clone)] pub struct SaveData { - lock: RawMutexGuard<'static>, - access: &'static dyn RawSaveAccess, - info: &'static MediaInfo, - timeout: utils::Timeout, + lock: RawMutexGuard<'static>, + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, + timeout: utils::Timeout, } impl SaveData { - /// Creates a new save accessor around the current save implementaiton. - fn new(timer: Option) -> Result { - match get_save_implementation() { - Some(access) => Ok(SaveData { - lock: utils::lock_media()?, - access, - info: access.info()?, - timeout: utils::Timeout::new(timer), - }), - None => Err(Error::NoMedia), + /// Creates a new save accessor around the current save implementaiton. + fn new(timer: Option) -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveData { + lock: utils::lock_media()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), + None => Err(Error::NoMedia), + } } - } - /// Returns the media info underlying this accessor. - pub fn media_info(&self) -> &'static MediaInfo { - self.info - } - - /// Returns the save media type being used. - pub fn media_type(&self) -> MediaType { - self.info.media_type - } - - /// Returns the sector size of the save media. It is generally optimal to - /// write data in blocks that are aligned to the sector size. - pub fn sector_size(&self) -> usize { - 1 << self.info.sector_shift - } - - /// Returns the total length of this save media. - pub fn len(&self) -> usize { - self.info.sector_count << self.info.sector_shift - } - - fn check_bounds(&self, range: Range) -> Result<(), Error> { - if range.start >= self.len() || range.end >= self.len() { - Err(Error::OutOfBounds) - } else { - Ok(()) + /// Returns the media info underlying this accessor. + pub fn media_info(&self) -> &'static MediaInfo { + self.info } - } - fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> { - self.check_bounds(offset..(offset + len)) - } - /// 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> { - self.check_bounds_len(offset, buffer.len())?; - self.access.read(offset, buffer) - } - - /// Verifies that a given block of memory matches the save media. - pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { - self.check_bounds_len(offset, buffer.len())?; - self.access.verify(offset, buffer) - } - - /// Returns a range that contains all sectors the input range overlaps. - /// - /// This can be used to calculate which blocks would be erased by a call - /// to [`prepare_write`](`SaveAccess::prepare_write`) - pub fn align_range(&self, range: Range) -> Range { - let shift = self.info.sector_shift; - let mask = (1 << shift) - 1; - (range.start & !mask)..((range.end + mask) & !mask) - } - - /// Prepares a given span of offsets for writing. - /// - /// This will erase any data in any sector overlapping the input range. To - /// calculate which offset ranges would be affected, use the - /// [`align_range`](`SaveAccess::align_range`) function. - pub fn prepare_write(&mut self, range: Range) -> Result { - self.check_bounds(range.clone())?; - 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)?; + /// Returns the save media type being used. + pub fn media_type(&self) -> MediaType { + self.info.media_type + } + + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + pub fn sector_size(&self) -> usize { + 1 << self.info.sector_shift + } + + /// Returns the total length of this save media. + pub fn len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + fn check_bounds(&self, range: Range) -> Result<(), Error> { + if range.start >= self.len() || range.end >= self.len() { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> { + self.check_bounds(offset..(offset + len)) + } + + /// 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> { + self.check_bounds_len(offset, buffer.len())?; + self.access.read(offset, buffer) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + self.check_bounds_len(offset, buffer.len())?; + self.access.verify(offset, buffer) + } + + /// Returns a range that contains all sectors the input range overlaps. + /// + /// This can be used to calculate which blocks would be erased by a call + /// to [`prepare_write`](`SaveAccess::prepare_write`) + pub fn align_range(&self, range: Range) -> Range { + let shift = self.info.sector_shift; + let mask = (1 << shift) - 1; + (range.start & !mask)..((range.end + mask) & !mask) + } + + /// Prepares a given span of offsets for writing. + /// + /// This will erase any data in any sector overlapping the input range. To + /// calculate which offset ranges would be affected, use the + /// [`align_range`](`SaveAccess::align_range`) function. + pub fn prepare_write(&mut self, range: Range) -> Result { + self.check_bounds(range.clone())?; + 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)?; + } + Ok(SavePreparedBlock { + parent: self, + range + }) } - Ok(SavePreparedBlock { - parent: self, - range - }) - } } /// A block of save memory that has been prepared for writing. pub struct SavePreparedBlock<'a> { - parent: &'a mut SaveData, - range: Range, + parent: &'a mut SaveData, + range: Range, } impl<'a> SavePreparedBlock<'a> { - /// Writes a given buffer into the save media. - /// - /// Multiple overlapping writes to the same memory range without a separate - /// 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> { - 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) + /// Writes a given buffer into the save media. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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> { + 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) + } } - } - /// Writes and validates a given buffer into the save media. - /// - /// This function will verify that the write has completed successfully, and - /// return an error if it has not done so. - /// - /// Multiple overlapping writes to the same memory range without a separate - /// 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> { - self.write(offset, buffer)?; - if !self.parent.verify(offset, buffer)? { - Err(Error::WriteError) - } else { - Ok(()) + /// Writes and validates a given buffer into the save media. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// 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> { + self.write(offset, buffer)?; + if !self.parent.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} + +mod marker { + #[repr(align(4))] + struct Align(T); + + static EEPROM: Align<[u8; 12]> = Align(*b"EEPROM_Vnnn\0"); + static SRAM: Align<[u8; 12]> = Align(*b"SRAM_Vnnn\0\0\0"); + static FLASH512K: Align<[u8; 16]> = Align(*b"FLASH512_Vnnn\0\0\0"); + static FLASH1M: Align<[u8; 16]> = Align(*b"FLASH1M_Vnnn\0\0\0\0"); + + #[inline(always)] + pub fn emit_eeprom_marker() { + crate::sync::memory_read_hint(&EEPROM); + } + #[inline(always)] + pub fn emit_sram_marker() { + crate::sync::memory_read_hint(&SRAM); + } + #[inline(always)] + pub fn emit_flash_512k_marker() { + crate::sync::memory_read_hint(&FLASH512K); + } + #[inline(always)] + pub fn emit_flash_1m_marker() { + crate::sync::memory_read_hint(&FLASH1M); } - } } /// Allows access to the cartridge's save data. -pub struct SaveManager; +#[non_exhaustive] +pub struct SaveManager {} impl SaveManager { - pub fn access() -> Result { - SaveData::new(None) - } - pub fn access_with_timer(timer: Timer) -> Result { - SaveData::new(Some(timer)) - } + pub(crate) const fn new() -> Self { + SaveManager {} + } + + /// Declares that the ROM uses battery backed SRAM/FRAM. + /// + /// Battery Backed SRAM is generally very fast, but limited in size compared + /// to flash chips. + /// + /// 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_sram() { + marker::emit_sram_marker(); + set_save_implementation(&sram::BatteryBackedAccess); + } + + /// Creates a new accessor to the save data. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access() -> Result { + SaveData::new(None) + } + /// Creates a new accessor to the save data that uses the given timer for timeouts. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access_with_timer(timer: Timer) -> Result { + SaveData::new(Some(timer)) + } } \ No newline at end of file diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs new file mode 100644 index 00000000..b4e21eca --- /dev/null +++ b/agb/src/save/sram.rs @@ -0,0 +1,56 @@ +//! Module for battery backed SRAM save media support. +//! +//! SRAM acts as ordinary memory mapped into the memory space, and as such +//! is accessed using normal memory read/write commands. + +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; + +const SRAM_SIZE: usize = 32 * 1024; // 32 KiB + +/// Checks whether an offset is contained within the bounds of the SRAM. +fn check_bounds(offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() || offset + len > SRAM_SIZE { + return Err(Error::OutOfBounds); + } + Ok(()) +} + +/// The [`RawSaveAccess`] used for battery backed SRAM. +pub struct BatteryBackedAccess; +impl RawSaveAccess for BatteryBackedAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Sram32K, + sector_shift: 0, + sector_count: SRAM_SIZE, + uses_prepare_write: false, + }) + } + + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + read_raw_buf(buffer, 0x0E000000 + offset); + } + Ok(()) + } + + fn verify(&self, offset: usize, buffer: &[u8]) -> 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> { + Ok(()) + } + + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + write_raw_buf(0x0E000000 + offset, buffer); + } + Ok(()) + } +} From 2be44c12e5ec5f560d32e29bf035483c0a4fb149 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 01:04:07 -0700 Subject: [PATCH 04/11] Improve codestyle in asm_routines.s, remove unsafe save implementation in hyperspace-roll. --- agb/src/asm_include.s | 16 +++++++ agb/src/save/asm_routines.s | 64 ++++++---------------------- agb/src/save/asm_utils.rs | 14 +++--- agb/src/save/mod.rs | 7 +-- examples/hyperspace-roll/src/main.rs | 7 +-- examples/hyperspace-roll/src/save.rs | 56 ++++++++++++------------ 6 files changed, 70 insertions(+), 94 deletions(-) diff --git a/agb/src/asm_include.s b/agb/src/asm_include.s index 5508e4fe..1682792c 100644 --- a/agb/src/asm_include.s +++ b/agb/src/asm_include.s @@ -13,3 +13,19 @@ .size \functionName,.-\functionName .endfunc .endm + +.macro agb_thumb_func functionName:req +.section .iwram.\functionName, "ax", %progbits +.thumb +.align 2 +.global \functionName +.type \functionName, %function +.func \functionName +\functionName: +.endm + +.macro agb_thumb_end functionName:req +.pool +.size \functionName,.-\functionName +.endfunc +.endm diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s index 41e8df74..b04d407b 100644 --- a/agb/src/save/asm_routines.s +++ b/agb/src/save/asm_routines.s @@ -1,89 +1,49 @@ +.include "src/asm_include.s" + @ @ char WramReadByte(const char* offset); @ @ A routine that reads a byte from a given memory offset. @ - .thumb - .global WramReadByte - .thumb_func - .align 2 -WramReadByte: - ldr r1, =WramReadByteInner - bx r1 - - .section .data - - .thumb - .thumb_func - .align 2 -WramReadByteInner: +agb_thumb_func agb_rs__WramReadByte ldrb r0, [r0] mov pc, lr - - .section .text +agb_thumb_end agb_rs__WramReadByte @ @ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); @ @ A routine that compares two memory offsets. @ - .thumb - .global WramVerifyBuf - .thumb_func - .align 2 -WramVerifyBuf: +agb_thumb_func agb_rs__WramVerifyBuf push {r4-r5, lr} movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result movs r0, #0 @ set up r0 so the default return result is false - ldr r4, =WramVerifyBufInner - bx r4 @ jump to the part in WRAM - .section .data - - .thumb - .thumb_func - .align 2 -WramVerifyBufInner: @ At this point, buf1 is actually in r5, so r0 can be used as a status return - ldrb r3, [r5,r2] +1: ldrb r3, [r5,r2] ldrb r4, [r1,r2] cmp r3, r4 bne 0f sub r2, #1 - bpl WramVerifyBufInner + bpl 1b @ Returns from the function successfully movs r0, #1 0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point pop {r4-r5, pc} +agb_thumb_end agb_rs__WramVerifyBuf - .section .text @ @ void WramXferBuf(const char* source, char* dest, int count); @ @ A routine that copies one buffer into another. @ - .thumb - .global WramXferBuf - .thumb_func - .align 2 -WramXferBuf: - ldr r3, =WramXferBufInner - bx r3 - - .pool - .section .data - - .thumb - .thumb_func - .align 2 -WramXferBufInner: - sub r2, #1 +agb_thumb_func agb_rs__WramXferBuf +0: sub r2, #1 ldrb r3, [r0,r2] strb r3, [r1,r2] - bne WramXferBufInner + bne 0b mov pc, lr - - .pool - .section .text +agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index 030fe0df..930995ad 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -3,9 +3,9 @@ //! performed via code in WRAM and cannot be accessed by DMA. extern "C" { - fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize); - fn WramReadByte(src: *const u8) -> u8; - fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; + fn agb_rs__WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn agb_rs__WramReadByte(src: *const u8) -> u8; + fn agb_rs__WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; } /// Copies data from a given memory address into a buffer. @@ -18,7 +18,7 @@ extern "C" { #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { if dst.len() != 0 { - WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + agb_rs__WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); } } @@ -31,7 +31,7 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { if src.len() != 0 { - WramXferBuf(src.as_ptr(), dst as _, src.len()); + agb_rs__WramXferBuf(src.as_ptr(), dst as _, src.len()); } } @@ -45,7 +45,7 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { if buf1.len() != 0 { - WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + agb_rs__WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) } else { true } @@ -59,5 +59,5 @@ pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn read_raw_byte(src: usize) -> u8 { - WramReadByte(src as _) + agb_rs__WramReadByte(src as _) } diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index eef39947..af9d7927 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -370,7 +370,7 @@ impl SaveManager { /// given save type. /// /// Only one `init_*` function may be called in the lifetime of the program. - pub fn init_sram() { + pub fn init_sram(&mut self) { marker::emit_sram_marker(); set_save_implementation(&sram::BatteryBackedAccess); } @@ -379,14 +379,15 @@ impl SaveManager { /// /// You must have initialized the save manager beforehand to use a specific /// type of media before calling this method. - pub fn access() -> Result { + pub fn access(&mut self) -> Result { SaveData::new(None) } + /// Creates a new accessor to the save data that uses the given timer for timeouts. /// /// You must have initialized the save manager beforehand to use a specific /// type of media before calling this method. - pub fn access_with_timer(timer: Timer) -> Result { + pub fn access_with_timer(&mut self, timer: Timer) -> Result { SaveData::new(Some(timer)) } } \ No newline at end of file diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs index 9acf3c04..9ec4cc1a 100644 --- a/examples/hyperspace-roll/src/main.rs +++ b/examples/hyperspace-roll/src/main.rs @@ -96,10 +96,10 @@ struct Agb<'a> { } fn main(mut gba: agb::Gba) -> ! { - save::init_save(); + save::init_save(&mut gba).expect("Could not initialize save game"); if save::load_high_score() > 1000 { - save::save_high_score(0); + save::save_high_score(&mut gba, 0).expect("Could not reset high score"); } let gfx = gba.display.object.get(); @@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! { agb.obj.commit(); agb.sfx.customise(); if save::load_high_score() < current_level { - save::save_high_score(current_level); + save::save_high_score(&mut gba, current_level) + .expect("Could not save high score"); } break; } diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs index e5df03e8..0db839de 100644 --- a/examples/hyperspace-roll/src/save.rs +++ b/examples/hyperspace-roll/src/save.rs @@ -1,44 +1,42 @@ -use agb::interrupt::free; -use bare_metal::Mutex; -use core::cell::RefCell; +use agb::Gba; +use agb::save::Error; +use agb::sync::Static; -const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; -const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1); +static HIGHSCORE: Static = Static::new(0); -static HIGHSCORE: Mutex> = Mutex::new(RefCell::new(0)); +pub fn init_save(gba: &mut Gba) -> Result<(), Error> { + gba.save.init_sram(); -pub fn init_save() { - if (unsafe { RAM_ADDRESS.read_volatile() } == !0) { - save_high_score(0); - unsafe { RAM_ADDRESS.write_volatile(0) }; - } + let mut access = gba.save.access()?; - let mut a = [0; 4]; - for (idx, a) in a.iter_mut().enumerate() { - *a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() }; - } + let mut buffer = [0; 1]; + access.read(0, &mut buffer)?; - let high_score = u32::from_le_bytes(a); + if buffer[0] != 0 { + access.prepare_write(0..1)?.write(0, &[0])?; + core::mem::drop(access); + save_high_score(gba, 0)?; + } else { + let mut buffer = [0; 4]; + access.read(1, &mut buffer)?; + let high_score = u32::from_le_bytes(buffer); - free(|cs| { if high_score > 100 { - HIGHSCORE.borrow(cs).replace(0); + HIGHSCORE.write(0) } else { - HIGHSCORE.borrow(cs).replace(high_score); + HIGHSCORE.write(high_score) } - }); + } + + Ok(()) } pub fn load_high_score() -> u32 { - free(|cs| *HIGHSCORE.borrow(cs).borrow()) + HIGHSCORE.read() } -pub fn save_high_score(score: u32) { - let a = score.to_le_bytes(); - - for (idx, &a) in a.iter().enumerate() { - unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) }; - } - - free(|cs| HIGHSCORE.borrow(cs).replace(score)); +pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> { + gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?; + HIGHSCORE.write(score); + Ok(()) } From 4397bb0d6649c662ab2435cf4241dfd872b218b7 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:10 -0700 Subject: [PATCH 05/11] Add tests for cartridge save access. --- agb-tests/.cargo/config.toml | 14 ++++ agb-tests/Cargo.toml | 19 +++++ agb-tests/rust-toolchain.toml | 3 + agb-tests/tests/save_test_common/mod.rs | 105 ++++++++++++++++++++++++ agb-tests/tests/test_save_sram.rs | 16 ++++ agb/src/save/mod.rs | 18 +++- agb/src/sync/statics.rs | 2 +- justfile | 3 + 8 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 agb-tests/.cargo/config.toml create mode 100644 agb-tests/Cargo.toml create mode 100644 agb-tests/rust-toolchain.toml create mode 100644 agb-tests/tests/save_test_common/mod.rs create mode 100644 agb-tests/tests/test_save_sram.rs diff --git a/agb-tests/.cargo/config.toml b/agb-tests/.cargo/config.toml new file mode 100644 index 00000000..d5f7f86c --- /dev/null +++ b/agb-tests/.cargo/config.toml @@ -0,0 +1,14 @@ +[unstable] +build-std = ["core", "alloc"] +build-std-features = ["compiler-builtins-mem"] + +[build] +target = "thumbv4t-none-eabi" + +[target.thumbv4t-none-eabi] +rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-test-runner" + +[target.armv4t-none-eabi] +rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] +runner = "mgba-test-runner" diff --git a/agb-tests/Cargo.toml b/agb-tests/Cargo.toml new file mode 100644 index 00000000..9917f81b --- /dev/null +++ b/agb-tests/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "agb-tests" +version = "0.1.0" +edition = "2018" + +[profile.dev] +opt-level = 3 +debug = true + +[profile.release] +lto = true +debug = true + +[dependencies] +agb = { version = "*", path = "../agb", features = ["testing"] } + +[package.metadata.docs.rs] +default-target = "thumbv6m-none-eabi" +targets = [] diff --git a/agb-tests/rust-toolchain.toml b/agb-tests/rust-toolchain.toml new file mode 100644 index 00000000..06842486 --- /dev/null +++ b/agb-tests/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb-tests/tests/save_test_common/mod.rs new file mode 100644 index 00000000..9e494424 --- /dev/null +++ b/agb-tests/tests/save_test_common/mod.rs @@ -0,0 +1,105 @@ +use core::cmp; +use agb::save::{Error, MediaInfo}; +use agb::sync::InitOnce; + +fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo { + static ONCE: InitOnce = InitOnce::new(); + ONCE.get(|| { + crate::save_setup(gba); + gba.save.access().unwrap().media_info().clone() + }) +} + +#[derive(Clone)] +struct Rng(u32); +impl Rng { + fn iter(&mut self) { + self.0 = self.0.wrapping_mul(2891336453).wrapping_add(100001); + } + fn next_u8(&mut self) -> u8 { + self.iter(); + (self.0 >> 22) as u8 ^ self.0 as u8 + } + fn next_under(&mut self, under: u32) -> u32 { + self.iter(); + let scale = 31 - under.leading_zeros(); + ((self.0 >> scale) ^ self.0) % under + } +} + +const MAX_BLOCK_SIZE: usize = 4 * 1024; + +#[allow(clippy::needless_range_loop)] +fn do_test( + gba: &mut agb::Gba, seed: Rng, offset: usize, len: usize, block_size: usize, +) -> Result<(), Error> { + let mut buffer = [0; MAX_BLOCK_SIZE]; + + let timers = gba.timers.timers(); + 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 rng = seed.clone(); + let mut current = offset; + let end = offset + len; + while current != end { + let cur_len = cmp::min(end - current, block_size); + for i in 0..cur_len { + buffer[i] = rng.next_u8(); + } + prepared.write(current, &buffer[..cur_len])?; + current += cur_len; + } + + // validates the save media + rng = seed; + current = offset; + while current != end { + let cur_len = cmp::min(end - current, block_size); + access.read(current, &mut buffer[..cur_len])?; + for i in 0..cur_len { + let cur_byte = rng.next_u8(); + assert_eq!( + buffer[i], cur_byte, + "Read does not match earlier write: {} != {} @ 0x{:05x}", + buffer[i], cur_byte, current + i, + ); + } + current += cur_len; + } + + Ok(()) +} + +#[test_case] +fn test_4k_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + + if info.len() >= (1 << 12) { + do_test(gba, Rng(2000), 0, info.len(), 4 * 1024).expect("Test encountered error"); + } +} + +#[test_case] +fn test_512b_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + do_test(gba, Rng(1000), 0, info.len(), 512).expect("Test encountered error"); +} + +#[test_case] +fn test_partial_writes(gba: &mut agb::Gba) { + let info = init_sram(gba); + + // test with random segments now. + let mut rng = Rng(12345); + for i in 0..8 { + let rand_length = rng.next_under((info.len() >> 1) as u32) as usize + 50; + let rand_offset = rng.next_under(info.len() as u32 - rand_length as u32) as usize; + let block_size = cmp::min(rand_length >> 2, MAX_BLOCK_SIZE - 100); + let block_size = rng.next_under(block_size as u32) as usize + 50; + + do_test(gba, Rng(i * 10000), rand_offset, rand_length, block_size) + .expect("Test encountered error"); + } +} \ No newline at end of file diff --git a/agb-tests/tests/test_save_sram.rs b/agb-tests/tests/test_save_sram.rs new file mode 100644 index 00000000..f3348eda --- /dev/null +++ b/agb-tests/tests/test_save_sram.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_sram(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index af9d7927..7de71242 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -165,6 +165,18 @@ pub struct MediaInfo { /// Whether the save media type requires media be prepared before writing. pub uses_prepare_write: bool, } +impl MediaInfo { + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + pub fn sector_size(&self) -> usize { + 1 << self.sector_shift + } + + /// Returns the total length of this save media. + pub fn len(&self) -> usize { + self.sector_count << self.sector_shift + } +} /// A trait allowing low-level saving and writing to save media. trait RawSaveAccess: Sync { @@ -221,16 +233,16 @@ impl SaveData { /// Returns the sector size of the save media. It is generally optimal to /// write data in blocks that are aligned to the sector size. pub fn sector_size(&self) -> usize { - 1 << self.info.sector_shift + self.info.sector_size() } /// Returns the total length of this save media. pub fn len(&self) -> usize { - self.info.sector_count << self.info.sector_shift + self.info.len() } fn check_bounds(&self, range: Range) -> Result<(), Error> { - if range.start >= self.len() || range.end >= self.len() { + if range.start >= self.len() || range.end > self.len() { Err(Error::OutOfBounds) } else { Ok(()) diff --git a/agb/src/sync/statics.rs b/agb/src/sync/statics.rs index 3970589f..62ec9d1d 100644 --- a/agb/src/sync/statics.rs +++ b/agb/src/sync/statics.rs @@ -284,7 +284,7 @@ mod test { // the actual main test loop let mut interrupt_seen = false; let mut no_interrupt_seen = false; - for i in 0..100000 { + for i in 0..250000 { // write to the static let new_value = [i; COUNT]; value.write(new_value); diff --git a/justfile b/justfile index 23837d70..ba8f243c 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ clippy: test: just _test-debug agb + just _test-debug agb-tests just _test-debug agb-fixnum just _test-debug-arm agb just _test-debug tools @@ -20,6 +21,8 @@ test: test-release: just _test-release agb just _test-release-arm agb + just _test-release agb-tests + just _test-release-arm agb-tests doctest-agb: (cd agb && cargo test --doc -Z doctest-xcompile) From 8dd0f4768ab379b88debb6c2b18f19255e6b1078 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 04:27:42 -0700 Subject: [PATCH 06/11] 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), + } } From 821098dd268cb40ebbb7cc174baf0bdef909c5f6 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Wed, 17 Aug 2022 04:52:41 -0700 Subject: [PATCH 07/11] Implement EEPROM save media. Fix EEPROM implementation. --- agb-tests/tests/test_save_eeprom_512b.rs | 16 ++ agb-tests/tests/test_save_eeprom_8k.rs | 16 ++ agb/src/dma.rs | 26 +++ agb/src/save/eeprom.rs | 271 +++++++++++++++++++++++ agb/src/save/mod.rs | 32 ++- agb/src/save/utils.rs | 7 + 6 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 agb-tests/tests/test_save_eeprom_512b.rs create mode 100644 agb-tests/tests/test_save_eeprom_8k.rs create mode 100644 agb/src/save/eeprom.rs diff --git a/agb-tests/tests/test_save_eeprom_512b.rs b/agb-tests/tests/test_save_eeprom_512b.rs new file mode 100644 index 00000000..19c3f6c9 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_512b.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_eeprom_512b(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-tests/tests/test_save_eeprom_8k.rs b/agb-tests/tests/test_save_eeprom_8k.rs new file mode 100644 index 00000000..95677321 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_8k.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_eeprom_8k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 71651cfc..1bf1b0bd 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) { DMA3_CONTROL.set(count as u32 | (1 << 31)); } + +pub(crate) fn dma3_exclusive(f: impl FnOnce() -> R) -> R { + const DMA0_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) }; + const DMA1_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; + const DMA2_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; + + crate::interrupt::free(|_| { + let dma0_ctl = DMA0_CTRL_HI.get(); + let dma1_ctl = DMA1_CTRL_HI.get(); + let dma2_ctl = DMA2_CTRL_HI.get(); + DMA0_CTRL_HI.set(dma0_ctl & !(1 << 15)); + DMA1_CTRL_HI.set(dma1_ctl & !(1 << 15)); + DMA2_CTRL_HI.set(dma2_ctl & !(1 << 15)); + + // Executes the body of the function with DMAs and IRQs disabled. + let ret = f(); + + // Continues higher priority DMAs if they were enabled before. + DMA0_CTRL_HI.set(dma0_ctl); + DMA1_CTRL_HI.set(dma1_ctl); + DMA2_CTRL_HI.set(dma2_ctl); + + // returns the return value + ret + }) +} \ No newline at end of file diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs new file mode 100644 index 00000000..51753d50 --- /dev/null +++ b/agb/src/save/eeprom.rs @@ -0,0 +1,271 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use crate::memory_mapped::MemoryMapped; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::utils::Timeout; +use core::cmp; + +const PORT: MemoryMapped = unsafe { MemoryMapped::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + crate::dma::dma_copy16(source.as_ptr() as *mut u16, 0x0DFFFF00 as *mut u16, ct); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + crate::dma::dma_copy16(0x0DFFFF00 as *mut u16, source.as_ptr() as *mut u16, ct); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + }); +} + +/// Union type to help build/receive commands. +struct BufferData { + idx: usize, + data: BufferContents, +} +#[repr(align(4))] +union BufferContents { + uninit: (), + bits: [u16; 82], + words: [u32; 41], +} +impl BufferData { + fn new() -> Self { + BufferData { idx: 0, data: BufferContents { uninit: () } } + } + + /// Writes a bit to the output buffer. + fn write_bit(&mut self, val: u8) { + unsafe { + self.data.bits[self.idx] = val as u16; + self.idx += 1; + } + } + + /// Writes a number to the output buffer + fn write_num(&mut self, count: usize, num: u32) { + for i in 0..count { + self.write_bit(((num >> (count - 1 - i)) & 1) as u8); + } + } + + /// Reads a number from the input buffer. + fn read_num(&mut self, off: usize, count: usize) -> u32 { + let mut accum = 0; + unsafe { + for i in 0..count { + accum <<= 1; + accum |= self.data.bits[off + i] as u32; + } + } + accum + } + + /// Receives a number of words into the input buffer. + fn receive(&mut self, count: usize) { + unsafe { + dma_receive(&mut self.data.words, count); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx); + } + } +} + +/// The properties of a given EEPROM type. +struct EepromProperties { + addr_bits: usize, + byte_len: usize, +} +impl EepromProperties { + /// Reads a block from the save media. + fn read_sector(&self, word: usize) -> [u8; 8] { + // Set address command. The command is two one bits, followed by the + // address, followed by a zero bit. + // + // 512B Command: [1 1|n n n n n n|0] + // 8KiB Command: [1 1|n n n n n n n n n n n n n n|0] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(1); + buf.write_num(self.addr_bits, word as u32); + buf.write_bit(0); + buf.submit(); + + // Receive the buffer data. The EEPROM sends 3 irrelevant bits followed + // by 64 data bits. + buf.receive(68); + let mut out = [0; 8]; + for i in 0..8 { + out[i] = buf.read_num(4 + i * 8, 8) as u8; + } + out + } + + /// Writes a sector directly. + fn write_sector_raw( + &self, word: usize, block: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + // Write sector command. The command is a one bit, followed by a + // zero bit, followed by the address, followed by 64 bits of data. + // + // 512B Command: [1 0|n n n n n n|v v v v ...] + // 8KiB Command: [1 0|n n n n n n n n n n n n n n|v v v v ...] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(0); + buf.write_num(self.addr_bits, word as u32); + for i in 0..8 { + buf.write_num(8, block[i] as u32); + } + buf.write_bit(0); + buf.submit(); + + // Wait for the sector to be written for 10 milliseconds. + timeout.start(); + while PORT.get() & 1 != 1 { + if timeout.check_timeout_met(10) { + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Writes a sector to the EEPROM, keeping any current contents outside the + /// buffer's range. + fn write_sector_safe( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf, timeout) + } + + /// Writes a sector to the EEPROM. + fn write_sector( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data, timeout) + } else { + self.write_sector_safe(word, data, start, timeout) + } + } + + /// Checks whether an offset is in range. + fn check_offset(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() && (offset + len) > self.byte_len { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + + /// Implements EEPROM reads. + fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + let sector = self.read_sector(offset >> SECTOR_SHIFT); + buf[..end_len].copy_from_slice(§or[start..start + end_len]); + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Implements EEPROM verifies. + fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + if &buf[..end_len] != &self.read_sector(offset >> SECTOR_SHIFT) { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Implements EEPROM writes. + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } +} +const PROPS_512B: EepromProperties = EepromProperties { addr_bits: 6, byte_len: 512 }; +const PROPS_8K: EepromProperties = EepromProperties { addr_bits: 14, byte_len: 8 * 1024 }; + +/// The [`RawSaveAccess`] used for 512 byte EEPROM. +pub struct Eeprom512B; +impl RawSaveAccess for Eeprom512B { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom512B, + sector_shift: 3, + sector_count: 64, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_512B.write(offset, buffer, timeout) + } +} + +/// The [`RawSaveAccess`] used for 8 KiB EEPROM. +pub struct Eeprom8K; +impl RawSaveAccess for Eeprom8K { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom8K, + sector_shift: 3, + sector_count: 1024, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_8K.write(offset, buffer, timeout) + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 34195676..8639666d 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -108,7 +108,7 @@ use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; -//pub mod eeprom; +mod eeprom; mod flash; mod sram; mod utils; @@ -420,6 +420,36 @@ impl SaveManager { set_save_implementation(&flash::FlashAccess); } + /// Declares that the ROM uses 512 bytes EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// 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_eeprom_512b(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom512B); + } + + /// Declares that the ROM uses 8 KiB EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// 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_eeprom_8k(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom8K); + } + /// 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/utils.rs b/agb/src/save/utils.rs index a19879c2..b94cb3fe 100644 --- a/agb/src/save/utils.rs +++ b/agb/src/save/utils.rs @@ -42,6 +42,13 @@ impl Timeout { } } } +impl Drop for Timeout { + fn drop(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + } + } +} pub fn lock_media_access() -> Result, Error> { static LOCK: RawMutex = RawMutex::new(); From 8f3e438070fd981787c41e9ee0c70cb69f758332 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:31 -0700 Subject: [PATCH 08/11] Final round of bugfixes, documentation fixes and clippy warning fixes. --- agb/src/save/asm_routines.s | 4 +-- agb/src/save/asm_utils.rs | 6 ++--- agb/src/save/eeprom.rs | 10 +++++--- agb/src/save/flash.rs | 12 +++++---- agb/src/save/mod.rs | 51 +++++++++++++++---------------------- 5 files changed, 38 insertions(+), 45 deletions(-) diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s index b04d407b..f257a524 100644 --- a/agb/src/save/asm_routines.s +++ b/agb/src/save/asm_routines.s @@ -7,7 +7,7 @@ @ agb_thumb_func agb_rs__WramReadByte ldrb r0, [r0] - mov pc, lr + bx lr agb_thumb_end agb_rs__WramReadByte @ @@ -45,5 +45,5 @@ agb_thumb_func agb_rs__WramXferBuf ldrb r3, [r0,r2] strb r3, [r1,r2] bne 0b - mov pc, lr + bx lr agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs index 930995ad..577d99f0 100644 --- a/agb/src/save/asm_utils.rs +++ b/agb/src/save/asm_utils.rs @@ -17,7 +17,7 @@ extern "C" { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { - if dst.len() != 0 { + if !dst.is_empty() { agb_rs__WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); } } @@ -30,7 +30,7 @@ pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { - if src.len() != 0 { + if !src.is_empty() { agb_rs__WramXferBuf(src.as_ptr(), dst as _, src.len()); } } @@ -44,7 +44,7 @@ pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { /// This uses raw addresses into the memory space. Use with care. #[inline(always)] pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { - if buf1.len() != 0 { + if !buf1.is_empty() { agb_rs__WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) } else { true diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs index 51753d50..d2f7258a 100644 --- a/agb/src/save/eeprom.rs +++ b/agb/src/save/eeprom.rs @@ -93,6 +93,7 @@ struct EepromProperties { } impl EepromProperties { /// Reads a block from the save media. + #[allow(clippy::needless_range_loop)] fn read_sector(&self, word: usize) -> [u8; 8] { // Set address command. The command is two one bits, followed by the // address, followed by a zero bit. @@ -117,6 +118,7 @@ impl EepromProperties { } /// Writes a sector directly. + #[allow(clippy::needless_range_loop)] fn write_sector_raw( &self, word: usize, block: &[u8], timeout: &mut Timeout, ) -> Result<(), Error> { @@ -178,7 +180,7 @@ impl EepromProperties { /// Implements EEPROM reads. fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); let sector = self.read_sector(offset >> SECTOR_SHIFT); @@ -192,10 +194,10 @@ impl EepromProperties { /// Implements EEPROM verifies. fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); - if &buf[..end_len] != &self.read_sector(offset >> SECTOR_SHIFT) { + if buf[..end_len] != self.read_sector(offset >> SECTOR_SHIFT) { return Ok(false); } buf = &buf[end_len..]; @@ -207,7 +209,7 @@ impl EepromProperties { /// Implements EEPROM writes. fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { self.check_offset(offset, buf.len())?; - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & SECTOR_MASK; let end_len = cmp::min(SECTOR_LEN - start, buf.len()); self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs index a3b470e5..0384926b 100644 --- a/agb/src/save/flash.rs +++ b/agb/src/save/flash.rs @@ -227,8 +227,8 @@ static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { impl FlashChipType { /// Returns the internal info for this chip. - fn chip_info(&self) -> &'static ChipInfo { - match *self { + 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, @@ -284,7 +284,7 @@ impl ChipInfo { /// 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 { + while !buf.is_empty() { self.set_bank(offset >> BANK_SHIFT)?; let start = offset & BANK_MASK; let end_len = cmp::min(BANK_LEN - start, buf.len()); @@ -299,7 +299,7 @@ impl ChipInfo { /// 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 { + while !buf.is_empty() { self.set_bank(offset >> BANK_SHIFT)?; let start = offset & BANK_MASK; let end_len = cmp::min(BANK_LEN - start, buf.len()); @@ -355,6 +355,7 @@ impl ChipInfo { } /// Writes an entire buffer to the save media. + #[allow(clippy::needless_range_loop)] 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() { @@ -368,6 +369,7 @@ impl ChipInfo { } /// Erases and writes an entire 128b sector on Atmel devices. + #[allow(clippy::needless_range_loop)] fn write_atmel_sector_raw( &self, offset: usize, buf: &[u8], timeout: &mut Timeout, ) -> Result<(), Error> { @@ -453,7 +455,7 @@ impl RawSaveAccess for FlashAccess { chip.check_len(offset, buf.len())?; if chip.uses_atmel_api { - while buf.len() != 0 { + while !buf.is_empty() { let start = offset & 127; let end_len = cmp::min(128 - start, buf.len()); chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 8639666d..485cc832 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -25,18 +25,17 @@ //! To use save media in your game, you must set which type to use. This is done //! by calling one of the following functions at startup: //! -//! * For 32 KiB battery-backed SRAM, call [`use_sram`]. -//! * For 64 KiB flash memory, call [`use_flash_64k`]. -//! * For 128 KiB flash memory, call [`use_flash_128k`]. -//! * For 512 byte EEPROM, call [`use_eeprom_512b`]. -//! * For 8 KiB EEPROM, call [`use_eeprom_8k`]. +//! * For 32 KiB battery-backed SRAM, call [`init_sram`]. +//! * For 64 KiB flash memory, call [`init_flash_64k`]. +//! * For 128 KiB flash memory, call [`init_flash_128k`]. +//! * For 512 byte EEPROM, call [`init_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`init_eeprom_8k`]. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::save; -//! save::use_flash_128k(); -//! save::set_timer_for_timeout(3); // Uses timer 3 for save media timeouts. -//! ``` +//! [`init_sram`]: SaveManager::init_sram +//! [`init_flash_64k`]: SaveManager::init_flash_64k +//! [`init_flash_128k`]: SaveManager::init_flash_128k +//! [`init_eeprom_512b`]: SaveManager::init_eeprom_512b +//! [`init_eeprom_8k`]: SaveManager::init_eeprom_8k //! //! ## Using save media //! @@ -46,29 +45,10 @@ //! Reading data from the savegame is simple. Use [`read`] to copy data from an //! offset in the savegame into a buffer in memory. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::{info, save::SaveAccess}; -//! let mut buf = [0; 1000]; -//! SaveAccess::new()?.read(1000, &mut buf)?; -//! info!("Memory result: {:?}", buf); -//! ``` -//! //! Writing to save media requires you to prepare the area for writing by calling //! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains //! the actual [`write`] method. //! -//! TODO Update example -//! ```rust,norun -//! # use gba::{info, save::SaveAccess}; -//! let access = SaveAccess::new()?; -//! access.prepare_write(500..600)?; -//! access.write(500, &[10; 25])?; -//! access.write(525, &[20; 25])?; -//! access.write(550, &[30; 25])?; -//! access.write(575, &[40; 25])?; -//! ``` -//! //! The `prepare_write` method leaves everything in a sector that overlaps the //! range passed to it in an implementation defined state. On some devices it may //! do nothing, and on others, it may clear the entire range to `0xFF`. @@ -169,11 +149,14 @@ pub struct MediaInfo { impl MediaInfo { /// Returns the sector size of the save media. It is generally optimal to /// write data in blocks that are aligned to the sector size. + #[must_use] pub fn sector_size(&self) -> usize { 1 << self.sector_shift } /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false pub fn len(&self) -> usize { self.sector_count << self.sector_shift } @@ -222,22 +205,27 @@ impl SaveData { } /// Returns the media info underlying this accessor. + #[must_use] pub fn media_info(&self) -> &'static MediaInfo { self.info } /// Returns the save media type being used. + #[must_use] pub fn media_type(&self) -> MediaType { self.info.media_type } /// Returns the sector size of the save media. It is generally optimal to /// write data in blocks that are aligned to the sector size. + #[must_use] pub fn sector_size(&self) -> usize { self.info.sector_size() } /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false pub fn len(&self) -> usize { self.info.len() } @@ -271,6 +259,7 @@ impl SaveData { /// /// This can be used to calculate which blocks would be erased by a call /// to [`prepare_write`](`SaveAccess::prepare_write`) + #[must_use] pub fn align_range(&self, range: Range) -> Range { let shift = self.info.sector_shift; let mask = (1 << shift) - 1; @@ -311,7 +300,7 @@ impl<'a> SavePreparedBlock<'a> { /// state. If an error is returned, the contents of the save media is /// unpredictable. pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { - if buffer.len() == 0 { + if buffer.is_empty() { Ok(()) } else if !self.range.contains(&offset) || !self.range.contains(&(offset + buffer.len() - 1)) { From 223ef150abfa3a7d65aad56b671840d4dc8ec287 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:21:45 -0700 Subject: [PATCH 09/11] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c9d4bb..a2f75f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for using windows on the GBA. Windows are used to selectively enable rendering of certain layers or effects. - Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white. - Added a new agb::sync module that contains GBA-specific synchronization primitives. +- Added support for save files. ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. From b920d94f8352b4216e6561a3bd7adb6cb9389b96 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Thu, 15 Sep 2022 23:28:38 -0700 Subject: [PATCH 10/11] Remove agb-tests in favor of putting the tests directly in agb. --- agb-tests/.cargo/config.toml | 14 -------------- agb-tests/Cargo.toml | 19 ------------------- agb-tests/rust-toolchain.toml | 3 --- .../tests/save_test_common/mod.rs | 0 .../tests/test_save_eeprom_512b.rs | 0 .../tests/test_save_eeprom_8k.rs | 0 .../tests/test_save_flash_128k.rs | 0 .../tests/test_save_flash_64k.rs | 0 {agb-tests => agb}/tests/test_save_sram.rs | 0 justfile | 3 --- 10 files changed, 39 deletions(-) delete mode 100644 agb-tests/.cargo/config.toml delete mode 100644 agb-tests/Cargo.toml delete mode 100644 agb-tests/rust-toolchain.toml rename {agb-tests => agb}/tests/save_test_common/mod.rs (100%) rename {agb-tests => agb}/tests/test_save_eeprom_512b.rs (100%) rename {agb-tests => agb}/tests/test_save_eeprom_8k.rs (100%) rename {agb-tests => agb}/tests/test_save_flash_128k.rs (100%) rename {agb-tests => agb}/tests/test_save_flash_64k.rs (100%) rename {agb-tests => agb}/tests/test_save_sram.rs (100%) diff --git a/agb-tests/.cargo/config.toml b/agb-tests/.cargo/config.toml deleted file mode 100644 index d5f7f86c..00000000 --- a/agb-tests/.cargo/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[unstable] -build-std = ["core", "alloc"] -build-std-features = ["compiler-builtins-mem"] - -[build] -target = "thumbv4t-none-eabi" - -[target.thumbv4t-none-eabi] -rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] -runner = "mgba-test-runner" - -[target.armv4t-none-eabi] -rustflags = ["-Clink-arg=-T../agb/gba.ld", "-Ctarget-cpu=arm7tdmi"] -runner = "mgba-test-runner" diff --git a/agb-tests/Cargo.toml b/agb-tests/Cargo.toml deleted file mode 100644 index 9917f81b..00000000 --- a/agb-tests/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "agb-tests" -version = "0.1.0" -edition = "2018" - -[profile.dev] -opt-level = 3 -debug = true - -[profile.release] -lto = true -debug = true - -[dependencies] -agb = { version = "*", path = "../agb", features = ["testing"] } - -[package.metadata.docs.rs] -default-target = "thumbv6m-none-eabi" -targets = [] diff --git a/agb-tests/rust-toolchain.toml b/agb-tests/rust-toolchain.toml deleted file mode 100644 index 06842486..00000000 --- a/agb-tests/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "nightly" -components = ["rust-src", "clippy"] \ No newline at end of file diff --git a/agb-tests/tests/save_test_common/mod.rs b/agb/tests/save_test_common/mod.rs similarity index 100% rename from agb-tests/tests/save_test_common/mod.rs rename to agb/tests/save_test_common/mod.rs diff --git a/agb-tests/tests/test_save_eeprom_512b.rs b/agb/tests/test_save_eeprom_512b.rs similarity index 100% rename from agb-tests/tests/test_save_eeprom_512b.rs rename to agb/tests/test_save_eeprom_512b.rs diff --git a/agb-tests/tests/test_save_eeprom_8k.rs b/agb/tests/test_save_eeprom_8k.rs similarity index 100% rename from agb-tests/tests/test_save_eeprom_8k.rs rename to agb/tests/test_save_eeprom_8k.rs diff --git a/agb-tests/tests/test_save_flash_128k.rs b/agb/tests/test_save_flash_128k.rs similarity index 100% rename from agb-tests/tests/test_save_flash_128k.rs rename to agb/tests/test_save_flash_128k.rs diff --git a/agb-tests/tests/test_save_flash_64k.rs b/agb/tests/test_save_flash_64k.rs similarity index 100% rename from agb-tests/tests/test_save_flash_64k.rs rename to agb/tests/test_save_flash_64k.rs diff --git a/agb-tests/tests/test_save_sram.rs b/agb/tests/test_save_sram.rs similarity index 100% rename from agb-tests/tests/test_save_sram.rs rename to agb/tests/test_save_sram.rs diff --git a/justfile b/justfile index ba8f243c..23837d70 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,6 @@ clippy: test: just _test-debug agb - just _test-debug agb-tests just _test-debug agb-fixnum just _test-debug-arm agb just _test-debug tools @@ -21,8 +20,6 @@ test: test-release: just _test-release agb just _test-release-arm agb - just _test-release agb-tests - just _test-release-arm agb-tests doctest-agb: (cd agb && cargo test --doc -Z doctest-xcompile) From ae5d8818b67788880dc8315be3c5b91788258534 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 1 Oct 2022 17:09:48 +0100 Subject: [PATCH 11/11] Fix test in release mode (don't know why this works :/ but is more correct) --- agb/src/display/example_logo.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agb/src/display/example_logo.rs b/agb/src/display/example_logo.rs index bec4df92..9b6dbe25 100644 --- a/agb/src/display/example_logo.rs +++ b/agb/src/display/example_logo.rs @@ -36,5 +36,8 @@ mod tests { display_logo(&mut map, &mut vram); crate::test_runner::assert_image_output("gfx/test_logo.png"); + + map.clear(&mut vram); + vram.gc(); } }