Interpreter scope
CHIP-8 is a bytecode virtual machine created by Joseph Weisbecker in 1977 for the COSMAC VIP microcomputer, implemented as an interpreter on the RCA 1802. Writing a CHIP-8 emulator means writing an interpreter for a virtual machine rather than modeling hardware components. It lacks hardware interrupts, memory mapping, a bus cycle model, and dedicated video processors. Later projects like the Intel 8080 or Game Boy introduce those subsystems.
High level view of the emulator
The emulator consists of a struct holding the machine's state and a loop that advances it. For CHIP-8 the state fits in ten lines:
pc: u16 // program counter, starts at 0x200
i: u16 // index register (addresses, not values)
v: [u8; 16] // V0..VF — VF doubles as the flag register
ram: [u8; 4096] // font at 0x50, program from 0x200
stack: [u16; 16] // return addresses only, no frames
sp: u8
delay: u8 // both tick down at exactly 60 Hz
sound: u8 // beeps while nonzero
fb: [bool; 64*32]// naive form; §04 packs this to [u64; 32]
keys: [bool; 16] // hex keypad, not a keyboard
The loop executes three steps: fetch, decode, and execute. Read two bytes at pc (every instruction is 2 bytes, stored high byte first), advance pc by 2, split the 16 bits into four nibbles, and match.
Most bugs in this project come down to incorrect decoding or wrong timing.
Glyphs for text
The font contains sixteen glyphs of five bytes each, totaling 80 bytes. It is conventionally loaded at 0x50, below the program area. Only the high nibble of each byte is used, so each glyph is 4 pixels wide and each byte ends in 0. In binary, the bytes 0xF0, 0x90, 0x90, 0x90, 0xF0 form the digit 0:
0xF0 -> 1111 .... ####
0x90 -> 1001 .... #..#
0x90 -> 1001 .... #..#
0x90 -> 1001 .... #..#
0xF0 -> 1111 .... ####
Instruction FX29 sets the index register I to a glyph's address, which DXYN then reads. The address is FONT_START + (VX & 0x0F) * 5. The mask is required because VX is an 8-bit register. Without masking, VX * 5 can point I to address 1355, nearly 1200 bytes past the 80-byte table and inside program memory. Games use this instruction to draw scores. The table:
0xF0,0x90,0x90,0x90,0xF0, // 0
0x20,0x60,0x20,0x20,0x70, // 1
0xF0,0x10,0xF0,0x80,0xF0, // 2
0xF0,0x10,0xF0,0x10,0xF0, // 3
0x90,0x90,0xF0,0x10,0x10, // 4
0xF0,0x80,0xF0,0x10,0xF0, // 5
0xF0,0x80,0xF0,0x90,0xF0, // 6
0xF0,0x10,0x20,0x40,0x40, // 7
0xF0,0x90,0xF0,0x90,0xF0, // 8
0xF0,0x90,0xF0,0x10,0xF0, // 9
0xF0,0x90,0xF0,0x90,0x90, // A
0xE0,0x90,0xE0,0x90,0xE0, // B
0xF0,0x80,0x80,0x80,0xF0, // C
0xE0,0x90,0x90,0x90,0xE0, // D
0xF0,0x80,0xF0,0x80,0xF0, // E
0xF0,0x80,0xF0,0x80,0x80, // F
Transcribing this table manually makes the sprite format clear, each set bit represents a lit pixel which is the mechanism DXYN relies on.
Three independent clocks
CHIP-8 has three separate clock rates that must not be coupled:
- CPU execution runs at roughly 500–700 instructions per second. This rate was never standardised, so it should be configurable.
- The delay and sound timers decrement at 60 Hz regardless of CPU speed. Games rely on the delay timer for wall-clock timing, so coupling it to instruction cycles will cause games to run at the wrong speed across platforms.
- Display refresh occurs at 60 Hz. On original hardware,
DXYNblocked until vblank, which is handled as a quirk (see §02).
A standard loop structure uses a 60 Hz timer tick that executes N CPU cycles, decrements both timers by one, and presents the frame. On desktop, N ≈ 10 produces 600 Hz. On the RP2350 a hardware timer alarm drives that outer tick.
Isolating the core
Keep windowing, printing, and std dependencies out of the emulator core. The core should receive inputs through methods and expose its framebuffer through a borrowed slice. This separation allows the same crate to compile for desktop, WebAssembly, and the RP2350 without changes. Adding no_std compatibility from the beginning avoids major refactoring later.
Guides, References & Tools
Optional
- Code: The Hidden Language of Computer Hardware and Software — Charles Petzold, 2nd edition
optional - Constructs a CPU from switches and relays to demonstrate instruction fetch, decode, and execution. The chapters on memory, control units, and machine code are directly relevant here. The 2022 second edition expands these chapters, and the companion site hosts interactive circuits for 16 of them to demonstrate signal propagation.
- codehiddenlanguage.com · 2nd edition, Microsoft Press 2022
This is optional if the model above is familiar. Readers who want a concrete grounding in registers, buses, and instruction decoding will find it in this text. Also it's a fantastic book if you haven't read it
Reading through the CPU construction chapters provides helpful context when starting, where binary data is first decoded into mnemonics.
There's two guides worth reading end to end, five references and two community tools below. Definitely start with Tobias V. Langhoff's "Guide to making a CHIP-8 emulator" before writing code, and consult Cowgod and Mikolay during implementation.
Guides
- Guide to making a CHIP-8 emulator — Tobias V. Langhoff
start here - A language-agnostic guide that explains the purpose of each architectural component and covers platform quirks alongside standard instructions. Read this before starting implementation.
- tobiasvl.github.io/blog/write-a-chip-8-emulator/ · archived 3 Jul 2025, in case a personal blog ever goes quiet
- An Introduction to Chip-8 Emulation using the Rust Programming Language — aquova
Required - This project provides a walkthrough in Rust with a core, desktop, and wasm workspace layout. It offers solid code organisation and idioms. Cross-reference it with Langhoff and the changes detailed in §06.
- github.com/aquova/chip8-book · PDF and EPUB builds on the releases page
References
- Cowgod's Chip-8 Technical Reference v1.0
has errors - A widely cited 1997 opcode reference. It serves as a useful lookup table, but its entries for shift (
8XY6/8XYE) and register store/load (FX55/FX65) describe SUPER-CHIP behavior rather than the original COSMAC VIP. - devernay.free.fr/hacks/chip8/C8TECH10.HTM
- Matt Mikolay's CHIP-8 Instruction Set
- A precise technical reference that handles ambiguous instructions more carefully than Cowgod. The accompanying "Mastering CHIP-8" document describes the architecture from a programmer's perspective.
- github.com/mattmikolay/chip-8/wiki/CHIP‐8-Instruction-Set · wiki root for "Mastering CHIP-8"
- Gulrak's CHIP-8 opcode table
- A comparison matrix documenting instruction behavior across CHIP-8, CHIP-48, SCHIP 1.0/1.1, and XO-CHIP, maintained by the author of the Cadmium emulator. Use it to resolve conflicting specifications when implementing quirks in milestone M4.
- chip8.gulrak.net
- Chip-8 on the COSMAC VIP — Laurence Scotford
- A detailed reverse-engineering series covering the original RCA 1802 interpreter instruction by instruction, including timing measurements and hardware-level reasons for
DXYNbehavior. Helpful background once your core passes the initial test ROMs. - laurencescotford.net/chip-8-on-the-cosmac-vip-index/ · tag/cosmac-vip for every post, including ones the index misses
- Awesome CHIP-8
- A community directory listing emulators, documentation, ROM archives, and extension specifications. Useful for comparing against reference implementations and finding tested ROMs.
- chip-8.github.io/links/
Tools
- Octo
- Octo is a browser-based IDE and interpreter that serves as a dependable reference when stepping through ROM execution frame by frame.
- johnearnest.github.io/Octo/
- CHIP-8 Archive
- The CHIP-8 Archive provides modern software tagged with specific quirk requirements, which simplifies debugging compared to unlabelled legacy ROM dumps.
- johnearnest.github.io/chip8Archive/
- Building a CHIP-8 Emulator — Austin Morlan
- A concise C++ walkthrough focusing on decode and dispatch architecture. Useful as a second reference if an instruction explanation in the primary text is unclear.
- austinmorlan.com/posts/chip8_emulator/
Embedded Rust docs
- impl Rust for RP2350
most current - A Pico 2–specific book covering the basics and the embassy setup, written against the actual board we're using. At most get the onboard LED blinking so you know your machine is setup.
- pico.implrust.com
- The Embedded Rust Book
- The official Rust Embedded guide:
no_std, cross-compilation, memory-mapped peripherals, the PAC/HAL/BSP layering. Read the first four chapters and then skim the rest as needed. - docs.rust-embedded.org/book/
- rp-hal RP2350 examples
- Working code for every peripheral on the board using embassy: GPIO, PWM, I²C, timers, multicore.
- github.com/rp-rs/rp-hal · rp235x-hal-examples
- probe-rs + defmt
buy the probe - Deploying firmware via UF2 drag-and-drop offers no diagnostic feedback when an emulator execution halts silently. Connecting a hardware debug probe and using RTT (Real-Time Transfer) logging provides a definitive stack trace to identify the faulting instruction. A secondary Raspberry Pi Pico can be configured to function as this debug interface. I will try to configure a Black Pill as a Black Magic Probe and if I can get it to work will update this
- probe.rs · github.com/knurling-rs/defmt
The quirks and test suites
CHIP-8 implementations diverged between 1977 and 1990 as new interpreters altered instruction behaviors. Software written for one platform frequently fails on another, meaning an emulator that decodes every opcode correctly can still fail to run specific games due to behavioural differences.
Six behaviours account for most compatibility issues. Store each as a bool field in a configuration struct early in development to avoid refactoring later.
| Quirk | Original COSMAC VIP | SUPER-CHIP / modern | Symptom if wrong |
|---|---|---|---|
vf_reset8XY1/2/3 | OR/AND/XOR also reset VF to 0 | VF left untouched | Subtle logic corruption, some games hang |
memoryFX55/FX65 | I is incremented by X+1 | I unchanged (or +X on CHIP-48) | Save/load of register blocks reads garbage |
display_waitDXYN | Blocks until vblank — caps drawing at 60 sprites/sec | Draws immediately | Games run absurdly fast, heavy flicker |
clippingDXYN | Starting coords wrap (x%64, y%32), then the sprite clips at the right and bottom edges | XO-CHIP and many modern emulators wrap the overhanging pixels to the opposite edge instead | Sprites tear at the edges, or smear across to the far side |
shifting8XY6/8XYE | VX = VY shifted | VX shifted in place, VY ignored | Very common, breaks lots of games silently |
jumpingBNNN | Jump to NNN + V0 | Read as BXNN: jump to XNN + VX | Wild jumps into data, instant crash |
Automated test suite
github.com/Timendus/chip8-test-suite provides ROMs that render visual pass/fail output directly to the screen once
DXYNis functional. The suite contains eight ROMs, numbered in execution order:1-chip8-logo,2-ibm-logo,3-corax+,4-flags,5-quirks,6-keypad,7-beep, and8-scrolling(the last requires SUPER-CHIP or XO-CHIP support). Running1-chip8-logofirst is useful because it omits7XNNand requires only byte-aligned sprite drawing. Running these ROMs headlessly incargo testby stepping a fixed number of cycles and comparing a hash of the framebuffer provides reliable regression testing.
Implementation details
- The COSMAC VIP used a 4×4 hexadecimal keypad laid out
1 2 3 C / 4 5 6 D / 7 8 9 E / A 0 B F. Standard emulator implementations map these rows to1234 / QWER / ASDF / ZXCVon modern keyboards. Games assume the physical key positions, so mapping key codes purely by digit label produces unintuitive controls. - The wait-for-key instruction
FX0Ahalts execution until an input event occurs. The standard approach decrementspcby 2 so the opcode repeats until satisfied. On original hardware, execution resumed on key release rather than initial keypress. - Drawing with
DXYNsets VF to 1 if any active pixel is cleared by the XOR operation, and 0 otherwise. Games use this mechanism as their primary collision detection. - Sprites have a fixed width of 8 pixels and a variable height from 1 to 15 rows. Each row is defined by one byte, with the most significant bit mapped to the leftmost pixel.
- The stack stores only return addresses, with no concept of stack frames or arguments. Modern implementations generally support 16 levels, whereas the original VIP interpreter reserved space for 12. Opcode
2NNNpushes the return address, and00EEpops it.
Build order
This sequence introduces debugging checkpoints at each milestone. Building verification tools before implementing the full opcode set helps isolate decode and timing errors early.
- Load and disassemble
- Load ROM bytes into
ram[0x200..], copy the font table into0x50, and write a disassembler that prints addresses, raw opcodes, and mnemonics without executing instructions. - Done when the disassembler produces readable assembly for
2-ibm-logo. - Splash screen rendering
- Implement five instructions to run
1-chip8-logo:00E0,1NNN,6XNN,ANNN, andDXYN. Next add7XNNand horizontal bit-shifting for unaligned sprites in2-ibm-logo. Output the framebuffer to the terminal using#characters. A simple[bool; 2048]array works at this stage; §04 covers packing to[u64; 32]. - Done when both
1-chip8-logoand2-ibm-logodisplay correctly in terminal output. - Remaining instructions
- Implement the rest of the 35 standard opcodes, including subroutine handling (
2NNN,00EE), binary-coded decimal conversion (FX33), and font indexing (FX29). Verify execution against3-corax+and4-flags. - Done when
3-corax+reports all passes and4-flagsconfirms correct VF handling across arithmetic and logic opcodes. - Quirk configuration
- Add configuration toggles for the six behaviours detailed in §02, defaulting to original COSMAC VIP behavior. Verify both VIP and modern profiles with
5-quirks. - Done when
5-quirkspasses under both COSMAC VIP and SUPER-CHIP configurations. - Input, timers, and audio
- Implement the hex keypad mapping, decouple timer decrements to a stable 60 Hz tick, gate a square-wave audio tone on the sound timer, and connect a desktop display window.
- Done when playable games like Brix or Pong run at stable speed with responsive input and audio feedback.
- Platform extensions or embedded port
- Choose an extension path: add SUPER-CHIP and XO-CHIP instructions, or port the core to the RP2350 microcontroller described in §05.
- Done when the selected extension passes its test suite or runs standalone on the target hardware.
Diagnostic tools
A ring buffer recording recent (pc, opcode) pairs helps diagnose crashes by dumping trace history on panic or error states. Adding single-step execution and register inspection to the desktop frontend during M3 simplifies tracking bugs before running on microcontroller targets.
Rust workspace architecture
A three-crate Cargo workspace isolates platform-independent emulation logic from platform-specific I/O:
chip8/
├── chip8-core/ #![no_std], zero I/O, all the logic
├── chip8-desktop/ winit + pixels (or minifb)
└── chip8-pico/ #![no_std] + #![no_main], rp235x-hal or embassy-rp
The public API for the core crate requires only a minimal surface:
impl Chip8 {
pub fn new(cfg: Quirks) -> Self;
pub fn load_rom(&mut self, bytes: &[u8]);
pub fn step(&mut self); // one instruction
pub fn tick_timers(&mut self) -> bool; // true = beep
pub fn set_key(&mut self, k: u8, down: bool);
pub fn framebuffer(&self) -> &[u64; 32]; // 1 bit per pixel
}
Implementation choices
- Instruction decoding using
match (a, b, c, d)on the four nibbles is clearer than nestedifbranches, and the compiler flags unreachable patterns automatically. - Representing the display as
[u64; 32]stores oneu64per row, matching the 64-pixel width. XOR drawing for an 8-pixel sprite row becomes a bit shift and XOR operation. Collision detection evaluates asold & shifted_sprite != 0before modifying the row. This layout occupies 256 bytes instead of 2048 bytes for a boolean array. - Because the core is
no_std, opcodeCXNNcannot call standardrandfunctions that depend on an operating system entropy source. Instead, provide a deterministic generator inside theno_stdcore, such as a lightweight xorshift seeded by the caller. Avoidingrandor depending on an older release likerand 0.7prevents pulling in unexpected dependencies. Microcontrollers like the RP2350 have a hardware TRNG, but abstracting RNG generation behind the core's API keeps the crate portable. - Arithmetic operations require explicit overflow handling. Opcode
7XNNwraps without modifying VF, while8XY4sets VF on carry. Usingwrapping_addandoverflowing_addprevents debug-build overflow panics and ensures consistent behavior. - Testing the core does not require a window or an event loop. Unit tests can load raw
u16opcode sequences into RAM, instantiate aChip8struct, and callstep(), asserting on register and flag states directly.
Desktop frontends
The aquova guide uses sdl2, which requires linking external SDL2 development libraries. Alternatives include minifb, which manages a window and framebuffer without external C dependencies on macOS or Windows (though Linux requires libx11-dev), or pixels combined with winit for hardware-accelerated presentation.
The Pico 2 track
The Pico-2 with RP2350 is a nice target board for this, easy to get and this project doesn't need wifi/bt, plus Rust easily works with it. The board has dual ARM Cortex-M33 and/or Hazard3 RISC-V cores at 150 MHz with 520 KB of SRAM, and putting a state a machine that needs 4 KB of RAM and 256 bytes of framebuffer so plenty of headroom. If I come across a smaller μC that is easy to get and affordable I might put a guide to get CHIP-8 in Rust on that controller too.
Pick a HAL
| Option | Style | Take |
|---|---|---|
rp235x-hal 0.4rp-rs/rp-hal | Blocking, bare-metal, embedded-hal traits | Exposes bare-metal peripherals directly using embedded-hal traits. Now on crates.io — earlier write-ups telling you to vendor the repo are out of date. |
embassy-rp 0.9feature rp235xa | async executor, batteries included | Provides an async executor with built-in timer and USB abstractions. A 60 Hz tick evaluates to a single Ticker instantiation, at the cost of hardware abstraction. Slightly more magic between you and the silicon. |
Either works. If the goal is understanding, start with rp235x-hal, if the goal is a working handheld then embassy will get you there faster. I myself started with embassy because that's what I learned embedded Rust with.
Hardware choices
- Display: an SSD1306 128×64 I²C OLED is a good option, exactly twice the CHIP-8's resolution in both axes, so every CHIP-8 pixel is a 2×2 block and there is no scaling arithmetic at all. Drive it with the ssd1306 crate over embedded-graphics. If I²C's refresh rate annoys you, an ST7789 SPI LCD is much faster and gives you colour for a HUD.
- Input: 16 keys. A 4×4 tactile matrix scanned across 8 GPIOs matches the VIP's real layout. I can't believe there was a time when people punched in game's using hex, blew my mind when I realized it. A capacitive TTP229 or MPR121 board is an easy option.
- Sound: PWM to a piezo. Set a square wave on a PWM slice and gate the output on
sound_timer > 0. Add some buzzing to give the project life - ROMs:
include_bytes!a handful into flash for the first version. A menu reading from SD via embedded-sdmmc is a good version 2.0 variant if you want to continue with the project.
Timing on bare metal
Do not build your loop out of delay_ms. Use the hardware timer to fire a 60 Hz alarm; on each fire, run your N CPU cycles, decrement the timers once, and push the framebuffer to the display. With two cores available you could put the display flush on core 1 and keep the interpreter on core 0 but that sorta feels like overkill for a CHIP-8. The hardware is there so I guess make use of it.
Sequencing advice
Get the desktop version passing the full Timendus suite before you touch the microcontroller. Debugging interpreter logic simultaneously with hardware abstractions via a 128×64 display without logging will be confusing. The port should be a frontend swap, nothing more — and if it isn't, that's useful information about your core.
The aquova book
An Introduction to Chip-8 Emulation using the Rust Programming Language by aquova contains nine chapters, a three-crate workspace (core, desktop, wasm), and bundled test ROMs.
Ideas to use from it
- The crate workspace structure cleanly separates core emulation logic from frontend presentation.
- The material targets readers who know Rust syntax but have not written an emulator, with full implementations for each opcode.
- The WebAssembly chapter provides a functional browser build using the same core crate.
What's stale or missing in 2026
| Issue | What to do |
|---|---|
| Rust edition 2018 throughout | Bump to 2021 or 2024; nothing in the code should break. |
rand ^0.7.3 in the core crate | Replace with your own xorshift. Required for no_std; this is the blocker for the Pico port. |
sdl2 ^0.34.3 for the desktop frontend | Bump it, or swap for minifb. Expect to install SDL2 natively (brew install sdl2) and possibly fight linker paths on macOS. |
wasm-bindgen ^0.2.69 / web-sys ^0.3.46 | Years old. Bump both sicne the APIs used are stable. |
| No quirks discussion at all | This is real gap, so do read §02 here and Langhoff's guide alongside chapter 5. |
| No test ROMs / no test harness | Add the Timendus suite yourself at M3, don't skip this. |
Core is std, not no_std | Add #![no_std] from the get go. Requires minor modifications, see the BCD row. |
Font loaded at 0x000, and FX29 is a bare c * 5 (lib.rs:60, lib.rs:390) | The book does not use the conventional 0x50 base. If you follow this guide's layout but the book's FX29, I points at uninitialised memory and scores render as garbage. Pick one convention. Either way add the & 0x0F mask, which the book also omits. |
BCD (FX33) uses f32 and .floor() (lib.rs:395-402) | Float methods don't exist in core, Float methods do not exist in core, this fails to compile under #![no_std]. Rewrite with integers: vx / 100, (vx / 10) % 10, vx % 10. |
Use chapters 1–6 as a structural guide while referencing Langhoff for opcode semantics and the Timendus test ROMs for validation. Implementing the core crate as #![no_std] and removing external rand dependencies early will avoid refactoring during the microcontroller port.
After CHIP-8
This guide covers simple interpretive execution, where an emulator decodes and executes instructions sequentially. While standard for CHIP-8 and introductory projects, other CPU emulation techniques exist along a performance spectrum, including direct-threaded dispatch, dynamic recompilation, and just-in-time (JIT) compilation. High-level emulation (HLE) forms another approach, replacing lower-level hardware execution with native reimplementations of system API calls.
- NES Emulator Part #1: Bitwise Basics & Overview — javidx9 (One Lone Coder)
watch this - A video series implementing an NES emulator from the 6502 CPU upward with narrated design decisions. While the NES is substantially more complex than CHIP-8, the series demonstrates bus routing and precision trade-offs common to hardware emulation.
- youtu.be/F8kx56OZQhg · full series on youtube.com/@javidx9
- Architecture of Consoles — Rodrigo Copetti
- An illustrated architectural analysis of production gaming hardware from the Game Boy through modern consoles. Useful for understanding system components, memory buses, and video controllers before tackling full hardware emulation.
- copetti.org/writings/consoles/
- emudev.org — Getting Started
architecture overview - A community documentation hub covering emulation fundamentals, including HLE versus LLE and core CPU interpreter architectures, before moving into JIT compilation, graphics pipelines, audio generation, and reverse-engineering methods. It includes suggested progression paths and source code references.
- emudev.org/getting_started
Common subsequent hardware projects, ordered by complexity:
- Intel 8080 arcade hardware (such as Space Invaders, documented at emulator101.com) introduces real hardware features: paired 8-bit registers, condition flags, dedicated I/O ports via
INandOUT, memory-mapped video RAM, and display interrupts. Instruction cycle timing becomes necessary for correct game speed. - The Game Boy (DMG) is a common second project. Documentation is centralized at Pan Docs with community tools at gbdev.io and comprehensive test ROMs by Blargg and Mooneye. Implementing the Sharp SM83 CPU core is straightforward, while accurately modeling the Pixel Processing Unit (PPU) scanline timing requires significantly more effort.
- The Nintendo Entertainment System (NES) introduces cartridge memory mappers and strict Picture Processing Unit (PPU) timing requirements. Technical reference material is maintained on the NESdev Wiki alongside the javidx9 video walkthrough.
The r/EmuDev subreddit and its Discord community maintain the emudev.org documentation and provide active technical discussion for verifying edge-case hardware behaviors and test results.