Reintegrate SRAM save media reader.

This commit is contained in:
Alissa Rao 2022-08-17 00:42:34 -07:00
parent d50413a3cc
commit ec41db2fc9
No known key found for this signature in database
GPG key ID: 9314D8F6745E881E
4 changed files with 302 additions and 195 deletions

View file

@ -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(),
}
}

View file

@ -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 _)
}

View file

@ -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<bool, Error>;
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<bool, Error>;
fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error>;
fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error>;
}
static CURRENT_SAVE_ACCESS: Mutex<Option<&'static dyn RawSaveAccess>> = 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<Timer>) -> Result<SaveData, Error> {
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<Timer>) -> Result<SaveData, Error> {
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<usize>) -> 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<bool, Error> {
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<usize>) -> Range<usize> {
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<usize>) -> Result<SavePreparedBlock, Error> {
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<usize>) -> 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<bool, Error> {
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<usize>) -> Range<usize> {
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<usize>) -> Result<SavePreparedBlock, Error> {
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<usize>,
parent: &'a mut SaveData,
range: Range<usize>,
}
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>(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, Error> {
SaveData::new(None)
}
pub fn access_with_timer(timer: Timer) -> Result<SaveData, Error> {
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, Error> {
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, Error> {
SaveData::new(Some(timer))
}
}

56
agb/src/save/sram.rs Normal file
View file

@ -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<bool, Error> {
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(())
}
}