DEV Community

Sandeep
Sandeep

Posted on

SYNTH-16 — A Custom 16-Bit CPU With a Fantasy Instruction Set (and How to Run It Yourself)

I built a 16-bit CPU from scratch in Verilog — its own instruction set, its own assembler, its own testing framework — and gave every instruction a fantasy-themed name instead of the usual ADD/MOV/JMP. ADD is FUSE. LOAD is SUMMON. CALL is INVOKE. HALT is FREEZE.

This is SYNTH-16, and this post is a walkthrough of how it works and how to run it on your own machine in about five minutes.

Repo: github.com/vaishna-v/project16

What it actually is

  • A 16-bit processor implemented entirely in Verilog, targeting the Sipeed Tang Primer 20K FPGA (Gowin GW2A)
  • A variable-length instruction set — most instructions are one 16-bit word, but anything needing a full 16-bit immediate or address takes a second word
  • A multi-cycle FSM core: FETCH → DECODE → [FETCH_EXT] → EXECUTE
  • A custom two-pass Python assembler that turns .asm source into program.hex
  • 23 hand-written test programs, each with a matching Verilog testbench, covering everything from basic arithmetic to loops to bit manipulation

The architecture, briefly

        ┌─────────────────────────────┐
        │           CPU Core           │
        │  PC · SP · IR · IR_EXT ·     │
        │  FLAGS · Register File R0-R7 │
        └──────────────┬───────────────┘
                        │
              ┌─────────┴─────────┐
              │   Read/Write Unit  │──── Word-addressable RAM
              └─────────┬─────────┘
                        │
      ┌─────────┬───────┴───────┬─────────┐
  Arithmetic  Data Move   Control Flow   System
   Module      Module       Module      Module
Enter fullscreen mode Exit fullscreen mode

Only one execution module is active per EXECUTE cycle, selected by the top bits of the opcode. Each module is a self-contained combinational block, which keeps the whole design easy to extend — adding a new instruction category means writing one new module and wiring it into execution_unit.v, not touching the FSM.

Memory is a flat, word-addressable Von Neumann space:

Range Purpose Size
0000–7FFF Program 32 KB
8000–EFFF Data 28 KB
F000–FFFF Stack (grows downward) 4 KB

The instruction set

Every mnemonic maps to a normal CPU concept underneath:

SYNTH-16 Meaning Category
SUMMON LOAD Data Movement
SEAL STORE Data Movement
MIRROR MOV Data Movement
ENCHANT LOAD IMMEDIATE Data Movement
FUSE / DRAIN ADD / SUB Arithmetic
RISE / FALL INC / DEC Arithmetic
JUDGE CMP Arithmetic
AND / OR / XOR / NOT bitwise ops Arithmetic
SHL / SHR shift left/right Arithmetic
WARP JMP Control Flow
WARPZ / WARPNZ / WARPC / WARPNC conditional jumps Control Flow
INVOKE / RETURN CALL / RET Control Flow
IDLE / FREEZE NOP / HALT System

Two flags: Z (zero) and C (carry), updated per-instruction according to a table in the README.

Try it yourself

1. Install Icarus Verilog

# Debian/Ubuntu
sudo apt-get install iverilog

# macOS
brew install icarus-verilog
Enter fullscreen mode Exit fullscreen mode

2. Clone and pick a test program

git clone https://github.com/vaishna-v/project16.git
cd project16
python tester.py
Enter fullscreen mode Exit fullscreen mode

tester.py will list the available programs in programs/ and prompt you for a number. Under the hood it copies the chosen .asm and testbench into the project root, assembles it, compiles the full processor with Icarus, and runs the simulation.

3. Watch a program actually execute

Here's prog01.asm — the simplest one, adding two numbers:

ENCHANT R0, NUM1
SUMMON R1, R0

ENCHANT R0, NUM2
SUMMON R2, R0

FUSE R1, R2

ENCHANT R0, RESULT
SEAL R0, R1

FREEZE

NUM1:
DW 84

NUM2:
DW 37

RESULT:
DW 0
Enter fullscreen mode Exit fullscreen mode

Run it and the testbench prints the register contents straight out of simulated RAM:

NUM1   : 84
NUM2   : 37
RESULT : 121
Enter fullscreen mode Exit fullscreen mode

For something a bit more interesting, prog17.asm reverses the bits of a 16-bit number using a loop with SHL, SHR, AND, OR, and a conditional branch — 13 (0000000000001101) becomes 45056 (1011000000000000). Run it with tester.py and you'll see the full bit-reversal execute cycle-by-cycle if you hook up GTKWave to the generated waves.vcd.

4. Write your own program

The pattern is: load values with ENCHANT/SUMMON, operate on them with the arithmetic ops, branch with WARP*, and end every program with FREEZE so the testbench knows when to stop. Drop a new .asm file in programs/, write a matching testbench modeled on the existing ones (they're short — mostly just $display calls on memory addresses you care about), and run it through tester.py.

What's next for the project

Right now every instruction category has passing simulation coverage except INVOKE/RETURN — the subroutine call/return mechanism is implemented in control_flow_module.v but has no test exercising it yet. That's next on the list, along with getting a program actually running on the physical Tang Primer 20K board instead of just in simulation.

If you want to poke at the ISA, the ISA reference and per-module docs are all in the repo's docs/ folder.

Top comments (0)

⌂ 换网址