mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-24 00:31:34 +11:00
Merge pull request #314 from gwilymk/lymia-saves
Add save support (copy of 298 with fixed tests)
This commit is contained in:
commit
c699709aff
|
@ -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 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.
|
- 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 a new agb::sync module that contains GBA-specific synchronization primitives.
|
||||||
|
- Added support for save files.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts.
|
- Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts.
|
||||||
|
|
|
@ -7,6 +7,7 @@ fn main() {
|
||||||
"src/sound/mixer/mixer.s",
|
"src/sound/mixer/mixer.s",
|
||||||
"src/agbabi/memset.s",
|
"src/agbabi/memset.s",
|
||||||
"src/agbabi/memcpy.s",
|
"src/agbabi/memcpy.s",
|
||||||
|
"src/save/asm_routines.s",
|
||||||
];
|
];
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=gba.ld");
|
println!("cargo:rerun-if-changed=gba.ld");
|
||||||
|
|
|
@ -13,3 +13,19 @@
|
||||||
.size \functionName,.-\functionName
|
.size \functionName,.-\functionName
|
||||||
.endfunc
|
.endfunc
|
||||||
.endm
|
.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
|
||||||
|
|
|
@ -36,5 +36,8 @@ mod tests {
|
||||||
display_logo(&mut map, &mut vram);
|
display_logo(&mut map, &mut vram);
|
||||||
|
|
||||||
crate::test_runner::assert_image_output("gfx/test_logo.png");
|
crate::test_runner::assert_image_output("gfx/test_logo.png");
|
||||||
|
|
||||||
|
map.clear(&mut vram);
|
||||||
|
vram.gc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -168,6 +168,7 @@ pub use agb_fixnum as fixnum;
|
||||||
pub mod hash_map;
|
pub mod hash_map;
|
||||||
/// Simple random number generator
|
/// Simple random number generator
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
|
pub mod save;
|
||||||
mod single;
|
mod single;
|
||||||
/// Implements sound output.
|
/// Implements sound output.
|
||||||
pub mod sound;
|
pub mod sound;
|
||||||
|
@ -223,6 +224,8 @@ pub struct Gba {
|
||||||
pub sound: sound::dmg::Sound,
|
pub sound: sound::dmg::Sound,
|
||||||
/// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files.
|
/// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files.
|
||||||
pub mixer: sound::mixer::MixerController,
|
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.
|
/// Manages access to the Game Boy Advance's 4 timers.
|
||||||
pub timers: timer::TimerController,
|
pub timers: timer::TimerController,
|
||||||
}
|
}
|
||||||
|
@ -239,6 +242,7 @@ impl Gba {
|
||||||
display: display::Display::new(),
|
display: display::Display::new(),
|
||||||
sound: sound::dmg::Sound::new(),
|
sound: sound::dmg::Sound::new(),
|
||||||
mixer: sound::mixer::MixerController::new(),
|
mixer: sound::mixer::MixerController::new(),
|
||||||
|
save: save::SaveManager::new(),
|
||||||
timers: timer::TimerController::new(),
|
timers: timer::TimerController::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
agb/src/save/asm_routines.s
Normal file
49
agb/src/save/asm_routines.s
Normal file
|
@ -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
|
63
agb/src/save/asm_utils.rs
Normal file
63
agb/src/save/asm_utils.rs
Normal file
|
@ -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 _)
|
||||||
|
}
|
273
agb/src/save/eeprom.rs
Normal file
273
agb/src/save/eeprom.rs
Normal file
|
@ -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<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.
|
||||||
|
#[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<bool, 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());
|
||||||
|
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<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)
|
||||||
|
}
|
||||||
|
}
|
472
agb/src/save/flash.rs
Normal file
472
agb/src/save/flash.rs
Normal file
|
@ -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<u8> = unsafe { MemoryMapped::new(0x0E000000) };
|
||||||
|
const FLASH_PORT_A: MemoryMapped<u8> = unsafe { MemoryMapped::new(0x0E005555) };
|
||||||
|
const FLASH_PORT_B: MemoryMapped<u8> = unsafe { MemoryMapped::new(0x0E002AAA) };
|
||||||
|
const FLASH_DATA: MemoryMapped1DArray<u8, 65536> = 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<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.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<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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the raw ID of the flash chip currently in use.
|
||||||
|
pub fn detect_chip_id() -> Result<u16, Error> {
|
||||||
|
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<bool, 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());
|
||||||
|
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<bool, Error> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
457
agb/src/save/mod.rs
Normal file
457
agb/src/save/mod.rs
Normal file
|
@ -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<bool, Error>;
|
||||||
|
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<Option<&'static dyn RawSaveAccess>> = 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<Timer>) -> Result<SaveData, Error> {
|
||||||
|
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<usize>) -> 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<bool, Error> {
|
||||||
|
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<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(&mut self, range: Range<usize>) -> Result<SavePreparedBlock, Error> {
|
||||||
|
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<usize>,
|
||||||
|
}
|
||||||
|
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>(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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
SaveData::new(Some(timer))
|
||||||
|
}
|
||||||
|
}
|
57
agb/src/save/sram.rs
Normal file
57
agb/src/save/sram.rs
Normal file
|
@ -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<bool, Error> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
59
agb/src/save/utils.rs
Normal file
59
agb/src/save/utils.rs
Normal file
|
@ -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<Timer>,
|
||||||
|
}
|
||||||
|
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<Timer>) -> 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<RawMutexGuard<'static>, Error> {
|
||||||
|
static LOCK: RawMutex = RawMutex::new();
|
||||||
|
match LOCK.try_lock() {
|
||||||
|
Some(x) => Ok(x),
|
||||||
|
None => Err(Error::MediaInUse),
|
||||||
|
}
|
||||||
|
}
|
|
@ -284,7 +284,7 @@ mod test {
|
||||||
// the actual main test loop
|
// the actual main test loop
|
||||||
let mut interrupt_seen = false;
|
let mut interrupt_seen = false;
|
||||||
let mut no_interrupt_seen = false;
|
let mut no_interrupt_seen = false;
|
||||||
for i in 0..100000 {
|
for i in 0..250000 {
|
||||||
// write to the static
|
// write to the static
|
||||||
let new_value = [i; COUNT];
|
let new_value = [i; COUNT];
|
||||||
value.write(new_value);
|
value.write(new_value);
|
||||||
|
|
105
agb/tests/save_test_common/mod.rs
Normal file
105
agb/tests/save_test_common/mod.rs
Normal file
|
@ -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<MediaInfo> = 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");
|
||||||
|
}
|
||||||
|
}
|
16
agb/tests/test_save_eeprom_512b.rs
Normal file
16
agb/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/test_save_eeprom_8k.rs
Normal file
16
agb/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 {}
|
||||||
|
}
|
16
agb/tests/test_save_flash_128k.rs
Normal file
16
agb/tests/test_save_flash_128k.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_flash_128k();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[agb::entry]
|
||||||
|
fn entry(_gba: agb::Gba) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
16
agb/tests/test_save_flash_64k.rs
Normal file
16
agb/tests/test_save_flash_64k.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_flash_64k();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[agb::entry]
|
||||||
|
fn entry(_gba: agb::Gba) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
16
agb/tests/test_save_sram.rs
Normal file
16
agb/tests/test_save_sram.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_sram();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[agb::entry]
|
||||||
|
fn entry(_gba: agb::Gba) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
|
@ -96,10 +96,10 @@ struct Agb<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(mut gba: agb::Gba) -> ! {
|
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 {
|
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();
|
let gfx = gba.display.object.get();
|
||||||
|
@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! {
|
||||||
agb.obj.commit();
|
agb.obj.commit();
|
||||||
agb.sfx.customise();
|
agb.sfx.customise();
|
||||||
if save::load_high_score() < current_level {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,42 @@
|
||||||
use agb::interrupt::free;
|
use agb::Gba;
|
||||||
use bare_metal::Mutex;
|
use agb::save::Error;
|
||||||
use core::cell::RefCell;
|
use agb::sync::Static;
|
||||||
|
|
||||||
const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8;
|
static HIGHSCORE: Static<u32> = Static::new(0);
|
||||||
const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1);
|
|
||||||
|
|
||||||
static HIGHSCORE: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
|
pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
|
||||||
|
gba.save.init_sram();
|
||||||
|
|
||||||
pub fn init_save() {
|
let mut access = gba.save.access()?;
|
||||||
if (unsafe { RAM_ADDRESS.read_volatile() } == !0) {
|
|
||||||
save_high_score(0);
|
|
||||||
unsafe { RAM_ADDRESS.write_volatile(0) };
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut a = [0; 4];
|
let mut buffer = [0; 1];
|
||||||
for (idx, a) in a.iter_mut().enumerate() {
|
access.read(0, &mut buffer)?;
|
||||||
*a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() };
|
|
||||||
}
|
|
||||||
|
|
||||||
let high_score = u32::from_le_bytes(a);
|
if buffer[0] != 0 {
|
||||||
|
access.prepare_write(0..1)?.write(0, &[0])?;
|
||||||
free(|cs| {
|
core::mem::drop(access);
|
||||||
if high_score > 100 {
|
save_high_score(gba, 0)?;
|
||||||
HIGHSCORE.borrow(cs).replace(0);
|
|
||||||
} else {
|
} else {
|
||||||
HIGHSCORE.borrow(cs).replace(high_score);
|
let mut buffer = [0; 4];
|
||||||
|
access.read(1, &mut buffer)?;
|
||||||
|
let high_score = u32::from_le_bytes(buffer);
|
||||||
|
|
||||||
|
if high_score > 100 {
|
||||||
|
HIGHSCORE.write(0)
|
||||||
|
} else {
|
||||||
|
HIGHSCORE.write(high_score)
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_high_score() -> u32 {
|
pub fn load_high_score() -> u32 {
|
||||||
free(|cs| *HIGHSCORE.borrow(cs).borrow())
|
HIGHSCORE.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_high_score(score: u32) {
|
pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> {
|
||||||
let a = score.to_le_bytes();
|
gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?;
|
||||||
|
HIGHSCORE.write(score);
|
||||||
for (idx, &a) in a.iter().enumerate() {
|
Ok(())
|
||||||
unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) };
|
|
||||||
}
|
|
||||||
|
|
||||||
free(|cs| HIGHSCORE.borrow(cs).replace(score));
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue