Retrochallenge: The Final Entry

Well. Here it is, the final entry for my Summer Retrochallenge project. I wanted to do more, but as so often happens, real life intervened and I didn’t have nearly as much time to work on it as I’d wanted. C’est la vie!

But I’m still proud of what I accomplished. I have a working ROM monitor, and I’m happy to report that as a final hurrah, I got it fully integrated with Lee Davison’s Enhanced 6502 BASIC.

Integrating with BASIC

I didn’t start this project thinking I’d tackle BASIC integration, but as Retrochallenge drew to a close, it seemed like it might be the best use of my precious time. What I mean by “Integration” here is simply having my ROM monitor code live side by side with BASIC and provide the underlying I/O functionality needed for BASIC to talk to the terminal and get input, plus the ability to take over and run in the foreground when the user wants it.

If you’ve ever used an Apple II, you may recall that it, too, has a built in ROM monitor, and it can be reached by typing CALL -151 into Apple BASIC. Well, when running Enhanced 6502 BASIC with my monitor, you can get the same thing by typing CALL -1239. Once you’re in the ROM monitor, you can quit back to BASIC just by typing Q. Like so:

BASIC Interop

It actually turned out to be fairly simple to get the integration working.

The first thing I had to do was re-organize my memory usage so as not to conflict with EhBASIC’s memory map:

;;; ----------------------------------------------------------------------
;;; 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

There’s not much room left in Page Zero after EhBASIC gets done with it, but the locations $DE thorugh $E2 are free, so I made use of them. Page Two is similar, so I stuck my variables all the way up at $02A0 and on.

After that, I had to change the implementation of CIN a little bit, because EhBASIC requires that it not block. Instead, it expects this routine to return immediately and set the Carry flag if a character was read, and clear the Carry flag if no character was read. The new routine looks like this:

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

It’s a little ugly and could easily be optimized at the expense of readability, but hey, it works!

So that’s it. With just those changes, I was pretty much able to integrate the monitor into EhBASIC in just a few hours. As always, all of my code is available on GitHub for your viewing pleasure.

I’d also like to add that this has been a fantastic Summer Retrochallenge. There were so many amazing projects, I can’t even pick a favorite. I’ve really enjoyed reading what everyone else has been up to, and I’m already looking forward to January. Onward and upward, everyone!

State of the Retrochallenge

As I write this, it’s early on the evening of July 25th, and I’m staring next Thursday’s deadline in the face. I haven’t been able to work on my Retrochallenge entry for over a week, and it’s in poor shape.

But am I going to give up? No way. I’m going to go down fighting.

My over-enthusiastic early plans called for me to finish up my 6502 ROM monitor so early that I’d have time to work on cleaning and sorting my RL02 packs. That, needless to say, is not going to happen. Instead, I’m going to concentrate on polishing up and documenting my monitor this weekend. Whatever I have ready to go next week will just have to be good enough. It won’t be as fully-featured as I originally wanted, but at least it’s something, and at least it works.

I have to pick my remaining features pretty carefully now. I want to enhance the Deposit and Examine commands to add syntax that will allow auto-incrementing the address, and then work on tying my monitor into EhBASIC, so I can run it on my home-brew 6502 after Retrochallenge is over.

It’s a race to the finish, now. Expect an update from me on Sunday or Monday. Until then, I’m face down in the code!

Parsing and Tokenizing

I’m fairly happy with my parsing and tokenizing code now. I wanted to give a little breakdown of how it works.

The over-all goal here is to take a command from the user in the form:

C NNNN [(NN)NN [NN NN NN NN NN ... NN]]

where C is a command such as “E” for Examine, “D” for Deposit, etc., and store it in memory, tokenized and converted from ASCII to binary.

I wanted to give the user flexibility. For example, numbers do not need to be zero-padded on entry. You should be able to type E 1F and have the monitor know you mean E 001F, or D 2FF 1A and know that you mean D 02FF 1A.

I wanted whitespace between tokens to be ignored. Typing "E 1FF" should be the same as typing "E    1FF "

And finally, I wanted to support multiple forms of commands with the same tokenizing code. The Examine command can take either one or two 16-bit addresses as arguments—for example, E 100 1FF should dump all memory contents from 0100 to 01FF. But the Deposit command takes one 16-bit address and between one and sixteen 8-bit values, for example D 1234 1A 2B 3C to deposit three bytes starting at address 1234

So I decided I’d reserve 20 bytes of memory in page 0 to hold the tokenized, converted data.

  • Byte 0 stores the number of arguments entered, not including the command itself
  • Byte 1 stores the command, for example “E” for Examine or “D” for Deposit.
  • Bytes 2 and 3 store the first argument, which is always a 16-bit address value.
  • Bytes 4 and 5 store the second argument, which is sometimes a full 16-bits, and sometimes only 8-bits occupying byte 4.
  • Bytes 6 through 20 are always 8-bit values.

Each implemented command then knows how to use this parsed data to fulfill its operation.

[An aside: Using page zero for this is controversial in my mind — 21 bytes is a lot of space to use, and page zero is precious, so I will move it to page 2 at a later date. But it will work the same, just a change in addressing mode from Zero Page,X to Absolute,X would be required.]

How It’s Implemented

The implementation is fairly straightforward. Here’s a very rough flowchart with some details elided:

flow

In general, the idea is to start at the beginning of IBUF, the input buffer, and scan until the start of a token is found. This location is then stored in TKST. Next, we continue scanning until the end of the token is found. The end is stored in TKND. Once it is, we walk the token backward, one character at a time, converting it from a hexadecimal ASCII representation into a number. Once we’ve reached the start of the token (or we’ve done 4 characters, whichever comes first), we know we’re done with the token. We jump back to TKND and start the process over again.

We do this until either:

  • The buffer is exhausted, or
  • We’ve scanned 17 arguments, total

whichever comes first. At that point, we fall through and start executing whatever command has been decoded.

The code is below.

;;; ----------------------------------------------------------------------
;;; 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     #'0'            ; < '0'?
        BCC     TKSVPTR         ; It's not a digit, we're done.
        CMP     #'9'+1          ; < '9'?
        BCC     TKNEND          ; Yup, it's a digit. Keep going.
        CMP     #'A'            ; < 'A'
        BCC     TKSVPTR         ; It's not a digit, we're done.
        CMP     #'Z'+1          ; < 'Z'?
        BCC     TKNEND          ; Yup, it's a digit. Keep going.
        ;; 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     TKDONE          ; Yes, we're 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)

        LDA     TKCNT           ; If TKCNT is > 1, we can skip
        CMP     #$02            ;   the low byte
        BCS     TKDONE

        DEC     OPBYT           ; Have we done 2 bytes?
        BEQ     TKDONE          ; Yes, we're done with this token.
        BNE     TK2BIN          ; If not, do next byte

        ;; We've finished converting a token.

TKDONE: INC     TKCNT           ; Increment the count of tokens parsed

        CMP     #$0F            ; 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

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 at 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. It looks like this when it’s running:

Running

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:

In Use

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.

Getting Input

I’m moving kind of fast here because I really want to get to the meaty parts of the code, but I want to cover how I’m getting input from the console into the system. As you might expect, it’s all about reading from the ACIA instead of writing to it.

Recall the ACIA’s addresses:

;;; ----------------------------------------------------------------------
;;; 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

To read in a byte of data, we’ll first need to check the IOST status and see whether bit 3 is a 1. If it is, there’s data to read. On the other hand, if it’s a 0 the ACIA doesn’t have any data for us. We can just loop checking that status. This is a technique called polling, and it’s the same technique we used for writing to the ACIA.

Here’s what the code looks like:

CIN:    LDA     IOST
        AND     #$08            ; Is RX register full?
        BEQ     CIN             ; No, wait for it to fill up.
        LDA     IORW            ; Yes, load character.
        RTS                     ; and return.

Pretty straightforward. Now we can use this in conjunction with COUT to just read in keyboard input, and echo it back out to the console.

ECHO:   JSR     CIN             ; Read a character into the accumulator
        JSR     COUT            ; Echo it
        JMP     ECHO            ; And repeat

But for my monitor, I want to do something a little bit more. I want to convert all lower-case letters to upper-case, primarily so later on when I’m parsing input, I know everything will be in the same case. Turns out we can do that with a pretty simple AND mask. But we only want to do it if the character is a-z, inclusive, so we’ll need a little branching magic.

Here’s my change:

CIN:    LDA     IOST
        AND     #$08            ; Is RX register full?
        BEQ     CIN             ; 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:  RTS                     ; and return.

Now if the character is < 'a' or >= ‘{‘ (which comes right after ‘z’ in ASCII land), we’ll skip the mask. But if it’s between ‘a’ and ‘z’ inclusive, we’ll apply a mask of $5F to it, which will strip off bit 5 and convert lower case to upper case.

That’s it for today. Tomorrow we’ll tackle saving input to memory and parsing it for commands.

Oh, and if you’re in the United States, have a happy and safe Independence Day!

Our First Macro

Last night I got string output working. But what if I want to make string output generic? I want to write a STOUT (STring OUT) subroutine that can take the address of any null-terminated string and print it to the console, but there’s a problem: The 6502 is an 8-bit machine, so passing a 16-bit address as an argument takes some wrangling.

To get around this, I’ve designated two locations in the precious Zero Page (arbitrarily choosing $20 and $21) to store the locations of the low byte and high byte of the string’s address, respectively.

;;; ----------------------------------------------------------------------
;;; Memory Definitions
;;; ----------------------------------------------------------------------

;;; Zero Page
	STRLO	= $20		; Low byte of STOUT STRING
	STRHI	= $21		; Hi byte of STOUT STRING

Now I can write my STOUT subroutine. The change here is that I’m going to use the Indirect Indexed addressing mode to look up the characters of the string from the zero-page addresses, instead of the Absolute,X addressing that I used in my last post.

;;; ----------------------------------------------------------------------
;;; 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

But calling this subroutine is still a little weird. Each time I want to print a string, I need to put the low byte of the string’s address in $20, the high byte in $21, and then call STOUT. It’s just slightly unwieldy, so… enter our first assembler macro!

The ca65 assembler supports putting a series of instructions together into a macro call, sort of like an inline function in C, so that code can be re-used without the overhead of a subroutine call. I’ve created a macro named STR that takes a 16-bit address and prints the string to the console.

;;;
;;; 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

Great. Now all I have to do is call STR to print my “HELLO, 6502 WORLD!” string over and over again.

LOOP:   STR     HELLO
        JMP     LOOP

HELLO:  .byte   "HELLO, 6502 WORLD! ",0

With this slight diversion out of the way, next up will be console input, which I promised to talk about today. But that’s a subject for another post.

Talking to the World

Now that I have a skeleton 6502 assembly project set up and building, it’s time to get going and writing some real code for the ROM monitor. But wait, there’s just one more thing I need to get set up, and that’s an emulator so I can test code easily. Without an emulator, I’d have to flash a new ROM image to an EPROM and put it into a real 6502 computer every time I wanted to run it, and debugging would be very, very hard. Luckily for me, I wrote a 6502 emulator called Symon a couple of years ago! You can download it from GitHub if you want to follow along.

Symon will allow me to run code from a ROM image, single-step through the instructions, and examine the state of the registers and memory for debugging purposes. With that up and running, I can finally get started.

Console IO

The 6502 computer I’m using talks to the world through a 6551 ACIA located at base address $8800. The ACIA has a couple of special registers at the following addresses

Address Name Description
$8800 R/W Read and Write data to and from the console
$8801 STATUS Read the current status of the ACIA
$8802 COMMAND Write set-up commands to the ACIA
$8803 CONTROL Control the ACIA’s baud rate generator

In order to write data out to the console, we first need to check the status register and make sure that bit 4 (“Transmitter data register empty”) is set to a 1. If it is, we just write the character to address $8800 and it will be sent to the console. If it’s a 0, we just wait for it to become a 1. Easy! Here’s what it looks like in assembler:

First, we’ll define some constants:

;;; ----------------------------------------------------------------------
;;; 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

And then, we’ll write the code:

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

I’ve named this subroutine COUT. It will be used quite a bit by the ROM monitor.

It also demonstrates one of my favorite features of the ca65 assembler: cheap local labels. Any label starting with an @-sign is only has scope between the nearest two regular labels, so I can re-use the name @loop in other contexts without worry. Very convenient!

Testing It

OK, so let’s test it. First, I’ll change my monitor start-up code to set the baud rate properly, then I’ll print a single “@” to the console, and finally enter an infinite loop.

START:
        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           ;

        LDA     #'@'            ; Load the character '@' into A
        JSR     COUT            ; Call COUT

        BNE     *               ; Just drop into an infinite loop

Now I’ll compile it and test it in the emulator.

ACIA Test 1

Rock on! That worked! I can print a character to the console now.

Going Further

If I can print one character, I can print lots of characters. Let’s make a tiny change to print “@” to the console continuously instead of just one time, by adding a label and changing where the BNE branches to.

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           ;

PRLOOP: LDA     #'@'            ; Load the caracter '@' into A
        JSR     COUT            ; Call COUT
        BNE     PRLOOP          ; Accumulator is not 0, so do it again

Now we get a continuous stream of “@” printed to the console, just as predicted

ACIA Test 2

Wrapping It Up

The final thing I’d like to do for today is get whole strings printing to the console. Sure, it feels good to print a character, but wouldn’t it feel better if we were doing more?

Let’s start by defining a string to print. Let’s just make it a variation on the standard “Hello, world!”

;;; ----------------------------------------------------------------------
;;; Data
;;; ----------------------------------------------------------------------

HELLO:  .byte   "HELLO, 6502 WORLD! ",0

Now, we’ll modify the printing code to use the X register as an offset into the string, indexing into it character by character. Since the string is null-terminated with a 0, we’ll always be able to tell when we’re at the end of the string.

PRSTR:  LDX     #$00            ; Set the X index to 0
        
NXTCHR: LDA     HELLO,X         ; Load character pointed to by X
        BEQ     PRSTR           ; If it's == 0, we're done - loop!
        JSR     COUT            ; If we're not done, Call COUT
        INX                     ; Point to the next character
        JMP     NXTCHR          ;

And voilà! It’s working.

acia_test_3

OK, that’s enough for tonight. As always, you can follow along by looking at the project on GitHub as I go.

Tomorrow, I’ll tackle the other direction – reading input from the console.

Baby Steps

Before we get our ROM monitor off the ground, we’ll need to sort out a few things first. The most important decision will be what assembler to use. I’ve decided to go with the CC65 tool chain, because I’m already familiar with it and I don’t have a lot of time to come up to speed with a new assembler. Now, a word of caution: CC65 is no longer being developed, so its fate is uncertain. There are other assemblers out there, and if I were doing this outside of Retrochallenge and had more time, I would probably look into them. Chief among these seem to be Ophis, a 6502 cross assembler written in Python, and XA, a venerable cross assembler with a long history.

Now that I’ve picked my assembler, it’s time to set up the project. I’m going to get things rolling with a very simple skeleton directory. If you want to follow along in real time, the project is hosted here on GitHub

seth$ ls -lF
total 40
-rw-r--r--  1 seth  staff   208 Jul  1 18:11 Makefile
-rw-r--r--  1 seth  staff   913 Jul  1 18:15 monitor.asm
-rw-r--r--  1 seth  staff   387 Jul  1 18:11 symon.config

I’ll explain what each of these files is for.

Makefile

Let’s start with the Makefile. This is a pretty bog-standard Makefile that knows how to assemble and link the source code in monitor.asm.

CA=ca65
LD=ld65

all: monitor

monitor: monitor.o
	$(LD) -C symon.config -vm -m monitor.map -o monitor.rom monitor.o

monitor.o:
	$(CA) --listing -o monitor.o monitor.asm

clean:
	rm -f *.o *.rom *.map *.lst

This is pretty self-explanatory. Typing make will use ca65 to assemble the monitor.asm file and output the object file monitor.o. Then, the ld65 linker will link the object file and produce the final ROM image, monitor.rom.

That symon.config file needs further explanation.

symon.config

MEMORY {
RAM1: start = $0000, size = $8000;
ROM1: start = $C000, size = $3B00, fill = yes;
MONITOR: start = $FB00, size = $4FA, fill = yes;
ROMV: start = $FFFA, size = $6, file = %O, fill = yes;
}

SEGMENTS {
CODE:     load = ROM1, type = ro;
DATA:     load = ROM1, type = ro;
MONITOR:  load = MONITOR, type = ro;
VECTORS:  load = ROMV, type = ro;
}

This file is basically a memory map of the RAM and ROM in the system I’m targeting. For me, this is my home-brew 6502 with 32KB of RAM and 16KB of ROM. The MEMORY section is the full memory map, and the SEGMENTS section spells out where the assembly language segments live in the memory map.

monitor.asm

Finally, there’s the assembly source code itself, in monitor.asm. Right now, it does absolutely nothing:

;;
;; Retrochallenge Summer 2014
;; 6502 ROM Monitor
;;

;;************************************************************************
;; Non-monitor code, e.g. BASIC, utilities, etc., resides
;; in the bottom 14KB of ROM
;;************************************************************************
.segment        "CODE"
                .org    $C000

;;************************************************************************
;; ROM monitor code resides in the top 2KB of ROM
;;************************************************************************
.segment        "MONITOR"
                .org    $FB00

START:          LDA     #$00    ; Set zero flag
                BEQ     *       ; Loop forever

;;************************************************************************
;; Reset and Interrupt vectors
;;************************************************************************
.segment        "VECTORS"
                .org    $FFFA

                .word   START   ; NMI vector
                .word   START   ; Reset vector
                .word   START   ; IRQ vector

Notice how each .segment pseudo-op maps to one of the segments from the symon.config file? That tells the linker exactly where to put things in the final ROM image.

Right now, this code really doesn’t do much. On reset, the 6502 will load the address located at $FFFC and $FFFD into the program counter and start running from that address. Here, the destination is the address of the START label, which works out to $FB00. The instruction at that address loads $00 into the accumulator, which sets the zero flag. The very next instruction tells the 6502 to branch back to itself if the zero flag is set — an infinite loop.

That’s it for today. I just wanted to get a project off the ground and give me a framework to start developing in. More tomorrow.