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.


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.

        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.


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


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.


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.