Digital Design & Verification (Verilog, SystemVerilog, VLSI) Module 14 – Reading VHDL and other hardware languages
Chisel, Amaranth, Bluespec and HLS
See how Chisel, Amaranth and Bluespec generate Verilog, read and lint what they write, and learn what high-level synthesis does with C++ code.
What you will learn
- Describe how embedded hardware languages generate Verilog
- Read and lint the Verilog that a hardware generator writes
- Explain what high-level synthesis does and where it fits
- Decide when an alternative hardware language helps a project
Before you start
What you’ll learn · Before you start
What you will learn
- Describe how embedded hardware languages generate Verilog
- Read and lint the Verilog that a hardware generator writes
- Explain what high-level synthesis does and where it fits
- Decide when an alternative hardware language helps a project
Before you start
On this page
The counter of this module took 11 lines of Python in Amaranth and became 55 lines of Verilog, and the reading
lesson’s SystemVerilog testbench printed done: q=2 wraps=1 errors=0 for it, as for the hand-written counter. The
alternative hardware languages end where every flow starts: in Verilog.
Here you see how Chisel, Amaranth and Bluespec turn a program into Verilog, how high-level synthesis turns C++ into clock cycles, how to read and lint what they write, and when one of them helps a project.
A program in another language builds the hardware, then writes Verilog
amaranth_counter.py describes the counter as a Python class. Running it prints the Verilog that Amaranth 0.5.10 makes from the class, through Yosys, and Icarus Verilog runs that Verilog under the testbench:
hdl/amaranth_counter.py
"""The 4-bit counter of this module written in Amaranth 0.5, a hardware language embedded in Python.
Running the file prints the Verilog that Amaranth generates for it, through Yosys. The counter has the ports of
counter.sv: q goes up by one at every rising edge of clk at which en is 1 and wraps from 15 to 0; rst is synchronous
and wins over en; wrap is 1 for the one cycle after q went from 15 to 0.
"""
from amaranth import Module
from amaranth.back import verilog
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out
class Counter(wiring.Component):
en: In(1)
q: Out(4)
wrap: Out(1)
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.q.eq(self.q + 1)
m.d.sync += self.wrap.eq(self.en & (self.q == 15))
return m
if __name__ == "__main__":
print(verilog.convert(Counter(), name="counter")) Commands · hdl/amaranth_session.py
from session import run
# The Amaranth counter becomes Verilog through Yosys, then the reading lesson's SystemVerilog testbench runs it on
# Icarus Verilog. grep counts the lines of the generated file and shows its module line and its registers.
run("""
python amaranth_counter.py > counter_am.v
grep -c "" counter_am.v
grep -E "^module|always @|reg \\[3:0\\] q" counter_am.v
iverilog -g2012 -Wall -o am.vvp tb_counter.sv counter_am.v
vvp -n am.vvp
""", packages=("amaranth",), simulators=("yosys",)) Output
# Python 3.14.8 · amaranth 0.5.10 · Icarus Verilog 13.0 (stable) · Yosys 0.69 $ python amaranth_counter.py > counter_am.v $ grep -c "" counter_am.v 55 $ grep -E "^module|always @|reg \[3:0\] q" counter_am.v module counter(clk, rst, q, wrap, en); reg [3:0] q = 4'h0; always @(posedge clk) always @(posedge clk) always @* begin always @* begin $ iverilog -g2012 -Wall -o am.vvp tb_counter.sv counter_am.v $ vvp -n am.vvp done: q=2 wraps=1 errors=0 tb_counter.sv:29: $finish called at 240 (1s)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 amaranth_session.py
An embedded HDL: A hardware description language built as a library of a programming language, its host, such as Chisel in Scala or Amaranth in Python. A program builds the design, and a compiler writes it out as Verilog. is a library of a programming language, its host. The program runs once, builds a description of registers and logic in memory, and a compiler writes it out as Verilog. The host’s loops, functions and classes therefore decide what hardware exists, which is what these languages offer over SystemVerilog’s generate blocks. What they describe is still register-transfer level: the code says what each register takes at each clock edge. Step through the counter’s path:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A Python class. Counter is a Python class with three ports declared as members, en, q and wrap. Its elaborate method says what happens at each clock edge, with m.d.sync for the clocked logic.
- The program builds the hardware. Running the file calls elaborate. The Python code runs once, like a generator of hardware, and leaves a description of registers and logic in memory; a Python loop or if here would decide what hardware exists.
- Into Yosys's form. Amaranth hands the design to Yosys as RTLIL, the form every Yosys pass reads and writes.
- Verilog out. Yosys's write_verilog turns the RTLIL into Verilog. The clock and the reset, clk and rst, appear as ports that the Python never declared, because Amaranth's sync domain brings them.
- A Verilog file like any other. counter_am.v has 55 lines, with names such as $1 and $4 for the values the Python computed and src attributes that point back to lines of amaranth_counter.py.
- The same testbench. The reading lesson's SystemVerilog testbench runs it on Icarus Verilog and prints done, q=2, wraps=1, errors=0, as for the hand-written counter.
The languages differ in their host and in the compiler that writes the HDL:
How each language reaches the HDL that every flow reads
Text description of the diagram
Each box on the left names a language and the tool that turns it into Verilog, SystemVerilog or VHDL, on the right.
- Amaranth is written in Python; the program builds the design and hands it to Yosys as RTLIL, and Yosys writes Verilog.
- Chisel is written in Scala; the program produces FIRRTL, and firtool, from the CIRCT project, writes Verilog.
- Bluespec SystemVerilog has its own compiler, BSC, which writes Verilog.
- SpinalHDL is written in Scala too, and its own generator writes VHDL or Verilog.
- Veryl is a language of its own, close to SystemVerilog, and its transpiler writes SystemVerilog.
- High-level synthesis starts from C or C++: an HLS tool schedules the operations into clock cycles, binds them to units and writes RTL in Verilog or VHDL.
Whatever the language, the result is ordinary HDL, so lint, simulation, formal checks and synthesis treat it as they treat hand-written RTL.
- Amaranth is a Python library for register-transfer modelling, under BSD-2-Clause. It hands the design to Yosys, which wrote the 55 lines above, and it has its own simulator, written in Python.
- Chisel is a hardware design language whose designs are Scala programs, under Apache-2.0. A Chisel program produces FIRRTL, and firtool, from the CIRCT project, writes the Verilog; Chisel manages the firtool version it needs.
- Bluespec describes modules by their methods and by rules, guarded atomic actions that the compiler schedules. Its compiler, BSC, takes two syntaxes, BSV and Bluespec Haskell, and emits standard Verilog.
- SpinalHDL is embedded in Scala too, and its generator writes VHDL or Verilog.
- Veryl is a language of its own, close to SystemVerilog, whose transpiler writes readable SystemVerilog.
Checkpoint
Check: How an embedded HDL becomes Verilog
High-level synthesis schedules C++ into clock cycles
high-level synthesis (HLS): Turning a C or C++ function into RTL with a tool that chooses the clock cycle of every operation and the unit that runs it, then writes a state machine and a datapath in Verilog or VHDL. starts from a C or C++ function instead of registers. The
tool decides what RTL would make you decide: in which clock cycle each operation runs, its
scheduling (HLS): The step of high-level synthesis that gives each operation of the C or C++ code a clock cycle to start in, within the units available and after the operations whose results it reads., and which unit runs it. For y = a*b + c*d with one pipelined multiplier,
the products cannot start together:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Three operations, two units. The C line y = a*b + c*d has two multiplications and one addition. The tool has one multiplier and one adder to put them on, and must give each operation a start cycle.
- Cycle 0. a*b has its inputs ready, so it starts on the multiplier at cycle 0. c*d is ready too, but the only multiplier is taken this cycle.
- Cycle 1. The multiplier is pipelined, so it takes a new pair one cycle later, while a*b is still inside it. c*d starts at cycle 1.
- Cycle 2. a*b is ready at the end of its 2 cycles, but y needs both products, and c*d is not ready yet. The adder waits.
- Cycle 3. c*d is ready, so y starts on the adder at cycle 3.
- The latency. y is ready after 4 cycles. With a second multiplier both products start at cycle 0 and y after 3 cycles, one cycle faster for one more multiplier. That trade is the decision a tool makes for you.
The program below makes the same decisions, the way Bambu, an open-source HLS tool, schedules by default: each cycle
it starts every operation whose inputs are ready, while a unit of its kind is idle. Change MULTIPLIERS and run it
again:
"""What high-level synthesis does with y = a*b + c*d: it gives every operation a clock cycle and a unit to run on.
A multiplier takes 2 cycles and accepts a new pair every cycle (it is pipelined); an adder takes 1. The program lists,
cycle by cycle, which operation each unit starts, with one multiplier and with two, and prints the latency: the
cycles from the first operation starting to y being ready. Change MULTIPLIERS to try other numbers.
"""
MULTIPLIERS = (1, 2)
LATENCY = {"mul": 2, "add": 1}
# The operations, the kind of unit each needs and the operations whose results it reads.
OPS = {
"a*b": ("mul", []),
"c*d": ("mul", []),
"y": ("add", ["a*b", "c*d"]),
}
def schedule(units):
"""List scheduling: each cycle, start every operation whose inputs are ready, while units of its kind are free."""
done_at = {}
start = {}
cycle = 0
while len(start) < len(OPS):
free = dict(units)
for name, (kind, inputs) in OPS.items():
ready = all(i in done_at and done_at[i] <= cycle for i in inputs)
if name not in start and ready and free[kind] > 0:
start[name] = cycle
done_at[name] = cycle + LATENCY[kind]
free[kind] -= 1
cycle += 1
return start, max(done_at.values())
for count in MULTIPLIERS:
start, latency = schedule({"mul": count, "add": 1})
print(f"{count} multiplier(s), 1 adder:")
for name, cycle in sorted(start.items(), key=lambda item: item[1]):
print(f" cycle {cycle}: start {name} on the {OPS[name][0]} unit")
print(f" y is ready after {latency} cycles") Output
1 multiplier(s), 1 adder: cycle 0: start a*b on the mul unit cycle 1: start c*d on the mul unit cycle 3: start y on the add unit y is ready after 4 cycles 2 multiplier(s), 1 adder: cycle 0: start a*b on the mul unit cycle 0: start c*d on the mul unit cycle 2: start y on the add unit y is ready after 3 cycles
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 schedule_two.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
One multiplier gave a latency of 4 cycles and two gave 3: the tool trades units for cycles, a trade that RTL makes you write by hand. HLS fits algorithms on data, such as filters, codecs and transforms, which are easier to state in C++ than as a pipeline. It fits control logic badly, because a protocol fixes the cycle of each action, and the tool picks cycles itself. Its RTL is verified like any other, and AMD’s guide to Vitis HLS has a section on C/RTL co-simulation, which runs the C++ test against the generated design.
Checkpoint
Check: What high-level synthesis decides
Reading and linting generated Verilog, measured on two generators Pro: opens with a Pro, Premium or Ultimate pass
Generated Verilog enters the same lint, simulation and synthesis as hand-written RTL, and it looks different there. This part lints Amaranth’s output, reads what BSC writes, and lists what to check before trusting either.
In this part:
- Amaranth’s Verilog under lint: four warnings, none of them bugs
- What BSC writes: methods become ports, and registers start at 4’hA
1 runnable example (Commands, Counter.bsv, counter_bsv.sv) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
HLS by numbers: latency and initiation interval of a filter Pro: opens with a Pro, Premium or Ultimate pass
A tool’s schedule decides two different numbers, how long one result takes and how often a new one can start. This part schedules a 4-tap filter with five sets of units and shows which unit limits the rate in each. It ends with the directives that reach the same points in a real tool: pipelining a loop, unrolling it into copies and partitioning an array into several memories.
In this part:
- Five sets of units for one filter
- What the directives of a real tool change
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Choosing a language for a block, with your answers Pro: opens with a Pro, Premium or Ultimate pass
None of these languages is better in general; each fits some blocks and some teams. This part turns the lesson’s measurements into a decision table that takes your answers, then works four projects through it.
In this part:
- The decision table
- Four projects worked through
about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: write the list scheduler
The exercise is the scheduler of schedule_two.py made general: any operations, any units and any latencies. Its sample tests include a chain that a second multiplier cannot speed up.
Exercise · Medium · Python
Schedule operations onto units, as an HLS tool does
Write schedule(ops, units, latency), the list scheduler of schedule_two.py made general.
opsmaps each operation's name to a pair: the kind of unit it needs, such as"mul", and the list of operations whose results it reads. The order ofopsis the priority order.unitsmaps each kind to how many units of it there are. Every unit is pipelined: it can start one new operation in every cycle.latencymaps each kind to the cycles its operations take.
Go cycle by cycle from 0. In each cycle, walk through the operations in order and start every one that has not started, whose inputs have all finished by this cycle, and for which a unit of its kind is still free in this cycle. An operation started at cycle c finishes at c + latency[kind].
Return a pair: a dictionary of each operation's start cycle, and the cycle at which the last one finishes. If an operation needs a kind that has no units, raise ValueError.
Starter code · schedule.py
def schedule(ops, units, latency):
"""List-schedules ops onto pipelined units; returns ({name: start cycle}, last finish cycle)."""
return {}, 0 The sample tests · test_schedule.py
from schedule import schedule
LATENCY = {"mul": 2, "add": 1}
TWO_PRODUCTS = {"a*b": ("mul", []), "c*d": ("mul", []), "y": ("add", ["a*b", "c*d"])}
def test_one_multiplier():
"""schedules y = a*b + c*d on one multiplier: y starts at cycle 3 and is ready after 4"""
assert schedule(TWO_PRODUCTS, {"mul": 1, "add": 1}, LATENCY) == ({"a*b": 0, "c*d": 1, "y": 3}, 4)
def test_two_multipliers():
"""with two multipliers both products start at cycle 0 and y is ready after 3"""
assert schedule(TWO_PRODUCTS, {"mul": 2, "add": 1}, LATENCY) == ({"a*b": 0, "c*d": 0, "y": 2}, 3)
def test_chain_ignores_extra_units():
"""a chain of two multiplies needs 4 cycles however many multipliers there are"""
chain = {"p": ("mul", []), "q": ("mul", ["p"])}
assert schedule(chain, {"mul": 2}, LATENCY) == ({"p": 0, "q": 2}, 4)
def test_three_products_on_two():
"""three independent products on two multipliers start at cycles 0, 0 and 1"""
ops = {"m0": ("mul", []), "m1": ("mul", []), "m2": ("mul", [])}
assert schedule(ops, {"mul": 2}, LATENCY) == ({"m0": 0, "m1": 0, "m2": 1}, 3)
def test_missing_unit():
"""refuses an operation whose kind has no units"""
try:
schedule(TWO_PRODUCTS, {"mul": 1}, LATENCY)
except ValueError:
return
raise AssertionError("no ValueError when there is no adder") A hint
Keep two dictionaries, the start cycle and the finish cycle of each operation started so far. In each cycle, copy units into a dictionary of free units and take one away for every operation you start.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (2 to 5 years): what is an embedded HDL, and what does it generate?
It is a hardware description language built as a library of a programming language, such as Chisel in Scala or Amaranth in Python. A program in that language runs once, builds the design in memory and hands it to a compiler that writes Verilog, so the host’s loops, functions and classes decide what hardware exists. The result is ordinary RTL: this lesson’s 11-line Amaranth counter became 55 lines of Verilog that passed the same testbench as the hand-written counter.
-
Mid to senior
A block written in an embedded HDL fails one test in the SoC simulation. The waveform shows a signal called $27 going wrong one cycle after reset, and the designer who wrote the block has left. How do you find the cause, and what would you put in place so the next failure is quicker to trace?
-
Mid to senior
A 16-tap FIR filter must process 50 million samples a second on a 200 MHz clock. The algorithm exists as a C model. Do you write it in RTL or use high-level synthesis? Size the multipliers and adders either way, and say what you would verify.
-
Senior to staff
Your team of ten writes SystemVerilog and UVM. A new hire proposes moving new blocks to Chisel, and another to Amaranth. How do you decide, what would you try first, and what must stay true of the team's flow whatever language wins?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is an embedded HDL?
A hardware description language built as a library of a programming language, its host. The program runs once, builds the design in memory, and a compiler writes it out as Verilog.
-
What does each of Amaranth, Chisel and Bluespec hand its design to?
Amaranth, a Python library, hands it to Yosys; Chisel, written as Scala programs, produces FIRRTL that firtool from the CIRCT project writes as Verilog; Bluespec's compiler, BSC, takes BSV or Bluespec Haskell and emits Verilog.
-
Do embedded HDLs describe hardware above register-transfer level?
No: the code still says what each register takes at each clock edge. What they add over generate blocks is that the host's loops, functions and classes decide what hardware exists.
-
What is a Bluespec rule?
A guarded atomic action, which the compiler schedules; Bluespec describes modules by their methods and by rules.
-
What does high-level synthesis decide that RTL makes you decide?
In which clock cycle each operation of a C or C++ function runs, its schedule, and which unit runs it.
-
How did a second multiplier change the schedule of y = a*b + c*d?
The latency fell from 4 cycles with one pipelined multiplier to 3 with two: the tool trades units for cycles.
-
Which designs suit high-level synthesis, and which do not?
Algorithms on data, such as filters, codecs and transforms, which are easier to state in C++. Control logic suits it badly, because a protocol fixes the cycle of each action while the tool picks cycles itself.
-
How is the RTL that HLS generates checked?
Like any other RTL. AMD's Vitis HLS guide describes C/RTL co-simulation, in which the C++ test drives the RTL the tool generated.
Key takeaways
- An embedded HDL is a program in Python or Scala that builds the hardware and hands it to a compiler that writes Verilog: Amaranth through Yosys, Chisel through firtool; Bluespec has its own compiler, BSC.
- What they describe is still register-transfer level, and Amaranth’s Verilog passed the same testbench as the hand-written counter.
- High-level synthesis schedules a C or C++ function into clock cycles and units: one more multiplier cut
y = a*b + c*dfrom 4 cycles to 3. It suits algorithms on data, not protocols.
Terms in this lesson
- embedded HDL
- A hardware description language built as a library of a programming language, its host, such as Chisel in Scala or Amaranth in Python. A program builds the design, and a compiler writes it out as Verilog.
- high-level synthesis (HLS)
- Turning a C or C++ function into RTL with a tool that chooses the clock cycle of every operation and the unit that runs it, then writes a state machine and a datapath in Verilog or VHDL.
- scheduling (HLS)
- The step of high-level synthesis that gives each operation of the C or C++ code a clock cycle to start in, within the units available and after the operations whose results it reads.
References
- Amaranth documentation, introduction (Amaranth project)
- Amaranth source repository (BSD-2-Clause) (Amaranth project)
- Chisel, software-defined hardware (CHIPS Alliance (Chisel, a Series of LF Projects))
- Chisel source repository (Apache-2.0) (CHIPS Alliance (Chisel, a Series of LF Projects))
- Chisel installation: Java, Scala CLI and firtool (CHIPS Alliance (Chisel, a Series of LF Projects))
- BSC: compiler, simulator and tools for the Bluespec hardware description language (B-Lang-org (Bluespec))
- SpinalHDL documentation (SpinalHDL project)
- Veryl, a modern hardware description language (Veryl project)
- Bambu: a free framework for high-level synthesis (PandA project, Politecnico di Milano)
- Vitis High-Level Synthesis User Guide (UG1399) (AMD)
- Verilator warnings: PROCASSINIT, DECLFILENAME, UNUSEDSIGNAL (Verilator)
Report a problem with this lesson
Kept only in this browser. Your Learn progress