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
.asmsource intoprogram.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
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
2. Clone and pick a test program
git clone https://github.com/vaishna-v/project16.git
cd project16
python tester.py
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
Run it and the testbench prints the register contents straight out of simulated RAM:
NUM1 : 84
NUM2 : 37
RESULT : 121
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)