This tutorial covers SRAM — the battery-backed RAM region on a SNES cartridge that survives power-off, used for save games on every cart that has one (Zelda, Final Fantasy VI, Super Mario World, …). The lib exposes a small set of byte-array helpers; the discipline lives in treating SRAM as untrusted data that needs validation on every load.
It assumes you have read the Graphics tutorial. The DMA tutorial is useful background but not required — SRAM access goes through plain CPU loads/stores, not DMA.
A SNES cartridge contains:
Sizes range from 2 KB (early carts, Final Fantasy II) up to 32 KB (RPGs, Final Fantasy VI). The lib supports the standard sizes via SRAM_SIZE_2KB, SRAM_SIZE_4KB, SRAM_SIZE_8KB (the common case), SRAM_SIZE_16KB, SRAM_SIZE_32KB.
On real hardware, two failure modes:
Both are silent failures. Your code must validate SRAM on every load — magic number, checksum, version field. A blind sramLoad() into a struct gives you whatever was in that battery-backed memory, correct or not.
The SNES MMU maps SRAM into bank space differently for LoROM vs HiROM:
| Layout | SRAM banks | SRAM addresses | Notes |
|---|---|---|---|
| LoROM | $70–$7D | $70:0000–$77:7FFF | 32 KB max (eight 4 KB regions). Most common. |
| HiROM | $30–$3F (mirror to $B0–$BF) | $30:6000–$3F:7FFF | 8 KB per bank in the lower half, 32 KB max. |
| SA-1 | Different — see SA-1 chapter | — | SA-1 cart layouts depend on per-cart configuration. |
The lib's sramSave/sramLoad hide these details. You pass a WRAM/RAM pointer and a byte count; the helper assembles the correct 24-bit SRAM address. Unless you're writing custom SRAM access code, you don't need to know the bank/offset arithmetic.
Two changes to enable SRAM in a project:
The SRAM_SIZE numeric value matches the ROM header's SRAMSIZE byte. Map:
| SRAM_SIZE | Cart SRAM | Header byte |
|---|---|---|
| 1 | 2 KB | $01 |
| 2 | 4 KB | $02 |
| 3 | 8 KB | $03 |
| 4 | 16 KB | $04 |
| 5 | 32 KB | $05 |
Most homebrew picks 8 KB unless you genuinely need more. Larger SRAM declarations affect emulator save-state allocation and on real flash carts the actual reserved region.
The build system fills the SNES ROM header automatically based on USE_SRAM and SRAM_SIZE:
Emulators read these bytes at ROM load to know how big to allocate the SRAM buffer and where to persist the .srm file. Mismatched declarations (Makefile says 8 KB, header says 2 KB) produce silent "my saves disappear after 2 KB worth of data" bugs.
| Function | Purpose |
|---|---|
| sramSave(data, size) | Copy size bytes from WRAM data to SRAM offset 0. |
| sramLoad(data, size) | Copy size bytes from SRAM offset 0 to WRAM data. |
| sramSaveOffset(data, size, offset) | Save to SRAM at byte offset — used for multi-slot saves. |
| sramLoadOffset(data, size, offset) | Load from SRAM at byte offset. |
| sramClear(size) | Zero size bytes of SRAM starting at offset 0. Used for "delete save". |
| sramChecksum(data, size) | Compute an 8-bit XOR checksum of data. Use this for save-integrity validation. |
The byte arrays are flat — there's no filesystem on SRAM, just a linear address space starting at offset 0. You pick the layout.
Single save slot with magic-number validation:
The two non-obvious bits:
For "Slot 1, Slot 2, Slot 3" RPG-style saves, use offset addressing:
The SDK's examples/memory/save_game is exactly this pattern: two slots, distinct offsets, full struct round-trip with on-screen verification of the loaded values.
Zero the relevant region:
The next load attempt fails magic-number validation and falls back to defaults. (Don't try "delete by setting magic to invalid" — a direct write is simpler and survives partial-write failures.)
examples/memory/save_game — two save slots, each holding a SaveState { posX, posY, camX, camY }. Press A to save test data to slot 1, B to load it back; X/**Y** for slot 2. Loaded values are displayed as hex on screen so you can verify round-trip integrity. The example exercises:
It does not implement magic-number / checksum validation — that's left as the production-readiness step the tutorial above documents. For real games, always add it.
Real hardware: whatever was in the cells at cart assembly. Emulators: typically zero-filled, but flash carts and some emulator configurations preserve previous content across cart swaps. Your code must validate before trusting:
This is not optional. A sramLoad into a struct without validation gives you garbage and your game silently boots into corrupted state. The save_game example (which doesn't validate) is fine for a tech demo; production code is not.
A 10–20-year-old cartridge's coin-cell battery can fail. When it does, SRAM contents drift toward "all $00" or "all $FF" or random patterns. Your validation must catch both:
The lib's sramSave/sramLoad will happily write past the size declared in the ROM header. On emulators, the .srm file may be truncated; on real hardware, writes past the actual chip size wrap around or vanish. Always set SRAM_SIZE in your Makefile to match or exceed the largest offset your code writes.
A common bug: SRAM_SIZE := 1 (2 KB) in Makefile, then code uses SLOT_SIZE = 1024 × 4 slots = 4 KB. Slots 2 and 3 silently fail to persist on hardware that respects the header.
Unlike VRAM/CGRAM/OAM (which need VBlank or force blank), SRAM is plain RAM mapped into the CPU's address space. You can read or write it at any time during active display. The lib's helpers do not call WaitForVBlank() and don't need to.
This is good news ergonomically — sramSave mid-frame is fine — but newcomers from a "VBlank everything" mental model sometimes add unnecessary WaitForVBlank() calls around SRAM operations.
A pointer in WRAM points to bank $00 (or $7E) at some specific address; that address is meaningful only while the cartridge runs this exact ROM. If you save a pointer and reload it on a future boot, the pointer might be valid (same ROM) or not (different game or emulator's memory layout). Always serialise structs as plain-old-data: counts, flags, indices, NEVER pointers.
The SNES is little-endian (low byte first). sramSave/sramLoad copies raw bytes; struct fields land in the order the C compiler laid them out. Same compiler reading and writing → bit-perfect round-trip. If you ever migrate save data between PVSnesLib and OpenSNES (or any two compilers with different alignment rules), check struct layout first — padding bytes can shift between compilers.
If you write a tutorial / demo that "saves on cycle X and loads on boot", expect different behaviour across platforms. The save_game example uses two slots specifically so you can verify round-trip in a single boot cycle without depending on persistence.
sramClear(size) writes zeros to SRAM. It does not disable SRAM, prevent future writes, or "format" the chip. Re-saving after clear works as normal. The function is for "wipe save data" UI flows, not for "secure erase".
SRAM access is plain CPU loads/stores via the cartridge bus, around 8 master cycles per byte. A 256-byte save:
A full 8 KB save: ~64 K cycles, ~3 % of a 60 Hz frame. Still small enough to do during gameplay if you have to (auto-save).
There's no DMA path for SRAM; the lib uses CPU MVN/MVP block-move instructions which are fast enough that DMA wouldn't materially help.