From 79099d807b510637d48d8d206caa330a14d65086 Mon Sep 17 00:00:00 2001 From: Alissa Rao Date: Mon, 22 Feb 2021 22:19:37 -0800 Subject: [PATCH] Adds support for reading/writing to save media. (#109) * Write some of the basic infrastructure for SRAM support. * Implement battery-backed SRAM. * Implement non-Atmel Flash chips. * Implement support for Atmel Flash SRAM chips. * Implement EEPROM support, various refactorings to SRAM system. * Replace Save API with one based more cleanly on the flash chip API. * Run rustfmt on new save media code. * Improve test_savegame and fix remaining bugs caught by the changes. * Proofreading on comments/documentation for save module. * Fix addresses for read/verify routines in save::flash. * Rebase save_api onto current master. --- examples/test_savegame.rs | 180 +++++++++++++++ src/lib.rs | 4 +- src/save.rs | 257 +++++++++++++++++++++ src/save/asm_routines.s | 89 +++++++ src/save/asm_utils.rs | 84 +++++++ src/save/eeprom.rs | 299 ++++++++++++++++++++++++ src/save/flash.rs | 475 ++++++++++++++++++++++++++++++++++++++ src/save/setup.rs | 100 ++++++++ src/save/sram.rs | 51 ++++ src/save/utils.rs | 111 +++++++++ src/sram.rs | 1 - 11 files changed, 1648 insertions(+), 3 deletions(-) create mode 100644 examples/test_savegame.rs create mode 100644 src/save.rs create mode 100644 src/save/asm_routines.s create mode 100644 src/save/asm_utils.rs create mode 100644 src/save/eeprom.rs create mode 100644 src/save/flash.rs create mode 100644 src/save/setup.rs create mode 100644 src/save/sram.rs create mode 100644 src/save/utils.rs delete mode 100644 src/sram.rs diff --git a/examples/test_savegame.rs b/examples/test_savegame.rs new file mode 100644 index 0000000..9a8513f --- /dev/null +++ b/examples/test_savegame.rs @@ -0,0 +1,180 @@ +#![no_std] +#![feature(start)] +#![forbid(unsafe_code)] + +use core::cmp; +use gba::{ + fatal, warn, + io::{ + display::{DisplayControlSetting, DisplayMode, DISPCNT}, + timers::{TimerControlSetting, TimerTickRate, TM0CNT_H, TM0CNT_L, TM1CNT_H, TM1CNT_L}, + }, + save::*, + vram::bitmap::Mode3, + Color, +}; + +fn set_screen_color(r: u16, g: u16, b: u16) { + const SETTING: DisplayControlSetting = + DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); + DISPCNT.write(SETTING); + Mode3::dma_clear_to(Color::from_rgb(r, g, b)); +} +fn set_screen_progress(cur: usize, max: usize) { + let lines = cur * (Mode3::WIDTH / max); + let color = Color::from_rgb(0, 31, 0); + for x in 0..lines { + for y in 0..Mode3::HEIGHT { + Mode3::write(x, y, color); + } + } +} + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + set_screen_color(31, 0, 0); + fatal!("{}", info); + loop {} +} + +#[derive(Clone)] +struct Rng(u32); +impl Rng { + fn iter(&mut self) { + self.0 = self.0 * 2891336453 + 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; + +fn check_status(r: Result) -> T { + match r { + Ok(v) => v, + Err(e) => panic!("Error encountered: {:?}", e), + } +} + +fn setup_timers() { + TM0CNT_L.write(0); + TM1CNT_L.write(0); + + let ctl = TimerControlSetting::new().with_tick_rate(TimerTickRate::CPU1024).with_enabled(true); + TM0CNT_H.write(ctl); + let ctl = TimerControlSetting::new().with_tick_rate(TimerTickRate::Cascade).with_enabled(true); + TM1CNT_H.write(ctl); +} + +// I'm fully aware how slow this is. But this is just example code, so, eh. +fn get_timer_secs() -> f32 { + let raw_timer = (TM1CNT_L.read() as u32) << 16 | TM0CNT_L.read() as u32; + (raw_timer as f32 * 1024.0) / ((1 << 24) as f32) +} +macro_rules! output { + ($($args:tt)*) => { + // we use warn so it shows by default on mGBA, nothing more. + warn!("{:7.3}\t{}", get_timer_secs(), format_args!($($args)*)) + }; +} + +fn do_test(seed: Rng, offset: usize, len: usize, block_size: usize) -> Result<(), Error> { + let access = SaveAccess::new()?; + let mut buffer = [0; MAX_BLOCK_SIZE]; + + output!(" - Clearing media..."); + access.prepare_write(offset..offset+len)?; + + output!(" - Writing media..."); + 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(); + } + access.write(current, &buffer[..cur_len])?; + current += cur_len; + } + + output!(" - Validating media..."); + rng = seed.clone(); + 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!( + buffer[i] == cur_byte, + "Read does not match earlier write: {} != {} @ 0x{:05x}", + buffer[i], + cur_byte, + current + i, + ); + } + current += cur_len; + } + + output!(" - Done!"); + + Ok(()) +} + +#[start] +fn main(_argc: isize, _argv: *const *const u8) -> isize { + // set a pattern to show that the ROM is working at all. + set_screen_color(31, 31, 0); + + // sets up the timers so we can print time with our outputs. + setup_timers(); + + // set the save type + use_flash_128k(); + set_timer_for_timeout(3); + + // check some metainfo on the save type + let access = check_status(SaveAccess::new()); + output!("Media info: {:#?}", access.media_info()); + output!("Media size: {} bytes", access.len()); + output!(""); + + // actually test the save implementation + if access.len() >= (1 << 12) { + output!("[ Full write, 4KiB blocks ]"); + check_status(do_test(Rng(2000), 0, access.len(), 4 * 1024)); + set_screen_progress(1, 10); + } + + output!("[ Full write, 0.5KiB blocks ]"); + check_status(do_test(Rng(1000), 0, access.len(), 512)); + set_screen_progress(2, 10); + + // test with random segments now. + let mut rng = Rng(12345); + for i in 0..8 { + let rand_length = rng.next_under((access.len() >> 1) as u32) as usize + 50; + let rand_offset = rng.next_under(access.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; + + output!( + "[ Partial, offset = 0x{:06x}, len = {}, bs = {}]", + rand_offset, rand_length, block_size, + ); + check_status(do_test(Rng(i * 10000), rand_offset, rand_length, block_size)); + set_screen_progress(3 + i as usize, 10); + } + + // show a pattern so we know it worked + set_screen_color(0, 31, 0); + loop { } +} diff --git a/src/lib.rs b/src/lib.rs index ee159e0..636bc1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(test), no_std)] -#![feature(asm, isa_attribute)] +#![feature(asm, global_asm, isa_attribute)] #![allow(clippy::cast_lossless)] #![deny(clippy::float_arithmetic)] #![warn(missing_docs)] @@ -40,7 +40,7 @@ pub mod oam; pub mod rom; -pub mod sram; +pub mod save; pub mod sync; diff --git a/src/save.rs b/src/save.rs new file mode 100644 index 0000000..ab83ea0 --- /dev/null +++ b/src/save.rs @@ -0,0 +1,257 @@ +//! 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 +//! [`timers`](`crate::io::timers`) module's documentation. +//! +//! ```rust +//! # use gba::save; +//! save::use_flash_128k(); +//! save::set_timer_for_timeout(3); // Uses timer 3 for save media timeouts. +//! ``` +//! +//! ## Using save media +//! +//! + +use crate::sync::Static; +use core::ops::Range; + +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, +} + +/// 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: Static> = Static::new(None); + +/// Sets the save media implementation in use. +pub fn set_save_implementation(access: Option<&'static dyn RawSaveAccess>) { + CURRENT_SAVE_ACCESS.write(access) +} + +/// Gets the save media implementation in use. +pub fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + CURRENT_SAVE_ACCESS.read() +} + +/// 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 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> { + let range = self.align_range(range); + let shift = self.info.sector_shift; + self.access.prepare_write(range.start >> shift, range.len() >> shift) + } + + /// Writes a given buffer into the save media. + /// + /// You must call [`prepare_write`](`SaveAccess::prepare_write`) on the range + /// you intend to write for this to function correctly. + 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. + /// + /// You must call [`prepare_write`](`SaveAccess::prepare_write`) on the range + /// you intend to write for this to function correctly. + /// + /// 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(()) + } + } +} diff --git a/src/save/asm_routines.s b/src/save/asm_routines.s new file mode 100644 index 0000000..adc3094 --- /dev/null +++ b/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 + subs 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: + subs r2, #1 + ldrb r3, [r0,r2] + strb r3, [r1,r2] + bne WramXferBufInner + mov pc, lr + + .pool + .section .text diff --git a/src/save/asm_utils.rs b/src/save/asm_utils.rs new file mode 100644 index 0000000..e9051ee --- /dev/null +++ b/src/save/asm_utils.rs @@ -0,0 +1,84 @@ +//! 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. + +#![cfg_attr(not(target_arch = "arm"), allow(unused_variables, non_snake_case))] + +#[cfg(target_arch = "arm")] +global_asm!(include_str!("asm_routines.s")); + +#[cfg(target_arch = "arm")] +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; +} + +#[cfg(not(target_arch = "arm"))] +fn WramXferBuf(src: *const u8, dst: *mut u8, count: usize) { + unimplemented!() +} + +#[cfg(not(target_arch = "arm"))] +fn WramReadByte(src: *const u8) -> u8 { + unimplemented!() +} + +#[cfg(not(target_arch = "arm"))] +fn WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool { + unimplemented!() +} + +/// 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/src/save/eeprom.rs b/src/save/eeprom.rs new file mode 100644 index 0000000..e1022bb --- /dev/null +++ b/src/save/eeprom.rs @@ -0,0 +1,299 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use super::{Error, MediaType, RawSaveAccess}; +use crate::{ + io::dma::*, + save::{lock_media, MediaInfo, Timeout}, + sync::with_irqs_disabled, +}; +use core::cmp; +use voladdress::VolAddress; + +const PORT: VolAddress = unsafe { VolAddress::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Disable IRQs and DMAs during each read block. +fn disable_dmas(func: impl FnOnce()) { + with_irqs_disabled(|| unsafe { + // Disable other DMAs. This avoids our read/write from being interrupted + // by a higher priority DMA channel. + let dma0_ctl = DMA0::control(); + let dma1_ctl = DMA1::control(); + let dma2_ctl = DMA2::control(); + DMA0::set_control(dma0_ctl.with_enabled(false)); + DMA1::set_control(dma1_ctl.with_enabled(false)); + DMA2::set_control(dma2_ctl.with_enabled(false)); + + // Executes the body of the function with DMAs and IRQs disabled. + func(); + + // Continues higher priority DMAs if they were enabled before. + DMA0::set_control(dma0_ctl); + DMA1::set_control(dma1_ctl); + DMA2::set_control(dma2_ctl); + }); +} + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: u16) { + disable_dmas(|| unsafe { + DMA3::set_source(source.as_ptr()); + DMA3::set_dest(0x0DFFFF00 as *mut _); + DMA3::set_count(ct); + let dma3_ctl = DMAControlSetting::new() + .with_dest_address_control(DMADestAddressControl::Increment) + .with_source_address_control(DMASrcAddressControl::Increment) + .with_enabled(true); + DMA3::set_control(dma3_ctl); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: u16) { + disable_dmas(|| unsafe { + DMA3::set_source(0x0DFFFF00 as *const _); + DMA3::set_dest(source.as_ptr() as *mut _); + DMA3::set_count(ct); + let dma3_ctl = DMAControlSetting::new() + .with_dest_address_control(DMADestAddressControl::Increment) + .with_source_address_control(DMASrcAddressControl::Increment) + .with_enabled(true); + DMA3::set_control(dma3_ctl); + }); +} + +/// 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 as u16); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx as u16); + } + } +} + +/// 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]) -> Result<(), Error> { + unsafe { + // 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. + let timeout = Timeout::new()?; + timeout.start(); + while PORT.read() & 1 != 1 { + if timeout.is_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) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf) + } + + /// Writes a sector to the EEPROM. + fn write_sector(&self, word: usize, data: &[u8], start: usize) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data) + } else { + self.write_sector_safe(word, data, start) + } + } + + /// 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())?; + let _guard = lock_media()?; + 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())?; + let _guard = lock_media()?; + 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]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + let _guard = lock_media()?; + 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)?; + 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 }) + } + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + PROPS_512B.write(offset, buffer) + } +} + +/// 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 }) + } + fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8]) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + PROPS_8K.write(offset, buffer) + } +} diff --git a/src/save/flash.rs b/src/save/flash.rs new file mode 100644 index 0000000..a1cc624 --- /dev/null +++ b/src/save/flash.rs @@ -0,0 +1,475 @@ +//! Module for flash save media support. +//! +//! Flash may be read with ordinary read commands, but writing requires +//! sending structured commands to the flash chip. + +use super::{ + lock_media, read_raw_buf, read_raw_byte, verify_raw_buf, Error, MediaInfo, MediaType, + RawSaveAccess, +}; +use crate::sync::{with_irqs_disabled, InitOnce, Static}; +use core::cmp; +use typenum::consts::U65536; +use voladdress::{VolAddress, VolBlock}; + +// Volatile address ports for flash +const FLASH_PORT_BANK: VolAddress = unsafe { VolAddress::new(0x0E000000) }; +const FLASH_PORT_A: VolAddress = unsafe { VolAddress::new(0x0E005555) }; +const FLASH_PORT_B: VolAddress = unsafe { VolAddress::new(0x0E002AAA) }; +const FLASH_DATA: VolBlock = unsafe { VolBlock::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.write(0xAA); + FLASH_PORT_B.write(0x55); +} + +/// Helper function for issuing commands to the flash chip. +fn issue_flash_command(c2: u8) { + start_flash_command(); + FLASH_PORT_A.write(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.write(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, + } + } + + /// Returns the `u16` id of the chip, or `0` for `Unknown`. + pub fn id(&self) -> u16 { + match *self { + FlashChipType::Sst64K => 0xD4BF, + FlashChipType::Macronix64K => 0x1CC2, + FlashChipType::Panasonic64K => 0x1B32, + FlashChipType::Atmel64K => 0x3D1F, + FlashChipType::Sanyo128K => 0x1362, + FlashChipType::Macronix128K => 0x09C2, + FlashChipType::Unknown => 0x0000, + } + } +} + +/// Determines the raw ID of the flash chip currently in use. +pub fn detect_chip_id() -> Result { + let _lock = lock_media()?; + 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. +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 mililseconds 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 +}; +static INFO_64K_ATMEL: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 7, // 128 bytes + sector_count: 512, // 128 bytes * 512 = 64 KIB +}; +static INFO_128K: MediaInfo = MediaInfo { + media_type: MediaType::Flash128K, + sector_shift: 12, + sector_count: 32, // 4 KiB * 32 = 128 KiB +}; + +// 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) -> Result<(), Error> { + let timeout = super::Timeout::new()?; + timeout.start(); + let offset = 0x0E000000 + offset; + + while unsafe { read_raw_byte(offset) != val } { + if timeout.is_timeout_met(ms) { + if self.requires_cancel_command { + FLASH_PORT_A.write(0xF0); + } + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Erases a sector to flash. + fn erase_sector(&self, sector: usize) -> 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.index(offset & BANK_MASK).write(CMD_ERASE_SECTOR_CONFIRM); + self.wait_for_timeout(offset & BANK_MASK, 0xFF, self.erase_sector_timeout) + } + + /// Erases the entire chip. + fn erase_chip(&self) -> Result<(), Error> { + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + issue_flash_command(CMD_ERASE_SECTOR_ALL); + self.wait_for_timeout(0, 0xFF, 3000) + } + + /// Writes a byte to the save media. + fn write_byte(&self, offset: usize, byte: u8) -> Result<(), Error> { + issue_flash_command(CMD_WRITE); + FLASH_DATA.index(offset).write(byte); + self.wait_for_timeout(offset, byte, self.write_timeout) + } + + /// Writes an entire buffer to the save media. + fn write_buffer(&self, offset: usize, buf: &[u8]) -> 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])?; + } + Ok(()) + } + + /// Erases and writes an entire 128b sector on Atmel devices. + fn write_atmel_sector_raw(&self, offset: usize, buf: &[u8]) -> Result<(), Error> { + with_irqs_disabled(|| { + issue_flash_command(CMD_WRITE); + for i in 0..128 { + FLASH_DATA.index(offset + i).write(buf[i]); + } + self.wait_for_timeout(offset + 127, buf[127], self.erase_sector_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) -> 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) + } + + /// 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) -> Result<(), Error> { + if start == 0 && buf.len() == 128 { + self.write_atmel_sector_raw(offset, buf) + } else { + self.write_atmel_sector_safe(offset, buf, start) + } + } +} + +/// 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]) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + let _lock = lock_media()?; + chip.read_buffer(offset, buf) + } + + fn verify(&self, offset: usize, buf: &[u8]) -> Result { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + let _lock = lock_media()?; + chip.verify_buffer(offset, buf) + } + + fn prepare_write(&self, sector: usize, count: usize) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_sector_len(sector, count)?; + + let _lock = lock_media()?; + if chip.uses_atmel_api { + Ok(()) + } else if count == chip.info.sector_count { + chip.erase_chip() + } else { + for i in sector..sector + count { + chip.erase_sector(i)?; + } + Ok(()) + } + } + + fn write(&self, mut offset: usize, mut buf: &[u8]) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + let _lock = lock_media()?; + 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)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } else { + // Write the bytes one by one. + chip.write_buffer(offset, buf)?; + Ok(()) + } + } +} diff --git a/src/save/setup.rs b/src/save/setup.rs new file mode 100644 index 0000000..c1686d6 --- /dev/null +++ b/src/save/setup.rs @@ -0,0 +1,100 @@ +//! A module that produces the marker strings used by emulators to determine +//! which save media type a ROM uses. +//! +//! This takes advantage of the LLVM's usual dead code elimination. The +//! functions that generate the markers use `volatile_mark_read` to force the +//! LLVM to assume the statics used. Therefore, as long as one of these +//! functions is called, the corresponding static is emitted with very little +//! code actually generated. + +use super::*; + +#[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)] +fn emit_eeprom_marker() { + crate::sync::memory_read_hint(&EEPROM); +} +#[inline(always)] +fn emit_sram_marker() { + crate::sync::memory_read_hint(&SRAM); +} +#[inline(always)] +fn emit_flash_512k_marker() { + crate::sync::memory_read_hint(&FLASH512K); +} +#[inline(always)] +fn emit_flash_1m_marker() { + crate::sync::memory_read_hint(&FLASH1M); +} + +/// Declares that the ROM uses battery backed SRAM/FRAM. +/// +/// This creates a marker in the ROM that allows emulators to understand what +/// save type the Game Pak uses, and sets the accessor to one appropriate for +/// memory type. +/// +/// Battery Backed SRAM is generally very fast, but limited in size compared +/// to flash chips. +pub fn use_sram() { + emit_sram_marker(); + set_save_implementation(Some(&sram::BatteryBackedAccess)); +} + +/// Declares that the ROM uses 64KiB flash memory. +/// +/// This creates a marker in the ROM that allows emulators to understand what +/// save type the Game Pak uses, and sets the accessor to one appropriate for +/// memory type. +/// +/// 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. +pub fn use_flash_64k() { + emit_flash_512k_marker(); + set_save_implementation(Some(&flash::FlashAccess)); +} + +/// Declares that the ROM uses 128KiB flash memory. +/// +/// This creates a marker in the ROM that allows emulators to understand what +/// save type the Game Pak uses, and sets the accessor to one appropriate for +/// memory type. +/// +/// 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. +pub fn use_flash_128k() { + emit_flash_1m_marker(); + set_save_implementation(Some(&flash::FlashAccess)); +} + +/// Declares that the ROM uses 512 bytes EEPROM memory. +/// +/// This creates a marker in the ROM that allows emulators to understand what +/// save type the Game Pak uses, and sets the accessor to one appropriate for +/// memory type. +/// +/// EEPROM is generally pretty slow and also very small. It's mainly used in +/// Game Paks because it's cheap. +pub fn use_eeprom_512b() { + emit_eeprom_marker(); + set_save_implementation(Some(&eeprom::Eeprom512B)); +} + +/// Declares that the ROM uses 8 KiB EEPROM memory. +/// +/// This creates a marker in the ROM that allows emulators to understand what +/// save type the Game Pak uses, and sets the accessor to one appropriate for +/// memory type. +/// +/// EEPROM is generally pretty slow and also very small. It's mainly used in +/// Game Paks because it's cheap. +pub fn use_eeprom_8k() { + emit_eeprom_marker(); + set_save_implementation(Some(&eeprom::Eeprom8K)); +} diff --git a/src/save/sram.rs b/src/save/sram.rs new file mode 100644 index 0000000..c7f05cf --- /dev/null +++ b/src/save/sram.rs @@ -0,0 +1,51 @@ +//! 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 super::{read_raw_buf, write_raw_buf, Error, MediaType, RawSaveAccess}; +use crate::save::{verify_raw_buf, MediaInfo}; + +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 }) + } + + 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(()) + } +} diff --git a/src/save/utils.rs b/src/save/utils.rs new file mode 100644 index 0000000..d3fc55a --- /dev/null +++ b/src/save/utils.rs @@ -0,0 +1,111 @@ +//! A package containing useful utilities for writing save accessors. This is +//! mainly used internally, although the types inside are exposed publically. + +use super::Error; +use crate::{ + io::timers::*, + sync::{RawMutex, RawMutexGuard, Static}, +}; +use voladdress::VolAddress; + +/// Internal representation for our active timer. +#[derive(Copy, Clone, PartialEq)] +#[repr(u8)] +enum TimerId { + None, + T0, + T1, + T2, + T3, +} + +/// Stores the timer ID used for timeouts created by save accessors. +static TIMER_ID: Static = Static::new(TimerId::None); + +/// Sets the timer to use to implement timeouts for operations that may hang. +/// +/// At any point where you call functions in a save accessor, this timer may be +/// reset to a different value. +pub fn set_timer_for_timeout(id: u8) { + if id >= 4 { + panic!("Timer ID must be 0-3."); + } else { + TIMER_ID.write([TimerId::T0, TimerId::T1, TimerId::T2, TimerId::T3][id as usize]) + } +} + +/// Disables the timeout for operations that may hang. +pub fn disable_timeout() { + TIMER_ID.write(TimerId::None); +} + +/// A timeout type used to prevent hardware errors in save media from hanging +/// the game. +pub struct Timeout { + _lock_guard: RawMutexGuard<'static>, + active: bool, + timer_l: VolAddress, + timer_h: VolAddress, +} +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() -> Result { + static TIMEOUT_LOCK: RawMutex = RawMutex::new(); + let _lock_guard = match TIMEOUT_LOCK.try_lock() { + Some(x) => x, + None => return Err(Error::MediaInUse), + }; + let id = TIMER_ID.read(); + Ok(Timeout { + _lock_guard, + active: id != TimerId::None, + timer_l: match id { + TimerId::None => unsafe { VolAddress::new(0) }, + TimerId::T0 => TM0CNT_L, + TimerId::T1 => TM1CNT_L, + TimerId::T2 => TM2CNT_L, + TimerId::T3 => TM3CNT_L, + }, + timer_h: match id { + TimerId::None => unsafe { VolAddress::new(0) }, + TimerId::T0 => TM0CNT_H, + TimerId::T1 => TM1CNT_H, + TimerId::T2 => TM2CNT_H, + TimerId::T3 => TM3CNT_H, + }, + }) + } + + /// Starts this timeout. + pub fn start(&self) { + if self.active { + self.timer_l.write(0); + let timer_ctl = + TimerControlSetting::new().with_tick_rate(TimerTickRate::CPU1024).with_enabled(true); + self.timer_h.write(TimerControlSetting::new()); + self.timer_h.write(timer_ctl); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`start`]. + pub fn is_timeout_met(&self, check_ms: u16) -> bool { + self.active && check_ms * 17 < self.timer_l.read() + } +} + +/// 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), + } +} diff --git a/src/sram.rs b/src/sram.rs deleted file mode 100644 index a294d5f..0000000 --- a/src/sram.rs +++ /dev/null @@ -1 +0,0 @@ -//! Module for things related to SRAM.