{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreif3mbn4tzjf6kcjdkw3odf5gwioaiq5rk55w3h2owqpcqo2p2hsji",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpny723ntav2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreihtw5neblnnx3ebneyotzwnh37oqruh64t7r4p7igxl36xitvzzxq"
},
"mimeType": "image/webp",
"size": 65998
},
"path": "/adityatr64/cba-a-from-scratch-arm7tdmi-game-boy-advance-emulator-written-in-c-42me",
"publishedAt": "2026-07-02T11:43:39.000Z",
"site": "https://dev.to",
"tags": [
"computerscience",
"cpp",
"gamedev",
"showdev",
"GBATEK",
"github.com/adityatr64/CBA"
],
"textContent": "## `$ CBA` : - an ARM7TDMI / GBA emulator, built from scratch\n\n**Author:** Aditya R\n\n### Contents\n\n * Introduction\n * From the ground up\n * What's an emulator, actually?\n * Meet the ARM7TDMI\n * The GBA's memory map\n * ARM's party trick: conditional execution\n * Genesis\n * All about CBA\n * File structure\n * Workflow of CBA\n * Instructions supported\n * Main features\n * How you can run CBA locally\n * Where things stand\n * Experience\n * Fun stuff\n * References\n\n\n\n## Introduction\n\n### What made me pick this project?\n\nA few months after wrapping up **psh** a POSIX-ish shell I built with a team over a summer : - I wanted to go one level lower. A shell talks to the kernel. I wanted to know what's underneath the kernel: the chip itself, decoding raw bytes into \"move this register into that one\" and nothing else. An emulator felt like the natural next rabbit hole.\n\nI picked the **ARM7TDMI** specifically because it's small enough for one person to reasonably understand end-to-end : - unlike, say, a modern x86 core and because it's the CPU inside the **Game Boy Advance** , which means every decision I make can be checked against real hardware, real games, and an extremely well-documented memory map. **CBA** is the result: a from-scratch attempt at a hardware-accurate GBA emulator in C++, with no borrowed CPU cores or existing emulator code, just the ARM architecture reference manual, GBATEK, and a lot of trial and error.\n\nUnlike psh, this one's solo no teammates, no mentors, just me and a very large PDF.\n\n## From the ground up\n\nSome background before diving in.\n\n### What's an emulator, actually?\n\nAn emulator is software that pretends to _be_ a different piece of hardware, closely enough that programs written for the real thing run on it unmodified. A GBA emulator needs to reproduce the GBA's CPU, its memory layout, its graphics and sound hardware, and its timers, closely enough that a real cartridge dump can't tell the difference. CBA is currently focused on the hardest and most foundational part of that: the CPU.\n\n### Meet the ARM7TDMI\n\nThe ARM7TDMI is a 32-bit RISC processor from the early '90s that ended up everywhere the original GBA, the Nintendo DS (as a co-processor), the classic iPod, and a long list of embedded devices. The name is basically a spec sheet:\n\n * **T** : - Thumb, a compressed 16-bit instruction encoding traded for smaller code size\n * **D** : - Debug hardware support\n * **M** : - a hardware Multiplier\n * **I** : - an embedded In-circuit emulator interface\n\n\n\nFor an emulator author, the part that matters most is that it's really two instruction sets sharing one CPU: 32-bit ARM instructions and 16-bit Thumb instructions, with the ability to switch between them at runtime.\n\n### The GBA's memory map\n\nReal hardware doesn't just have \"RAM\" : - it has several distinct memory regions, each with its own size, speed, and purpose, all mapped into one 32-bit address space:\n\n\n\n 0x00000000 – 0x00003FFF BIOS 16 KB\n 0x02000000 – 0x0203FFFF WRAM 256 KB (on-board work RAM)\n 0x03000000 – 0x03007FFF IWRAM 32 KB (on-chip, fast work RAM)\n 0x04000000 – 0x040003FE I/O registers\n 0x05000000 – 0x050003FF Palette RAM 1 KB\n 0x06000000 – 0x06017FFF VRAM 96 KB\n 0x07000000 – 0x070003FF OAM 1 KB (sprite attributes)\n 0x08000000 – 0x09FFFFFF Game Pak ROM / FlashROM\n 0x0E000000 – 0x0E00FFFF Game Pak SRAM\n\n\nEvery load/store instruction has to figure out which of these regions an address falls into and route it accordingly, which is most of what CBA's `Memory` class does.\n\n### ARM's party trick: conditional execution\n\nMost instruction sets only let _branches_ be conditional. ARM lets almost every instruction carry a 4-bit condition code, so \"add these two registers, but only if the last comparison was equal\" is a single instruction, no branch required. It's a neat trick for avoiding pipeline-stalling branches on simple if-statements and it means the first thing CBA's decoder has to do, before it even figures out _what_ instruction it's looking at, is check _whether_ to run it at all.\n\n## Genesis\n\nCBA started about as small as a project can start: a `Registers` struct 16 general-purpose registers, plus the current and saved status registers (CPSR/SPSR) and a `Memory` class that could barely do more than load a raw binary into a buffer. Then a fetch → decode → execute loop that, for a good while, decoded nothing at all, it just walked the ROM printing hex and hoping.\n\nEverything since has been filling in that decode step, one instruction category at a time, and checking the results against what the reference manual says should happen.\n\n## All about CBA\n\n### File structure\n\n\n CBA\n ├── CMakeLists.txt\n ├── LICENSE\n ├── README.md\n ├── link.ld\n ├── makeGBA.sh\n ├── makeGBA.ps1\n ├── include\n │ ├── arm.hpp\n │ ├── cpu.hpp\n │ └── memory.hpp\n └── src\n ├── arm.cpp\n ├── cpu.cpp\n ├── emulator.cpp\n ├── kernel.s\n ├── memory.cpp\n └── thumb.cpp\n\n\n### Workflow of CBA\n\nOn boot, CBA loads a raw `.gba` binary straight into ROM, peeks at the very first instruction to guess whether it's ARM or Thumb code, and sets the CPU's starting mode accordingly. From there it's a straightforward loop:\n\n 1. **Fetch** : - read 4 bytes (ARM mode) or 2 bytes (Thumb mode) at the program counter\n 2. **Decode** : - for ARM, walk a table of `{mask, pattern, handler}` entries and find the first one whose pattern matches the masked instruction bits; for Thumb, switch on the top opcode bits\n 3. **Execute** : - call the matching handler, which reads and writes registers and memory as needed\n\n\n\nThe mask-and-pattern dispatch table, instead of one giant `switch`, mirrors how real ARM decoders are usually built ARM's 32-bit encoding is dense enough that a table lookup stays a lot more maintainable than a wall of nested conditionals.\n\n### Instructions supported\n\nARM-mode decoding currently covers:\n\n * **BX** : - branch and exchange, the actual ARM ⟷ Thumb mode switch\n * **MRS / MSR** : - read and write the status registers, both immediate and register forms\n * **SWP** : - atomic register/memory swap\n * **MUL / MLA** : - multiply and multiply-accumulate\n * **Halfword / signed data transfer**\n * **Branch / Branch-with-Link**\n * **Load/Store** : - word and byte, base register + immediate offset\n * **SWI** : - software interrupt (today it logs the interrupt and halts, this is the eventual home for BIOS syscalls)\n * **Data processing** : - the ALU ops (MOV, ADD, CMP, and friends), including flag updates\n\n\n\nMultiply-long and block data transfer (LDM/STM) are stubbed but not wired up yet, and Thumb mode currently understands a handful of opcodes immediate MOV, register ADD, conditional branch, literal LDR rather than the full set.\n\n### Main features\n\n * Full condition-code evaluation : - all 16 ARM conditions, on almost every instruction, not just branches\n * A mask-and-pattern dispatch table for ARM decoding, instead of a wall of `if`s\n * Automatic ARM/Thumb mode detection on boot, by inspecting the first instruction in ROM\n * A real, GBA-accurate memory map (BIOS / WRAM / IWRAM / I/O / Palette / VRAM / OAM / ROM / SRAM), not one flat byte array\n * Hand-written ARM assembly test kernels, assembled and linked with a custom linker script, to check instruction behaviour against the reference manual\n * **In progress:** full Thumb mode, block data transfer, multiply-long, and a real BIOS/syscall layer\n * **Up next:** an SDL front-end for real-time framebuffer output, and tightening the fetch-decode-execute loop toward cycle-accurate timing\n\n\n\n## How you can run CBA locally\n\n\n git clone https://github.com/adityatr64/CBA\n cd CBA\n cmake -B build\n cmake --build build\n\n\nTo assemble a test ROM you'll need the `arm-none-eabi` GCC toolchain:\n\n\n\n mkdir -p bin\n ./makeGBA.sh # Linux / macOS\n ./makeGBA.ps1 # Windows (PowerShell)\n\n\nThis assembles `src/kernel.s` against `link.ld`, drops a `bin/kernel.gba`, and hex-dumps the first few lines so you can eyeball the header. Then run the emulator which, courtesy of `CMakeLists.txt`, is quietly named `PlusBoy` behind CBA's back:\n\n\n\n ./PlusBoy\n\n\n## Where things stand\n\nNo formal benchmarks yet that's further down the roadmap, once there's a display to measure frame timing against. For now, correctness gets checked the old-fashioned way: a small hand-assembled ARM program runs through `MOV`, `CMP`, `ADDS`, `MRS`, and `MSR`, and after _every single instruction_ the CPU dumps registers r0–r6 and all four condition flags to the terminal, so I can check them by hand against what the manual says should happen. It's not elegant. It's a lot of scrolling. It works.\n\n## Experience\n\nDoing this solo after a team project like psh is a genuinely different kind of hard, there's no one to rubber-duck a bug with, and no code review to catch a forgotten sign-extension before it costs you an evening. Most of the bugs here aren't the segfault-and-gdb kind; they're the quieter, more annoying kind, where the code compiles fine, runs fine, and just quietly computes the wrong number a flag off by one bit, an offset that needed sign-extending and didn't, a condition checked backwards. The only real fix is going back to the datasheet, instruction by instruction, and being willing to be wrong a lot before being right.\n\nIt's also given me a much deeper respect for how much a \"simple\" CPU actually does per instruction. Rereading the same page of the manual for the tenth time and finally noticing the one detail you'd missed is a very specific kind of satisfying.\n\n## Fun stuff\n\n * The compiled binary is secretly named `PlusBoy` `CMakeLists.txt`'s project name has never matched the repo name, and at this point it's staying that way on purpose.\n * The first program CBA ever ran \"successfully\" was 15 lines of hand-written ARM assembly that zeroes a register, compares it to zero, flips a couple of CPSR flags around, and then loops forever doing absolutely nothing. Watching those flags flip correctly in the terminal was, unreasonably, one of the best feelings of the project so far.\n * Every single cycle currently prints a full register-and-flag dump to the terminal, so running anything longer than a few dozen instructions turns your terminal into a wall of hex. Future-me's problem.\n\n\n\n## References\n\n * GBATEK : - Martin Korth's GBA hardware reference; CBA's memory map is credited straight from here\n * ARM7TDMI Technical Reference Manual\n * ARM Architecture Reference Manual (ARMv4T)\n * Stack Overflow\n * the gbadev community/wiki\n\n\n\n_Built solo, with reference material pulled from the sources above. Source: github.com/adityatr64/CBA_",
"title": "CBA A from-scratch ARM7TDMI / Game Boy Advance emulator, written in C++"
}