Console Emulation via Common Lisp
Brit Butler
2012-10-01
(use arrow keys or PgUp/PgDown to move slides)
Brit Butler
2012-10-01
(use arrow keys or PgUp/PgDown to move slides)
Let's emulate one.....with Lisp!
* - All SLOC figures courtesy of sloccount.
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.
;; clear the carry status bit CLC ;; accumulator, Y = 0 LDA #$00 LDY #$00 ;; loop incrementing Y. once Y wraps to zero, proceed to 0x0b loop: INY BNE &loop ;; accumulator = accumulator - (0x0001 + carry bit), halt. SBC $0001 BRK
Leaves the Accumulator holding 86, or 255 - LDA
(0xa9).
Ben Fry's Distellamap project is fantastic.
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.
Only in arithmetic, logical comparisons and loading.
LDA #$00 ; 0xa9, LDA 0 into the Accumulator SBC #$2a ; 0xe9, Subtract 42 from the Accumulator
Only in Bitwise Shift and Rotations.
ASL A ; 0x0a, shifts Accumulator left ROR A ; 0x6a, rotates Accumulator right
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.
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.
Only used by the JMP instruction.
JMP ($1234) ; 0x6c, Set PC to [[4660]].
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.
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.
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.
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/
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.
The following code has been rated GH-MA*
* - [Github Mature]
Remember when I said I'll Explain?
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.
Truthfully, CPU emulation isn't so bad. Whole systems are what's complicated.
Emulation as a Service!
Maybe build crazy dev tools on top a la Frodo Redpill.
No promises! ;)