[Today is kind of a big update, and not all the source code will be presented in-line here in the blog post. As always, you can look at the current source Here on my Github.]
Good news, everyone! After quite a lot of hacking, I can examine memory with my monitor. It’s a very primitive feature, right now: I can only examine one byte at a time, so I can’t dump memory regions yet. But hey, it’s a good start. Let’s dive into the code.
Here’s the entry point, where the CPU jumps after being reset:
;;; ---------------------------------------------------------------------- ;;; Main ROM Entry Point ;;; ---------------------------------------------------------------------- START: 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 ;
The code starts by making sure interrupts are disabled, then clears the Decimal Mode flag and sets the stack pointer to $01FF (it could be anywhere when the processor starts up!). Following that is the IO initialization routine, which is probably pretty familiar by now. We just set up the ACIA for 9600 baud 8-N-1 communication.
Then things start to get more interesting:
;; ;; Hard Reset. Initialize page 2. ;; HRESET: LDA #$02 ; Clear page 2 STA $01 LDA #$00 STA $00 TAY ; Pointer into page 2 @loop: DEY STA ($00),Y BNE @loop
Here, we’re clearing the entire contents of Page 2 ($0200 to $02FF) in order to use it for scratch space. I’m using page 2 to store whatever the user types on the command line, which I’m referring to as IBUF (the Input Buffer). I’ve named this entry point HRESET, in case I want to be able to jump to it directly in a future version of the monitor.
Next, we welcome the user and start looping on input, parsing one line at a time.
;; Start the monitor by printing a welcome message. STR BANNR ;; ;; Eval Loop - Get input, act on it, 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 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. ;; 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
There’s kind of a lot going on in this code, so let’s take it a little bit at a time.
At the very start, we print out a message (using our STR macro) that welcomes the user. Then, we print a “*” prompt, do some housekeeping, and finally wait for input.
EVLOOP is the main Eval Loop that takes a line of input and handles it. The NXTCHR block is the meat of this code. Every time a key is pressed, the character is appended to the IBUF input buffer, located at $0200. We’re using the Y register as a pointer into the buffer.
If the user ever presses the backspace key, there’s a separate routine that handles that. It decrements the Y pointer and echoes the backspace character to the terminal.
And when the user presses ENTER (which generates a Carriage Return/Line Feed combo), we jump immediately to the PARSE parsing code to handle the command.
And this is where I totally cheat:
;; ;; Parse the command currently in the IBUF, with length ;; stored in Y ;; PARSE: TYA ; Save Y to IBLEN. STA IBLEN BEQ EVLOOP ; No command? Short circuit. ;; Reset some parsing state LDX #$00 ; Reset Operand pointer LDY #$00 ; Reset IBUF pointer. STY CMD ; Clear command register. STY OP1L ; Clear operands. STY OP1H STY OP2L STY OP2H ;; ;; 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. @loop: INY INX CPX IBLEN ; Is X now pointing outside the buffer? BCS @err ; Error, incorrect input. LDA IBUF,Y CMP #' ' BEQ @loop ; The character is a space, skip. ;; Here, we've found a non-space character. ;; We want to walk the IBUF with the operand pointer ;; until we find the first non-digit (hex) STY TMPY ; Hold Y value for comparison @loop2: INX CPX IBLEN ; >= IBLEN? BCS @parse LDA IBUF,X CMP #'0' ; < '0'? BCC @parse ; It's not a digit, we're done. CMP #'9'+1 ; <= '9'? BCC @loop2 ; Yup, it's a digit. Keep going. CMP #'A' ; < 'A' BCC @parse ; It's not a digit, we're done. CMP #'Z'+1 ; < 'Z'? BCC @loop2 ; Yup, it's a digit. Keep going. ;; Fall through. ;; 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 OP1L and OP1H as it "fills up" the valuel @parse: ;; First Digit DEX ; Move the digit pointer back 1. CPX TMPY ; Is pointer < Y? BCC @succ ; Yes, we're done. LDA IBUF,X ; Grab the digit being pointed at. JSR H2BIN ; Convert it to an int. STA OP1L ; Store it in OP1L. ;; Second digit DEX ; Move the digit pointer back 1. CPX TMPY ; Is pointer < Y? BCC @succ ; Yes, we're done. LDA IBUF,X ; Grab the digit being pointed at. JSR H2BIN ; Convert it to an int. ASL ; Shift it left 4 bits. ASL ASL ASL ORA OP1L ; OR it with the value from the STA OP1L ; last digit, and re-store it. ;; Third digit DEX ; Move the digit pointer back 1. CPX TMPY ; Is pointer < Y? BCC @succ ; Yes, we're done. LDA IBUF,X ; Grab the digit being pointed at. JSR H2BIN ; Convert it to an int. STA OP1H ; Store it. ;; Fourth digit DEX ; Move the digit pointer back 1. CPX TMPY ; Is pointer < Y? BCC @succ ; Yes, we're done. LDA IBUF,X ; Grab the digit being pointed at. JSR H2BIN ; Convert it to an int. ASL ; Shift it left 4 bits. ASL ASL ASL ORA OP1H ; OR it with the current OP1H val. STA OP1H ; Store it. ;; Success handler @succ: CRLF JSR PRADDR ; Print the current address. LDX #$00 LDA (OP1L,X) ; Grab the byte at OP1L,OP1H JSR PRBYT ; Print it. JMP EVLOOP ; Done! Go back for more. ;; Error handler @err: JSR PERR JMP EVLOOP
This is a lot to take in, I know. But basically, it parses the command into two parts: A one-letter command, and a one- to four-letter operand.
The idea is that a user should be able to enter a command like this:
and be able to see the contents of memory address (“examine”) $01FF. I’m cheating because I totally ignore the value of the first letter, the “E” – right now, ONLY the examine command is implemented, you could use any letter in place of E. Ssshhhhh, don’t tell anyone. I’ll fix that later. Really.
But the rest of the code is not cheating. It works by using two pointers, Y and X, to walk the input buffer until the operand is found. When it is, Y will point at the start of the operand. Then we increment the second pointer, X, until it’s pointing at the end of the operand. At that point, we start taking the operand backward, one character at a time, and convert it from hex to binary. The value is stored in page zero in locations OP1H (high byte) and OP1L (low byte). If the input is malformed or isn’t hexadecimal, we abort by printing “?” to the console and then jumping back to the EVLOOP entry point.
When the operand has been decoded, we call another routine, PRADDR, to print the address, followed by a colon and space. Then, finally, we use PRBYT to print the contents of the memory at the desired address. Whew, that’s a lot of code. I haven’t shown the implementation of PRADDR or PRBYT here, but the source, as always, is up on Github. Feel free to dig through it if that’s your thing.
When running, it looks like this:
That’s plenty for one day. I hope to refactor this code a little and clean it up tomorrow, so I can start adding more commands.