diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c9d4bb..a2f75f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for using windows on the GBA. Windows are used to selectively enable rendering of certain layers or effects. - Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white. - Added a new agb::sync module that contains GBA-specific synchronization primitives. +- Added support for save files. ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. diff --git a/agb/build.rs b/agb/build.rs index 2bb2f4bb..d4245b4a 100644 --- a/agb/build.rs +++ b/agb/build.rs @@ -7,6 +7,7 @@ fn main() { "src/sound/mixer/mixer.s", "src/agbabi/memset.s", "src/agbabi/memcpy.s", + "src/save/asm_routines.s", ]; println!("cargo:rerun-if-changed=gba.ld"); diff --git a/agb/src/asm_include.s b/agb/src/asm_include.s index 5508e4fe..1682792c 100644 --- a/agb/src/asm_include.s +++ b/agb/src/asm_include.s @@ -13,3 +13,19 @@ .size \functionName,.-\functionName .endfunc .endm + +.macro agb_thumb_func functionName:req +.section .iwram.\functionName, "ax", %progbits +.thumb +.align 2 +.global \functionName +.type \functionName, %function +.func \functionName +\functionName: +.endm + +.macro agb_thumb_end functionName:req +.pool +.size \functionName,.-\functionName +.endfunc +.endm diff --git a/agb/src/display/example_logo.rs b/agb/src/display/example_logo.rs index bec4df92..9b6dbe25 100644 --- a/agb/src/display/example_logo.rs +++ b/agb/src/display/example_logo.rs @@ -36,5 +36,8 @@ mod tests { display_logo(&mut map, &mut vram); crate::test_runner::assert_image_output("gfx/test_logo.png"); + + map.clear(&mut vram); + vram.gc(); } } diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 71651cfc..1bf1b0bd 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) { DMA3_CONTROL.set(count as u32 | (1 << 31)); } + +pub(crate) fn dma3_exclusive(f: impl FnOnce() -> R) -> R { + const DMA0_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) }; + const DMA1_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; + const DMA2_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; + + crate::interrupt::free(|_| { + let dma0_ctl = DMA0_CTRL_HI.get(); + let dma1_ctl = DMA1_CTRL_HI.get(); + let dma2_ctl = DMA2_CTRL_HI.get(); + DMA0_CTRL_HI.set(dma0_ctl & !(1 << 15)); + DMA1_CTRL_HI.set(dma1_ctl & !(1 << 15)); + DMA2_CTRL_HI.set(dma2_ctl & !(1 << 15)); + + // Executes the body of the function with DMAs and IRQs disabled. + let ret = f(); + + // Continues higher priority DMAs if they were enabled before. + DMA0_CTRL_HI.set(dma0_ctl); + DMA1_CTRL_HI.set(dma1_ctl); + DMA2_CTRL_HI.set(dma2_ctl); + + // returns the return value + ret + }) +} \ No newline at end of file diff --git a/agb/src/lib.rs b/agb/src/lib.rs index fa3dd6f9..764cdbfa 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,6 +168,7 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; +pub mod save; mod single; /// Implements sound output. pub mod sound; @@ -223,6 +224,8 @@ pub struct Gba { pub sound: sound::dmg::Sound, /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. pub mixer: sound::mixer::MixerController, + /// Manages access to the Game Boy Advance cartridge's save chip. + pub save: save::SaveManager, /// Manages access to the Game Boy Advance's 4 timers. pub timers: timer::TimerController, } @@ -239,6 +242,7 @@ impl Gba { display: display::Display::new(), sound: sound::dmg::Sound::new(), mixer: sound::mixer::MixerController::new(), + save: save::SaveManager::new(), timers: timer::TimerController::new(), } } diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s new file mode 100644 index 00000000..f257a524 --- /dev/null +++ b/agb/src/save/asm_routines.s @@ -0,0 +1,49 @@ +.include "src/asm_include.s" + +@ +@ char WramReadByte(const char* offset); +@ +@ A routine that reads a byte from a given memory offset. +@ +agb_thumb_func agb_rs__WramReadByte + ldrb r0, [r0] + bx lr +agb_thumb_end agb_rs__WramReadByte + +@ +@ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); +@ +@ A routine that compares two memory offsets. +@ +agb_thumb_func agb_rs__WramVerifyBuf + push {r4-r5, lr} + movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result + movs r0, #0 @ set up r0 so the default return result is false + + @ At this point, buf1 is actually in r5, so r0 can be used as a status return +1: ldrb r3, [r5,r2] + ldrb r4, [r1,r2] + cmp r3, r4 + bne 0f + sub r2, #1 + bpl 1b + + @ Returns from the function successfully + movs r0, #1 +0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point + pop {r4-r5, pc} +agb_thumb_end agb_rs__WramVerifyBuf + + +@ +@ void WramXferBuf(const char* source, char* dest, int count); +@ +@ A routine that copies one buffer into another. +@ +agb_thumb_func agb_rs__WramXferBuf +0: sub r2, #1 + ldrb r3, [r0,r2] + strb r3, [r1,r2] + bne 0b + bx lr +agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs new file mode 100644 index 00000000..577d99f0 --- /dev/null +++ b/agb/src/save/asm_utils.rs @@ -0,0 +1,63 @@ +//! A module containing low-level assembly functions that can be loaded into +//! WRAM. Both flash media and battery-backed SRAM require reads to be +//! performed via code in WRAM and cannot be accessed by DMA. + +extern "C" { + fn agb_rs__WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn agb_rs__WramReadByte(src: *const u8) -> u8; + fn agb_rs__WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; +} + +/// Copies data from a given memory address into a buffer. +/// +/// 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.is_empty() { + agb_rs__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.is_empty() { + agb_rs__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.is_empty() { + agb_rs__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 { + agb_rs__WramReadByte(src as _) +} diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs new file mode 100644 index 00000000..d2f7258a --- /dev/null +++ b/agb/src/save/eeprom.rs @@ -0,0 +1,273 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use crate::memory_mapped::MemoryMapped; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::utils::Timeout; +use core::cmp; + +const PORT: MemoryMapped = unsafe { MemoryMapped::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + crate::dma::dma_copy16(source.as_ptr() as *mut u16, 0x0DFFFF00 as *mut u16, ct); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + crate::dma::dma_copy16(0x0DFFFF00 as *mut u16, source.as_ptr() as *mut u16, ct); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + }); +} + +/// Union type to help build/receive commands. +struct BufferData { + idx: usize, + data: BufferContents, +} +#[repr(align(4))] +union BufferContents { + uninit: (), + bits: [u16; 82], + words: [u32; 41], +} +impl BufferData { + fn new() -> Self { + BufferData { idx: 0, data: BufferContents { uninit: () } } + } + + /// Writes a bit to the output buffer. + fn write_bit(&mut self, val: u8) { + unsafe { + self.data.bits[self.idx] = val as u16; + self.idx += 1; + } + } + + /// Writes a number to the output buffer + fn write_num(&mut self, count: usize, num: u32) { + for i in 0..count { + self.write_bit(((num >> (count - 1 - i)) & 1) as u8); + } + } + + /// Reads a number from the input buffer. + fn read_num(&mut self, off: usize, count: usize) -> u32 { + let mut accum = 0; + unsafe { + for i in 0..count { + accum <<= 1; + accum |= self.data.bits[off + i] as u32; + } + } + accum + } + + /// Receives a number of words into the input buffer. + fn receive(&mut self, count: usize) { + unsafe { + dma_receive(&mut self.data.words, count); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx); + } + } +} + +/// The properties of a given EEPROM type. +struct EepromProperties { + addr_bits: usize, + byte_len: usize, +} +impl EepromProperties { + /// Reads a block from the save media. + #[allow(clippy::needless_range_loop)] + fn read_sector(&self, word: usize) -> [u8; 8] { + // Set address command. The command is two one bits, followed by the + // address, followed by a zero bit. + // + // 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. + #[allow(clippy::needless_range_loop)] + fn write_sector_raw( + &self, word: usize, block: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + // Write sector command. The command is a one bit, followed by a + // zero bit, followed by the address, followed by 64 bits of data. + // + // 512B Command: [1 0|n n n n n n|v v v v ...] + // 8KiB Command: [1 0|n n n n n n n n n n n n n n|v v v v ...] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(0); + buf.write_num(self.addr_bits, word as u32); + for i in 0..8 { + buf.write_num(8, block[i] as u32); + } + buf.write_bit(0); + buf.submit(); + + // Wait for the sector to be written for 10 milliseconds. + timeout.start(); + while PORT.get() & 1 != 1 { + if timeout.check_timeout_met(10) { + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Writes a sector to the EEPROM, keeping any current contents outside the + /// buffer's range. + fn write_sector_safe( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf, timeout) + } + + /// Writes a sector to the EEPROM. + fn write_sector( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data, timeout) + } else { + self.write_sector_safe(word, data, start, timeout) + } + } + + /// Checks whether an offset is in range. + fn check_offset(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() && (offset + len) > self.byte_len { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + + /// Implements EEPROM reads. + fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + let sector = self.read_sector(offset >> SECTOR_SHIFT); + buf[..end_len].copy_from_slice(§or[start..start + end_len]); + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Implements EEPROM verifies. + fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + if buf[..end_len] != self.read_sector(offset >> SECTOR_SHIFT) { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Implements EEPROM writes. + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } +} +const PROPS_512B: EepromProperties = EepromProperties { addr_bits: 6, byte_len: 512 }; +const PROPS_8K: EepromProperties = EepromProperties { addr_bits: 14, byte_len: 8 * 1024 }; + +/// The [`RawSaveAccess`] used for 512 byte EEPROM. +pub struct Eeprom512B; +impl RawSaveAccess for Eeprom512B { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom512B, + sector_shift: 3, + sector_count: 64, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_512B.write(offset, buffer, timeout) + } +} + +/// The [`RawSaveAccess`] used for 8 KiB EEPROM. +pub struct Eeprom8K; +impl RawSaveAccess for Eeprom8K { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom8K, + sector_shift: 3, + sector_count: 1024, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_8K.write(offset, buffer, timeout) + } +} diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs new file mode 100644 index 00000000..0384926b --- /dev/null +++ b/agb/src/save/flash.rs @@ -0,0 +1,472 @@ +//! Module for flash save media support. +//! +//! Flash may be read with ordinary read commands, but writing requires +//! sending structured commands to the flash chip. + +// TODO: Setup cartridge read timings for faster Flash access. + +use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray}; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::sync::{InitOnce, Static}; +use core::cmp; +use crate::save::utils::Timeout; + +// Volatile address ports for flash +const FLASH_PORT_BANK: MemoryMapped = unsafe { MemoryMapped::new(0x0E000000) }; +const FLASH_PORT_A: MemoryMapped = unsafe { MemoryMapped::new(0x0E005555) }; +const FLASH_PORT_B: MemoryMapped = unsafe { MemoryMapped::new(0x0E002AAA) }; +const FLASH_DATA: MemoryMapped1DArray = unsafe { MemoryMapped1DArray::new(0x0E000000) }; + +// Various constants related to sector sizes +const BANK_SHIFT: usize = 16; // 64 KiB +const BANK_LEN: usize = 1 << BANK_SHIFT; +const BANK_MASK: usize = BANK_LEN - 1; + +// Constants relating to flash commands. +const CMD_SET_BANK: u8 = 0xB0; +const CMD_READ_CHIP_ID: u8 = 0x90; +const CMD_READ_CONTENTS: u8 = 0xF0; +const CMD_WRITE: u8 = 0xA0; +const CMD_ERASE_SECTOR_BEGIN: u8 = 0x80; +const CMD_ERASE_SECTOR_CONFIRM: u8 = 0x30; +const CMD_ERASE_SECTOR_ALL: u8 = 0x10; + +/// Starts a command to the flash chip. +fn start_flash_command() { + FLASH_PORT_A.set(0xAA); + FLASH_PORT_B.set(0x55); +} + +/// Helper function for issuing commands to the flash chip. +fn issue_flash_command(c2: u8) { + start_flash_command(); + FLASH_PORT_A.set(c2); +} + +/// A simple thing to avoid excessive bank switches +static CURRENT_BANK: Static = Static::new(!0); +fn set_bank(bank: u8) -> Result<(), Error> { + if bank == 0xFF { + Err(Error::OutOfBounds) + } else if bank != CURRENT_BANK.read() { + issue_flash_command(CMD_SET_BANK); + FLASH_PORT_BANK.set(bank as u8); + CURRENT_BANK.write(bank); + Ok(()) + } else { + Ok(()) + } +} + +/// Identifies a particular f +/// lash chip in use by a Game Pak. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +#[repr(u8)] +pub enum FlashChipType { + /// 64KiB SST chip + Sst64K, + /// 64KiB Macronix chip + Macronix64K, + /// 64KiB Panasonic chip + Panasonic64K, + /// 64KiB Atmel chip + Atmel64K, + /// 128KiB Sanyo chip + Sanyo128K, + /// 128KiB Macronix chip + Macronix128K, + /// An unidentified chip + Unknown, +} +impl FlashChipType { + /// Returns the type of the flash chip currently in use. + pub fn detect() -> Result { + Ok(Self::from_id(detect_chip_id()?)) + } + + /// Determines the flash chip type from an ID. + pub fn from_id(id: u16) -> Self { + match id { + 0xD4BF => FlashChipType::Sst64K, + 0x1CC2 => FlashChipType::Macronix64K, + 0x1B32 => FlashChipType::Panasonic64K, + 0x3D1F => FlashChipType::Atmel64K, + 0x1362 => FlashChipType::Sanyo128K, + 0x09C2 => FlashChipType::Macronix128K, + _ => FlashChipType::Unknown, + } + } +} + +/// Determines the raw ID of the flash chip currently in use. +pub fn detect_chip_id() -> Result { + issue_flash_command(CMD_READ_CHIP_ID); + let high = unsafe { read_raw_byte(0x0E000001) }; + let low = unsafe { read_raw_byte(0x0E000000) }; + let id = (high as u16) << 8 | low as u16; + issue_flash_command(CMD_READ_CONTENTS); + Ok(id) +} + +/// Information relating to a particular flash chip that could be found in a +/// Game Pak. +#[allow(dead_code)] +struct ChipInfo { + /// The wait state required to read from the chip. + read_wait: u8, + /// The wait state required to write to the chip. + write_wait: u8, + + /// The timeout in milliseconds for writes to this chip. + write_timeout: u16, + /// The timeout in milliseconds for erasing a sector in this chip. + erase_sector_timeout: u16, + /// The timeout in milliseconds for erasing the entire chip. + erase_chip_timeout: u16, + + /// The number of 64KiB banks in this chip. + bank_count: u8, + /// Whether this is an Atmel chip, which has 128 byte sectors instead of 4K. + uses_atmel_api: bool, + /// Whether this is an Macronix chip, which requires an additional command + /// to cancel the current action after a timeout. + requires_cancel_command: bool, + + /// The [`MediaInfo`] to return for this chip type. + info: &'static MediaInfo, +} + +// Media info for the various chipsets. +static INFO_64K: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 12, // 4 KiB + sector_count: 16, // 4 KiB * 16 = 64 KiB + uses_prepare_write: true, +}; +static INFO_64K_ATMEL: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 7, // 128 bytes + sector_count: 512, // 128 bytes * 512 = 64 KiB + uses_prepare_write: false, +}; +static INFO_128K: MediaInfo = MediaInfo { + media_type: MediaType::Flash128K, + sector_shift: 12, + sector_count: 32, // 4 KiB * 32 = 128 KiB + uses_prepare_write: true, +}; + +// Chip info for the various chipsets. +static CHIP_INFO_SST_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 1, // 3 cycles + write_timeout: 10, + erase_sector_timeout: 40, + erase_chip_timeout: 200, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_MACRONIX_64K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_64K, +}; +static CHIP_INFO_PANASONIC_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 0, // 4 cycles + write_timeout: 10, + erase_sector_timeout: 500, + erase_chip_timeout: 500, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_ATMEL_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 40, + erase_chip_timeout: 40, + bank_count: 1, + uses_atmel_api: true, + requires_cancel_command: false, + info: &INFO_64K_ATMEL, +}; +static CHIP_INFO_GENERIC_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_128K, +}; +static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 2, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_128K, +}; + +impl FlashChipType { + /// Returns the internal info for this chip. + fn chip_info(self) -> &'static ChipInfo { + match self { + FlashChipType::Sst64K => &CHIP_INFO_SST_64K, + FlashChipType::Macronix64K => &CHIP_INFO_MACRONIX_64K, + FlashChipType::Panasonic64K => &CHIP_INFO_PANASONIC_64K, + FlashChipType::Atmel64K => &CHIP_INFO_ATMEL_64K, + FlashChipType::Sanyo128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Macronix128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Unknown => &CHIP_INFO_GENERIC_64K, + } + } +} +static CHIP_INFO: InitOnce<&'static ChipInfo> = InitOnce::new(); +fn cached_chip_info() -> Result<&'static ChipInfo, Error> { + CHIP_INFO + .try_get(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) }) + .map(Clone::clone) +} + +/// Actual implementation of the ChipInfo functions. +impl ChipInfo { + /// Returns the total length of this chip. + fn total_len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + // Checks whether a byte offset is in bounds. + fn check_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.total_len() { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + // Checks whether a sector offset is in bounds. + fn check_sector_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.info.sector_count { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + /// Sets the currently active bank. + fn set_bank(&self, bank: usize) -> Result<(), Error> { + if bank >= self.bank_count as usize { + Err(Error::OutOfBounds) + } else if self.bank_count > 1 { + set_bank(bank as u8) + } else { + Ok(()) + } + } + + /// Reads a buffer from save media into memory. + fn read_buffer(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + while !buf.is_empty() { + 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.is_empty() { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + if !unsafe { verify_raw_buf(&buf[..end_len], 0x0E000000 + start) } { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Waits for a timeout, or an operation to complete. + fn wait_for_timeout( + &self, offset: usize, val: u8, ms: u16, timeout: &mut Timeout, + ) -> Result<(), Error> { + timeout.start(); + let offset = 0x0E000000 + offset; + + while unsafe { read_raw_byte(offset) != val } { + if timeout.check_timeout_met(ms) { + if self.requires_cancel_command { + FLASH_PORT_A.set(0xF0); + } + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Erases a sector to flash. + fn erase_sector(&self, sector: usize, timeout: &mut Timeout) -> Result<(), Error> { + let offset = sector << self.info.sector_shift; + self.set_bank(offset >> BANK_SHIFT)?; + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + start_flash_command(); + FLASH_DATA.set(offset & BANK_MASK, CMD_ERASE_SECTOR_CONFIRM); + self.wait_for_timeout(offset & BANK_MASK, 0xFF, self.erase_sector_timeout, timeout) + } + + /// Erases the entire chip. + fn erase_chip(&self, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + issue_flash_command(CMD_ERASE_SECTOR_ALL); + self.wait_for_timeout(0, 0xFF, 3000, timeout) + } + + /// Writes a byte to the save media. + fn write_byte(&self, offset: usize, byte: u8, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_WRITE); + FLASH_DATA.set(offset, byte); + self.wait_for_timeout(offset, byte, self.write_timeout, timeout) + } + + /// Writes an entire buffer to the save media. + #[allow(clippy::needless_range_loop)] + fn write_buffer(&self, offset: usize, buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.set_bank(offset >> BANK_SHIFT)?; + for i in 0..buf.len() { + let byte_off = offset + i; + if (byte_off & BANK_MASK) == 0 { + self.set_bank(byte_off >> BANK_SHIFT)?; + } + self.write_byte(byte_off & BANK_MASK, buf[i], timeout)?; + } + Ok(()) + } + + /// Erases and writes an entire 128b sector on Atmel devices. + #[allow(clippy::needless_range_loop)] + fn write_atmel_sector_raw( + &self, offset: usize, buf: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + crate::interrupt::free(|_| { + issue_flash_command(CMD_WRITE); + for i in 0..128 { + FLASH_DATA.set(offset + i, buf[i]); + } + self.wait_for_timeout(offset + 127, buf[127], self.erase_sector_timeout, timeout) + })?; + Ok(()) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + #[inline(never)] // avoid allocating the 128 byte buffer for no reason. + fn write_atmel_sector_safe( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut sector = [0u8; 128]; + self.read_buffer(offset, &mut sector[0..start])?; + sector[start..start + buf.len()].copy_from_slice(buf); + self.read_buffer(offset + start + buf.len(), &mut sector[start + buf.len()..128])?; + self.write_atmel_sector_raw(offset, §or, timeout) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + /// + /// This avoids allocating stack if there is no need to. + fn write_atmel_sector( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if start == 0 && buf.len() == 128 { + self.write_atmel_sector_raw(offset, buf, timeout) + } else { + self.write_atmel_sector_safe(offset, buf, start, timeout) + } + } +} + +/// The [`RawSaveAccess`] used for flash save media. +pub struct FlashAccess; +impl RawSaveAccess for FlashAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(cached_chip_info()?.info) + } + + fn read(&self, offset: usize, buf: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.read_buffer(offset, buf) + } + + fn verify(&self, offset: usize, buf: &[u8], _: &mut Timeout) -> Result { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.verify_buffer(offset, buf) + } + + fn prepare_write( + &self, sector: usize, count: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_sector_len(sector, count)?; + + if chip.uses_atmel_api { + Ok(()) + } else if count == chip.info.sector_count { + chip.erase_chip(timeout) + } else { + for i in sector..sector + count { + chip.erase_sector(i, timeout)?; + } + Ok(()) + } + } + + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + if chip.uses_atmel_api { + while !buf.is_empty() { + let start = offset & 127; + let end_len = cmp::min(128 - start, buf.len()); + chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } else { + // Write the bytes one by one. + chip.write_buffer(offset, buf, timeout)?; + Ok(()) + } + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs new file mode 100644 index 00000000..485cc832 --- /dev/null +++ b/agb/src/save/mod.rs @@ -0,0 +1,457 @@ +//! Module for reading and writing to save media. +//! +//! ## 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 [`init_sram`]. +//! * For 64 KiB flash memory, call [`init_flash_64k`]. +//! * For 128 KiB flash memory, call [`init_flash_128k`]. +//! * For 512 byte EEPROM, call [`init_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`init_eeprom_8k`]. +//! +//! [`init_sram`]: SaveManager::init_sram +//! [`init_flash_64k`]: SaveManager::init_flash_64k +//! [`init_flash_128k`]: SaveManager::init_flash_128k +//! [`init_eeprom_512b`]: SaveManager::init_eeprom_512b +//! [`init_eeprom_8k`]: SaveManager::init_eeprom_8k +//! +//! ## Using save media +//! +//! To access save media, use the [`SaveData::new`] method to create a new +//! [`SaveData`] object. Its methods are used to read or write save media. +//! +//! Reading data from the savegame is simple. Use [`read`] to copy data from an +//! offset in the savegame into a buffer in memory. +//! +//! Writing to save media requires you to prepare the area for writing by calling +//! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains +//! the actual [`write`] method. +//! +//! The `prepare_write` method leaves everything in a sector that overlaps the +//! range passed to it in an implementation defined state. On some devices it may +//! do nothing, and on others, it may clear the entire range to `0xFF`. +//! +//! Because writes can only be prepared on a per-sector basis, a clear on a range +//! of `4000..5000` on a device with 4096 byte sectors will actually clear a range +//! of `0..8192`. Use [`sector_size`] to find the sector size, or [`align_range`] +//! to directly calculate the range of memory that will be affected by the clear. +//! +//! [`read`]: SaveData::read +//! [`prepare_write`]: SaveData::prepare_write +//! [`write`]: SavePreparedBlock::write +//! [`sector_size`]: SaveAccess::sector_size +//! [`align_range`]: SaveAccess::align_range +//! +//! ## Performance and Other Details +//! +//! The performance characteristics of the media types are as follows: +//! +//! * SRAM is simply a form of battery backed memory, and has no particular +//! performance characteristics. Reads and writes at any alignment are +//! efficient. Furthermore, no timer is needed for accesses to this type of +//! media. `prepare_write` does not immediately erase any data. +//! * Non-Atmel flash chips have a sector size of 4096 bytes. Reads and writes +//! to any alignment are efficient, however, `prepare_write` will erase all +//! data in an entire sector before writing. +//! * Atmel flash chips have a sector size of 128 bytes. Reads to any alignment +//! are efficient, however, unaligned writes are extremely slow. +//! `prepare_write` does not immediately erase any data. +//! * EEPROM has a sector size of 8 bytes. Unaligned reads and writes are +//! slower than aligned writes, however, this is easily mitigated by the +//! small sector size. + +use core::ops::Range; +use crate::save::utils::Timeout; +use crate::sync::{Mutex, RawMutexGuard}; +use crate::timer::Timer; + +mod asm_utils; +mod eeprom; +mod flash; +mod sram; +mod utils; + +/// A list of save media types. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[non_exhaustive] +pub enum MediaType { + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, +} + +/// 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)] +#[non_exhaustive] +pub struct MediaInfo { + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, +} +impl MediaInfo { + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + #[must_use] + pub fn sector_size(&self) -> usize { + 1 << self.sector_shift + } + + /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false + pub fn len(&self) -> usize { + self.sector_count << self.sector_shift + } +} + +/// A trait allowing low-level saving and writing to save media. +trait RawSaveAccess: Sync { + fn info(&self) -> Result<&'static MediaInfo, Error>; + fn read(&self, offset: usize, buffer: &mut [u8], timeout: &mut Timeout) -> Result<(), Error>; + fn verify(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result; + fn prepare_write(&self, sector: usize, count: usize, timeout: &mut Timeout) -> Result<(), Error>; + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>; +} + +static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); + +fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); +} + +fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + *CURRENT_SAVE_ACCESS.lock() +} + +/// Allows reading and writing of save media. +pub struct SaveData { + _lock: RawMutexGuard<'static>, + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, + timeout: utils::Timeout, +} +impl SaveData { + /// Creates a new save accessor around the current save implementaiton. + fn new(timer: Option) -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveData { + _lock: utils::lock_media_access()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), + None => Err(Error::NoMedia), + } + } + + /// Returns the media info underlying this accessor. + #[must_use] + pub fn media_info(&self) -> &'static MediaInfo { + self.info + } + + /// Returns the save media type being used. + #[must_use] + pub fn media_type(&self) -> MediaType { + self.info.media_type + } + + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + #[must_use] + pub fn sector_size(&self) -> usize { + self.info.sector_size() + } + + /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false + pub fn len(&self) -> usize { + self.info.len() + } + + fn check_bounds(&self, range: Range) -> Result<(), Error> { + if range.start >= self.len() || range.end > self.len() { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> { + self.check_bounds(offset..(offset + len)) + } + + /// Copies data from the save media to a buffer. + /// + /// If an error is returned, the contents of the buffer are unpredictable. + pub fn read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.check_bounds_len(offset, buffer.len())?; + self.access.read(offset, buffer, &mut self.timeout) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&mut self, offset: usize, buffer: &[u8]) -> Result { + self.check_bounds_len(offset, buffer.len())?; + self.access.verify(offset, buffer, &mut self.timeout) + } + + /// 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`) + #[must_use] + pub fn align_range(&self, range: Range) -> Range { + let shift = self.info.sector_shift; + let mask = (1 << shift) - 1; + (range.start & !mask)..((range.end + mask) & !mask) + } + + /// Prepares a given span of offsets for writing. + /// + /// This will erase any data in any sector overlapping the input range. To + /// calculate which offset ranges would be affected, use the + /// [`align_range`](`SaveAccess::align_range`) function. + pub fn prepare_write(&mut self, range: Range) -> Result { + self.check_bounds(range.clone())?; + if self.info.uses_prepare_write { + let range = self.align_range(range.clone()); + let shift = self.info.sector_shift; + self.access.prepare_write( + range.start >> shift, range.len() >> shift, &mut self.timeout, + )?; + } + Ok(SavePreparedBlock { + parent: self, + range + }) + } +} + +/// A block of save memory that has been prepared for writing. +pub struct SavePreparedBlock<'a> { + parent: &'a mut SaveData, + range: Range, +} +impl<'a> SavePreparedBlock<'a> { + /// Writes a given buffer into the save media. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// call to `prepare_write` will leave the save data in an unpredictable + /// state. If an error is returned, the contents of the save media is + /// unpredictable. + pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + if buffer.is_empty() { + Ok(()) + } else if !self.range.contains(&offset) || + !self.range.contains(&(offset + buffer.len() - 1)) { + Err(Error::OutOfBounds) + } else { + self.parent.access.write(offset, buffer, &mut self.parent.timeout) + } + } + + /// Writes and validates a given buffer into the save media. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// call to `prepare_write` will leave the save data in an unpredictable + /// state. If an error is returned, the contents of the save media is + /// unpredictable. + pub fn write_and_verify(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.write(offset, buffer)?; + if !self.parent.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} + +mod marker { + #[repr(align(4))] + struct Align(T); + + static EEPROM: Align<[u8; 12]> = Align(*b"EEPROM_Vnnn\0"); + static SRAM: Align<[u8; 12]> = Align(*b"SRAM_Vnnn\0\0\0"); + static FLASH512K: Align<[u8; 16]> = Align(*b"FLASH512_Vnnn\0\0\0"); + static FLASH1M: Align<[u8; 16]> = Align(*b"FLASH1M_Vnnn\0\0\0\0"); + + #[inline(always)] + pub fn emit_eeprom_marker() { + crate::sync::memory_read_hint(&EEPROM); + } + #[inline(always)] + pub fn emit_sram_marker() { + crate::sync::memory_read_hint(&SRAM); + } + #[inline(always)] + pub fn emit_flash_512k_marker() { + crate::sync::memory_read_hint(&FLASH512K); + } + #[inline(always)] + pub fn emit_flash_1m_marker() { + crate::sync::memory_read_hint(&FLASH1M); + } +} + +/// Allows access to the cartridge's save data. +#[non_exhaustive] +pub struct SaveManager {} +impl SaveManager { + pub(crate) const fn new() -> Self { + SaveManager {} + } + + /// Declares that the ROM uses battery backed SRAM/FRAM. + /// + /// Battery Backed SRAM is generally very fast, but limited in size compared + /// to flash chips. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_sram(&mut self) { + marker::emit_sram_marker(); + set_save_implementation(&sram::BatteryBackedAccess); + } + + /// Declares that the ROM uses 64KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_64k(&mut self) { + marker::emit_flash_512k_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 128KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_128k(&mut self) { + marker::emit_flash_1m_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 512 bytes EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_512b(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom512B); + } + + /// Declares that the ROM uses 8 KiB EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_8k(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom8K); + } + + /// Creates a new accessor to the save data. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access(&mut self) -> Result { + SaveData::new(None) + } + + /// Creates a new accessor to the save data that uses the given timer for timeouts. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access_with_timer(&mut self, timer: Timer) -> Result { + SaveData::new(Some(timer)) + } +} \ No newline at end of file diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs new file mode 100644 index 00000000..614b751e --- /dev/null +++ b/agb/src/save/sram.rs @@ -0,0 +1,57 @@ +//! Module for battery backed SRAM save media support. +//! +//! SRAM acts as ordinary memory mapped into the memory space, and as such +//! is accessed using normal memory read/write commands. + +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::save::utils::Timeout; + +const SRAM_SIZE: usize = 32 * 1024; // 32 KiB + +/// Checks whether an offset is contained within the bounds of the SRAM. +fn check_bounds(offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() || offset + len > SRAM_SIZE { + return Err(Error::OutOfBounds); + } + Ok(()) +} + +/// The [`RawSaveAccess`] used for battery backed SRAM. +pub struct BatteryBackedAccess; +impl RawSaveAccess for BatteryBackedAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Sram32K, + sector_shift: 0, + sector_count: SRAM_SIZE, + uses_prepare_write: false, + }) + } + + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + read_raw_buf(buffer, 0x0E000000 + offset); + } + Ok(()) + } + + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + check_bounds(offset, buffer.len())?; + let val = unsafe { verify_raw_buf(buffer, 0x0E000000 + offset) }; + Ok(val) + } + + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + + fn write(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + write_raw_buf(0x0E000000 + offset, buffer); + } + Ok(()) + } +} diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs new file mode 100644 index 00000000..b94cb3fe --- /dev/null +++ b/agb/src/save/utils.rs @@ -0,0 +1,59 @@ +//! A package containing useful utilities for writing save accessors. + +use super::Error; +use crate::sync::{RawMutex, RawMutexGuard}; +use crate::timer::{Timer, Divider}; + +/// A timeout type used to prevent hardware errors in save media from hanging +/// the game. +pub struct Timeout { + timer: Option, +} +impl Timeout { + /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. + /// + /// ## Errors + /// + /// If another timeout has already been created. + #[inline(never)] + pub fn new(timer: Option) -> Self { + Timeout { timer } + } + + /// Starts this timeout. + pub fn start(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + timer.set_divider(Divider::Divider1024); + timer.set_interrupt(false); + timer.set_overflow_amount(0xFFFF); + timer.set_cascade(false); + timer.set_enabled(true); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`Timeout::start()`]. + pub fn check_timeout_met(&self, check_ms: u16) -> bool { + if let Some(timer) = &self.timer { + check_ms * 17 < timer.value() + } else { + false + } + } +} +impl Drop for Timeout { + fn drop(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + } + } +} + +pub fn lock_media_access() -> Result, Error> { + static LOCK: RawMutex = RawMutex::new(); + match LOCK.try_lock() { + Some(x) => Ok(x), + None => Err(Error::MediaInUse), + } +} diff --git a/agb/src/sync/statics.rs b/agb/src/sync/statics.rs index 3970589f..62ec9d1d 100644 --- a/agb/src/sync/statics.rs +++ b/agb/src/sync/statics.rs @@ -284,7 +284,7 @@ mod test { // the actual main test loop let mut interrupt_seen = false; let mut no_interrupt_seen = false; - for i in 0..100000 { + for i in 0..250000 { // write to the static let new_value = [i; COUNT]; value.write(new_value); diff --git a/agb/tests/save_test_common/mod.rs b/agb/tests/save_test_common/mod.rs new file mode 100644 index 00000000..d3f823c2 --- /dev/null +++ b/agb/tests/save_test_common/mod.rs @@ -0,0 +1,105 @@ +use core::cmp; +use agb::save::{Error, MediaInfo}; +use agb::sync::InitOnce; + +fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo { + static ONCE: InitOnce = InitOnce::new(); + ONCE.get(|| { + crate::save_setup(gba); + gba.save.access().unwrap().media_info().clone() + }) +} + +#[derive(Clone)] +struct Rng(u32); +impl Rng { + fn iter(&mut self) { + self.0 = self.0.wrapping_mul(2891336453).wrapping_add(100001); + } + fn next_u8(&mut self) -> u8 { + self.iter(); + (self.0 >> 22) as u8 ^ self.0 as u8 + } + fn next_under(&mut self, under: u32) -> u32 { + self.iter(); + let scale = 31 - under.leading_zeros(); + ((self.0 >> scale) ^ self.0) % under + } +} + +const MAX_BLOCK_SIZE: usize = 4 * 1024; + +#[allow(clippy::needless_range_loop)] +fn do_test( + gba: &mut agb::Gba, seed: Rng, offset: usize, len: usize, block_size: usize, +) -> Result<(), Error> { + let mut buffer = [0; MAX_BLOCK_SIZE]; + + let timers = gba.timers.timers(); + let mut access = gba.save.access_with_timer(timers.timer2)?; + + // writes data to the save media + let mut prepared = access.prepare_write(offset..offset + len)?; + let mut rng = seed.clone(); + let mut current = offset; + let end = offset + len; + while current != end { + let cur_len = cmp::min(end - current, block_size); + for i in 0..cur_len { + buffer[i] = rng.next_u8(); + } + prepared.write(current, &buffer[..cur_len])?; + current += cur_len; + } + + // validates the save media + rng = seed; + current = offset; + while current != end { + let cur_len = cmp::min(end - current, block_size); + access.read(current, &mut buffer[..cur_len])?; + for i in 0..cur_len { + let cur_byte = rng.next_u8(); + assert_eq!( + buffer[i], cur_byte, + "Read does not match earlier write: {} != {} @ 0x{:05x}", + buffer[i], cur_byte, current + i, + ); + } + current += cur_len; + } + + Ok(()) +} + +#[test_case] +fn test_4k_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + + if info.len() >= (1 << 12) { + do_test(gba, Rng(2000), 0, info.len(), 4 * 1024).expect("Test encountered error"); + } +} + +#[test_case] +fn test_512b_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + do_test(gba, Rng(1000), 0, info.len(), 512).expect("Test encountered error"); +} + +#[test_case] +fn test_partial_writes(gba: &mut agb::Gba) { + let info = init_sram(gba); + + // test with random segments now. + let mut rng = Rng(12345); + for i in 0..8 { + let rand_length = rng.next_under((info.len() >> 1) as u32) as usize + 50; + let rand_offset = rng.next_under(info.len() as u32 - rand_length as u32) as usize; + let block_size = cmp::min(rand_length >> 2, MAX_BLOCK_SIZE - 100); + let block_size = rng.next_under(block_size as u32) as usize + 50; + + do_test(gba, Rng(i * 10000), rand_offset, rand_length, block_size) + .expect("Test encountered error"); + } +} \ No newline at end of file diff --git a/agb/tests/test_save_eeprom_512b.rs b/agb/tests/test_save_eeprom_512b.rs new file mode 100644 index 00000000..19c3f6c9 --- /dev/null +++ b/agb/tests/test_save_eeprom_512b.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_512b(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_eeprom_8k.rs b/agb/tests/test_save_eeprom_8k.rs new file mode 100644 index 00000000..95677321 --- /dev/null +++ b/agb/tests/test_save_eeprom_8k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_8k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_flash_128k.rs b/agb/tests/test_save_flash_128k.rs new file mode 100644 index 00000000..7256ddfb --- /dev/null +++ b/agb/tests/test_save_flash_128k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_128k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_flash_64k.rs b/agb/tests/test_save_flash_64k.rs new file mode 100644 index 00000000..6c179ae3 --- /dev/null +++ b/agb/tests/test_save_flash_64k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_64k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_sram.rs b/agb/tests/test_save_sram.rs new file mode 100644 index 00000000..f3348eda --- /dev/null +++ b/agb/tests/test_save_sram.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_sram(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs index 9acf3c04..9ec4cc1a 100644 --- a/examples/hyperspace-roll/src/main.rs +++ b/examples/hyperspace-roll/src/main.rs @@ -96,10 +96,10 @@ struct Agb<'a> { } fn main(mut gba: agb::Gba) -> ! { - save::init_save(); + save::init_save(&mut gba).expect("Could not initialize save game"); if save::load_high_score() > 1000 { - save::save_high_score(0); + save::save_high_score(&mut gba, 0).expect("Could not reset high score"); } let gfx = gba.display.object.get(); @@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! { agb.obj.commit(); agb.sfx.customise(); if save::load_high_score() < current_level { - save::save_high_score(current_level); + save::save_high_score(&mut gba, current_level) + .expect("Could not save high score"); } break; } diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs index e5df03e8..0db839de 100644 --- a/examples/hyperspace-roll/src/save.rs +++ b/examples/hyperspace-roll/src/save.rs @@ -1,44 +1,42 @@ -use agb::interrupt::free; -use bare_metal::Mutex; -use core::cell::RefCell; +use agb::Gba; +use agb::save::Error; +use agb::sync::Static; -const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; -const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1); +static HIGHSCORE: Static = Static::new(0); -static HIGHSCORE: Mutex> = Mutex::new(RefCell::new(0)); +pub fn init_save(gba: &mut Gba) -> Result<(), Error> { + gba.save.init_sram(); -pub fn init_save() { - if (unsafe { RAM_ADDRESS.read_volatile() } == !0) { - save_high_score(0); - unsafe { RAM_ADDRESS.write_volatile(0) }; - } + let mut access = gba.save.access()?; - let mut a = [0; 4]; - for (idx, a) in a.iter_mut().enumerate() { - *a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() }; - } + let mut buffer = [0; 1]; + access.read(0, &mut buffer)?; - let high_score = u32::from_le_bytes(a); + if buffer[0] != 0 { + access.prepare_write(0..1)?.write(0, &[0])?; + core::mem::drop(access); + save_high_score(gba, 0)?; + } else { + let mut buffer = [0; 4]; + access.read(1, &mut buffer)?; + let high_score = u32::from_le_bytes(buffer); - free(|cs| { if high_score > 100 { - HIGHSCORE.borrow(cs).replace(0); + HIGHSCORE.write(0) } else { - HIGHSCORE.borrow(cs).replace(high_score); + HIGHSCORE.write(high_score) } - }); + } + + Ok(()) } pub fn load_high_score() -> u32 { - free(|cs| *HIGHSCORE.borrow(cs).borrow()) + HIGHSCORE.read() } -pub fn save_high_score(score: u32) { - let a = score.to_le_bytes(); - - for (idx, &a) in a.iter().enumerate() { - unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) }; - } - - free(|cs| HIGHSCORE.borrow(cs).replace(score)); +pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> { + gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?; + HIGHSCORE.write(score); + Ok(()) }