Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

PlayStation Memory Cards: Serial Transactions and Save-Image Layout

Read PlayStation memory-card transactions and image structure through a pinned emulator implementation, while keeping backups separate from raw inspection.

A PlayStation memory-card file is not one save file with a console-shaped extension. It is a block device image with card-level metadata, directory entries, allocation state, and save payloads. At runtime, software does not simply open that host file: the console addresses a selected card through a serial command protocol and transfers fixed-size frames. Understanding the distinction helps when debugging an emulator, inspecting a backup, or deciding whether a utility is editing a whole card or exporting one game’s save.

There is an important evidence limit. Sony’s authoritative protocol documentation was not available for this article. The byte-level description below is explicitly the model implemented by DuckStation, pinned to source revision d4e5a09d97429a3513c53fec375221af2f6b1ce7. Emulator behavior is strong evidence for what that implementation does, not proof that every console revision, game, peripheral, or other emulator behaves identically.

A raw card image is a small structured device

In that DuckStation revision, MemoryCardImage::DATA_SIZE is 128 KiB (1 Mbit), split into 8 KiB blocks and 128-byte frames. That is 16 blocks and 1,024 frames in the implementation’s arithmetic. The first block contains the card header and directory-like information; save data occupies other blocks. Each 8 KiB block contains 64 frames, so frame offsets and block offsets are related but not interchangeable.

The first frame begins with the bytes MC. DuckStation’s formatter computes a one-byte XOR over the first 127 bytes and stores it in the last byte. The source deliberately matters here: its IsValid routine currently checks the MC marker and contains a TODO to check the checksum. LoadFromFile separately rejects a raw image whose total length is not exactly 128 KiB. Passing those checks is therefore not the same as proving that every directory, link, checksum, and payload is internally consistent.

Directory frames include an allocation state, file size, next-block number, filename, padding, and checksum. In DuckStation’s writer, the first save block is marked 0x51 even when it is the only block; a later final block is marked 0x53, and later non-final blocks are 0x52. Thus 0x53 is not the one-block-save state. 0xFFFF is used as the no-next-block sentinel. A save that spans several blocks is represented by a link chain rather than by assuming its blocks are consecutive. The title frame and optional icon data are found in the block associated with the save. These are implementation details worth preserving when building an importer or parser, not a license to hard-code them as a console-wide file specification.

This is also why file size alone does not identify a save. A 128 KiB raw card image is different from a per-game save export. DuckStation’s ExportSave path writes a directory frame followed by the save payload; it does not write all 128 KiB of card state. Keep the original card image, an exported game save, and any converted emulator format in separate files with explicit names.

The emulator models a byte-at-a-time serial exchange

DuckStation’s MemoryCard::Transfer is a state machine called for individual bytes. In the inspected code, the selection byte 0x81 moves an idle card into command state. 0x52 selects a read-data operation, 0x57 a write-data operation, and 0x53 a get-ID operation. Read and write paths then exchange card ID and address bytes before payload data. The implementation reports a reply byte and an acknowledgement result at each state transition; a single high-level “read block” call would hide those observable steps.

For a data read, the emulator returns one 128-byte frame, accumulates an XOR checksum over the address bytes and payload, emits the checksum, then ends the exchange with 0x47. The write path consumes 128 payload bytes, accumulates the analogous checksum, responds with its acknowledgement sequence, and ends with 0x47. Read and write do not return identical sequences at every position, so a test trace should record input bytes, output bytes, acknowledgement state, address, and current frame offset instead of comparing only the final buffer.

The frame boundary is a useful test unit. Exercise the first and last byte of a frame, checksum mismatch behavior, address masking, invalid command reset, and a read followed by a write to the same frame. Compare the exact behavior against the pinned implementation before assuming another emulator or hardware has the same edge cases. In this revision, the write path stores payload bytes as they arrive and queues a changed frame for a delayed save after the 128th payload byte, before it consumes the subsequent checksum byte. The WriteChecksum state does not compare that received byte with the accumulated checksum or roll back the frame. A mismatch fixture should record this implementation behavior rather than expect rejection. The source’s read path calculates a checksum; that alone does not imply that its ordinary raw-image loader validates every stored checksum before use.

Acknowledgement is not host-file durability

The emulated card writes bytes into its in-memory image as the serial transfer progresses. When a changed 128-byte frame is complete, DuckStation queues a delayed host-file save. The current implementation schedules that event for one second later, while other lifecycle paths can also call SaveIfChanged. The message 0x47 ends the modeled serial exchange; it is not a promise that the host file has already been durably committed to storage.

This is a DuckStation scheduling policy, not a universal timing rule for original hardware or other emulators. Operationally, avoid treating a game’s “save complete” screen as evidence that an image copy taken concurrently is stable. Let the emulator finish its write path, use its normal clean shutdown, wait for host activity to settle, and only then capture a backup. If the emulator reports a write error, preserve the image and logs before attempting repair.

Make a non-destructive working copy

For an ordinary file-backed card, start by closing the emulator and copying the card image without opening it in a game or save manager. Create a fresh directory beside the image before copying so a rerun cannot overwrite an existing backup, and check that the destination filesystem has enough space:

umask 077
backup_dir=$(mktemp -d "./memory-card-backup.XXXXXX") || exit 1
cp memory-card.mcd "$backup_dir/memory-card.mcd" || exit 1
shasum -a 256 memory-card.mcd "$backup_dir/memory-card.mcd"

The template form shown works with BSD/macOS and GNU mktemp; check the local manual before adapting it to another implementation. Use an operating-system-appropriate SHA-256 command if shasum is unavailable. Matching digests confirm that the copy matched the original at the time of hashing; they do not prove that the image is valid or that either file is the right card. Keep the newly created master-copy directory read-only by process and permissions where practical, and direct parsers, converters, recovery attempts, and game tests only at a separate working copy. Do not overwrite the only card image with output from a hex editor or an importer that silently reformats unsupported input.

Record the emulator and utility versions, image length, hash, and whether the file is a raw card or an exported save. If an inspection tool reports an invalid image, first determine which assumptions it makes about extensions, headers, checksums, and deleted entries. DuckStation accepts several import formats in its source, but that is evidence about its importer only; a filename suffix is not a reliable declaration of bytes or universal interoperability.

Build fixtures around observable structure

A parser fixture should include a freshly formatted image, a card with one small save, a save spanning multiple blocks, a deleted entry, and deliberately damaged copies. Assert the exact image length, header marker, known frame offsets, directory link traversal, save length, and icon metadata that the selected implementation actually exposes. Keep damaged fixtures separate from expected-valid fixtures. A test that only checks whether a title appears in the emulator’s browser will miss incorrect link traversal or a checksum bug. For checksum mutation, test and document what the selected implementation actually accepts; do not assume its raw-image validity check enforces the stored checksum.

For protocol work, store a trace for command selection, read, write, and get-ID operations. Include the bytes sent and received, ACK state, frame address, payload length, checksum, and terminal byte. Test transfer interruption at several byte offsets and verify how the emulated state is reset or resumed. The trace is a conformance artifact for the pinned emulator model; without authoritative Sony documentation or a captured hardware exchange, label it as such rather than claiming hardware conformance.

Finally, keep image format recovery separate from protocol emulation. A card file can be structurally inconsistent even when the serial state machine is correct; a game’s save payload can be corrupt even when its directory entry is readable. Diagnose the layer that failed before changing bytes. This avoids “repairing” a host-side header when the real issue is an incomplete game save, and prevents a successful emulator import from being mistaken for a complete archival validation.

Related:

Sources:

Comments