6502mon/monitor.asm

696 lines
18 KiB
NASM

;;;
;;; Retrochallenge Summer 2014
;;; 6502 ROM Monitor
;;;
;;; Last Updated: 2014-JUL-03
;;;
.feature labels_without_colons
.include "basic.asm"
;;;************************************************************************
;;; Enhanced 6502 BASIC Integration
;;;************************************************************************
IRQ_vec = VEC_SV + 2
NMI_vec = IRQ_vec + $0A
;;;************************************************************************
;;; Macro Definitions.
;;;************************************************************************
;;;
;;; STR <ADDR>
;;;
;;; Print out the null-terminated string located at address <ADDR>
;;;
;;; Modifies: Accumulator, STRLO, STRHI
;;;
.macro STR ADDR
LDA #<ADDR ; Grab the low byte of the address
STA STRLO
LDA #>ADDR ; ... and the high byte
STA STRHI
JSR STOUT ; then call STOUT.
.endmacro
;;;
;;; CRLF
;;;
;;; Print a Carriage Return / Line Feed pair
;;;
;;; Modifies: Accumulator
;;;
.macro CRLF
LDA #CR
JSR COUT
LDA #LF
JSR COUT
.endmacro
;;;************************************************************************
;;; Non-monitor code, e.g. BASIC, utilities, etc., resides
;;; in the bottom 14KB of ROM
;;;************************************************************************
;;; ----------------------------------------------------------------------
;;; Memory Definitions
;;; ----------------------------------------------------------------------
STRLO = $de ; Low byte of STRING (used by STR macro)
STRHI = $df ; Hi byte of STRING (used by STR macro)
HTMP = $e0 ; Hex parsing temp
OPADDRL = $e1 ; Addr of current operand (low)
OPADDRH = $e2 ; Addr of current operand (high)
OPBYT = $02a0 ; # of bytes parsed in 16-bit operands
TKCNT = $02a1 ; Count of parsed tokens
IBLEN = $02a2 ; Input buffer length
CMD = $02a3 ; Last parsed command
TKST = $02a4 ; Token start pointer
TKND = $02a5 ; Token end pointer
OPBASE = $02a6 ; Operand base
IBUF = $02c0 ; Input buffer base
;;; ----------------------------------------------------------------------
;;; Constants
;;; ----------------------------------------------------------------------
CR = $0A
LF = $0D
BS = $08
IBMAX = $40 ; Maxiumum length of input buffer
PROMPT = '*'
;;; ----------------------------------------------------------------------
;;; IO Addresses
;;; ----------------------------------------------------------------------
IORW = $8800 ; ACIA base address, R/W registers
IOST = IORW+1 ; ACIA status register
IOCMD = IORW+2 ; ACIA command register
IOCTL = IORW+3 ; ACIA control register
.segment "CODE"
.org $C000
;; BASIC lives here.
;;;************************************************************************
;;; ROM monitor code resides in the top 2KB of ROM
;;;************************************************************************
.segment "MONITOR"
.org $FB00
;;; ----------------------------------------------------------------------
;;; Main ROM Entry Point
;;; ----------------------------------------------------------------------
MSTART: CLI
CLD
LDX #$FF ; Init stack pointer to $FF
TXS
;;
;; Initialize IO
;;
IOINIT: LDA #$1D ; Set ACIA to 8N1, 9600 baud
STA IOCTL ; ($1D = 8 bits, 1 stop bit, 9600)
LDA #$0B ; ($0B = no parity, irq disabled)
STA IOCMD ;
;;
;; Hard Reset. Initialize page 2.
;;
HRESET: LDA #$02 ; Clear page 2
STA STRHI ; (We're borrowing STRHI,STRLO here
LDA #$00 ; just becuase it's convenient and
STA STRLO ; they're not being used right now)
TAY ; Pointer into page 2
@loop: DEY
STA (STRLO),Y
BNE @loop
;; Set up vectors for BASIC.
VECS: LDA LABVEC-1,Y
STA VEC_IN-1,Y
DEY
BNE VECS
;; Now jump to BASIC
JMP LAB_COLD
;;; ----------------------------------------------------------------------
;;; Welcome the User to the Monitor
;;; ----------------------------------------------------------------------
;; Start the monitor by printing a welcome message.
WELCM: STR BANNR
;;; ----------------------------------------------------------------------
;;; Main Eval Loop - Get input, act on it, and return here.
;;; ----------------------------------------------------------------------
EVLOOP: CRLF
LDA #PROMPT ; Print the prompt
JSR COUT
LDA #$00 ; Reset state by zeroing out
TAX ; all registers and temp storage.
TAY
STA IBLEN
STA HTMP
;; NXTCHR is responsible for getting the next character of
;; input.
;;
;; If the character is a CR, LF, or BS, there's special
;; handling. Otherwise, the character is added to the IBUF
;; input buffer, and then echoed to the screen.
;;
;; This routine uses Y as the IBUF pointer.
NXTCHR: JSR CIN ; Get a character, blocking until Carry
BCC NXTCHR ; is set.
BEQ BSPACE
CMP #CR ; Is it a carriage-return?
BEQ PARSE ; Done. Parse buffer.
CMP #LF ; Is it a line-feed?
BEQ PARSE ; Done. Parse buffer.
CMP #BS ; Is it a backspace?
BEQ BSPACE ; Yes, handle it.
CPY #IBMAX ; Is the buffer full?
BEQ NXTCHR ; Yes, ignore it, don't even echo.
;; It wasn't a CR,LF, or BS
JSR COUT ; Echo it
STA IBUF,Y ; Store the character into $200,Y
INY ; Move the pointer
BNE NXTCHR ; Go get the next character.
;; Handle a backspace by decrementing Y (the IBUF pointer)
;; unless Y is already 0.
BSPACE: CPY #0 ; If Y is already 0, don't
BEQ NXTCHR ; do anything.
DEY
LDA #BS
JSR COUT
JMP NXTCHR
;;; ----------------------------------------------------------------------
;;; Parse the input buffer (IBUF) by tokenizing it into a command and
;;; a series of operands.
;;;
;;; When the code reaches this point, Y will hold the length of the
;;; input buffer.
;;; ----------------------------------------------------------------------
PARSE: TYA ; Save Y to IBLEN.
STA IBLEN
BEQ EVLOOP ; No command? Short circuit.
;; Clear operand storage
LDY #$10
LDA #$00
@loop: STA OPBASE,Y ; Clear operands.
DEY
BPL @loop
;; Reset parsing state
LDX #$FF ; Reset Token Pointer
LDA #$00
STA TKCNT ; Number of tokens we've parsed
STA CMD ; Clear command register.
TAY ; Reset IBUF pointer.
;;
;; Tokenize the command and operands
;;
;; First character is the command.
LDA IBUF,Y
STA CMD
;; Now start looking for the next token. Read from
;; IBUF until the character is not whitespace.
SKIPSP: INY
CPY IBLEN ; Is Y now pointing outside the buffer?
BCS EXEC ; Yes, nothing else to parse
LDA IBUF,Y
CMP #' '
BEQ SKIPSP ; The character is a space, skip.
;; Here, we've found a non-space character. We can
;; walk IBUF until we find the first non-digit (hex),
;; at which point we'll be at the end of an operand
STY TKST ; Hold Y value for comparison
TKNEND: INY
CPY IBLEN ; >= IBLEN?
BCS TKSVPTR
LDA IBUF,Y
CMP #' ' ; Is it a space?
BNE TKNEND ; No, keep going.
;; Yes, fall through.
;; Y is currently pointing at the end of a token, so we'll
;; remember this location.
TKSVPTR:
STY TKND
;; Now we're going to parse the operand and turn it into
;; a number.
;;
;; This routine will walk the operand backward, from the least
;; significant to the most significant digit, placing the
;; value in OPBASE,X and OPBASE,X+1 as it "fills up" the value
LDA #$02
STA OPBYT
;; Token 2 Binary
TK2BIN: INX
;; low nybble
DEY ; Move the digit pointer back 1.
CPY TKST ; Is pointer < TKST?
BCC TKDONE ; Yes, we're done.
LDA IBUF,Y ; Grab the digit being pointed at.
JSR H2BIN ; Convert it to an int.
STA OPBASE,X ; Store it in OPBASE + X
;; high nybble
DEY ; Move the digit pointer back 1.
CPY TKST ; Is pointer < TKST?
BCC HIBCHK ; We might be done
LDA IBUF,Y ; Grab the digit being pointed at.
JSR H2BIN ; Convert it to an int.
ASL ; Shift it left 4 bits.
ASL
ASL
ASL
ORA OPBASE,X ; OR it with the value from the
STA OPBASE,X ; last digit, and re-store it.
;; Next byte - only if we're parsing the first two
;; operands, which are treated as 16-bit values.
;;
;; (Operands 2 through F are treated as 8-bit values)
HIBCHK: LDA TKCNT ; If TKCNT is > 2, we can skip
CMP #$02 ; the low byte
BCS TKDONE
DEC OPBYT ; Have we done 2 bytes?
BNE TK2BIN ; If not, do next byte
;; We've finished converting a token.
TKDONE: INC TKCNT ; Increment the count of tokens parsed
CMP #$10 ; Have we hit our maximum # of
; operands? (16 max)
BCS EXEC ; Yes, we're absolutely done, no more.
LDA TKND ; No, keep going. Restore Y to end of
TAY ; token position
CPY IBLEN ; Is there more in the buffer?
BCC SKIPSP ; Yes, try to find another token.
;; No, the buffer is now empty. Fall through to EXEC
;;; ----------------------------------------------------------------------
;;; Execute the current command with the decoded operands.
;;; ----------------------------------------------------------------------
EXEC: LDX #$00 ; Reset X
LDA CMD
CMP #'D'
BEQ @nolf
CRLF ; CRLF (unless the command is DEP)
@nolf: LDA CMD ; Have to reload CMD, CRLF will mess
; with it.
CMP #'H' ; Help requires no arguments,
BEQ HELP ; so comes first.
CMP #'Q' ; 'Quit' just jumps to BASIC warm
BEQ QUIT ; start
LDA TKCNT ; Now we check operand count.
BEQ @err ; No operands? Error.
LDA CMD ; Dispatch to the appropriate command.
CMP #'E'
BEQ EXDEP
CMP #'D'
BEQ EXDEP
CMP #'G'
BEQ GO
;; No idea what to do, fall through
@err: LDA CMD ; More spaghetti code to deal with the
CMP #'D' ; missing CRLF if the command was 'D',
BNE @nolf2 ; vs. all other commands.
CRLF
@nolf2: JSR PERR
JMP EVLOOP
;;; HELP command
HELP: STR HELPS
JMP EVLOOP
;;; QUIT command
QUIT: JMP LAB_1274 ; BASIC Warm Start.
;;; GO command
GO: JMP (OPBASE) ; Just jump to the appropriate address
;;; EXAMINE and DEPOSIT commands share code, and start the same way.
EXDEP: LDA OPBASE ; Transfer the address we want to load
STA OPADDRL ; from to OPADDRL
LDA OPBASE+1 ;
STA OPADDRH ; and OPADDRH
LDA CMD ; Are we in DEPOSIT?
CMP #'D'
BEQ DEP ; Yes. Go there.
;; Examine. How many operands?
LDA TKCNT
CMP #$02 ; Two?
BEQ PRANGE ; Print a range.
;; If just one...
JSR PRADDR ; Print the address
LDA (OPADDRL,X) ; Get data pointed at by address
JSR PRBYT ; Print it.
JMP EVLOOP ; Done.
;;; DEPOSIT command
DEP: LDA TKCNT ; We need at least 2 arguments
CMP #$02 ; (but more is OK)
BCC @err ; If not, error out.
;; Now we enter a loop, parsing each argument from
;; OPBASE+2 to OPBASE+TKCNT
LDY #$02
DEC TKCNT
LDA OPBASE,Y
STA (OPADDRL,X)
INY
INY
INC OPADDRL
BNE @loop
INC OPADDRH
@loop: LDA OPBASE,Y ; Grab the data to store
STA (OPADDRL,X) ; Store it
INY
DEC TKCNT
BEQ @done
INC OPADDRL
BNE @loop
INC OPADDRH
JMP @loop
;; We don't print anything back to the console on deposit
;; because we want to make it easy to pipe in data from a
;; serial console. All we do is jump back and print the prompt
;; again. Silence means success.
@done: JMP EVLOOP ; Done.
@err: CRLF
JSR PERR
JMP EVLOOP
;;; Print a range
PRANGE:
;; Do a 16-bit signed comparison of START and END addresses
;; to make sure the range is valid. If it's not, error out and
;; don't continue.
SEC
LDA OPBASE+3 ; Compare high bytes
SBC OPBASE+1
BVC @l1
EOR #$80
@l1: BMI @err
BVC @l2
EOR #$80
@l2: BNE @start
LDA OPBASE+2 ; Compare low bytes
SBC OPBASE
BCC @err ; START is > END, no good.
;; Now we know the range is valid. We can actually print the
;; contents of memory between [OPBASE,OPBASE+1] and
;; [OPBASE+2,OPBASE+3]
@start: JSR PRADDR ; Print the starting address
@loop: LDA (OPADDRL,X) ; Grab the contents of OPADDRL,H
JSR PR1B ; Print it
;; After printing each byte, check to see if we're at the end
;; of the range. If we are, we're done. Otherwise, continue.
LDA OPBASE+3 ; Compare high
CMP OPADDRH
BNE @next
LDA OPBASE+2 ; Compare low
CMP OPADDRL
BEQ @done
;; Now we increment OPADDRL,H so we can get the next location
;; in memory.
@next: INC OPADDRL ; Read next location in memory.
BNE @endck ; If L was incremented to 00,
INC OPADDRH ; inc H too.
;; Now we check to see if we're at the end of a line. We want
;; to insert a carriage return after every address which ends
;; in F (e.g. 100F, 3F, 1F0F, whatever) so we get consistent
;; starting addresses along the left-hand side of the
;; terminal.
@endck: LDA OPADDRL
AND #$0F
BNE @loop ; No CRLF/ADDR needed
CRLF
JMP @start ; Restart at new address.
@done:
JMP EVLOOP
@err: JSR PERR
JMP EVLOOP
;;; Print a space and the byte in A
PR1B: PHA
LDA #' '
JSR COUT
PLA
JSR PRBYT
RTS
;;; ----------------------------------------------------------------------
;;; Print the last stored address as four consecutive ASCII hex
;;; characters.
;;;
;;; Input: EMEMH/EMEML
;;; Output: ACIA
;;; ----------------------------------------------------------------------
PRADDR: LDA OPADDRH ; Load the byte at OPBASE+1.
JSR PRBYT ; Print it out.
LDA OPADDRL ; Load the byte at OPBASE.
JSR PRBYT ; Print it out.
LDA #':' ; Print a ": " separator.
JSR COUT
LDA #' '
JSR COUT
RTS ; Return.
;;; ----------------------------------------------------------------------
;;; Check to see if the value in A is a hex digit.
;;; Input: Accumulaotr
;;; Output: C - Clear if digit, set if not digit.
;;; ----------------------------------------------------------------------
ISNUM: CMP #'0' ; < '0'?
BCC @fail ; It's not a digit.
CMP #'9'+1 ; <= '9'?
BCC @succ ; Yup, it's a digit.
CMP #'A' ; < 'A'
BCC @fail ; It's not a digit.
CMP #'F'+1 ; <= 'F'?
BCC @succ ; Yup, it's a digit.
;; Fall through to failure
@fail: SEC
@succ: RTS
;;; ----------------------------------------------------------------------
;;; Check to see if the value in A is printable.
;;; Input: Accumulator
;;; Output: C - Clear if printable, Set if not printable
;;; ----------------------------------------------------------------------
PRTBLE: CMP #$20 ; < space?
BCC @fail ; It's not printable
CMP #$7F ; < DEL?
BCC @succ ; Yes, it's printable.
;; Fall through to failure
@fail: SEC
@succ: RTS
;;; ----------------------------------------------------------------------
;;; Convert a single ASCII hex character to an unsigned int
;;; (from 0 to 16).
;;;
;;; Input: Accumulator
;;; Output: Accumulator
;;; ----------------------------------------------------------------------
H2BIN: JSR ISNUM ; If this isn't a valid digit, error out.
BCS @err
SEC
SBC #'0' ; Subtract '0' from the digit.
CMP #10 ; Is the result <= 10? Digit was 0-9.
BCC @done ; We're done.
CMP #23 ; Is this a hex digit? (<= 'F' - 30)
BCS @err ; No, it's not a hex digit.
SEC
SBC #7 ; OK, it's a hex digit.
@done: RTS
@err: CRLF
JSR PERR
RTS
;;; ----------------------------------------------------------------------
;;; Parse Error
;;;
;;; Abort the current operation, print an error prompt ("?") and
;;; get next line.
;;; ----------------------------------------------------------------------
PERR: LDA #'?'
JSR COUT
JMP EVLOOP
RTS
;;; ----------------------------------------------------------------------
;;; Print the content of the accumulator as two consecutive ASCII
;;; hex characters.
;;;
;;; Input: Accumulator
;;; Output: ACIA
;;; ----------------------------------------------------------------------
PRBYT: PHA ; We'll need A later.
LSR ; Shift high nybble to low nybble
LSR
LSR
LSR
JSR PRHEX ; Print it as a single hex char
PLA ; Get A back
;; Fall through to PRHEX
;;; ----------------------------------------------------------------------
;;; Print the low nybble of of the accumulator as a single
;;; ASCII hex character.
;;;
;;; Input: Accumulator
;;; Output: ACIA
;;; ----------------------------------------------------------------------
PRHEX: AND #$0F ; Mask out the high nybble
CMP #$0A ; Is it less than 10?
BCC @done
ADC #6
@done: ADC #'0'
JSR COUT
RTS
;;; ----------------------------------------------------------------------
;;; Print the character in the Accumulator to the ACIA's output
;;; ----------------------------------------------------------------------
COUT: PHA ; Save accumulator
@loop: LDA IOST ; Is TX register empty?
AND #$10
BEQ @loop ; No, wait for empty
PLA ; Yes, restore char & print
STA IORW
RTS ; Return
;;; ----------------------------------------------------------------------
;;; Read a character from the ACIA and put it into the accumulator
;;; ----------------------------------------------------------------------
;; Rather ugly, but Enhanced BASIC requires this subroutine to
;; return immediately rather than block polling for data, so...
CIN: LDA IOST
AND #$08 ; Is RX register full?
BEQ @nochr ; No, wait for it to fill up.
LDA IORW ; Yes, load character.
;;
;; If the char is 'a' to 'z', inclusive, mask to upper case.
;;
CMP #'a' ; < 'a'?
BCC @done ; Yes, done.
CMP #'{' ; >= '{'?
BCS @done ; Yes, done.
AND #$5f ; No, convert lower case -> upper case,
@done: SEC ; Flag byte received, for BASIC
RTS ; and return.
@nochr: CLC
RTS
;;; ----------------------------------------------------------------------
;;; Print the null-terminated string located at STRLO,STRHI
;;; ----------------------------------------------------------------------
STOUT: LDY #$00 ; Initialize string pointer
@loop: LDA (STRLO),Y ; Get character
BEQ @done ; If char == 0, we're done
JSR COUT ; Otherwise, print it
INY ; Increment pointer
BNE @loop ; Continue
@done: RTS ; Return
STUB_SAVE:
STUB_LOAD:
RTS
;;; ----------------------------------------------------------------------
;;; Data
;;; ----------------------------------------------------------------------
;;; Vectors used by EhBASIC
LABVEC: .word CIN
.word COUT
.word STUB_SAVE
.word STUB_LOAD
;;; Strings
BANNR: .byte "RETROCHALLENGE 2014 ROM MONITOR",0
HELPS: .byte "COMMANDS ARE:",CR,LF
.byte "H HELP",CR,LF
.byte "E <addr> [<addr>] EXAMINE",CR,LF
.byte "D <addr> <val> [<val> ...] DEPOSIT",CR,LF
.byte "G <addr> GO",CR,LF,0
;;;************************************************************************
;;; Reset and Interrupt vectors
;;;************************************************************************
.segment "VECTORS"
.org $FFFA
.word MSTART ; NMI vector
.word MSTART ; Reset vector
.word MSTART ; IRQ vector