Skip to content

Instantly share code, notes, and snippets.

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:
; 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:
; 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:
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
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)
cmp #$df ; '_' -- woz treated this as a backspace
beq backsp
cmp #$9b ; ESC
beq abort
bpl readch ; if we overran 128 bytes, fall thru to abort
; $ff1a
lda #$dc ; '\'
jsr cout
; $ff1f
; Y: buffer index
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.
bmi readline ; backspace too far and we'll reset
; $ff29
; 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
; $ff40
; $ff41
sta state
; $ff43
; A: current char being parsed
; X: 0 always
; Y: index into buffer
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
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
asl ; shift the low nybble into the high nybble
; roll the high nybble thru the carry bit into addr2
ldx #4
rol addr2l
rol addr2h
bne rollnyb
bne parsehex
; $ff7f
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
jmp parsech1
; $ff94
jmp (addr0l)
; $ff97
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.
ldx #2
; $ff9b
lda addr2l-1,x
sta addr1l-1,x
sta addr0l-1,x
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.
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
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
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
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
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'
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
bit dsp
bmi cout ; wait for the display to be not-busy
sta dsp
; $fff8
; free space! 2 whole bytes!
; $fffa
; interrupt vectors
.data $0f00 ; NMI
.data start ; reset
.data $0000 ; IRQ
Copy link

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.

Copy link

ranobrega commented Sep 11, 2018

Excellent work, thanks for sharing.

Copy link

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.

Copy link

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.

Copy link

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