Instantly share code, notes, and snippets.

Embed
What would you like to do?
apple 1 ROM disassembly
;
; the "monitor ROM" of an apple 1 fit in one page (256 bytes).
;
; this is my attempt to take the disassembled code, give names to the
; variables and routines, and try to document how it worked.
;
;
; an apple 1 had 8KB of RAM (more, if you hacked on the motherboard), and a
; peripheral chip that drove the keyboard and video. the video was run by a
; side processor that could treat the display as an append-only terminal that
; displayed ASCII characters $20 - $5F (uppercase Latin letters, digits, and
; basic punctuation). the apple 1 hardware is described in lucious detail
; here: https://www.applefritter.com/replica/chapter7
;
; this ROM initialized the two peripherals, then displayed a prompt ('\'
; followed by a linefeed), and waited for a text command.
;
; the commands are:
; <addr>R
; execute code starting at <addr>. to restart the monitor ROM, you
; could do: "FF00R"
; <addr>.<addr>
; dump out hex values from RAM or ROM, between the two <addr>
; inclusive. the dot and second address can be omitted. for example,
; "FF00.FF03" would display:
; FF00: D8 58 A0 7F
; <addr>: <bytes...>
; write data into RAM. for example: "300: 20 00 FF" (which writes
; the instruction "JSR FF00" starting at $300)
;
; any character less than '.' (including space) is ignored. addresses and
; data are always in hex. because there is no backspace, the input routine
; treats '_' as if it erased the previous character.
;
; this would be enough to enter the 4KB of integer BASIC into RAM and then
; execute it, if you wanted to stay up all night doing that. in practice,
; most people added a cassette interface and loaded BASIC using a tape
; recorder:
; https://commons.wikimedia.org/wiki/File:Original_1976_Apple_1_Computer_In_A_Briefcase.JPG
;
; the constrained space means woz used a lot of tricks, like having a routine
; "fall thru" to another instead of calling it explicitly. to take another
; example, the "state" variable is carefully manipulated so that the top two
; bits indicate one of 3 states: normal, '.' has been parsed (hex dump mode),
; or ':' has been parsed (hex entry mode). these top two bits can be checked
; by performing BIT and branching on bit 7 N (BPL/BMI) or bit 6 V (BVC/BVS).
;
; the code for reaching the end of a parsed hex string is particularly
; byzantine: if the state is 0, this is the first address entered, so it's
; copied into both addr0 (for "run" or a hex dump) and addr1 (for hex data
; entry). then, it falls thru into the hex dump code to print out the first
; byte at that address before jumping back into the parser. so even if it's
; parsing a "run" or hex data entry, it will print out the (old) first byte.
;
; i suspect that the use of 3 separate address entries is unnecessary: "run"
; and hex data entry could probably use same address slot. however, this may
; not save more than 2 bytes of code.
;
; you can play with a fun online emulator here: https://www.scullinsteel.com/apple1/
;
org $ff00
buffer equ $0200 ; text input buffer ($200-$27F)
addr0l equ $24 ; target address for "run", or start of hex dump
addr0h equ $25
addr1l equ $26 ; target address for hex data entry mode
addr1h equ $27
addr2l equ $28 ; stores hex data as it's parsed
addr2h equ $29
saveidx equ $2a ; temp storage for Y (buffer index) while parsing
state equ $2b ; normally 0, but:
; '.' ($ae = 10101110) if a dot was seen
; ':'<<1 ($ba -> $74 = 01110100) if a colon was seen
; so bit 7 (N) says we're in dot mode, and bit 6 (V)
; says we're in colon mode
; peripheral I/O ports: only 4 of them exist on the apple 1!
kbd equ $d010 ; read key
kbdcr equ $d011 ; control port
dsp equ $d012 ; write ascii
dspcr equ $d013 ; control port
; $ff00
start:
cld
cli
ldy #$7f
sty dsp ; 01111111 - all periphs are output except highest bit
lda #$a7 ; 10100111 - configure both periphs
sta kbdcr
sta dspcr
; this is too clever. here, A=$a7 Y=$7f, so ckmeta will behave as if the
; buffer is at index 127 and the user entered a single quote ('). when it
; increments the index (to 128), that will overflow, falling into the abort
; routine, which prints a backslash and a linefeed and clears the buffer.
;
; this also means that the text input buffer is only 128 chars long (a bit
; over 3 lines on the screen).
; $ff0f
; handle special keys (backspace and esc)
ckmeta:
cmp #$df ; '_' -- woz treated this as a backspace
beq backsp
cmp #$9b ; ESC
beq abort
iny
bpl readch ; if we overran 128 bytes, fall thru to abort
; $ff1a
abort:
lda #$dc ; '\'
jsr cout
; $ff1f
; Y: buffer index
readline:
lda #$8d ; CR
jsr cout
ldy #1 ; fall thru, which will decr Y to 0, and into readch
; $ff26
; there's no "cursor" on the apple 1 (the display behaves like a printer
; terminal), so it decrements the buffer index, and the '_' on screen is your
; indicator that a character was erased.
; for example, seeing "RUM_N" means we have "RUN" in the buffer.
backsp:
dey
bmi readline ; backspace too far and we'll reset
; $ff29
readch:
; wait for high bit to be set on kbdcr, meaning there's a key on kbd
lda kbdcr
bpl readch
lda kbd
sta buffer,y
jsr cout
cmp #$8d ; CR
bne ckmeta
; we're 25% of the way through the ROM and so far we can only initialise
; and read a line of input text into a buffer. this ROM is *tight*.
; $ff3b
; process the input line: A=0 X=0 Y=0
; fall thru to writing 0 into state and begin parsing
ldy #$ff
lda #0
tax
; $ff40
wstate1:
asl
; $ff41
wstate:
sta state
; $ff43
; A: current char being parsed
; X: 0 always
; Y: index into buffer
parsech:
iny
parsech1:
lda buffer,y
cmp #$8d ; CR
beq readline
cmp #$ae ; '.'
bcc parsech ; ignore anything < '.'
beq wstate ; remember the '.' and continue
cmp #$ba ; ':'
beq wstate1 ; shift it to $74 and remember it as the state
cmp #$d2 ; 'R'
beq run
stx addr2l ; assume it's a hex digit, clear addr2
stx addr2h
sty saveidx
; $ff5f
; as long as there are hex digits in the buffer, roll them into addr2
parsehex:
lda buffer,y
eor #$b0 ; xor with '0': if it's a digit, A is now 0-9.
cmp #10
bcc digit ; <10? it was a digit
; if A was 'A'-'F' ($C1-$C6), the xor made it $71-$76 and carry is set.
; adding $89 will bring it up to $FA-$FF. a simple "<$FA" compare will then
; detect the hex range.
adc #$88
cmp #$fa
bcc donehex ; not 'A'-'F'
; $ff6e
; digits enter as $00-$09, hex enters as $FA-$FF
digit:
asl ; shift the low nybble into the high nybble
asl
asl
asl
; roll the high nybble thru the carry bit into addr2
ldx #4
rollnyb:
asl
rol addr2l
rol addr2h
dex
bne rollnyb
iny
bne parsehex
; $ff7f
donehex:
cpy saveidx
beq abort ; if we processed 0 hex digits, this was an error
bit state ; bit 7 (N) = dot mode, bit 6 (V) = colon mode
bvc ckdot ; not colon mode
; colon mode is hex data entry. assume only one byte was entered (or use the
; low byte of whatever was entered), store that into addr1, and incr addr1
lda addr2l
sta (addr1l,x)
inc addr1l
bne parsech1
inc addr1h
; $ff91
gojump:
jmp parsech1
; $ff94
run:
jmp (addr0l)
; $ff97
ckdot:
bmi dotmode ; dot mode
; $ff98
; not dot or colon mode, so this is the first hex value entered. copy it into
; addr1 and addr2, leaving X=0 again by the end.
firstaddr:
ldx #2
; $ff9b
addrcopy:
lda addr2l-1,x
sta addr1l-1,x
sta addr0l-1,x
dex
bne addrcopy
; $ffa4
; first time thru, the "bne" is skipped and the current address is printed.
; on subsequent rounds, A will be 0 if the address ends with 0 or 8, which
; will cause a new linefeed + address to be printed, making it look pretty.
prnext:
bne dumpbyte
; print a linefeed, addr0, ':', space
lda #$8d ; CR
jsr cout
lda addr0h
jsr prbyte
lda addr0l
jsr prbyte
lda #$ba ; ':'
jsr cout
; $ffba
dumpbyte:
lda #$a0 ; space
jsr cout
lda (addr0l,x)
jsr prbyte
; $ffc4
; if the addresses are the same (or reversed), clear the state and go parse
; another address or command
dotmode:
stx state ; clear state
lda addr0l
cmp addr2l
lda addr0h
sbc addr2h
bcs gojump ; trampoline to parsech1
; increment addr0 and loop back
inc addr0l
bne skip
inc addr0h
skip:
lda addr0l
and #7 ; setup so A=0 if the address now ends with 0 or 8
bpl prnext
; $ffdc
; print A as 2 hex digits
prbyte:
pha
lsr
lsr
lsr
lsr ; high nybble first (shift into low nybble)
jsr prnybble
pla ; fall thru to print low nybble
; $ffe5
; print the low nybble of A as '0'-'9' or 'A'-'F'
prnybble:
and #$0f
ora #$b0 ; '0'
cmp #$ba ; '9'+1
bcc cout ; <='9'
adc #6 ; $BA+6+carry = $C1 'A'
; $ffef
; print the ascii char in A to the display
cout:
bit dsp
bmi cout ; wait for the display to be not-busy
sta dsp
rts
; $fff8
; free space! 2 whole bytes!
brk
brk
; $fffa
; interrupt vectors
.data $0f00 ; NMI
.data start ; reset
.data $0000 ; IRQ
@RyuKojiro

This comment has been minimized.

Show comment
Hide comment
@RyuKojiro

RyuKojiro Sep 11, 2018

You know the original woz mon source is documented in the instruction manual for the Apple 1, right?
A transcribed copy exists in my Apple 1 emulator and there's a link to scans of the original instruction manual in the license file.

RyuKojiro commented Sep 11, 2018

You know the original woz mon source is documented in the instruction manual for the Apple 1, right?
A transcribed copy exists in my Apple 1 emulator and there's a link to scans of the original instruction manual in the license file.

@ranobrega

This comment has been minimized.

Show comment
Hide comment
@ranobrega

ranobrega Sep 11, 2018

Excellent work, thanks for sharing.

ranobrega commented Sep 11, 2018

Excellent work, thanks for sharing.

@42Bastian

This comment has been minimized.

Show comment
Hide comment
@42Bastian

42Bastian Sep 11, 2018

"and #7 ; setup so A=0 if the address now ends with 0 or 8"
This is always positive, so the "bpl prnext" is always executed.

42Bastian commented Sep 11, 2018

"and #7 ; setup so A=0 if the address now ends with 0 or 8"
This is always positive, so the "bpl prnext" is always executed.

@boomlinde

This comment has been minimized.

Show comment
Hide comment
@boomlinde

boomlinde Sep 11, 2018

@42Bastian Yes, bpl is used as a branch-always instruction here. The reason for using this trick over a a plain jump is that it's a byte shorter (branch instructions use 1-byte relative offsets instead of 2-byte addresses).

So the bpl should be considered a jump, and where the contents of the accumulator actually matter is under the prnext label, where bne is performed to insert a line feed every eight byte.

boomlinde commented Sep 11, 2018

@42Bastian Yes, bpl is used as a branch-always instruction here. The reason for using this trick over a a plain jump is that it's a byte shorter (branch instructions use 1-byte relative offsets instead of 2-byte addresses).

So the bpl should be considered a jump, and where the contents of the accumulator actually matter is under the prnext label, where bne is performed to insert a line feed every eight byte.

@sydbarrett74

This comment has been minimized.

Show comment
Hide comment
@sydbarrett74

sydbarrett74 Sep 11, 2018

@RyuKojiro Despite your already having a transcribed copy of the source, it's still a cool feat robey performed, and he doubtless learned a lot. That in itself makes the effort worth while.

sydbarrett74 commented Sep 11, 2018

@RyuKojiro Despite your already having a transcribed copy of the source, it's still a cool feat robey performed, and he doubtless learned a lot. That in itself makes the effort worth while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment