A few weeks ago I got sucked into designing a toy 8-bit CPU, ROSE-8, and got as far as writing an emulator for the machine that you could manually feed instructions to. At the end, I listed some future projects, the first of which was
- An assembler/interpreter, i.e. running from a text file (and outputting to a binary file, I guess). Writing arrays of instructions by hand (as shown above) isn’t so bad except for manually computing addresses and offsets, so I still want to get to this at some point.
This turned out to be quite the endeavor! I found out computing offsets and addresses is tricky in a program where not all instructions are the same length, and doubly so when certain parts of the program have to be located in certain parts of memory. It took me about a week to put together an assembler that had all the features I wanted.
The readme shows the architecture I eventually came up with
- Module.swift: Declares the basic representation for parsed instructions and such.
- Parser.swift: Converting textual assembly code to an in-memory parsed representation.
- Assembler.swift: Converting the parsed representation to architecture instructions (and then encoding them to machine code).
- Program.swift: The final representation of an assembled program, which can be run directly or emitted to a file.
and the fact that I needed an “architecture” at all shows that it was more complicated than I expected going in! I’m pretty happy with what I came up with, though—it’s got a good separation of data and logic, and uses immutability and lots of helper types to make it clear what the invariants are at each step. You can check it out on my newly-set-up gitweb instance. (Note that that URL is also a valid git “clone” URL if you want to play with ROSE-8 locally.)
This post also marks the “official” finalization of ROSE-8 v0.2.2, which comes with a handful of “new features”. You can check out the changelog in the readme, but I want to focus in on one new instruction in particular:
WAIT spin or sleep until data1[it] > 0, then decrement data1[it] (for MMIO)
“MMIO” stands for “memory-mapped input/output”, and
WAIT as the single primitive for this is the result of a design discussion between me and Cassie (the one who inspired the whole ROSE-8 project and who’s been providing contributions throughout). The simplest thing to do with this is to have a program that waits for (textual) input, instead of just running based on how it’s compiled. I’m not going to go into the details here, but the implementation was pretty straightforward (other than one hiccup), and it leads to actually-interactive programs!
% swift run rose8-as Examples/greet.rose8 -o $TMPDIR/greet % swift run rose8-console $TMPDIR/greet What is your name? > JUNE EGBERT Pleasure to meet you, JUNE EGBERT! %
The console extension provides a poll address (default: segment 255, address 0) and an input address (default: segment 255, address 1), which will be updated whenever a ROSE-8 program
WAITs for input. Reading a single byte looks something like this:
# Assuming 'data1' is already set to segment 255 GETI 1 SETR r0 # set up the address to read from (1) ZERO WAIT # wait on address 0 LD1R r0 # load the byte that was read BEZI eof # EOF is treated as a NUL byte # do something with the byte that was just read in
The “greet” example shown above is just the start; the ROSE8Console repository includes a full (tiny) maze game. It’s a game I could have written in C—or any more modern language. But this one’s written in an assembly I made up, for a CPU architecture I made, then put through an assembler I made, and run on an emulator I made. That’s a cool feeling.
But I’m not stopping here. In the original post I observed that the other 8-bit CPU I knew was the Game Boy, and to my brain that meant that I could make Game-Boy-like games based on ROSE-8. But was that just an idle dream?
…You’ll have to find out next time.