Modern hardware description languages (HDL)#

I curated a list of modern (newer than SystemVerilog or VHDL) open-source HDL tools. Click on the project name to jump to see further information.

With less than 10 commits last year:

Warning

Code examples shown below are not mine. I copied them from the corresponding repositories/guides and pasted them for an easier overview.

Chisel#

Scala-based:

import chisel3._

class Count100 extends Module {
  val io = IO(new Bundle {
    val cnt = Output(UInt(8.W))
  })

  //- start counter
  val cntReg = RegInit(0.U(8.W))

  cntReg := Mux(cntReg === 9.U, 0.U, cntReg + 1.U)
  //- end

  io.cnt := cntReg
}

cocotb#

  • not for describing hardware but testbenches

An excerpt from the introductory example which exercises dut’s clock:

import cocotb
from cocotb.triggers import Timer


@cocotb.test()
async def my_first_test(dut):
    """Try accessing the design."""

    for cycle in range(10):
        dut.clk.value = 0
        await Timer(1, units="ns")
        dut.clk.value = 1
        await Timer(1, units="ns")

    dut._log.info("my_signal_1 is %s", dut.my_signal_1.value)
    assert dut.my_signal_2.value[0] == 0, "my_signal_2[0] is not 0!"

SpinalHDL#

  • the ecosystem includes a plugin-based FPGA-friendly RISC-V-based processor VexRiscv

  • standard library includes FIFOs, clock domain crossing bridges, buses like AHB, peripherals UART & USB

  • forked from Chisel decade ago

Scala-based:

package spinaldoc.examples.simple

import spinal.core._

import scala.language.postfixOps

case class Counter(width: Int) extends Component {
  val io = new Bundle {
    val clear = in Bool()
    val value = out UInt(width bits)
  }

  val register = Reg(UInt(width bits)) init 0
  register := register + 1
  when(io.clear) {
    register := 0
  }
  io.value := register
}
// end case class Counter

object Counter extends App {
  SpinalVerilog(Counter(8))
}

Amaranth#

  • has an extensive ecosystem including processor generator, board files and request-for-comments infrastructure

  • standard library includes memory arrays, interfaces, clock domain crossing elements, FIFOs, etc

Python-based:

from amaranth import *


class LEDBlinker(Elaboratable):
    def elaborate(self, platform):
        m = Module()
        led = platform.request("led")
        half_freq = int(platform.default_clk_period.hertz // 2)
        timer = Signal(range(half_freq + 1))
        with m.If(timer == half_freq):
            m.d.sync += led.o.eq(~led.o)
            m.d.sync += timer.eq(0)
        with m.Else():
            m.d.sync += timer.eq(timer + 1)

        return m

Clash#

Haskell-based:

{-# LANGUAGE CPP #-}

module MatrixVect where

import Clash.Prelude
import Clash.Explicit.Testbench
import qualified Data.List as L

row1 = 1 :> 2 :> 3 :> Nil
row2 = 4 :> 5 :> 6 :> Nil
row3 = 7 :> 8 :> 9 :> Nil

matrix = row1 :> row2 :> row3 :> Nil
vector = 2 :> 3 :> 4 :> Nil

dotProduct xs ys = foldr (+) 0 (zipWith (*) xs ys)
matrixVector m v = map (`dotProduct` v) m

topEntity :: Vec 3 (Signed 16) -> Vec 3 (Signed 16)
topEntity = matrixVector matrix
-- See: https://github.com/clash-lang/clash-compiler/pull/2511
{-# CLASH_OPAQUE topEntity #-}

testBench :: Signal System Bool
testBench = done
  where
    testInput      = stimuliGenerator clk rst ((2 :> 3 :> 4 :> Nil) :> Nil)
    expectedOutput = outputVerifier' clk rst ((20 :> 47 :> 74 :> Nil) :> Nil)
    done           = expectedOutput (topEntity <$> testInput)
    clk            = tbSystemClockGen (not <$> done)
    rst            = systemResetGen

XLS: Accelerated HW Synthesis#

  • by Google

Rust-inspired domain-specific language (DSL) called DSLX:

fn adder8_with_carry(a: u8, b: u8) -> (u8, u1) {
  let sum_with_carry: u9 = a as u9 + b as u9;
  let sum = sum_with_carry[0:8];
  let carry_bit = sum_with_carry[8+:u1];

  (sum, carry_bit)
}

#[test]
fn adders_test() {
  assert_eq(adder8_with_carry(u8:41, u8:1), (u8:42, u1:0));
  assert_eq(adder8_with_carry(u8:255, u8:1), (u8:0 , u1:1));
}

MyHDL#

  • uses generators to model concurrency and decorators to annotate event information (e.g., triggering on a positive edge)

  • seems to focus on register transfer level abstraction

  • supports also high-level abstraction provided by Python, e.g.:

    • modeling a RS232 bus transmission using a function,

    • object-oriented modeling

    • memory modeling using list, dict

Python-based:

from myhdl import *

ACTIVE = 0
DirType = enum('RIGHT', 'LEFT')

@block
def jc2(goLeft, goRight, stop, clk, q):
    dir = Signal(DirType.LEFT)
    run = Signal(False)

    @always(clk.posedge)
    def logic():
        # direction
        if goRight == ACTIVE:
            dir.next = DirType.RIGHT
            run.next = True
        elif goLeft == ACTIVE:
            dir.next = DirType.LEFT
            run.next = True
        # stop
        if stop == ACTIVE:
            run.next = False
        # counter action
        if run:
            if dir == DirType.LEFT:
                q.next[4:1] = q[3:]
                q.next[0] = not q[3]
            else:
                q.next[3:] = q[4:1]
                q.next[3] = not q[0]

    return logic

Bluespec HDL#

  • actually decades old, but still worth looking into

  • developed by Bluespec Inc. and open-sourced in 2020

  • supports writing in two languages that are similar to SystemVerilog (BSV) and Haskell (BH)

  • standard library includes FIFOs, vectors, lists, LFSR, clock synchronizers

Hardcaml#

open Hardcaml
open Hardcaml.Signal
open Hardcaml_waveterm

module I = struct
  type 'a t =
    { clock : 'a
    ; clear : 'a
    ; incr : 'a
    }
  [@@deriving hardcaml]
end

module O = struct
  type 'a t =
    { dout : 'a[@bits 8]
    }
  [@@deriving hardcaml]
end

# let create (i : _ I.t) =
    { O.dout =
        reg_fb
          (Reg_spec.create ~clock:i.clock ~clear:i.clear ())
          ~enable:i.incr
          ~width:8
          ~f:(fun d -> d +:. 1)
    }
val create : t I.t -> t O.t = <fun>

DFiant HDL (DFHDL)#

  • supports three different abstraction levels

    • dataflow (functional simulation)

      • focuses on the sequence of operations based on data dependencies

      • does not mandate in which clock cycle each operation should start or finish

      • gives the compiler the freedom for pipelining

    • register transfer (cycle-accurate simulation)

      • anchors the design to specific timing requirements

      • different register types that we abstract away when we use higher level modeling: pipelining, CDC synchronization, clock division, state

    • event driven (event-based simulation)

      • known from VHDL/Verilog

  • compiles to VHDL/Verilog

Scala-based:

import dfhdl.*

@top class Counter8 extends RTDesign:
  val cnt = UInt(8) <> OUT.REG init 0
  cnt.din := cnt + 1

given options.CompilerOptions.PrintBackendCode = true

PipelineC#

  • compiler driven pipelining: a pure function (returns the same output for the same input) is autopipelined

  • only part of PipelineC can be compiled by C compilers

  • produces synthesizable VHDL/Verilog

C-based:

#include "uintN_t.h"  // uintN_t types for any N
#pragma MAIN_MHZ blink 200.0
uint1_t blink()
{
  static uint28_t counter;
  static uint1_t led;

  if(counter==(200000000-1)) {
    led = !led;
    counter = 0;
  }
  else {
    counter += 1; // one 5ns increment
  }
  return led;
}

PyMTL 3 (Mamba)#

  • focuses on teaching and research

Counter example copied from here

import pyrtl

def one_bit_add(a, b, carry_in):
    assert len(a) == len(b) == 1  # len returns the bitwidth
    sum = a ^ b ^ carry_in
    carry_out = a & b | a & carry_in | b & carry_in
    return sum, carry_out

def ripple_add(a, b, carry_in=0):
    a, b = pyrtl.match_bitwidth(a, b)
    # This function is a function that allows us to match the bitwidth of multiple
    # different wires. By default, it zero extends the shorter bits
    if len(a) == 1:
        sumbits, carry_out = one_bit_add(a, b, carry_in)
    else:
        lsbit, ripplecarry = one_bit_add(a[0], b[0], carry_in)
        msbits, carry_out = ripple_add(a[1:], b[1:], ripplecarry)
        sumbits = pyrtl.concat(msbits, lsbit)
    return sumbits, carry_out

counter = pyrtl.Register(bitwidth=3, name='counter')
sum, carry_out = ripple_add(counter, pyrtl.Const("1'b1"))
counter.next <<= sum

sim_trace = pyrtl.SimulationTrace()
sim = pyrtl.Simulation(tracer=sim_trace)
for cycle in range(15):
    sim.step()
    assert sim.value[counter] == cycle % 8
sim_trace.render_trace()

PyMTL 3 (Mamba)#

  • I could not find a documentation in five minutes 😐

Blarney#

  • can leverage functional programming properties of Haskell

Haskell-based. An example: Counter.hs