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.

  1. Overview - The big picture, why assembly, and the philosophy behind Cove’s design
  2. ISA - A deep dive into the ISA and why it looks the way it does
  3. Assembler - A look at the assembler and how it works
  4. Interrupts - What they are, how they work, and what they’re used for
  5. Debugger - How to use it to debug a program
  6. 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:

https://git.jimmy-b.se/JimBer110/Cove