Skip navigation
Published Sunday, July 6, 2014 at 9:53 PM

Examining Memory

[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:

E 1FF

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:

running_2.png

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.