Skip to content

lallenlowe/simpleCPU

Repository files navigation

simpleCPU icon

simpleCPU

This is an experiment in simulating a very simple CPU, for my own education. It is VERY loosely based on the classic 6502 Processor.

Quickstart

  • Node.js >= 20
  • Install deps: npm i
  • Build: npm run build
  • Run: node dist/index.js

This boots straight into EhBASIC (Enhanced 6502 BASIC). Type PRINT 6*7 to verify it works.

Running Programs

# Boot into EhBASIC (no arguments needed)
node dist/index.js

# Woz Monitor
node dist/index.js programs/roms/wozmon.bin

# Standalone assembly program (loads at $0400)
node dist/index.js programs/demos/bounce.bin --org 0400

# Apple 1 Integer BASIC (loads at $E000, overrides default ROM)
node dist/index.js programs/roms/a1basic.bin --org E000 --rom none

The EhBASIC ROM is always loaded at $C000$FFFF by default. Standalone programs get access to ROM routines (graphics primitives, I/O) automatically. Use --rom <file> to override the default ROM, or --org to set the load address for your binary.

Piping BASIC programs

You can pipe a .bas file as keyboard input — the simulator translates LF to CR and re-opens /dev/tty after EOF so you can keep typing:

# EhBASIC programs (pipe C for Cold start, blank line to skip memory size)
(printf "C\n\n"; cat programs/games/startrek.bas) | node dist/index.js

# Apple 1 BASIC programs
cat programs/games/lander.bas | node dist/index.js programs/roms/a1basic.bin --org E000

Sample programs

ROMs (programs/roms/)

Program Load address Description How to run
wozmon.bin $0200 (default) Woz Monitor node dist/index.js programs/roms/wozmon.bin
a1basic.bin $E000 Apple 1 Integer BASIC node dist/index.js programs/roms/a1basic.bin --org E000

Games (programs/games/)

Program Load address Description How to run
depths.bin $0400 DEPTHS — complete roguelike dungeon crawler (C/cc65) node dist/index.js programs/games/depths.bin --org 0400
snake.bin $0400 Snake with vsync and sound (C/cc65) node dist/index.js programs/games/snake.bin --org 0400
breakout.bin $0400 Breakout with sound & sprites (assembly) node dist/index.js programs/games/breakout.bin --org 0400
startrek.bas Super Star Trek (EhBASIC) (printf "C\n\n"; cat programs/games/startrek.bas) | node dist/index.js
lander.bas Lunar Lander (Apple 1 BASIC) cat programs/games/lander.bas | node dist/index.js programs/roms/a1basic.bin --org E000
tictac.bas Tic-tac-toe (Apple 1 BASIC) cat programs/games/tictac.bas | node dist/index.js programs/roms/a1basic.bin --org E000

Demos (programs/demos/)

Program Load address Description How to run
bounce.bin $0400 Bouncing ball with vsync (assembly) node dist/index.js programs/demos/bounce.bin --org 0400
bounce3.bin $0400 Mode 3 bouncing ball (assembly) node dist/index.js programs/demos/bounce3.bin --org 0400
gfxtest.bin $0400 Color gradient (assembly) node dist/index.js programs/demos/gfxtest.bin --org 0400
gfxtest2.bin $0400 Mode 2 vertical stripes (assembly) node dist/index.js programs/demos/gfxtest2.bin --org 0400
gfxtest3.bin $0400 Mode 3 color bars (assembly) node dist/index.js programs/demos/gfxtest3.bin --org 0400
gfxtest4.bin $0400 Mode 4 color bars (assembly) node dist/index.js programs/demos/gfxtest4.bin --org 0400
soundtest.bin $0400 C major scale (assembly) node dist/index.js programs/demos/soundtest.bin --org 0400
chord.bin $0400 3-channel chord (assembly) node dist/index.js programs/demos/chord.bin --org 0400
noisetest.bin $0400 White noise channel (assembly) node dist/index.js programs/demos/noisetest.bin --org 0400
bounce.bas Bouncing ball (EhBASIC) (printf "C\n\n"; cat programs/demos/bounce.bas) | node dist/index.js
gfxtest.bas Color gradient (EhBASIC) (printf "C\n\n"; cat programs/demos/gfxtest.bas) | node dist/index.js
colorbars3.bas Mode 3 color bars (EhBASIC) (printf "C\n\n"; cat programs/demos/colorbars3.bas) | node dist/index.js
colorbars4.bas Mode 4 color bars (EhBASIC) (printf "C\n\n"; cat programs/demos/colorbars4.bas) | node dist/index.js
soundtest.bas C major scale (EhBASIC) (printf "C\n\n"; cat programs/demos/soundtest.bas) | node dist/index.js

Assembling programs

The included programs use cc65 syntax. Install with brew install cc65 on macOS.

# Standalone assembly program at $0400
ca65 --feature loose_string_term --feature labels_without_colons -o prog.o prog.asm
ld65 -C programs/standalone.cfg -o prog.bin prog.o

# Rebuild the EhBASIC ROM
ca65 --feature loose_string_term --feature labels_without_colons -o ehbasic.o programs/roms/ehbasic.asm
ca65 --feature loose_string_term --feature labels_without_colons -o ehbasic_mon.o programs/roms/ehbasic_mon.asm
ld65 -C programs/roms/ehbasic.cfg -o programs/roms/ehbasic.bin ehbasic.o ehbasic_mon.o

Writing C programs (cc65)

A complete cc65 target for simpleCPU lives in target/cc65/. It includes a custom linker script, C runtime startup, and simplecpu.h with all hardware registers pre-defined as memory-mapped C pointers.

# From inside programs/games/ or programs/demos/
make -f ../../target/cc65/Makefile mygame.bin
node dist/index.js programs/games/mygame.bin --org 0400

simplecpu.h provides:

  • IO_PUTCHAR, IO_STATUS, IO_DATA — character I/O
  • MODE_REG, VSYNC, FRAMEBUFFER — graphics
  • CH1_FREQ_LONOISE_VOLUME — sound chip
  • MOUSE_X, MOUSE_Y, MOUSE_BTN — mouse
  • SPR0_XSPR7_COL, SPR_PATTERNS — sprites
  • waitvsync(), pollkey(), getkey(), plot3(), plot4() — helpers

DEPTHS — Roguelike

A complete permadeath dungeon crawler written in C and compiled with cc65. Inspired by Dungeon Crawl Stone Soup.

node dist/index.js programs/games/depths.bin --org 0400

Goal

Descend 10 floors, find the Amulet of Descent (guarded by the Dragon), and escape back to the surface alive.

Controls

Key Action
WASD Move / bump-to-attack
> Descend stairs
< Ascend stairs (requires Amulet)
g Pick up item
i Open inventory
ah Use or equip item in inventory
AH Drop item from inventory
S / D Level-up stat choice (STR or DEX)
? Help screen

Features

  • Procedural dungeons — BSP room generation with L-corridor connections; unique layout every run
  • Field of view — symmetric raycasting (radius 8); explored tiles remembered in dim memory
  • 13 monster types — Rat through Dragon, each with unique stats and colour; IDLE → CHASE → FLEE state machine
  • Combat — d8 hit roll vs. dexterity, d6+STR damage, weapon and armour modifiers; every number shown
  • Items — 6 weapons, 4 armours, 5 potions, 4 scrolls, gold piles; items scale with floor depth
  • Identification — potions have randomised colours, scrolls have nonsense labels; revealed on use
  • Inventory — 8 slots; equip weapons and armour, drink potions, read scrolls
  • Character progression — 9 levels; choose +2 STR or +2 DEX on level-up; full HP restore on level
  • Permadeath — death screen shows cause, stats, and score; new run begins immediately
  • Win condition — Amulet of Descent on floor 10 (Dragon guards it); carry it to the surface
  • Scoring — gold + kills × 50 + deepest floor × 100 + 5000 win bonus

Memory footprint

~10 KB of code + data; well within the 31 KB user RAM limit.

Graphics

The simulator includes a terminal-based graphics chip using half-block characters (▀) with truecolor ANSI escape codes. Graphics are memory-mapped — the CPU communicates with the display purely through the memory map.

Graphics modes

Mode Resolution Colors Bit depth Framebuffer size Terminal size
0 Text only Any
1 64×48 256 8bpp 3,072 bytes 64×24
2 256×192 2 1bpp 6,144 bytes 256×96
3 128×96 16 4bpp 6,144 bytes 128×48
4 128×128 256 8bpp 16,384 bytes 128×64

Enable a mode by writing to the mode register: POKE 65028,1 (mode 1) or STA $FE04 in assembly.

Graphics primitives (ROM)

The ROM includes drawing routines callable from BASIC via CALL or from assembly via JSR:

Primitive Address BASIC usage Parameters (zero page)
CLG (clear) 59598 ($E8CE) POKE 224,color : CALL 59598 $E0=color
PLOT (pixel) 59623 ($E8E7) POKE 225,x : POKE 226,y : CALL 59623 $E0=color, $E1=X, $E2=Y
LINE (Bresenham) 59675 ($E91B) Set $E1-$E4, CALL 59675 $E0=color, $E1=X1, $E2=Y1, $E3=X2, $E4=Y2
FILL (circle) 59802 ($E99A) POKE 229,r : CALL 59802 $E0=color, $E1=X, $E2=Y, $E5=radius

Vsync

Programs can synchronize with the display by writing 1 to the vsync register ($FE05) after completing a frame, then busy-waiting until the renderer clears it to 0. If a program never writes to vsync, the renderer runs freely at ~30 FPS.

; Assembly vsync
LDA #1
STA $FE05
@wait: LDA $FE05
       BNE @wait

Sound

The simulator includes a sound chip running on its own worker thread, using the Web Audio API. It provides 3 independent tone channels and 1 noise channel, all controlled through memory-mapped registers.

Sound registers

Address Description
$FE06 Channel 1 frequency (low byte)
$FE07 Channel 1 frequency (high byte)
$FE08 Channel 1 waveform (0=sine, 1=square, 2=sawtooth, 3=triangle)
$FE09 Channel 1 volume (0–255, 0=silent)
$FE0A$FE0D Channel 2 (same layout)
$FE0E$FE11 Channel 3 (same layout)
$FE12 Noise channel rate (controls noise color)
$FE13 Noise channel volume (0–255)

Frequency is a 16-bit value in Hz (e.g., 440 for concert A). Write 0 to a channel's volume to silence it.

; Play a 440 Hz square wave on channel 1
LDA #1
STA $FE08       ; square waveform
LDA #128
STA $FE09       ; half volume
LDA #<440
STA $FE06       ; freq low byte
LDA #>440
STA $FE07       ; freq high byte
REM Play 440 Hz square wave
POKE 65032,1:REM SQUARE WAVE
POKE 65033,128:REM VOLUME
POKE 65030,184:REM 440 LOW BYTE
POKE 65031,1:REM 440 HIGH BYTE

Color palette

Modes 1, 3, and 4 share a 256-color palette: 16 CGA primaries (0–15), a 6×6×6 color cube (16–231), and a 24-step grayscale ramp (232–255). Mode 3 uses the first 16 entries; modes 1 and 4 use all 256.

Memory Map

Address Description
$0000$00FF Zero page (fast access)
$0100$01FF Stack (SP initialized to $FF)
$0200$03FF System / input buffers
$0400$7FFF User RAM
$8000$BFFF Framebuffer (16 KB)
$C000$FFFF ROM (EhBASIC + monitor + graphics primitives)
$FE00 I/O: decimal number + newline
$FE01 I/O: ASCII character output
$FE02 I/O: input status ($80 = data ready)
$FE03 I/O: input data (read byte)
$FE04 Graphics: mode register (0=text, 1=64×48, 2=256×192, 3=128×96, 4=128×128)
$FE05 Graphics: vsync register
$FE06$FE09 Sound: channel 1 (freq lo/hi, waveform, volume)
$FE0A$FE0D Sound: channel 2
$FE0E$FE11 Sound: channel 3
$FE12 Sound: noise rate
$FE13 Sound: noise volume

The default load address is $0200. Use --org HEX to load elsewhere.

Supported Instructions

All 55 official 6502 instructions are implemented across all addressing modes (151 opcodes), except RTI.

Category Instructions
Load/Store LDA, LDX, LDY, STA, STX, STY
Arithmetic ADC, SBC
Logical AND, ORA, EOR
Compare CMP, CPX, CPY
Shift/Rotate ASL, LSR, ROL, ROR
Inc/Dec INC, DEC, INX, DEX, INY, DEY
Branch BEQ, BNE, BCS, BCC, BMI, BPL, BVS, BVC
Jump/Call JMP, JSR, RTS
Stack PHA, PLA, PHP, PLP
Transfer TAX, TAY, TXA, TYA, TXS, TSX
Flags CLC, SEC, CLI, SEI, CLD, SED, CLV
Other NOP, BRK, BIT

Addressing modes: Implied, Immediate, Zero Page, Zero Page,X, Zero Page,Y, Absolute, Absolute,X, Absolute,Y, Indirect, (Indirect,X), (Indirect),Y

Debug Mode

Pass --debug to log PC, opcode, registers, and status flags to stderr at each instruction boundary:

PC=$0200 LDAI  A=$00 X=$00 Y=$00 SP=$ff [nobdizc]
PC=$0202 STAA  A=$01 X=$00 Y=$00 SP=$ff [nobdizc]

Performance

The simulator runs at approximately 3.86 MHz on modern hardware (reported on exit). This is faster than original 6502 machines like the Apple II (1.023 MHz) and BBC Micro (2 MHz), despite modeling every microcode step.

What this project is NOT

  • It is not an emulator. An emulator seeks to reproduce the effects of each instruction as efficiently as possible. This project seeks to reproduce the logic of each CPU cycle and microcode word, without much regard to performance.
  • It is not a circuit simulator. It models the logic of a CPU, but not the electrical circuit itself.
  • It is not useful. No seriously, don't bother trying to find a use for this. For any actual work, an emulator is a much better choice, or a real CPU, or an FPGA, or just some normal modern code.

What this project IS

  • It is an aesthetic experiment. I am choosing the level at which to simulate each component based on an aesthetic feeling, like art.
  • It is an educational device, at least for me. I am learning a great deal about how real CPUs work by trying to convert their parallel electronic logic into a synchronous script.

Goals

  1. ✔ Learn more about how CPUs work
  2. ✔ Practice thinking in functional
  3. ✔ Practicing Typescript
  4. ✔ Run 6502 binaries (near-complete instruction set)
  5. ✔ Run EhBASIC and real BASIC programs (Star Trek!)
  6. ✔ Terminal-based graphics with memory-mapped I/O

TODO

  • Mode 4: 128×128, 256 colors, 8bpp (16 KB framebuffer)
  • Sound chip — 3 tone channels (sine/square/sawtooth/triangle) + 1 noise channel, memory-mapped registers, running on its own worker thread like the graphics chip
  • cc65 C target — custom linker script, crt0, and simplecpu.h header; build C programs with make -f target/cc65/Makefile
  • Snake — toolchain validation game written in C
  • DEPTHS — complete roguelike dungeon crawler: procedural maps, FOV, 13 monster types, items, inventory, identification, permadeath, win condition
  • Double buffering for flicker-free BASIC graphics
  • Interrupt system (IRQ, NMI, authentic BRK behavior, RTI instruction)
  • Bank switching for ROM/I/O overlay — I/O registers ($FE00+) currently sit inside the ROM region ($C000–$FFFF), handled implicitly; a proper bank-switching mechanism would make this explicit
  • Snapshot / restore (dump full 64KB + CPU registers to a file)
  • Tape I/O via memory-mapped ports
  • Programming manual — retro-style reference in the spirit of the BBC Micro User Guide, once features stabilize
  • AssemblyScript / Rust / WASM — explore compiling the hot loop to WASM to push past 4 MHz while keeping the pedagogical TypeScript as the reference implementation
  • Keyboard interrupt for BASIC — proper IRQ from keypress so BASIC programs can respond without busy-polling IO_STATUS
  • Tilemap / character mode — a text/tile mode like the C64's character ROM for much faster BASIC text output than pixel-by-pixel drawing
  • Joystick / gamepad input — gamepad support for games
  • Built-in machine monitor — like the C64's monitor, letting you inspect memory and poke assembly from the BASIC prompt
  • Disk I/O — a simple block device so EhBASIC can SAVE/LOAD programs to files
  • DEPTHS: god system (altar on floor 5, sacrifice kills for power)
  • DEPTHS: status effects (poison DoT, confused movement)
  • DEPTHS: special rooms (treasure vault, monster den)
  • DEPTHS: shops (spend gold for identified items)
  • DEPTHS: ranged weapons and spells

About

A very simple Typescript/Javascript CPU simulator for my own learning purposes

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors