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 |