https://github.com/carledwards/go6sim
6502 Simulator
https://github.com/carledwards/go6sim
6502 6502-tools foxpro
Last synced: 12 days ago
JSON representation
6502 Simulator
- Host: GitHub
- URL: https://github.com/carledwards/go6sim
- Owner: carledwards
- License: mit
- Created: 2026-04-27T04:03:01.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-25T23:16:21.000Z (21 days ago)
- Last Synced: 2026-05-26T01:18:27.882Z (21 days ago)
- Topics: 6502, 6502-tools, foxpro
- Language: Go
- Homepage: https://carledwards.github.io/go6sim/
- Size: 40.6 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Roadmap: docs/roadmap.md
Awesome Lists containing this project
README
# go6sim
A floating-window 6502 microcomputer simulator with two interchangeable
CPU cores, a memory-mapped VIC video chip, a real **6522 VIA** peripheral
ticking on its own crystal, and a small library of demo programs. Built
on top of [`foxpro-go`](https://github.com/carledwards/foxpro-go)
(FoxPro-for-DOS-style TUI framework) and
[`6502-netsim-go`](https://github.com/carledwards/6502-netsim-go)
(transistor-level [Visual6502](https://github.com/trebonian/visual6502) port). Each component plugs into a shared
bus at hardware-realistic chip-select boundaries; each gets its own
draggable window.
The point is to make a 6502 system you can *see*: every memory access,
every register, every framebuffer cell, every timer underflow, in real
time. Long-term goal: a teaching tool where demos written here transfer
to real silicon unmodified.
## Screenshots
**TUI Logic Analyzer** — bus + control-line trace in a real terminal,
captured at 20 Hz with single-step granularity.

**Wasm Logic Analyzer** — same widget rendered to a browser canvas
via the pixel-overlay path; ~8× the sample density per cell.

**Wasm Emulation** — the full simulator running in a browser tab:
CPU window, memory viewer, Monitor REPL, and the VIC graphics demo
all driven by the same backend that runs in the terminal.

**Remote CPU, in the terminal** — the TUI started with
`-cpu=remote`. RAM, ROM, VIA, and the framebuffer live here; the
CPU itself is the Visual 6502 transistor sim running in a browser
tab, talking back over a WebSocket. The CPU window flips from
"waiting" to live the instant the browser page connects.

**Visual 6502 view** — the browser build (the same `wasm_emu.gif`
machine above) with the Visual 6502 window open. It's a live,
transistor-level rendering of the actual 6502 die — every one of
the chip's ~3,500 transistors, drawn from the original
[Visual6502](https://github.com/trebonian/visual6502) project's
polygon data — lit up node-by-node as your code executes. Click
the chip to flip on labelled annotations for the pin pads and
internal register blocks.

## Quickstart
```bash
make tidy
make run
```
`go.mod` pins
[`foxpro-go`](https://github.com/carledwards/foxpro-go) (TUI framework)
and [`6502-netsim-go`](https://github.com/carledwards/6502-netsim-go)
(transistor-level CPU) to tagged versions, so a clean clone fetches
everything from the module proxy with no sibling-checkout setup.
If you want to iterate on either dependency locally, add a temporary
`replace` directive in `go.mod` pointing at your sibling checkout —
remove it before pushing.
Defaults are tuned for "open it, see something happening": the TUI
boots on the interpretive CPU at Max speed with batch auto-tuned to
fit the per-tick budget, the marquee demo is loaded, and the clock
is running. Esc or Ctrl+Q to quit.
## Browser build
Same code, same demos, same dual-CPU backend, same VIA — running in
the browser via WebAssembly through
[`foxpro-go/wasm`](https://github.com/carledwards/foxpro-go). The
simulator's `tcell.Screen` is swapped for a `tcell.SimulationScreen`
(pure-Go cell buffer) and the JS side renders cells to a canvas.
Graphics-mode pixels are layered onto the cell grid via a sentinel-
rune trick — windows, drop shadows, and z-order all work over the
bitmap.
```bash
make wasm # build web/sim.wasm + copy wasm_exec.js
make wasm-serve # python3 -m http.server on port 8765 (override with PORT=)
```
Then open `http://localhost:8765/`.
The wasm build defaults to:
- **interp** CPU (netsim is slow under wasm; swap via the CPU menu if
you want to watch transistors crawl)
- auto-start running so visitors see motion immediately
- BouncingBalls graphics demo as the boot program
- Esc / Ctrl+Q disabled (would terminate the wasm runtime and brick
the page); close the tab instead
Bundle size: ~5.3 MB raw, ~1.4 MB gzipped. Standard static-host MIME
config (`application/wasm`) is enough — no cross-origin headers
required.
## Bridge protocol + Monitor REPL
Beyond the on-screen widgets, the simulator exposes a debugger
protocol — JSON-RPC 2.0 over NDJSON/TCP for remote drivers, or as
direct Go calls in-process. Both share one Go interface
(`bridge.Target`), so the same Monitor REPL drives every edge:
| Edge | Command | How it talks to the sim |
|---|---|---|
| Built-in Monitor (terminal) | `cmd/6502-sim` (Window → Toggle Monitor) | in-process via `bridge.HubDirect` |
| Built-in Monitor (browser) | `cmd/6502-wasm` (Monitor visible by default) | in-process via `bridge.HubDirect` |
| Headless bridge server | `cmd/6502-sim-serve` | listens on `:6502`, NDJSON/TCP |
| Shared-bridge TUI | `cmd/6502-sim --serve` | TUI runs locally + accepts remote bridge clients |
| Remote controller TUI | `cmd/6502-control` | dials a bridge server, healing reconnect |
| **Future** | MCP server, VS Code extension, etc. | implement `bridge.Target` |
The Monitor itself lives in `internal/monitor`. Its command set:
| Group | Verbs |
|---|---|
| CPU & run | `r` regs · `g [addr]` run · `s [n]` step · `.` stop · `reset` · `stack [r]` |
| Memory | `m [addr] [n]` hex · `d [addr] [n]` disasm · `: …` poke · `f ` fill · `t ` transfer · `h …` hunt |
| Breakpoints | `bp ` · `bc [id\|all]` · `bl` |
| Interrupts | `irq` · `nmi` |
| Hardware | `hw` / `info` · `via ` (list/dump/set) |
| Monitor | `cls` · `help [cmd]` · `help window` · `reconnect` · `q` |
Addresses are hex (`$E000` / `0xE000` / `E000`) or **symbolic**:
`pc`, `sp`, `reset`, `irq`, `nmi`. The symbolic forms read live —
`d pc` disassembles wherever you currently are; `m irq` dumps the
IRQ handler the CPU would jump to right now.
The remote controller auto-reconnects with backoff if the sim
restarts. CPU state preservation across reconnect depends on which
loader: `cmd/6502-sim --serve` keeps the live Hub across reconnects;
`cmd/6502-sim-serve`'s per-session Hub is fresh on each reconnect.
The protocol contract lives in [`docs/bridge-v2.md`](docs/bridge-v2.md).
### CLI flags (terminal build only)
| Flag | Default | Notes |
|----------------|-----------|-------------------------------------------------------|
| `-cpu` | `interp` | CPU backend: `interp` or `netsim` (transistor) |
| `-run` | `true` | Start the clock running immediately |
| `-speed` | `max` | Initial clock target: `1`, `10`, `20`, `100`, `1k`, `max` |
| `-batch` | `0` | Max half-cycles per UI tick (0 = auto-tune at startup)|
| `-cpuprofile` | (off) | Write CPU pprof to file |
| `-memprofile` | (off) | Write heap pprof at exit |
The wasm build doesn't take flags; it boots with the same defaults.
User-facing controls live in the menus and keyboard shortcuts.
## Memory map
Hardware-realistic address decoding — components claim their ranges
exactly the way a 74HC138 chip-select decoder would on a real board.
A two-stage decoder (A13–A15 → 8 KB regions; A8–A11 within the I/O
region → 256 B sub-regions) gives every peripheral its own CS line
with no chip-select collisions.
| Range | Component | Size |
|--------------------|----------------------------|---------|
| `$0000`–`$1FFF` | RAM | 8 KB |
| `$A000`–`$A3FF` | VIC color plane | 1 KB CS (520 B used) |
| `$A400`–`$A7FF` | VIC char plane | 1 KB CS (520 B used) |
| `$A800`–`$ABFF` | VIC controller | 1 KB CS (16 B used) |
| `$B000`–`$B0FF` | 6522 VIA #1 | 256 B CS (regs mirror ×16) |
| `$B100`–`$BFFF` | peripheral slots (15 ×) | 256 B CS each |
| `$C000`–`$DFFF` | VIC graphics plane | 8 KB (160 × 100 @ 4bpp) |
| `$E000`–`$FFFF` | ROM (reset vector at `$FFFC`) | 8 KB |
VIC controller registers (offsets within `$A800`):
| Off | Reg | Behavior |
|------|--------------|---------------------------------------------------|
| `+0` | Cmd | Write triggers an op (Clear, Shift\*, Rot\*, Invert, Rect\*, Gfx\*) |
| `+1` | Pause | `1` = UI shows snapshot; `0` = UI shows live memory |
| `+2` | Frame | Any write captures a new snapshot (use while paused) |
| `+3` | RectX | Rect parameters consumed by `CmdRect*` and `CmdGfx*` |
| `+4` | RectY | opcodes — clamped to display bounds |
| `+5` | RectW | |
| `+6` | RectH | |
| `+7` | GfxColor | Current draw color for `CmdGfx*` (palette idx 0–15) |
| `+8` | Mode | `0` = char (default), `1` = graphics |
VIA #1 — Phase 1 implements **Timer 1** in free-running and one-shot
modes plus IFR/IER semantics; ports, T2, SR, and PCR are stubbed and
read/write a backing byte without side effects yet. The chip is
clocked from its own 1 MHz oscillator (independent of the CPU), so
demos that pace off T1 keep ticking even while the CPU is single-
stepping or paused — same as a real 65C22S board with a separate
timer crystal. Pacing pattern (canonical W65C22):
```asm
; Set up T1 free-run with latch = $C350 (~50 ms @ 1 MHz)
LDA #$50 : STA $B006 ; T1L-L
LDA #$C3 : STA $B005 ; T1C-H — copies latch→counter, starts T1
LDA #$40 : STA $B00B ; ACR bit 6 = T1 free-run
; Poll for underflow
WAIT: LDA $B00D ; IFR
AND #$40 ; T1 flag
BEQ WAIT
LDA $B004 ; T1C-L read clears IFR T1
```
## CPU backends
| Backend | Speed | What it is |
|----------|-------------|-------------------------------------------------------|
| `interp` | several MHz | Conventional 151-opcode interpretive 6502 (default) |
| `netsim` | ~26 kHz | Transistor-level [Visual6502](https://github.com/trebonian/visual6502) port — every cycle simulates ~3500 transistors |
| `remote` | wire-bound | The CPU lives in another process — a browser tab, an FPGA on the LAN, a Pi across the room — dialed in over a WebSocket. The TUI keeps the bus, RAM, ROM, and VIA local; every cycle round-trips for memory access |
The `Backend` interface (`cpu/backend.go`) lets you swap at runtime
via the **CPU** menu. All three expose the same address/data bus
state plus IRQ/NMI for the simulator's introspection windows.
### Remote CPU — watch the silicon think in a browser tab
Start the TUI with `-cpu=remote` and it boots into "waiting" mode
with no CPU. The terminal binds an HTTP listener (`-remote-addr
:7777` by default) that serves both the `/cpu` WebSocket endpoint
*and* a self-hosted browser page at `/`:
```sh
./bin/6502-sim -cpu=remote
# then open http://localhost:7777/ in your browser
```
The page boots a foxpro-go shell containing the transistor-level
`netsim` core wired to a [Visual6502](https://github.com/trebonian/visual6502)-style
live die rendering. As soon as the page connects, the TUI's CPU
window stops saying "waiting" and the demo starts running — every
half-cycle round-trips between the terminal (which owns RAM, ROM,
VIA timers, and the framebuffer) and the browser (which owns the
CPU). The chip lights up node-by-node as instructions execute, with
the TUI showing the same activity on the bus side.
It's not fast — roughly 400 Hz on a localhost loop, slower over a
LAN — but that's the point. You can read it. Close the browser tab
and the TUI auto-pauses; open it again and it auto-resumes from
reset.
Same protocol works for any client that speaks the wire
(`cpu/remote/proto.go`): there's a Go-only reference at
`cmd/6502-cpu-fake/` for smoke testing, and the door is open for an
FPGA-hosted real 6502 driving a TUI over TCP.
## Windows
Every component gets its own floating, draggable window. Click in the
title bar to drag, click the corner to resize.
- **CPU** — A/X/Y/S/PC, P flags, half-cycle counter, live address bus,
data bus, R/W direction, IRQ/NMI line states.
- **RAM** / **ROM** (Memory views) — hex view + ASCII column with
editable base address (click the `$XXXX:` button, type 4 hex
digits). Trace tinting: yellow = write that *changed* the byte,
brown = write that left it unchanged, green = read. `v` cycles
Hex / Disasm / Labels — Labels shows declared symbols within the
current region (or a per-byte fallback view for regions without
symbols). The disasm column substitutes operand addresses with
symbol names where known and appends per-instruction comments.
- **VIC / Video** — 40 × 13 framebuffer with 16-color palette, plus a
160 × 100 graphics plane (when in graphics mode). Right column has
buttons for every controller command. Below the framebuffer, a
scrollable hex strip shows the VIC's controller region.
- **VIA 1** — live snapshot of the chip's state: ports + DDRs at the
top, then Timer 1 (counter / latch / mode / armed flag), then ACR
decoded + IFR + IER bit dots (● set, . clear). The chip's base
address + crystal speed live in the title bar. The counter ticks
down even when the CPU is paused or stepping, because the VIA's
crystal runs independently.
- **Monitor** — the shared REPL described above. Toggleable from
the Window menu in the terminal build; visible on startup in the
browser build. Common pane across all three apps.
- **Logic Analyzer** (scope) — hidden by default; toggleable. 256
cycles of bus-trace history with auto-tuned sampling stride.
Run/Stop/Step + speed controls live on the menu bar's right-side
tray (clickable). The Clock window was removed when the bridge
landed — every action is in the menu, keyboard hotkeys, or the
Monitor's command line.
## Demos
Selectable from the **Demo** menu, in three sections:
| Demo | What it does |
|---------------------|---------------------------------------------------------|
| Marquee | Scrolling "HELLO 6502 SIM"; paces via VIA T1 (default boot demo for TUI) |
| Bouncer | Single `*` bouncing across row 6 |
| Scroller | Diagonal gradient scrolling up the display |
| Snow (LFSR) | 8-bit Galois LFSR fills + clears the framebuffer |
| Scroller (framed) | Same as Scroller but Pause + Frame for clean snapshots |
| Blitter (RAM→VIC) | Copies byte patterns out of RAM into the VIC planes |
| Quadrants | 4 independent rect rotations using `CmdRect*` |
| Bouncing Balls | Four colored balls in graphics mode, paced via VIA T1 (wasm only — TUI has no graphics plane) |
All demos are built via the in-tree `asm` package — a small fluent
6502 assembler that emits bytes plus per-instruction comments and
named memory symbols, surfaced by the Memory window's Labels and
Disasm views.
## Menu shortcuts
| Key | Action |
|-----------|-------------------------------------|
| `Z` | Reset machine (does NOT stop the clock — like a hardware reset button) |
| `F2` | Toggle command window |
| `R` | Run |
| `.` | Stop |
| `S` | Step one instruction (until PC changes) |
| `T` | Step one half-cycle ("tick") |
| `Esc` | Quit (terminal) · close menu (browser, where Quit is disabled) |
In the Memory window:
| Key | Action |
|-----------|-------------------------------------|
| `g` | Edit base address |
| `v` | Cycle view: Hex / Disasm / Labels |
| `i` | Toggle disassembly info panel |
In the VIC window's hex strip:
| Key / mouse | Action |
|--------------------|-----------------------------|
| Mouse wheel | Scroll memBase by 1 row |
| `[` / `]` | Scroll by 1 row (16 bytes) |
| `{` / `}` | Scroll by 1 page (112 bytes) |
| Click `▲` / `▼` | ±1 row |
| Click track | Page up/down |
| Drag `◆` | Jump to position |
## Architecture
The execution model has two goroutines: the **foxpro UI thread**
(handles input, draws windows) and the **Hub Pump** (drives the CPU
+ peripherals in slices, synchronously). They serialise through a
single mutex — concurrent reads from the UI side take a query lock;
the Pump holds it during each slice. Same model native + wasm.
In wasm, the Pump yields to the JS event loop every ~10 ms in Max
mode so the browser tab can render.
The Pump slices each "tick" into 200-half-cycle chunks and pairs
each `RunUntil` with a `bus.Tick(virtualDt)` for peripherals — so
polling-based demos (those that LDA/AND/BEQ a peripheral flag in a
tight wait loop) observe timer underflows multiple times per app
frame instead of just once. The driver auto-tunes per-tick batch
size at startup to fit the host's tick budget.
Components self-describe their register layouts via the optional
`bus.Labeller` interface (`Symbols() []asm.Symbol`), so the Memory
window's Labels view annotates the VIC and VIA register regions
automatically — no hand-maintained mapping. Time-driven peripherals
implement `bus.Ticker` and get fanned out automatically.
The **bridge protocol** is a separate layer above the Hub. Clients
(remote `cmd/6502-control`, in-process Monitor in `cmd/6502-sim` /
`cmd/6502-wasm`, future MCP / VS Code) implement / consume the
`bridge.Target` interface; the transport (TCP NDJSON or direct Go
calls via `bridge.HubDirect`) is the only thing that differs.
Read `docs/architecture.md` for layering + component contracts,
`docs/bridge-v2.md` for the protocol surface, and `docs/roadmap.md`
for remaining work.
## Project layout
```
cmd/6502-sim/ terminal entry — main wiring, flags, profiling
cmd/6502-sim-serve/ headless bridge server (no UI, listens on :6502)
cmd/6502-control/ remote controller TUI — bridge client over NDJSON/TCP
cmd/6502-wasm/ browser entry — wasm-tagged, uses foxpro-go's wasm bridge
asm/ fluent 6502 assembler used by demos
backplane/ machine bus + interrupt aggregation + reset capability
bridge/ protocol layer — Hub + Pump + Target interface + HubDirect
clock/ Driver, Speeds, halfStep accumulator
cpu/ Backend interface + PCSetter capability
cpu/netsim/ netsim adapter
cpu/interp/ interpretive 151-opcode 6502 (with IRQ/NMI service paths)
components/ ram, rom, display, via
disasm/ 151-opcode disassembler with cycle counts and effects
instrument/ Instrument facade — wraps backplane + driver
internal/bridgeclient/ Go wire client for the bridge protocol
internal/monitor/ shared Monitor REPL — drives any bridge.Target
internal/demos/ shared demo programs (text + graphics)
ui/ cpuwin, ramwin, displaywin, viawin, scopewin, clockwin
web/ static frontend served by the wasm build (built artifacts)
docs/ architecture, bridge protocol, roadmap, systems
```
## Status
Working on both terminal and browser. The transistor-level core hits
~26 kHz on a recent Mac; the interpretive core is several MHz. Both
pass the same demos.
The simulator is set up so each peripheral lives on its own
chip-select region with realistic mirroring (the 6522's 16 registers
mirror through a 256-byte CS block, exactly as on a real board with
only RS0–RS3 hooked up). Demos written here should run on real
silicon without modification.
## Credits
The transistor-level CPU backend (`netsim`) and the live die view
(`ui/visualcpuwin`) are built on data from
[Visual6502](https://github.com/trebonian/visual6502) by **Greg James,
Brian Silverman, and Barry Silverman** — the segment-definition polygon
table, node IDs, and layer color palette all come from that project.
The Visual6502 work is licensed under
[CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/);
see [NOTICES](NOTICES) for the redistribution details.
## License
MIT — see [LICENSE](LICENSE).