jsbeeb
jsbeeb emulates the BBC Micro Model B and Master in JavaScript with cycle-accurate 6502 and video timing, disc and tape support, and shareable URLs that boot straight into a program. No install required.
Runs on: Web browser
jsbeeb Online Emulator
Play jsbeeb using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| BBC Model B | jsbeeb | BBC Micro Model B | open | Open ⛶ | |
| BBC Master 128 | jsbeeb | BBC Master | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
jsbeeb is an ES-module project, not a single script. We bundle a small boot entry with Vite into jsbeeb-boot.js, load it as <script type="module">, and drive the machine ourselves rather than using jsbeeb's own UI, so the loop can be paused and single-stepped, which the debugger needs.
Boot. The boot follows jsbeeb's own fake6502 recipe: pick a model, make a real Video pointed at a <canvas>, construct the Cpu6502, then run a loop on processor.execute():
import { Cpu6502 } from "./6502.js";
import { Video } from "./video.js";
import { Canvas } from "./canvas.js";
import { findModel } from "./models.js";
const model = findModel("B-DFS1.2");
const canvas = new Canvas(document.getElementById("screen"));
const video = new Video(model.isMaster, canvas.fb32, function(a,b,c,d){ canvas.paint(a,b,c,d,this.frameCount); }, { isAtom:false });
const processor = new Cpu6502(model, { dbgr, video, soundChip, ddNoise, relayNoise, music5000, cmos });
await processor.initialise(); // loads os.rom, BASIC.ROM, the DFS ROM
(function loop(){ processor.execute(40000); requestAnimationFrame(loop); })();
The processor is fully inspectable. jsbeeb exposes the whole machine as ordinary properties; no wasm heap to reach into:
| Member | Kind | What it does |
|---|---|---|
execute(cycles) | method | Run for a number of clocks; returns false if a debug hook (breakpoint) stopped it. Single-step by looping execute(1) until pc changes. |
reset(hard) | method | Reset the machine (Break). |
peekmem(addr) | method | Read a byte with no side effects, what the hex and disassembly views use. |
readmem/writemem(addr[,v]) | method | Bus read/write (with hardware side effects); writemem pokes memory live. |
pc, a, x, y, s | fields | The 6502 registers, readable and writable. |
p | field | The status flags: p.asByte() / p.setFromByte(b), and p.c/z/i/d/v/n. |
ramRomOs | field | The whole 128 KB+ store: main RAM, the OS ROM and every sideways ROM/RAM bank. |
debugInstruction | hook | Add a per-instruction callback; return truthy to halt - real execution breakpoints for free. |
debugRead / debugWrite | hooks | Per-access callbacks used here for memory watchpoints. |
fdc.loadDisc(drive, disc) | method | Insert a disc image (SSD/DSD) built by fdc.discFor. |
Because it is all JavaScript, the debugger single-steps with execute(1), reads and writes the registers directly, disassembles from peekmem, and gets breakpoints and watchpoints from jsbeeb's own CPU debug hooks, with no changes to the emulator core.
Debugger integration
Wiring jsbeeb into the shared in-browser debugger needed two pieces of custom work, plus a set of techniques for reaching into the live machine.
1 · A boot shim, because jsbeeb has no drop-in build. jsbeeb is an ES-module project whose own main.js owns an internal requestAnimationFrame loop that cannot be paused or single-stepped from outside, which the debugger needs. So we wrote a small boot entry that follows jsbeeb's fake6502 recipe to construct just the machine, and drives it from a loop we control:
// our loop, not jsbeeb's, so pause / step / breakpoints work
function chunk(cyc){ watchHit = false; const ok = processor.execute(cyc); if (!ok){ running = false; return true; } }
function stepInsn(){ const p = processor.pc; do { processor.execute(1); } while (processor.pc === p); }
2 · Bundled as an ES module, not an IIFE. Flattening jsbeeb's (circularly-importing) modules into a single IIFE scope throws "Cannot access X before initialization". Building with Vite in library mode and formats:['es'] keeps live module bindings and fixes it; the page loads the bundle with <script type="module">. Because a module is deferred, the boot publishes window.EMU_BOOT asynchronously, so the debugger plug-in polls for it rather than reading it once.
Techniques for deeper access. The plug-in then reaches into the running machine through jsbeeb's own surfaces, no fork of the emulator core:
- Side-effect-free reads. The hex and disassembly views scrub memory with
processor.peekmem(addr)(notreadmem), so auto-polling the view can never trip a VIA/CRTC read side effect. - Direct state. Registers are plain properties (
processor.pc/.a/.x/.y/.s, andprocessor.pwithasByte()/setFromByte()), read and written live;processor.ramRomOsis the whole RAM + ROM + sideways-bank store. - Native breakpoints. jsbeeb already exposes CPU debug hooks. Execution breakpoints ride
processor.debugInstruction.add(pc => bps.has(pc))- when it returns truthy,execute()returnsfalseand our loop stops, so breakpoints are at-speed and free. - Native watchpoints. Memory watchpoints ride
processor.debugWrite(anddebugRead) the same way. - Single instruction step.
execute(1)runs one clock; looping it untilpcchanges advances exactly one instruction.
Everything the debugger shows, registers read/write, memory hex/disassembly, follow-PC, single-step, breakpoints and watchpoints, is built from these, with no changes to jsbeeb itself. jsbeeb is the strongest debugging target of the emulators here precisely because it already ships these hooks.
Architecture
jsbeeb is a cycle-accurate BBC Micro emulator by Matt Godbolt. Each hardware block is its own module hanging off the Cpu6502:
Cpu6502- the 6502 interpreter, with the full memory map (RAM, paged ROMs and I/O) inramRomOsand the banking logic.Video- the 6845 CRTC + video ULA, including the SAA5050 teletext chip for MODE 7; it rasters into a 1024×625 framebuffer painted to the canvas.SysVia/UserVia- the two 6522 VIAs (keyboard, timers, the user port).IntelFdc/WdFdc- the floppy disc controller reading SSD/DSD/HFE images.SoundChip- the SN76489;Acia+ tape for cassette.- ROMs, the MOS operating system, BBC BASIC and a DFS, loaded into the sideways banks.
Each execute() steps the CPU and clocks the video, VIAs and sound in lockstep. Because every component is an ordinary object, the whole machine state is inspectable, which is what the debugger reads each refresh.