I’ve been posting about creating a Forth interpreter for a 65c02, and at this point I’m pretty close to something that could be tested. However, I still need one more piece of infrastructure before I can begin writing and testing my Forth interpreter: some way to communicate with a user.
My 6502 build includes a 65C22 VIA and 65C11 ACIA for general I/O and serial connections, respectively. The first step was to write some way to receive data over a serial connection. I found the datasheet at this link, and started to figure out how to get my computer to interact with the world around it.
The 65C51 ACIA adapter is a pretty nifty little device–it even includes an onboard baud rate generator (this is why the computer requires a 1.8432 MHz external clock). The Symon simulator I’m using maps the ACIA to addresses
$8000-8004, and examples from their Github use a baud rate of 9600. The actual code to make this work is significantly less daunting than I expected.
This initial implementation is based off of Michael Billington’s original post. There’s quite a few random hardcoded values in here, so let’s break them down based on the information in the datasheet.
reset_acia, we push and pull to the a register at the beginning and end of the subroutine to preserve whatever was in the A register when we started. In the middle, we load three seemingly random values into three registers. The ACIA has four main addresses accessible to our computer:
R/TX, STATUS, COMMAND, CONTROL. The first of these is the register that receives and transmits data depending on if we read or write to it (resp.). We’ll come back to that register later.
STATUS register is the simplest: the value at this address contains status of the ACIA device. Writing any value to this address resets the chip, which is why we start by writing
$00 to it. The other bits of note in this address are bits 3 and 4, which correspond to the the receiver/transmitter data registers being full/empty (resp.). This will come in handy in our read/write methods later on.
Next is the
COMMAND register. This register, when written to, changes how the ACIA handles data. Bits 5-7 control parity check controls, bit 4 changes the receiver between normal and echo mode, and bits 0-3 change if interrupt signals are sent when sending data (and if so, how they’re sent). I had initially planned to use these interrupt signals to trigger interrupts when data is ready to be read/written, but unfortunately I found out from this forum post that the 65C51 ACIA has a hardware bug that renders all the interrupts essentially unusable. Instead, we’ll have to disable them and rely on the 65C22’s clock to trigger interrupts. For the
COMMAND register, we’re loading the value
$0B = 0000 0111, corresponding to “Parity checks disabled”, “Normal Receiver mode”, “All interrupts disabled, receiver/transmitter enabled”.
Finally, we have the
CONTROL register, which controls how the ACIA works. Bit 7 controls number of stop bits, bits 5-6 control the length of each data word, bit 4 controls the clock source for generating baud rate, and the lower 4 bits control the baud rate. We load a value of
$1E = 0001 1110, corresponding to “1 stop bit”, “8 bit data word”, “Baud rate generator”, “9600 baud”. If we wanted to process at 19200 baud, we would instead set the value to
Once we’ve set up the ACIA, we can write some methods to read data from the chip:
Remember from before that bits 3 and 4 correspond to if the chip is ready to read/write. A
1 in bit 3 corresponds to a value of
0000 0100 = $08, so we do a bitwise comparison between
$08 and the
STATUS register to see if the data are ready to read. If they are, we read the value.
beq acia_rx_full will branch if the result is 0, meaning if the
STATUS register does not have a 1 in the third bit. If it does, the result is not zero and we can read the value out of the read register.
The method to send data is very similar, just with an additional operation to store the character in the a register while we check if we’re ready to write.
Notice there’s a method commented out here. As mentioned before, a hardware bug prevents us from using the 65C51’s internal clock, which is unfortunate. Instead, I set up the 6522 VIA to send continuous interrupts at a timespan equivalent to the 6551’s baud rate. Without waiting a little bit after each transmission, we can get corrupt data or duplicated bytes. Unfortunately, the Symon emulator does too good a job replicating the performance of the machine, and so they don’t seem to have this error at all. As a result, I’m going to skip this for now and revisit it in the future when we get to hardware.
Testing the Serial Connection
It’s time for everyone’s favorite program: the Hello World script. For now, I’m just going to hardcode a text string to print, then have the program print the string. After this, it’ll echo back any input given over the serial connection.
Most of this code is pretty simple–we just initialize the ACIA chip, jump to a method to print out “Hello, world!”, then return and start echoing back characters. The
message initializer stores an
ascii string corresponding to the text string, then
.dsb 1,0 fills the byte 0 immediately after the string. This makes the string null-terminated, so we can iterate over it by using
lda message,y until the value loaded in
a is zero. This can be checked with
beq, and once we reach the end of the string we jump out of the loop to
mainloop. At this point, we just alternate between reading and sending characters forever.
I also wanted to start writing code as if it’s going into the ROM chip, since eventually I’d like this to be loaded into the system from ROM. To do this, I’d need to compile my code so that the file is exactly 16 KiB. Additionally, I’d like to be able to set the beginning of the program and correctly link
irq handlers to the right addresses (
$FFFC-D the start of the program).
This caused some issues, since
xa lacks a
.org command and has very sparse documentation. I found a post from the original developer of xa, but unfortunately it was from 2005 and the syntax has been updated since then. However, I was able to figure out the following syntax based on the docs and the 2005 post:
This begins by initializing the program counter to
$C000, which is the start of the ROM address space. The original suggestion was to use
.byte $fffa-*, $ff, but
.byte is no longer a command in
xa. However, we can instead use
.dsb, which specifies a data block to be filled with a particular value. The first part of the command,
$fffa-*, specifies that the size of the fill block should be
$fffa minus the value of the program counter, meaning the width of the space between the end of the code and
$fffa. The second value is the value to fill the blocks with, which in this case is just
$ea (which corresponds to a
nop instruction). The final three lines fill the last 3 bytes of the program space with addresses for
The complete test code looks like this:
ROMSTART is just a constant set to
$C000, which I’ve defined in a constants file.
If we compile and hexdump this, we can see that it is actually 16KiB, occupying memory from addresses
$0000-3FFF. These will be mapped to
$C000-FFFF, so this is all working correctly.
After this, we just need to run the program!
Adding Text Buffers
This setup is not bad, but it’s far from ideal. Since the maximum transmission rate of the ACIA is 19,200 baud, with this current implementation we’re limited to sending one character every ~0.53ms. With my current setup, the computer will have to wait between each of these send/receives, meaning that it can’t do any work in the meantime. It would be nice if we could just precalculate what we want to send over serial, and then process it when the ACIA has time. Likewise, if we send input, we don’t want the computer to have to process it immediately character by character.
To solve this, I’m going to set up a pair of buffers for input/output. I’ll write outgoing text to the
$7Dxx memory page, and I’ll write incoming text to the
$7Exx page. This way, I can store a string of up to 255 characters prior to having to send it out, and I can receive up to 255 characters before I have to process them with code. My constants file is getting a little large, so I’ll include it here:
I’ve also defined four pointers, corresponding to the start and end of each buffer. One pointer will be incremented as the buffer is read/written to, and the other as the buffer is written/read from. This results in a minor modification to the ACIA software, including two new helper functions:
Because we’re now using a buffer to read/write, the test program changes slightly.
Shown below is an example output, along with the memory map for page
$7E to illustrate that the buffer is getting filled up. I typed a bunch of
j’s to verify that the buffer correctly rolls back to
I now have a way to send and receive data from the computer! I know there will be problems down the line with hardware bugs that I can’t replicate on the simulator, but I’m confident I’ll be able to troubleshoot that when I get there. Additionally, we don’t have any way to use arrow keys, backspace, or return/newline, but that can be handled later when I write the interpreter interface. For now, it’s finally time to start testing the Forth interpreter.