6502 FORTH, Part 1: Setup

5 minute read

Published:

Now that the hardware for my 65C02 computer is more or less complete, it’s time to start working on the software. I’ve been very interested in the Forth language since discovering it a couple years ago due to its relative simplicity, low size requirements, efficiency, and departure from many conventions seen in other programming languages.

Writing Forth interpreters from scratch has been done many times online, and the implementations don’t seem super complex (at least simple enough for me to figure out how to write my own). The plan is to write an operating system implementing a Forth REPL for my 6502 computer.

Disclaimer: This is my first big project in assembly, and I am by no means an expert. I’d always welcome tips/suggestions from more experienced people along the way!

Setting up a Development Environment

Two of the biggest lessons I’ve learned from programming are:

  1. Always tackle problems one at a time if you can.
  2. Never assume your code works perfectly until you’ve thoroughly tested it.

As much as I’d like to think that I’ve wired up my computer perfectly and everything will work great immediately, I have a sneaking suspicion that I’ll discover some errors/problems when I start getting software to run on my new 8-bit machine. As I mentioned in (1), I’d really like to avoid having to simultaneously solve problems in hardware while also dealing with inevitable bugs in software (see (2)).

Instead, I’m going to put my computer to the side for a bit while I develop the software to run on it. I’m instead using Symon, a 6502 simulator that has been thoroughly tested to make sure it works correctly. The default simulator’s memory map allocates addresses $0000-$7FFF for RAM and $C000-$FFFF for ROM, which is identical to my build. It also implements a 65C22 VIA and a 65C51 ACIA for serial output just like my computer. I’m pretty sure the memory map for I/O is a little different than mine, but that can be solved later on.

The code for this project is hosted on GitHub at https://github.com/ahl27/FORTH, and I’m initially going to be following the SECND implementation of Forth developed by Paul Dourish for the 65C02, at least until my implementation gets off the ground.

Writing the Interpreter: Memory, Variables, and Initialization

Forth interpreters are small relative to how much work it takes to write an interpreter for most modern languages, but it’s still a nontrivial amount of work to get the system operational. For now, I’m just going to start by allocating my variables and writing some placeholder code for the rest. These first posts will likely involve a lot of code with very little outputs, but that’s part of the process!

;;;
;;; Variables/Setup
;;; (zero paging common variables for speed)
;;;

;;; 2 byte variables
IP=$0050                   ; Forth instruction pointer
RP=$0052                   ; return stack pointer
DT=$0054                   ; pointer to top of dictionary stack
TMP1=$0056                 ; temp value
TMP2=$0058                 ; temp value

;; 1 byte variables
TPTR=$005A
TCNT=$005B

;; other
DPTR=$005C
INPUT=$7F00                ; input space
WORDSPC=$7EC0              ; temp space for parsing words (<=63 chars)

jmp initstart

All we’re doing here is initializing some variables, then jumping to an initialization routine. Most of these values are based off of the SECND implementation, compiled with xa <file.asm> -M. I was going to use vasm as in Ben Eater’s videos, but xa seems to be easier with better documentation available.

Next, we have to initialize the system.

;;;
;;; Initialization/Configuration
;;;
initstart:
  ldx #$FF                 ; initializes stack pointer to $00FF, top of zero page
  
  ;; Initializing values for variables

  stz IP                   ; zero out the instruction pointer
  stz IP+1

  stz RP                   ; store $0200 in return stack pointer (second page of memory)
  lda #$02                 ; this stack will grow upwards
  sta RP+1


  ;; Initialize dictionary top to last entry on dictionary (defined below)
  lda #d0entry 
  sta DT                   ; store first byte
  ina
  sta DT+1                 ; store second byte


  ;; jump to test code
  jmp testcode

Nothing crazy is happening here either, we’re just loading in some initial values for the pointers declared in the previous section, then jumping to a test routine. The final section here initializes our initial dictionary of built in words, which currently just has a single word with no associated code. We finish by implementing the testcode routine, which is also an infinite loop of nothing (for the time being).

;;;
;;; Dictionary
;;;

;;; TODO, just have one entry as a placeholder (associated code is not yet defined)
d0entry:
  .byte 4
  .byte "exit"
d0link:
  .word $0000
d0code:
  .word exit


;;; Test code will go here
testcode:
  nop
  jmp gotest

Next Steps

This project is far from even being functional, but it’s a good start! The next steps are going to be to implement the critical foundational Forth words next, exit, and dolit. From there, I’ll probably implement some basic arithmetic words and then start testing to see if the basic underlying code functions properly. Stay tuned!