The Nintendo e‑Reader was a peripheral released for the Game Boy Advance in 2001. The Nintendo e‑Reader allowed scanning “dotcode strips” to access extra content within games or to play mini-games. Today I’ll show you how to use the GB Operator, a Game Boy ROM dumping tool, in order to access the ROM encoded onto e‑Reader card dotcodes.
I’ll be demonstrating using a new entrant to e‑Reader game development for the venerable platform: Retro Dot Codes by Matt Greer. Matt regularly posts about his process developing and prin…
The Nintendo e‑Reader was a peripheral released for the Game Boy Advance in 2001. The Nintendo e‑Reader allowed scanning “dotcode strips” to access extra content within games or to play mini-games. Today I’ll show you how to use the GB Operator, a Game Boy ROM dumping tool, in order to access the ROM encoded onto e‑Reader card dotcodes.
I’ll be demonstrating using a new entrant to e‑Reader game development for the venerable platform: Retro Dot Codes by Matt Greer. Matt regularly posts about his process developing and printing e‑Reader cards and games in 2026. I was a recipient for one of his free e‑Reader card giveaways and purchased Retro Dot Cards “Series 1” pack which I’m planning to open and play for the blog.
Dumping a Nintendo e-Reader card contents
The process is straightforward but requires a working GBA or equivalent (GBA, GBA SP, Game Boy Player, DS, or Analogue Pocket *), a Nintendo e-Reader cartridge, and a GBA ROM dumper like GB Operator. Launch the e‑Reader cartridge using a Game Boy Advance, Analogue Pocket, or Game Boy Player. The e-Reader software prompts you to scan the dotcodes.
The Solitaire card stores its program data on two “long” dotcode strips consisting of 28 “blocks” per-strip encoding 104 bytes-per-block for a total of 5824 bytes on two strips (2×28×104=5824). If you want to see approximately how large a dotcode strip is, open this page in a desktop browser. After scanning each side of the Solitaire card you can play Solitaire on your console:
I’ll be honest, I was never into Solitaire as a kid, I was more “Space Cadet Pinball” on Windows... Anyways, let’s archive the “ROM” of this game so even if we lose the physical card we can still play.
Turn off your device and connect the e‑Reader cartridge to the GB Operator. Following the steps I documented for Ubuntu, start “Epilogue Playback” software and dump the e‑Reader ROM and critically: the save data. The Nintendo e‑Reader supports saving your already scanned game in the save data of the cartridge so you can play again next time you boot without needing to re-scan the cards.
Now we have a .sav file. This file works as an archive of the program, as we can load our e-Reader ROM and this save into a GBA emulator to play again. Success!
Examining e-Reader Card ROMs
Now that we have the .sav file for the Solitaire ROM, let’s see what we can find inside. The file itself is mostly empty, consisting almost entirely of 0xFF and 0x00 bytes:
>>> data = open("Solitaire.sav", "rb").read()
>>> len(data), hex(len(data))
(131072, '0x20000')
>>> sum(b == 0xFF for b in data)
118549
>>> sum(b == 0x00 for b in data)
8200
We know from the data limits of 2 dotcode strips that there’s only 5824 bytes maximum for program data. If we look at the format of e‑Reader save files documented at caitsith2.com we can see what this data means. I’ve duplicated the page below, just in case:
ereader save format.txt
US E-reader save format
Base Address = 0x00000 (Bank 0, address 0x0000)
Offset Size Description
0x0000 0x6FFC Bank 0 continuation of save data.
0x6FFD 0x6004 All 0xFF
0xD000 0x0053 43617264 2D452052 65616465 72203230
30310000 67B72B2E 32333333 2F282D2E
31323332 302B2B30 31323433 322F2A2C
30333333 312F282C 30333233 3230292D
30303131 2F2D2320 61050000 80FD7700
000001
0xD053 0x0FAD All 0x00s
0xE000 0x1000 Repeat 0xD000-0xDFFF
0xF000 0x0F80 All 0xFFs
0xFF80 0x0080 All 0x00s
Base Address = 0x10000 (Bank 1, address 0x0000)
Offset Size Description
0x0000 0x04 CRC (calculated starting from 0x0004, and amount of data to calculate is
0x30 + [0x002C] + [0x0030].)
0x0004 0x24 Program Title (Null terminated) - US = Straight Ascii, Jap = Shift JIS
0x0028 0x04 Program Type
0x0204 = ARM/THUMB Code/data (able to use the GBA hardware directly, Linked to 0x02000000)
0x0C05 = 6502 code/data (NES limitations, 1 16K program rom + 1-2 8K CHR rom, mapper 0 and 1)
0x0400 = Z80 code/data (Linked to 0x0100)
0x002C 0x04 Program Size
= First 2 bytes of Program data, + 2
0x0030 0x04 Unknown
0x0034 Program Size Program Data (vpk compressed)
First 2 bytes = Size of vpk compressed data
0xEFFF 0x01 End of save area in bank 1. Resume save data in bank 0.
The CRC is calculated on Beginning of Program Title, to End of Program Data.
If the First byte of Program Title is 0xFF, then there is no save present.
If the CRC calculation does not match stored CRC, then the ereader comes up with
an ereader memory error.
CRC calculation Details
CRC table is calculated from polynomial 0x04C11DB7 (standard CRC32 polynomial)
with Reflection In. (Table entry 0 is 0, and 1 is 0x77073096...)
CRC calculation routine uses Initial value of 0xAA478422. The Calculation routine
is not a standard CRC32 routine, but a custom made one, Look in "crc calc.c" for
the complete calculation algorithm.
Revision history
v1.0 - First release
V1.1 - Updated/Corrected info about program type.
v1.2 - Updated info on Japanese text encoding
v1.3 - Info on large 60K+ vpk files.
From this format specification we can see that the program data starts around offset 0x10000 with the CRC, the program title, type, size, and the program data which is compressed using the VPK0 compression algorithm. Searching through our save data, sure enough we see some data at the offsets we expect like the program title and the VPK0 magic bytes vpk0:
>>> hex(data.index(b"Solitaire\x00"))
'0x10004'
>>> hex(data.index(b"vpk0"))
'0x10036'
We know that the VPK0-compressed blob length encoded to the two bytes before the magic header in little-endian. Let’s grab that value and write the VPK0-compressed blob to a new file:
>>> vpk_idx = data.index(b"vpk0")
>>> vpk_len = int.from_bytes(
... data[vpk_idx-2:vpk_idx], "little")
>>> with open("Solitaire.vpk", "wb") as f:
... f.write(data[vpk_idx:vpk_idx+vpk_len])
In order to decompress the program data we’ll need a tool that can decompress VPK0. The e‑Reader development tools repository points to nevpk. You can download the source code for multi-platform support and compile using cmake:
curl -L https://github.com/breadbored/nedclib/archive/749391c049756dc776b313c87da24b7f47b78eea.zip \
-o nedclib.zip
unzip nedclib.zip
cmake . && make
# Now we can use nevpk to decompress the program.
nevpk -d -i Solitaire.vpk -o Solitaire.bin
md5sum Solitaire.bin
3a898e8e1aedc091acbf037db6683a41 Solitaire.bin
This Solitaire.bin file is the original binary that Matt compiled before compressing, adding headers, and printing the program onto physical cards. Pretty cool that we can reverse the process this far!
Nintendo e-Reader and Analogue Pocket
The Analogue Pocket is a hardware emulator that uses an FPGA to emulate multiple retro gaming consoles, including the GBA. One of the prominent features of this device is its cartridge slot, allowing you to play cartridges without dumping them to ROM files first.
But there’s just one problem with using the Analogue Pocket with the Nintendo e-Reader. The cartridge slot is placed low on the device, making it impossible to insert oddly-shaped cartridges like the Nintendo e-Reader. Enter the E-Reader Extender! This project by Brian Hargrove extends the cartridge slot while giving your Analogue Pocket a big hug.
Playing Nintendo e-Reader games on Delta Emulator
The best retro emulator is the one you bring with you, for this reason the Delta Emulator is my emulator of choice as it runs on iOS. However, there are challenges to running e-Reader games on Delta: specifically that Delta only allows one save file per GBA ROM. This means to change games you’d need to import a new e-Reader save file. Delta stores ROMs and saves by their ROM checksum (MD5).