gb_pak/sram.rs
1//! The cartridge's own RAM, usually kept alive by a battery.
2//!
3//! SRAM is only readable and writable while the controller has it switched on, so
4//! every access happens inside [`Sram::open`], which switches it on for the
5//! closure and off again after. Leaving it off otherwise protects the contents
6//! while the console powers down or the cartridge is pulled.
7//!
8//! # Declaring
9//!
10//! ```ignore
11//! use core::cell::Cell;
12//! use gb_pak::{CriticalSection, sram};
13//! use zerocopy::FromBytes;
14//!
15//! #[repr(C)]
16//! #[derive(FromBytes)]
17//! struct Save {
18//! magic: Cell<[u8; 4]>,
19//! version: Cell<u8>,
20//! hp: Cell<u8>,
21//! gold: Cell<u16>,
22//! }
23//!
24//! /// The save file.
25//! #[sram(0)]
26//! static FILE: Save;
27//!
28//! fn hurt(cs: CriticalSection<'_>) {
29//! FILE.open(cs, |f| f.hp.set(f.hp.get().saturating_sub(1)));
30//! }
31//! ```
32//!
33//! A static has no initializer: the bytes are the cartridge's already. A bank
34//! holds one of them, at its start (`0xA000`), and the attribute says which bank.
35//!
36//! Turned down at compile time: a missing bank number, a bank the cartridge does
37//! not have, any bank when it has no SRAM, a value larger than one 8 KiB bank,
38//! and an initializer.
39//!
40//! # Editing in place
41//!
42//! The closure is handed a shared reference, so every field is written as a
43//! [`Cell`](core::cell::Cell), the way a peripheral crate writes a register
44//! block. A field that is not one can never be written.
45//!
46//! ```ignore
47//! unsafe { gb::interrupt::free(|cs| FILE.open(cs, |f| f.gold.set(f.gold.get() + 10))) };
48//! ```
49//!
50//! # Trusting the contents
51//!
52//! A cartridge that has never been written holds noise, so the value is bounded
53//! on [`FromBytes`]: no byte pattern may be invalid for it, which rules out
54//! `bool`, `char`, enums, and references.
55//!
56//! `repr(C)` is recommended: a `repr(Rust)` layout is unspecified and may differ
57//! between toolchain versions, so a save an earlier build wrote could be read
58//! back wrong.
59//!
60//! That bounds the type, not the contents. The bytes are still whatever survived:
61//! noise on a new cartridge, decay on a failing battery, another build's layout
62//! after an update. Check a magic number, a schema version, and a checksum before
63//! believing a save.
64
65use zerocopy::FromBytes;
66
67use crate::{CriticalSection, WINDOW, WINDOW_LEN, reg};
68
69/// A value at the base of SRAM bank `BANK`.
70///
71/// Declared by [`#[sram]`](macro@crate::sram).
72pub struct Sram<T: FromBytes, const BANK: u8> {
73 _value: core::marker::PhantomData<T>,
74}
75
76// The handle holds no `T`; the bytes are the cartridge's and are reached only
77// inside `open`.
78unsafe impl<T: FromBytes, const BANK: u8> Sync for Sram<T, BANK> {}
79
80impl<T: FromBytes, const BANK: u8> Sram<T, BANK> {
81 /// # Safety
82 ///
83 /// The cartridge must have bank `BANK`. The attribute checks that against
84 /// `header.toml`, which is why this module is reachable on a cartridge with
85 /// no SRAM at all: the check belongs at the declaration, not at the module.
86 #[doc(hidden)]
87 pub const unsafe fn declare() -> Self {
88 Sram { _value: core::marker::PhantomData }
89 }
90
91 /// Switch the RAM on, run `f`, switch it off.
92 ///
93 /// The [`CriticalSection`] keeps an interrupt handler from reaching the same
94 /// bytes while `f` runs.
95 ///
96 /// The window holds one thing at a time, so `f` must not open another scope
97 /// or read another peripheral: the rest of `f` would look at whatever that
98 /// left selected.
99 #[inline]
100 pub fn open<R>(&self, _cs: CriticalSection<'_>, f: impl FnOnce(&T) -> R) -> R {
101 reg::select(BANK);
102 reg::enable();
103 let r = f(unsafe { &*(WINDOW as *const T) });
104 reg::disable();
105 r
106 }
107}
108
109/// Bytes one bank holds.
110pub const BANK_LEN: usize = WINDOW_LEN;
111
112/// SRAM banks this cartridge has, from `ram_size` in `header.toml`.
113pub const BANKS: u8 = reg::BANKS;