writing

Emulating a MOS 6502 CPU Inside MIT Scratch

2026-10-30
4 min read
··· people have read this piece

If building a transformer in MIT Scratch pushes the limits of floating-point arithmetic and memory layouts, building a CPU emulator pushes the limits of discrete logic.

Modern CPU emulators rely heavily on bitwise operations (&, |, ^, <<, >>), unsigned byte overflows, direct pointer arithmetic, and fast instruction jump tables. MIT Scratch provides none of these:

  • There are no bitwise operators. No AND, no OR, no XOR, no shifts.
  • Numbers are IEEE 754 double-precision floats, not unsigned 8-bit or 16-bit integers.
  • Scratch has no function pointers or switch statements, turning opcode dispatch into a non-trivial architectural challenge.

To see how far an educational block environment could be taken, I built cpuFromScratch: a cycle-conforming CPU emulator implemented in MIT Scratch 3.0, generated programmatically from Python, and verified headlessly against industry-standard test suites.

The emulator supports SUBLEQ (a one-instruction computer), SCRATCH-8, full CHIP-8, and a complete MOS 6502 microprocessor core—the chip that powered the Apple II, Commodore 64, and NES.


1. Synthesizing Bitwise Logic Without Bitwise Operators

The first wall in building any CPU in Scratch is the Arithmetic Logic Unit (ALU). How do you execute AND, ORA, EOR, ASL, or LSR on 8-bit registers when your language only has +, -, *, /, and mod?

We evaluated two approaches:

Approach A: Arithmetic Bit Extraction

You can extract bits mathematically by successive division and modulo: bitk(x)=⌊x2k⌋(mod2)\text{bit}_k(x) = \left\lfloor \frac{x}{2^k} \right\rfloor \pmod 2 While mathematically pure, extracting 8 bits dynamically for every single instruction creates unacceptable interpreter overhead under Scratch's frame budget.

Approach B: LUT & Page Decomposition

Instead of bit-banging per cycle, we synthesized bitwise operations using structured lookups and decomposed power-of-two arithmetic. We verified this with exhaustive automated testing:

  • Exhaustive 65,536-pair tests for every combination of (a, b) ∈ [0, 255] × [0, 255] for AND, OR, and XOR.
  • Bit shifts (ASL, LSR, ROL, ROR) implemented with explicit carry-flag extraction and 8-bit wrap masks (val mod 256).

Every bitwise operation was proven bit-identical to native C/Python behavior before a single opcode was wired up.


2. Emulating the MOS 6502 Core

The MOS 6502 is notorious for subtle edge-case behaviors:

  1. Decimal Mode (SED/CLD): When the Decimal flag (DD) is set, addition (ADC) and subtraction (SBC) operate in Binary Coded Decimal (BCD). Implementing correct BCD carry and overflow flags under IEEE floats required meticulous nibble-by-nibble masking.
  2. The Overflow Flag (VV): Two's-complement signed overflow occurs when adding two numbers of the same sign produces a result of the opposite sign: V=¬(A⊕M)∧(A⊕R)∧0x80V = \neg (A \oplus M) \land (A \oplus R) \land 0x80
  3. Zero-Page and Indexed Indirect Addressing: Handling the 6502's 13 addressing modes (Indexed Indirect (zp,X), Indirect Indexed (zp),Y, Absolute,X) requires wrapping 8-bit page boundaries cleanly within a 1-indexed 64KB memory arena list.

3. Automated Verification & Conformance

In emulator engineering, writing code is 20% of the effort; passing edge-case test suites is the remaining 80%. We tested the Scratch 6502 implementation headlessly using Node.js scratch-vm against the most grueling community benchmarks:

Test Suite Scope Result
Klaus Dormann Functional Suite 30,646,176 instructions executed to $3469 PASSED (0 errors)
Klaus Dormann Decimal Suite 14,464,187 instructions tested for BCD parity to $024B PASSED (ERROR=0)
Tom Harte ProcessorTests 2,510,059 test cases across all 151 legal NMOS opcodes PASSED
nestest (nestest.nes) 5,003 official opcode instructions verified against golden logs PASSED (bit-identical)
Timendus CHIP-8 Suite 8 ROM suites (1-chip8-logo to 8-scrolling) PASSED (golden frame hashes verified)
Full Automated Harness tests/test_m0.py through tests/test_m11.py 133 passed, 1 skipped, 0 failed

4. CHIP-8 Graphics and Live Input

In addition to the 6502 core, the project implements a complete CHIP-8 virtual machine capable of running original 1970s game ROMs (Pong, Space Invaders, Tetris).

  • Display: CHIP-8 renders a 64×3264 \times 32 monochrome framebuffer. In Scratch, the display is rendered through a contiguous VRAM list, drawing pixels using coordinate mapping into Scratch stage dimensions.
  • Input: Standard 16-key hex keypads (0–F) mapped cleanly to keyboard inputs 1234 / qwer / asdf / zxcv.
  • Timers: Emulated 60 Hz delay and sound timers decremented independently of instruction fetch cycles.

5. Architectural Lessons

  1. Emulation is Specification Compliance: A CPU emulator is either 100% correct or it cannot boot an operating system or game. The difference between an emulator that "sort of works" and one that passes Klaus Dormann is hundreds of subtle flag interactions (like the undocumented behavior of decimal mode on invalid BCD inputs).
  2. Generators Beat Hand-Coding: Hand-drawing blocks in the Scratch UI for 151 distinct opcodes with 13 addressing modes would take months and introduce endless human transcription bugs. Writing a compiler in Python that emits valid Scratch .sb3 JSON project archives turns an impossible GUI task into a deterministic software build pipeline.
  3. The Substrate Doesn't Limit Rigor: Constraints (like Scratch's lack of bitwise logic) do not excuse sloppy engineering. If a language lacks an abstraction, you synthesize it from first principles and test it until the error rate is zero.

The complete code, generators, and test harness are open source on GitHub: