HDL Compiler

Compiler for a self-invented hardware description language

After borrowing the “Dragon Book” from the library, I was interested in trying to write my own compiler. Without ever having looked at VHDL or Verilog, I thought about creating a language to “program” simple digital circuits.

Initially written in C, it is now a Rust application that parses and compiles source code in this hardware description language which I developed myself. It produces files that can be read by my LogicSimulator project, a web-based simulator for digital circuits.

I have written an extensive README containing a lot more details and instructions on how to use the compiler which is available on GitLab together with the source code: gitlab.com/maxkl2/hdl-compiler.

This is how a simple 2-bit counter would be implemented:

sequential block Counter {
    clock rising_edge clk;
    out[2] y;

    y = y + 1#2;

block main {
    in clk;
    out[2] y;

    block Counter c;
    c.clk = clk;
    y = c.y;

I did not implement any sophisticated graph drawing algorithm, so the circuits are not clearly layed out or aesthetically pleasing. The circuit generated from the code above looks like this:


Example: 16-bit RISC CPU

To ensure the compiler’s correct functionality and to test its limits (especially regarding optimizations) I have written a 16-bit RISC core in this hardware description language: gitlab.com/maxkl2/hdl-cpu. The programs it executes can either be written by hand or created using a simple assembler that I created for this purpose. This assembler, also written in Rust, can be found in the same repository as the core’s HDL code.

This is a part of the circuit as generated by the compiler (yes, it’s ugly):


Here you can see a counter program which also reacts to user input:

The CPU is capable of executing programs with subroutines, stack memory and memory-mapped peripherals. This is an excerpt of the README file, detailing the instruction set of the CPU:

Instruction set

The CPU implements a load/store architecture, inspired by existing RISC architectures such as MIPS and Arm.

There are eight 16-bit registers:

Index Register Description
0 R0 General purpose register 0. Target and source of instructions with immediate values.
1 R1 General purpose register 1.
2 R2 General purpose register 2.
3 Scratch General purpose register which does not need to be preserved by subroutines.
4 SP Stack Pointer.
5 RAddr Return Address. Target register of the jal (“Jump And Link”) instruction.
6 SR Status Register. Layout: 15-4: x, 3: LT (“Less Than”), 2: EQ (“EQuals”), 1: Z (“Zero”), 0: C (“Carry”)
7 PC Program Counter.

The bits 0-3 of the status register are set by the arithmetic and bitwise instructions as detailed below.

There are currently 22 (out of the possible 32) instructions. Every instruction executes in a single cycle.

Instr. Operation Upd. flags Opcode [15:11] 10 9 8 7 6 5 4 3 2 1 0
mov Rt, Rs Rt = Rs 0x00 s s s t t t
ld Rt, @Rs Rt = @Rs 0x01 s s s t t t
ldi imm R0 = imm 0x11 i i i i i i i i i i i
st @Rt, Rs @Rt = Rs 0x02 s s s t t t
and Rt, Ra, Rb Rt = Ra & Rb Z 0x03 b b b a a a t t t
andi imm R0 = R0 & imm Z 0x13 i i i i i i i i i i i
or Rt, Ra, Rb Rt = Ra | Rb Z 0x04 b b b a a a t t t
ori imm R0 = R0 | imm Z 0x14 i i i i i i i i i i i
xor Rt, Ra, Rb Rt = Ra ^ Rb Z 0x05 b b b a a a t t t
xori imm R0 = R0 ^ imm Z 0x15 i i i i i i i i i i i
not Rt, Rs Rt = ~Rs Z 0x06 s s s t t t
add Rt, Ra, Rb Rt = Ra + Rb Z, C 0x07 b b b a a a t t t
addi imm R0 = R0 + imm Z, C 0x17 i i i i i i i i i i i
sub Rt, Ra, Rb Rt = Ra - Rb Z, C 0x08 b b b a a a t t t
subi imm R0 = R0 - imm Z, C 0x18 i i i i i i i i i i i
sl Rt, Ra, Rb Rt = Ra << Rb Z 0x09 b b b a a a t t t
sli imm R0 = R0 << imm Z 0x19 i i i i
sr Rt, Ra, Rb Rt = Ra >> Rb Z 0x0A b b b a a a t t t
sri imm R0 = R0 >> imm Z 0x1A i i i i
cmp Ra, Rb Ra - Rb LT, EQ 0x0B b b b a a a
cmpi imm R0 - imm LT, EQ 0x1B i i i i i i i i i i i
jmp.cond Rs if cond: PC = Rs 0x0C 0 s s s c c c
jal.cond Rs if cond: RAddr = PC; PC = Rs 0x0C 1 s s s c c c

The ld and st instructions access the memory, where the “@” sign indicates that a register is interpreted as an absolute address. The letters in the rightmost columns denote the operands as follows:

Letter Description
t Target register
s, a, b Source registers
i Immediate value
c Condition code

The jmp/jal instruction can be executed conditionally, with the following possible conditions:

Code Mnemonic Behaviour Description
0 <none> Always executed
1 z SR.Z == 1 Zero
2 eq SR.EQ == 1 Equal
3 ne SR.EQ == 0 Not equal
4 lt SR.LT == 1 Less than
5 le SR.LT == 1 || SR.EQ == 1 Less than or equal
6 gt SR.LT == 0 && SR.EQ == 0 Greater than
7 ge SR.LT == 0 Greater than or equal