I came across the Amaranth project, which is yet another meta HDL language in Python. What I find interesting is that Amaranth implements some of the ideas I’ve had for years. The language has Verilog-like constructs that map to Verilog If statements.

Setup Link to heading

The setup is easy. Just install it with pip, and you’re good to go.

pip install --upgrade "amaranth[builtin-yosys]"

Counter Example Link to heading

The first example from the Amaranth website implements a counter with limit as an internal signal and ovf to indicate overflow.

from amaranth import *
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out

class UpCounter(wiring.Component):
    """
    A 16-bit up counter with a fixed limit.

    Parameters
    ----------
    limit : int
        The value at which the counter overflows.

    Attributes
    ----------
    en : Signal, in
        The counter is incremented if ``en`` is asserted, and retains
        its value otherwise.
    ovf : Signal, out
        ``ovf`` is asserted when the counter reaches its limit.
    """

    en: In(1)
    ovf: Out(1)

    def __init__(self, limit):
        self.limit = limit
        self.count = Signal(16)

        super().__init__()

    def elaborate(self, platform):
        m = Module()

        m.d.comb += self.ovf.eq(self.count == self.limit)

        with m.If(self.en):
            with m.If(self.ovf):
                m.d.sync += self.count.eq(0)
            with m.Else():
                m.d.sync += self.count.eq(self.count + 1)

        return m

Running Simulation Link to heading

Amaranth includes a built-in simulator to write small and simple testbenches for the DUT, dumping Verilog, and running Icarus Verilog/Verilator.

from amaranth.sim import Simulator, Period

dut = UpCounter(25)

async def bench(ctx):
    # Disabled counter should not overflow.
    ctx.set(dut.en, 0)
    for _ in range(30):
        await ctx.tick()
        assert not ctx.get(dut.ovf)

    # Once enabled, the counter should overflow in 25 cycles.
    ctx.set(dut.en, 1)
    for _ in range(24):
        await ctx.tick()
        assert not ctx.get(dut.ovf)
    await ctx.tick()
    assert ctx.get(dut.ovf)

    # The overflow should clear in one cycle.
    await ctx.tick()
    assert not ctx.get(dut.ovf)

sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(bench)

with sim.write_vcd("upcounter.vcd"):
    sim.run()

Verilog Generation Link to heading

Finally, we can dump Verilog for the counter. It seems to use Yosys under the hood, so I assume it uses an IR after elaborating the design.

from amaranth.back import verilog

top = UpCounter(25)
with open("upcounter.v", "w") as f:
    f.write(verilog.convert(top))
/* Generated by Yosys 0.54 (git sha1 db72ec3bde296a9512b2d1e6fabf81cfb07c2c1b, clang++ 17.0.0 -fPIC -O3) */

(* top =  1  *)
(* src = "upcounter.py:33" *)
(* generator = "Amaranth" *)
module top(clk, rst, ovf, en);
  reg \$auto$verilog_backend.cc:2359:dump_module$1  = 0;
  wire [16:0] \$1 ;
  reg [15:0] \$2 ;
  (* src = "venv/lib/python3.12/site-packages/amaranth/hdl/_ir.py:209" *)
  input clk;
  wire clk;
  (* src = "upcounter.py:28" *)
  reg [15:0] count = 16'h0000;
  (* src = "upcounter.py:23" *)
  input en;
  wire en;
  (* src = "upcounter.py:24" *)
  output ovf;
  wire ovf;
  (* src = "venv/lib/python3.12/site-packages/amaranth/hdl/_ir.py:209" *)
  input rst;
  wire rst;
  assign ovf = count == (* src = "upcounter.py:35" *) 5'h19;
  assign \$1  = count + (* src = "upcounter.py:41" *) 1'h1;
  (* src = "upcounter.py:28" *)
  always @(posedge clk)
    count <= \$2 ;
  always @* begin
    if (\$auto$verilog_backend.cc:2359:dump_module$1 ) begin end
    \$2  = count;
    if (en) begin
      (* full_case = 32'd1 *)
      if (ovf) begin
        \$2  = 16'h0000;
      end else begin
        \$2  = \$1 [15:0];
      end
    end
    if (rst) begin
      \$2  = 16'h0000;
    end
  end
endmodule