Console Emulation via Common Lisp

Brit Butler

(use arrow keys or PgUp/PgDown to move slides)

Enter cl-6502

Let's emulate one.....with Lisp!


Goals: Definition

Goals: Status

Why Lisp? (or, a few good points)

On emulators, portability, and methods of emulation

It's basically a VM, duh. Simulate hardware and run a program inside the simulated hardware.

Portability thinking seems to be: Write it in C. After all, C compilers are ported everywhere.

I stayed with interpretation for simplicity's sake. Crawl, then walk. For a great overview, see this paper.

Enter the 6502

An introductory 6502 program

;; clear the carry status bit
;; accumulator, Y = 0
LDA #$00
LDY #$00
;; loop incrementing Y. once Y wraps to zero, proceed to 0x0b
  BNE &loop
;; accumulator = accumulator - (0x0001 + carry bit),  halt.
SBC $0001

Leaves the Accumulator holding 86, or 255 - LDA (0xa9).

zomg machine codes!

Ben Fry's Distellamap project is fantastic.

Addressing Modes, or Pointers redux

None of this "Declare a variable" nonsense.

6502 has 13 addressing modes. No instruction supports every mode. Indirect mode only used by one instruction.

Some opcodes use address, some use byte at address.

"Register" modes

Immediate - PC, 1 byte

Only in arithmetic, logical comparisons and loading.

LDA #$00 ; 0xa9, LDA 0 into the Accumulator
SBC #$2a ; 0xe9, Subtract 42 from the Accumulator
Accumulator - A, 0 bytes

Only in Bitwise Shift and Rotations.

ASL A ; 0x0a, shifts Accumulator left
ROR A ; 0x6a, rotates Accumulator right

"Zero-page" modes, 1 byte

Fast access to anything in bottom page of RAM.

Along with absolute, indirect comes with -x, -y variants.

ORA $51 ; 0x05, Bitwise Or [81] with Accumulator.
STY $1b, X ; 0x94, Store Y register at [27+X].
LDX $1b, Y ; 0xb6, Store [27+Y] in X register.

"Absolute" modes, 2 bytes.

Specify any address in memory.

DEC $dead, X ; 0xde, Decrement [57005+X].
LDX $beef, Y ; 0xbe, Load [48879+Y] into X register.
CMP $1234 ; Compare [4660] with Accumulator, set flags.

"Indirect" modes

Indirect mode, 2 bytes.

Only used by the JMP instruction.

JMP ($1234) ; 0x6c, Set PC to [[4660]].
Indirect-x,y: 1 byte.

Slightly simplified: get word at (zero-page + register).

ADC ($bc, X) ; 0x61, Add Indirect-X(188) to Accumulator.
AND ($ad, Y) ; 0x31, Bitwise And Indirect-Y(173) with Accumulator.

Other modes

Implied mode, 0 bytes.

Instruction has no operand or knows operand location.
Tons of these, Stack+Status register handling, etc.

NOP ; Do nothing
DEX ; or INY to Increment/Decrement the specified register.
Relative mode, 1 byte.

Trickiest to test because it's stateful.
Moves PC forward/back. Used in all branching instructions.

BCC &19 ; 0x90, Move PC forward by 25 when :carry == 0.
BNE &ac ; 0xfd, Move PC back by 2 (255 - fd) when A != 0.

On dispatch strategies

Which method goes with this byte in the array?

Wound up doing dispatch via opcodes just like a switch/case interpreter...

But using Lisp's EQL-specialized methods! \o/

On Assembly and Disassembly

Once you have an opcodes array full of metadata, disassembly is *trivial*.

Assembly is more involved even though I was lazy and didn't write a proper parser, just regexes. Still not so bad.

2-pass assembler. First sets labels, second resolves uses.

Supports labels, constants, comments. Here's a small example.

Get ready to poop your pampers

The following code has been rated GH-MA*
* - [Github Mature]

An executable spec, or Enter defopcode

Remember when I said I'll Explain?

A Defopcode Aside

Interesting hurdles

Decimal mode - Dodged that bullet. Not in the NES! ;)

Status bits - Sort of ad-hoc and messy. No defaults for you!

Relative addressing - Oh God, Oh God.

Emulator hurdles

Truthfully, CPU emulation isn't so bad. Whole systems are what's complicated.

What's next?

Emulation as a Service!

Maybe build crazy dev tools on top a la Frodo Redpill.

No promises! ;)


Slides Source