# Some New Nintendo e-Reader Reverse-Engineering August 6, 2025 So recently I've been doing a bit more reverse engineering of the Nintendo e-Reader[1] for the GBA. The really really cool RetroDotCards[2] project released late last month and I've been having a great time playing the games; and it's inspired me to dig into a few aspects of the e-Reader again (and make some previous research actually discoverable). In case you didn't see it and are interested, I've already made a document compiling nearly all known information on the internet about the programming API for the e-Reader: https://nytpu.com/nintendo-e-reader/ And Matt Greer, the creator of RetroDotCards, started a Wiki with lots of good additional links and a nice documentation of the Z80 API: https://ereader.miraheze.org/wiki/Main_Page [1]: https://en.wikipedia.org/wiki/Nintendo_e-Reader [2]: https://www.retrodotcards.com/ # Flash Userdata So I've been enjoying playing all the RetroDotCards games, but it is kinda annoying to have this massive-ass thing hanging out the top of my GBA while playing lol An original GBA with the e-Reader plugged in, which is almost the same size as the console itself: files/e-reader/e-reader.jpg It is also annoying how you're stuck with one game saved to the flash unless you want to spend a decent amount of time and tedium scanning in each card, and you have to have the cards at hand. So I've come upon a janky solution of scanning each game in, dumping the save file with my GBxCart RW[3], and then storing the dumped save and a duplicate of the e-Reader ROM on my normal flash cart, so I can play each e-Reader game as easy as just selecting it.[4] But then that means my e-Reader's flash won't have my high scores or any of the fun "Scavenger Hunt" achievements! For some reason I want my e-Reader to still be an artifact for everything I've done, so I want to merge the userdata back into it (but without corrupting the calibration data nor making me change out the stored game). Unfortunately, GBATEK[5] doesn't even know about the existence of the userdata functions, let alone where or how it was saved in the flash. So I just hacked together a test ROM to write every userdata block with easily identifiable data, so I could see where in the final save file the userdata ends up. Fun fact, turns out that writing the userdata is *incredibly* slow, about 400 milliseconds per block written. And it's not even the flash itself, it must be because of inefficiencies or hardcoded delays in whatever ROM routine writes the data. Writing every userdata block. Tick tock, tick tock.: files/e-reader/e-reader-userdata-slow.mp4 I found the userdata is a contiguous 4000 byte block stored in the save at byte offsets 0x9016–0x9FB5 in the flash data. Each sixteen- byte block is just written unmodified to the flash at `(specified index * 16) + 0x9016`. So I wrote my utility for managing the userdata based off of that: https://git.sr.ht/~nytpu/e-reader-userdata The utility allows you to view all non-empty userdata blocks in one or more save files, and has a mode to allow you to create a new save file while selecting which save file you want to take each used slot from (or clear slots if you want to reset your scores or something). As one more tidbit about the userdata, of the official cards, only the Pokémon cards are known to use it, and they used slots 0–23. Then Matt Greer's wiki has a list of the slots known to be used by homebrew games.: https://ereader.miraheze.org/wiki/FlashIndexList [3]: https://www.gbxcart.com/ [4]: One would think that I'd just play other full-size GBA games instead, but I do have a thing for fun small minigames. And the e- Reader games are great for short play sessions which is how I usually use my GBA when I'm on-the-go. [5]: http://problemkaputt.de/gbatek-gba-cart-e-reader.htm # Random-Number Generation I did the bulk of this research back in April[6], but since then I have wanted to flesh it out and document it somewhere that there's any chance of people being able to find. So the RNG state (`RNG_State`) is stored as a 32-bit little-endian value at RAM address 0x020326f8; which is initialized to 0x0000_007B on boot. Note that there are four bytes/32 bits reserved at the location, but in normal operation the code is very careful to mask the value down to sixteen bits. The `ERAPI_RandInit` function is at ROM address 0x08022160, it adds 6 to the given seed (explicitly truncating the result to sixteen bits), then calls a function at 0x08005360 (`RNG_Init`) which solely overwrites `RNG_State` with the given value. EDIT: not actually sure if it's adding six to the given seed or if it's reading the user parameter indirectly and had to offset the pointer passed to it by six for some reason. This and the startup routine setting it to 0x7B are the only two places where `RNG_State` is overwritten without using one of the two "shifting" routines. Here's all the places in the entire ROM that `RNG_State` is referenced, note that the unnamed first reference is the initialization routine that runs at start up and no other time (AFAICT): The locations in the ROM that reference the RNG_State value: files/e-reader/RNG_State-references.png The main function that steps the RNG (what I've dubbed `RNG_Shift`) is at ROM address 0x08005390, this is the one that's used when returning a random byte back out to the user API. It loads the RNG state, does a 32-bit multiply by 0x0000_080d, adds 7, and then masks it with 0x0000_ffff. The code higher-up the stack[7] takes the new returned value, masks off the lower eight bits, and returns that to the user code. The C-style expression used to shift the RNG is (being really explicit with the precedence, and note that every intermediate calculation gets truncated to 32 bits): RNG_State = ((RNG_STATE * 0x80D) + 7) & 0xFFFF; return (uint8_t)RNG_State; The main `RNG_Shift` function is referenced in three other places, all of which are fairly complex functions. I assume one is for the next frame/Wait8Bit routine and possibly another is the Wait16Bit routine, and the others are likely just internal RNG calls? The locations in the ROM that reference the RNG_Shift function: files/e-reader/RNG_Shift-references.png There is a second RNG function (`RNG_ShiftScaled`) at address 0x0800536c that takes one 32-bit parameter. It starts by executing an exact copy of the RNG shift code (the register allocation is different but results are the same) and stores it back to the RNG state like the first RNG shift function. i.e. no matter which of the two RNG shift functions were called, the resulting state will be the same. But it takes a copy of the shifted value and does a 32- bit multiply by the extra parameter, then shifts it right by sixteen, and as such returns the upper sixteen bits of the scaled value. This function appears to exclusively be called by other internal e-Reader functions. Places `RNG_ShiftScaled` is referenced: The locations in the ROM that reference the RNG_ShiftScaled function: files/e-reader/RNG_ShiftScaled-references.png [6]: https://tilde.zone/@nytpu/114342791977433626 [7]: The `ERAPI_Rand` function that generates a random byte is at 0x08023808, and the `ERAPI_RandMax` function that generates a byte in a given range is at 0x080237f. Finding references to these two in a table spaced 11 pointers apart is how I was first able to find the API jump tables listed below! # User API Jump Table in ROM and Other Observations While reverse-engineering the RNG functions, I found the locations of one of the jump tables for the user-facing API functions which doesn't seem to be documented anywhere (despite people clearly having figured it out before), so figured I should mention that here too. What I called the "Group 0" functions, the ones called from Z80 with `rst 0` and called from ARM with the `0x02xx` prefix, are stored in a jump table at 0x085eebf8. The table is a series of 32- bit little-endian function pointers. It seems to contain 0xF2 entries (968 bytes) before leading directly into the Group 1 jump table. It contains a good number of null pointers scattered throughout it, and a few other garbage pointers. The "Group 1" functions (called via `rst 8`/`0x01xx`) jump table is stored at 0x085eefa4, immediately after the Group 0 table. It seems to only contain 0x40 entries (256 bytes) before ending in a very large section of garbage data. The "Group 2" functions *may* be nearby, but I wouldn't be surprised if they were just hardcoded somewhere since there's only five of them, and in the Z80 API they're all on special-cased opcodes rather than being called with a number like the rest of the API functions. # API Entry Point Also, just using the mGBA debugger, I found the main entry point into the ROM when calling an API function from ARM is at 0x080387d4. In normal code you access it by reading the (constant) address the e-Reader places in RAM at 0x30075fc[8], but I really strongly suspect that's it's not because it changes dynamically (see details in footnote), but just because they don't trust the API consumers to branch to it with the LSB set, to make sure the function is called in ARM THUMB mode. [8]: The only place in the ROM that accesses 0x30075fc are two literally identical functions at 0x080388d8 and 0x080388f4, both of which write the constant 0x80387d5 to 0x30075fc and then call a `bx r0` trampoline/stub function with a literal of 0x2000000 in `r0`, which means it's just indirectly calling the ARM entry point in EWRAM. So I guess one of those must be called when the e-Reader begins executing a dot card's ARM code. # GBA Reverse-Engineering Other than the fully reversed RNG function, most of the stuff in this post is mostly meant as breadcrumbs and starting points for other people who might be interested to pursue. I could do a lot more, I just never feel like researching anything not immediately pertinent to a current development project lol. Especially since the e-Reader ROM is pretty irritating to inspect: it deals with a lot of odd hardware not present on other GBA games, it's confusing mess of code because it's mostly an API library called into by third-party programs rather than being self-contained where there's a coherent call graph, but it also has multiple emulators using that library and offering translated versions of the API. However, if I'm leaving a lot of starting points that other people could look at, I realized I should also give some additional tips for reverse-engineering the e-Reader, and a lot of it should be helpful for inspecting GBA games (or any other retro console's games) in general too. I'm putting that into a follow-up post, posted here: 2025-08-06-2 * * * Contact via email: alex [at] nytpu.com or through anywhere else I'm at: gopher://nytpu.com/0/about Copyright (c) 2025 nytpu - CC BY-SA 4.0