Skip navigation
Published Thursday, July 3, 2014 at 5:20 PM

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.

full.jpg

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.png

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.png

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.png

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

Tomorrow, I'll tackle the other direction - reading input from the console.