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