Emulating the
BBC Micro
in Javascript

Matt Godbolt

CppCon 2025

Trying to recapture a lost youth

What's a BBC Micro?

Why BBC Micro?

Why Javascript?

  • Surely it'd be too slow?
  • "Worse" than C++?
  • Why not?
  • Runs in browser

The 6502

6502 Computers

6502 101

Registers

  • 8-bit Accumulator
  • 8-bit X, Y index
  • Processor status
  • Stack pointer
  • Program counter

Instructions


LDA STA LDX ... ; load / store A, X, Y
TAX TXA ...     ; transfer A to X
PHA PLA         ; push A / pull A
CMP             ; compare with A
ADC SBC         ; add / subtract with carry
CLC SEC         ; clear / set carry
JMP             ; jump
BEQ BNE BCS BCC ; branch ==, !=, carry set/clear
BMI BPL ...     ; <0, >=0
JSR             ; jump to subroutine
RTS             ; return from subroutine
                     

Addressing modes


a9 20     LDA #$20     ; A = 0x20

a5 70     LDA $70      ; A = readmem(0x70)

ad 34 12  LDA $1234    ; A = readmem(0x1234)
                     

Addressing modes


bd 34 12  LDA $1234, X ; A = readmem(0x1234 + X)
b9 34 12  LDA $1234, Y ; A = readmem(0x1234 + Y)

b1 70     LDA ($70), Y ; t1 = readmem(0x70)
                       ; t2 = readmem(0x71)
                       ; addr = t1 | (t2<<8)
                       ; A = readmem(addr + Y)

b5 70     LDA $70, X   ; A = readmem(0x70 + X)
                     

Emulating a 6502

  • Fetch next instruction
  • Decode it
  • Execute it

Emulation


var a = 0, x = 0, y = 0, pc = readword(0xfffe);
while (true) {
  switch (readmem(pc++)) {
  case 0xa9: // LDA #imm
    a = readmem(pc++); break;
  case 0xad: // LDA $addr
    var addr = readword(pc); pc += 2;
    a = readmem(addr);
    break;
  // and so on for all other instructions...
  }
}

Except...

  • Setting flags
  • Memory access
  • Hardware
  • Interrupts
  • Handling time

Processor status: Flags

7 6 5 4 3 2 1 0
Negative oVerflow - - Decimal Interrupt
disable
Zero Carry

  • Z & N set on ALU/load instruction
  • C set by shifts & arithmetic
  • V set by arithmetic
  • D, V, C and I controlled by special instructions

So now we have:


case 0xa9: // LDA #imm
  a = readmem(pc++);
  p.z = !a;
  p.n = !!(a & 0x80);
  break;             

Memory

0x10000
0xff00 256 bytes OS ROM
0xfe00 256 bytes hardware
0xfd00 256 bytes ROM
0xfc00 256 bytes hardware
↑
↑
↑
0xc000
15.5KB OS ROM
↑
↑
↑
↑
0x8000
16KB Paged ROM
↑
↑
↑
↑
0x4000
16KB RAM
↑
↑
↑
↑
0x0000
16KB RAM

Memory - Reading


var ram = new Uint8Array(0x8000), romsel = 0, roms = [];
var os = load("OS");
roms[15] = load("BASIC");

function readmem(addr) {
  if (addr < 0x8000) return ram[addr];
  if (addr < 0xc000) return roms[romsel][addr - 0x8000];
  if ((addr >= 0xfe00 && addr < 0xff00)
      || (addr >= 0xfc00 && addr < 0xfd00))
    return readhw(addr);
  return os[addr - 0xc000];
}
                         

Memory - Writing


function writemem(addr, b) {
    if (addr < 0x8000) ram[addr] = b;
    if (addr >= 0xfe00 && addr < 0xff00) writehw(addr, b);
    // else does nothing - it's ROM
}
                         

The outside world

  • Video chip:
    • Pixel generator reads from RAM
    • Simple character-mapped mode
    • Bitmap modes with 2, 4, 16 colours
  • Sound (SN76489)
  • Timers (6522 VIA)

Demo

What happened?

Fixing it


case 0xa9: // LDA #imm
  a = readmem(pc++);
  p.z = !a;
  p.n = !!(a & 0x80);
  video.run(2);
  timers.run(2);
  break;

Demo

More games

Zalaga

Inside the 6502

    $84 10000100 STY zp
    $85 10000101 STA zp
    $86 10000110 STX zp
    $87 10000111 ???

Real decoding

©2010 Visual6502.org, used with permission

Inside the 6502

X A Y s t write bus opcode: 1 0 0 0 0 1 s t !s&&!t X A Y A X ???

Inside the 6502

Vdd out in GND

Inside the 6502

$84 10000100 STY zp
$85 10000101 STA zp
$86 10000110 STX zp
$87 10000111 SAX zp

Even more games

An admission

Protection systems

  • Decrypt game code
  • Keys include:
    • Magic values loaded from tape (or disc)
    • Decryption code itself
    • Hardware timers & registers
    • Interrupt information
    • Read-modify-write

Accurate timing information

Visual 6502

ROL $fe48

Cycle Read / Write Address Description
0 Read 0d2d Read opcode (2e)
1 Read 0d2e Read low byte of address (48)
2 Read 0d2f Read high byte of address (fe)
3 Read fe48 Read memory at $fe48
4 Write fe48 Do rotate — writes unmodified value!
5 Write fe48 Store to $fe48

Fixing it


case 0x2e: // ROL abs
  addr = readword(pc); pc += 2;
  hardware.run(3);
  val = readbyte(addr);
  hardware.run(1);
  writebyte(addr, val); // unmodified value
  hardware.run(1);
  val = (val << 1) | (carry ? 1);
  carry = val & 0x100;
  writebyte(addr, val & 0xff); // modified value
  hardware.run(1);
  break;

Cycle stretching

Conclusion

Resources

Some Demos