Emulating the
BBC Micro
in Javascript

Matt Godbolt

NNG | St Isidore's Day | 4th April 2019

Emulating the
BBC Micro
in Javascript

Matt Godbolt

Trying to recapture a lost youth

What's a BBC Micro?

Why 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

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];
}
                         

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

                 +-----+-----+-----+
                 |  A  |  X  |  Y  |
                 +-----+-----+-----+
                    |     |     |
        /+--------- | --- | (!s && !t)
        ||          |     |     |
+----------+        |     |     |
| opcode|| |        |     |     |
| 100001st |------->t     |     |
+----------+        |     |     |
     ^  \---------- | --->s     |
     |              \     |     /
     |               \    |    /
  select              write bus
   store
            

Inside the 6502

                 +-----+-----+-----+
                 |  A  |  X  |  Y  |
                 +-----+-----+-----+
                    |     |     |
        /+--------- | --- | (!s && !t)
        ||          |     |     |
+----------+        |     |     |
| opcode|| |        |     |     |
| 100001st |------->t     |     |
+----------+        |     |     |
     ^  \---------- | --->s     |
     |              \     |     /
     |               \    |    /
  select              write bus
   store
            

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