The Spark
Like many others who’ve studied programming (especially low-level), I started wondering how a computer actually works. Then you see videos online of people making games for the NES, and there’s a certain charm to working with such (in today’s terms) limited hardware. That’s why I created Cove.
What is Cove?
Cove is a complete 16-bit fantasy console with 64 KiB of memory, a 128x128 display supporting 16 colours at a time, and 4 audio channels. Everything is programmed in Cove’s own assembly (Cove assembly), which I’d say is primarily inspired by ARM.
Originally the idea for Cove was just a challenge, can I design a CPU? The plan was always to support a high-level language, possibly a BASIC-like language I’d design myself. But over time I came to realize that the goal of this project wasn’t a high-level language, it was assembly.
The Hello World That Changed My Perspective
During a university course on computer engineering, we learned about how computers and processors work. ALU, pipelining, IEEE 754, and of course some basic assembly. That was probably the first time I saw “Hello world!” in assembly.
section .data
msg db "Hello, World!", 0xA
len equ $-msg
section .text
global _start
_start:
; write(1, msg, len)
mov rax, 1 ; sys_write
mov rdi, 1 ; file descriptor (stdout)
mov rsi, msg ; address of the string
mov rdx, len ; length of the string
syscall
; exit(0)
mov rax, 60 ; sys_exit
xor rdi, rdi ; status 0
syscall
When I first saw this code, I thought, okay, I can kind of understand how
this works. But it was section .data that confused me. Now, having built
my own 16-bit computer, I understand exactly why NASM looks this way. For
comparison, here’s how you write “Hello world!” in Cove assembly.
.org 0x0000
.dw start ; reset vector, sets the entry point of the program
.org 0x0050 ; by convention programs should start at 0x0050, lower addresses are reserved for zero page and interrupt vectors
msg: .db "Hello world!\n" ; the actual text string to be printed now lives at the address of msg
start:
MOV R0, #0 ; SYS_DEBUG, prints to console
MOV R1, msg ; start address of the string
MOV R2, #13 ; length of the string to be printed
SYS ; runs the syscall
MOV R0, #13 ; SYS_EXIT, to cleanly exit the program
SYS
The takeaway is that even Cove assembly shares a lot of the same logic,
.db writes the following bytes directly into the binary, MOV works almost
exactly the same way, but I use different names for the registers, and
different values for the various syscalls. Other than that, the basic
structure is the same.
Design Goals
My goal for Cove was to replicate the development process of an old game console like the NES, while also providing more user-friendly tools. That’s why it has all those syscalls for easily drawing to the screen.
When I first understood that Cove was going to be a console you program in
assembly, I had an interesting realisation, I want to encourage users to
think in the right way even on this fantasy console. That’s why I introduced
clock cycles, something you have to be aware of when programming real
assembly. For example, I made MUL cost 3 cycles while SHL only costs 1.
This teaches you to use shifts when multiplying or dividing by powers of two,
an optimization used on real computers too.
I also wanted to encourage clean code, so I made instructions like CALL
and RET cheaper than they realistically should be, so as not to punish
users for using functions.
In short, Cove is an idealised game console that captures the feel of old systems while making certain choices that are bad from a hardware perspective but make it easier to program for.
The 2100+-Line Manual
This was one of the most fun parts of the project. Assembly isn’t as
straightforward as Python or even C, and I felt it needed a manual that
covered everything you need to know, something you could quickly reference
to check where the palette is in memory, what number SYS_SPR was, and so
on.
With the manual, I wanted to capture the feeling of reading a manual for, say, a Commodore 64. And I have to say, I’m really happy with it, especially if you actually print it out, you get that authentic feel.
What’s Coming in This Series
In this series I’m going to walk through Cove and why I made the choices I did, so look forward to the following parts.
- Overview - The big picture, why assembly, and the philosophy behind Cove’s design
- ISA - A deep dive into the ISA and why it looks the way it does
- Assembler - A look at the assembler and how it works
- Interrupts - What they are, how they work, and what they’re used for
- Debugger - How to use it to debug a program
- Retrospective - What I learned, what I’d do differently
AI Usage
Given the current climate around AI, it’s best to address this right away. During development of Cove I’ve used LLMs to help find solutions when I was stuck and as a sounding board for discussing design choices. But I understand every part of the codebase and made all the design decisions myself.
Try It Yourself
You can find all the source code and instructions for running Cove here: