mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-24 08:41:34 +11:00
Implement EEPROM save media.
Fix EEPROM implementation.
This commit is contained in:
parent
8dd0f4768a
commit
821098dd26
16
agb-tests/tests/test_save_eeprom_512b.rs
Normal file
16
agb-tests/tests/test_save_eeprom_512b.rs
Normal file
|
@ -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 {}
|
||||||
|
}
|
16
agb-tests/tests/test_save_eeprom_8k.rs
Normal file
16
agb-tests/tests/test_save_eeprom_8k.rs
Normal file
|
@ -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 {}
|
||||||
|
}
|
|
@ -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));
|
DMA3_CONTROL.set(count as u32 | (1 << 31));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dma3_exclusive<R>(f: impl FnOnce() -> R) -> R {
|
||||||
|
const DMA0_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) };
|
||||||
|
const DMA1_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) };
|
||||||
|
const DMA2_CTRL_HI: MemoryMapped<u16> = 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
|
||||||
|
})
|
||||||
|
}
|
271
agb/src/save/eeprom.rs
Normal file
271
agb/src/save/eeprom.rs
Normal file
|
@ -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<u16> = 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<bool, 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());
|
||||||
|
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<bool, Error> {
|
||||||
|
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<bool, Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,7 +108,7 @@ use crate::sync::{Mutex, RawMutexGuard};
|
||||||
use crate::timer::Timer;
|
use crate::timer::Timer;
|
||||||
|
|
||||||
mod asm_utils;
|
mod asm_utils;
|
||||||
//pub mod eeprom;
|
mod eeprom;
|
||||||
mod flash;
|
mod flash;
|
||||||
mod sram;
|
mod sram;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
@ -420,6 +420,36 @@ impl SaveManager {
|
||||||
set_save_implementation(&flash::FlashAccess);
|
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.
|
/// Creates a new accessor to the save data.
|
||||||
///
|
///
|
||||||
/// You must have initialized the save manager beforehand to use a specific
|
/// You must have initialized the save manager beforehand to use a specific
|
||||||
|
|
|
@ -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<RawMutexGuard<'static>, Error> {
|
pub fn lock_media_access() -> Result<RawMutexGuard<'static>, Error> {
|
||||||
static LOCK: RawMutex = RawMutex::new();
|
static LOCK: RawMutex = RawMutex::new();
|
||||||
|
|
Loading…
Reference in a new issue