diff --git a/agb-tests/tests/test_save_eeprom_512b.rs b/agb-tests/tests/test_save_eeprom_512b.rs new file mode 100644 index 00000000..19c3f6c9 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_512b.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_512b(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-tests/tests/test_save_eeprom_8k.rs b/agb-tests/tests/test_save_eeprom_8k.rs new file mode 100644 index 00000000..95677321 --- /dev/null +++ b/agb-tests/tests/test_save_eeprom_8k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_8k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 71651cfc..1bf1b0bd 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) { DMA3_CONTROL.set(count as u32 | (1 << 31)); } + +pub(crate) fn dma3_exclusive(f: impl FnOnce() -> R) -> R { + const DMA0_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) }; + const DMA1_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; + const DMA2_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; + + crate::interrupt::free(|_| { + let dma0_ctl = DMA0_CTRL_HI.get(); + let dma1_ctl = DMA1_CTRL_HI.get(); + let dma2_ctl = DMA2_CTRL_HI.get(); + DMA0_CTRL_HI.set(dma0_ctl & !(1 << 15)); + DMA1_CTRL_HI.set(dma1_ctl & !(1 << 15)); + DMA2_CTRL_HI.set(dma2_ctl & !(1 << 15)); + + // Executes the body of the function with DMAs and IRQs disabled. + let ret = f(); + + // Continues higher priority DMAs if they were enabled before. + DMA0_CTRL_HI.set(dma0_ctl); + DMA1_CTRL_HI.set(dma1_ctl); + DMA2_CTRL_HI.set(dma2_ctl); + + // returns the return value + ret + }) +} \ No newline at end of file diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs new file mode 100644 index 00000000..51753d50 --- /dev/null +++ b/agb/src/save/eeprom.rs @@ -0,0 +1,271 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use crate::memory_mapped::MemoryMapped; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::utils::Timeout; +use core::cmp; + +const PORT: MemoryMapped = unsafe { MemoryMapped::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + crate::dma::dma_copy16(source.as_ptr() as *mut u16, 0x0DFFFF00 as *mut u16, ct); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + crate::dma::dma_copy16(0x0DFFFF00 as *mut u16, source.as_ptr() as *mut u16, ct); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + }); +} + +/// Union type to help build/receive commands. +struct BufferData { + idx: usize, + data: BufferContents, +} +#[repr(align(4))] +union BufferContents { + uninit: (), + bits: [u16; 82], + words: [u32; 41], +} +impl BufferData { + fn new() -> Self { + BufferData { idx: 0, data: BufferContents { uninit: () } } + } + + /// Writes a bit to the output buffer. + fn write_bit(&mut self, val: u8) { + unsafe { + self.data.bits[self.idx] = val as u16; + self.idx += 1; + } + } + + /// Writes a number to the output buffer + fn write_num(&mut self, count: usize, num: u32) { + for i in 0..count { + self.write_bit(((num >> (count - 1 - i)) & 1) as u8); + } + } + + /// Reads a number from the input buffer. + fn read_num(&mut self, off: usize, count: usize) -> u32 { + let mut accum = 0; + unsafe { + for i in 0..count { + accum <<= 1; + accum |= self.data.bits[off + i] as u32; + } + } + accum + } + + /// Receives a number of words into the input buffer. + fn receive(&mut self, count: usize) { + unsafe { + dma_receive(&mut self.data.words, count); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx); + } + } +} + +/// The properties of a given EEPROM type. +struct EepromProperties { + addr_bits: usize, + byte_len: usize, +} +impl EepromProperties { + /// Reads a block from the save media. + fn read_sector(&self, word: usize) -> [u8; 8] { + // Set address command. The command is two one bits, followed by the + // address, followed by a zero bit. + // + // 512B Command: [1 1|n n n n n n|0] + // 8KiB Command: [1 1|n n n n n n n n n n n n n n|0] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(1); + buf.write_num(self.addr_bits, word as u32); + buf.write_bit(0); + buf.submit(); + + // Receive the buffer data. The EEPROM sends 3 irrelevant bits followed + // by 64 data bits. + buf.receive(68); + let mut out = [0; 8]; + for i in 0..8 { + out[i] = buf.read_num(4 + i * 8, 8) as u8; + } + out + } + + /// Writes a sector directly. + fn write_sector_raw( + &self, word: usize, block: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + // Write sector command. The command is a one bit, followed by a + // zero bit, followed by the address, followed by 64 bits of data. + // + // 512B Command: [1 0|n n n n n n|v v v v ...] + // 8KiB Command: [1 0|n n n n n n n n n n n n n n|v v v v ...] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(0); + buf.write_num(self.addr_bits, word as u32); + for i in 0..8 { + buf.write_num(8, block[i] as u32); + } + buf.write_bit(0); + buf.submit(); + + // Wait for the sector to be written for 10 milliseconds. + timeout.start(); + while PORT.get() & 1 != 1 { + if timeout.check_timeout_met(10) { + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Writes a sector to the EEPROM, keeping any current contents outside the + /// buffer's range. + fn write_sector_safe( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf, timeout) + } + + /// Writes a sector to the EEPROM. + fn write_sector( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data, timeout) + } else { + self.write_sector_safe(word, data, start, timeout) + } + } + + /// Checks whether an offset is in range. + fn check_offset(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() && (offset + len) > self.byte_len { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + + /// Implements EEPROM reads. + fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + let sector = self.read_sector(offset >> SECTOR_SHIFT); + buf[..end_len].copy_from_slice(§or[start..start + end_len]); + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Implements EEPROM verifies. + fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + if &buf[..end_len] != &self.read_sector(offset >> SECTOR_SHIFT) { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Implements EEPROM writes. + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while buf.len() != 0 { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } +} +const PROPS_512B: EepromProperties = EepromProperties { addr_bits: 6, byte_len: 512 }; +const PROPS_8K: EepromProperties = EepromProperties { addr_bits: 14, byte_len: 8 * 1024 }; + +/// The [`RawSaveAccess`] used for 512 byte EEPROM. +pub struct Eeprom512B; +impl RawSaveAccess for Eeprom512B { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom512B, + sector_shift: 3, + sector_count: 64, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_512B.write(offset, buffer, timeout) + } +} + +/// The [`RawSaveAccess`] used for 8 KiB EEPROM. +pub struct Eeprom8K; +impl RawSaveAccess for Eeprom8K { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom8K, + sector_shift: 3, + sector_count: 1024, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_8K.write(offset, buffer, timeout) + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs index 34195676..8639666d 100644 --- a/agb/src/save/mod.rs +++ b/agb/src/save/mod.rs @@ -108,7 +108,7 @@ use crate::sync::{Mutex, RawMutexGuard}; use crate::timer::Timer; mod asm_utils; -//pub mod eeprom; +mod eeprom; mod flash; mod sram; mod utils; @@ -420,6 +420,36 @@ impl SaveManager { set_save_implementation(&flash::FlashAccess); } + /// Declares that the ROM uses 512 bytes EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_512b(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom512B); + } + + /// Declares that the ROM uses 8 KiB EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_8k(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom8K); + } + /// Creates a new accessor to the save data. /// /// You must have initialized the save manager beforehand to use a specific diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs index a19879c2..b94cb3fe 100644 --- a/agb/src/save/utils.rs +++ b/agb/src/save/utils.rs @@ -42,6 +42,13 @@ impl Timeout { } } } +impl Drop for Timeout { + fn drop(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + } + } +} pub fn lock_media_access() -> Result, Error> { static LOCK: RawMutex = RawMutex::new();