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.
This commit is contained in:
Alissa Rao 2021-02-22 22:19:37 -08:00 committed by GitHub
parent 8bfa1a228a
commit 79099d807b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1648 additions and 3 deletions

180
examples/test_savegame.rs Normal file
View file

@ -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<T>(r: Result<T, Error>) -> 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 { }
}

View file

@ -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;

257
src/save.rs Normal file
View file

@ -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<bool, Error>;
/// 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<Option<&'static dyn RawSaveAccess>> = 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<SaveAccess, Error> {
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<bool, Error> {
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(&self, range: Range<usize>) -> 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(())
}
}
}

89
src/save/asm_routines.s Normal file
View file

@ -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

84
src/save/asm_utils.rs Normal file
View file

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

299
src/save/eeprom.rs Normal file
View file

@ -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<u16> = 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(&sector[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<bool, 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());
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<bool, Error> {
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<bool, Error> {
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)
}
}

475
src/save/flash.rs Normal file
View file

@ -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<u8> = unsafe { VolAddress::new(0x0E000000) };
const FLASH_PORT_A: VolAddress<u8> = unsafe { VolAddress::new(0x0E005555) };
const FLASH_PORT_B: VolAddress<u8> = unsafe { VolAddress::new(0x0E002AAA) };
const FLASH_DATA: VolBlock<u8, U65536> = 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<u8> = 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<Self, Error> {
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<u16, Error> {
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<bool, 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());
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, &sector)
}
/// 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<bool, Error> {
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(())
}
}
}

100
src/save/setup.rs Normal file
View file

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

51
src/save/sram.rs Normal file
View file

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

111
src/save/utils.rs Normal file
View file

@ -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<TimerId> = 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<u16>,
timer_h: VolAddress<TimerControlSetting>,
}
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<Self, Error> {
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<RawMutexGuard<'static>, Error> {
static LOCK: RawMutex = RawMutex::new();
match LOCK.try_lock() {
Some(x) => Ok(x),
None => Err(Error::MediaInUse),
}
}

View file

@ -1 +0,0 @@
//! Module for things related to SRAM.