Clash Language Tutorial
Welcome to the Clash Language Tutorial, part of the official documentation of the Clash Compiler. Clash is an open-source functional hardware description language (HDL) that borrows syntax and semantics from the Haskell programming language. To learn more about the language, we suggest reading the introduction.
The table of contents below (and in the sidebar) allows easy access to different pages in the documentation. You can also use the search function in the top left corner.
Note
The Clash Compiler and the Clash Language Tutorial are open-source efforts developed by QBayLogic B.V. and other volunteers. The Clash Team always appreciates feedback and contributions to the project to help improve the development experience.
If you do not understand something, or think something is missing or incorrect in the documentation you can open an issue or pull request in the GitHub repository.
Introduction
Clash is a functional hardware description language that borrows both its syntax and semantics from the functional programming language Haskell. It provides a familiar structural design approach to both combinational and synchronous sequential circuits.
Features of the Clash language:
- Strongly typed, but with a very high degree of type inference, enabling both safe and fast prototyping using concise descriptions.
- Interactive REPL: load your designs in an interpreter and easily test all your components without needing to set up a test bench.
- Higher-order functions, in combination with type inference, result in designs that are fully parametric by default.
- Synchronous sequential circuit design based on streams of values, called
Signal
s, leads to natural descriptions of feedback loops. - Multiple clock domains, with type-safe clock domain crossing.
Although we say that Clash borrows the semantics of Haskell, that statement should be taken with a grain of salt. What we mean to say is that the Clash compiler views a circuit description as a structural description. This means, in an academically handwavy way, that every function denotes a component and every function application denotes an instantiation of said component. Now, this has consequences on how we view recursively defined functions: structurally, a recursively defined function would denote an infinitely deeply structured component, something that cannot be turned into an actual circuit.
On the other hand, Haskell's by-default non-strict evaluation works very well for the simulation of feedback loops, which are ubiquitous in digital circuits. That is, when we take a structural view on circuit descriptions, value recursion corresponds directly to a feedback loop:
counter = s
where
s = register 0 (s + 1)
The above definition, which uses value recursion, can be synthesized to a circuit by the Clash compiler.
Over time, you will get a better feeling for the consequences of taking a structural view on circuit descriptions. What is always important to remember is that every applied function results in an instantiated component, and also that the compiler will never infer/invent more logic than what is specified in the circuit description.
With that out of the way, let us continue with installing Clash and building our first circuit.
Installing Clash
For installation instructions, see clash-lang.org/install/
Working with this tutorial
This tutorial can be followed best whilst having the Clash interpreter running at the same time.
If you followed the instructions to setup a starter project with Stack, you can also run clashi
inside such a project.
Change to the directory of the project, and invoke
stack run -- clashi
If you instead set up the starter project with GHC and Cabal, change to the directory of the project and invoke
cabal run -- clashi
If you instead followed the instructions under Run Clash on its own, you can start the Clash compiler in interpretive mode by:
stack exec --resolver lts-23.15 --package clash-ghc -- clashi
For those familiar with Haskell/GHC, this is indeed just GHCi
, with three added commands (:vhdl
, :verilog
, and :systemverilog
).
You can load files into the interpreter using the :l <FILENAME>
command.
Now, depending on your choice in editor, the following edit-load-run
cycle probably work best for you:
-
Commandline (e.g. emacs, vim):
- You can run system commands using
:!
, for example:! touch <FILENAME>
- Set the /editor/ mode to your favourite editor using:
:set editor <EDITOR>
- You can load files using
:l
as noted above. - You can go into /editor/ mode using:
:e
- Leave the editor mode by quitting the editor (e.g.
:wq
invim
)
- You can run system commands using
-
GUI (e.g. SublimeText, Notepad++):
- Just create new files in your editor.
- Load the files using
:l
as noted above. - Once a file has been edited and saved, type
:r
to reload the files in the interpreter
You are of course free to deviate from these suggestions as you see fit :-). It is just recommended that you have the Clash interpreter open during this tutorial.
Your first circuit
The very first circuit that we will build is the "classic" multiply-and-accumulate (MAC) circuit. This circuit is as simple as it sounds, it multiplies its inputs and accumulates them. Before we describe any logic, we must first create the file we will be working on and input some preliminaries:
-
Create the file:
MAC.hs
-
Write on the first line the module header:
module MAC where
Module names must always start with a Capital letter. Also make sure that the file name corresponds to the module name.
-
Add the import statement for the Clash prelude library:
import Clash.Prelude
This imports all the necessary functions and datatypes for circuit description.
We can now finally start describing the logic of our circuit, starting with just the multiplication and addition:
ma acc (x, y) = acc + x * y
The circuit we just wrote is a combinational circuit: no registers are inserted (you describe explicitly where Clash will insert registers, as we will later see).
We usually refer to circuits as functions, similar to programming languages such as C, Python, or Haskell.
In this case, the function we just defined is called ma
.
Its first argument is acc
, its second is (x, y)
- a composite type called a tuple.
This component is "unpacked", and its first element is called x
, its second y
.
Everything to the right of the equals symbol is ma
's result.
If you followed the instructions of running the interpreter side-by-side, you can already test this function:
>>> ma 4 (8, 9)
76
>>> ma 2 (3, 4)
14
We can also examine the inferred type of ma
in the interpreter:
>>> :t ma
ma :: Num a => a -> (a, a) -> a
You should read this as follows:
-
ma ::
,ma
is of type… -
Num a
, there is some type calleda
that is aNum
. Examples of instances ofNum
areInt
,Signed 16
,Index 32
, orFloat
. -
a
,ma
's first argument is of typea
-
(a, a)
,ma
's second argument is of type(a, a)
-
a
,ma
's result is of typea
Note that ma
therefore works on multiple types!
The only condition we imposed is that a
should be a Num
ber type.
In Clash this means it should support the operations Prelude.+
, Prelude.-
, Prelude.*
, and some others.
Indeed, this is why Clash adds the constraint in the first place: the definition of ma
uses +
and *
.
Whenever a function works over multiple types, we call it polymorphic ("poly" meaning "many", "morphic" meaning "forms").
While powerful, it is not clear how Clash should synthesize this as numbers come in a great variety of (bit)sizes.
We will later see how to use this function in a monomorphic manner.
Talking about types also brings us to one of the most important parts of this tutorial: types and synchronous sequential logic.
Especially how we can always determine, through the types of a specification, if it describes combinational logic or (synchronous) sequential logic.
We do this by examining the definition of one of the sequential primitives, the register
function:
register ::
( HiddenClockResetEnable dom
, NFDataX a
) =>
a ->
Signal dom a ->
Signal dom a
register i s = ...
Where we see that the second argument and the result are not just of the polymorphic a
type, but of the type: Signal dom a
.
All (synchronous) sequential circuits work on values of type Signal dom a
.
Combinational circuits always work on values of, well, not of type Signal dom a
.
A Signal
is an (infinite) list of samples, where the samples correspond to the values of the Signal
at discrete, consecutive ticks of the clock.
All (sequential) components in the circuit are synchronized to this global clock.
For the rest of this tutorial, and probably at any moment where you will be working with Clash, you should probably not actively think about Signal
s as infinite lists of samples, but just as values that are manipulated by sequential circuits.
To make this even easier, it is actually not possible to manipulate the underlying representation directly: you can only modify Signal
values through a set of primitives such as the register
function above.
Now, let us get back to the functionality of the register
function: it is a simple flip-flop that only changes state at the tick of the global clock, and it has an initial value a
which is its output at time 0.
We can further examine the register
function by taking a look at the first 4 samples of the register
functions applied to a constant signal with the value 8:
>>> sampleN @System 4 (register 0 (pure (8 :: Signed 8)))
[0,0,8,8]
Where we see that the initial value of the signal is the specified 0 value, followed by 8's.
You might be surprised to see two zeros instead of just a single zero.
What happens is that in Clash you get to see the output of the circuit before the clock becomes active.
In other words, in Clash you get to describe the powerup values of registers too.
Whether this is a defined or unknown value depends on your hardware target, and can be configured by using a different synthesis Domain
.
The default synthesis domain, @System
, assumes that registers do have a powerup value - as is true for most FPGA platforms in most contexts.
Sequential circuit
The register
function is our primary sequential building block to capture state.
It is used internally by one of the Clash.Prelude
functions that we will use to describe our MAC circuit.
A principled way to describe a sequential circuit is to use one of the classic machine models. Within the Clash prelude library we offer a standard function to support the Mealy machine. To improve sharing, we will combine the transition function and output function into one. This gives rise to the following Mealy specification of the MAC circuit:
macT acc (x, y) = (acc', o)
where
acc' = ma acc (x, y)
o = acc
Note that the where
clause and explicit tuple are just for demonstrative purposes, without loss of sharing we could have also written:
macT acc inp = (ma acc inp, acc)
Going back to the original specification we note the following:
acc
is the current state of the circuit.(x, y)
is its input.acc'
is the updated, or next, state.o
is the output.
When we examine the type of macT
we see that is still completely combinational:
>>> :t macT
macT :: Num a => a -> (a, a) -> (a, a)
The Clash.Prelude
library contains a function that creates a sequential circuit from a combinational circuit that has the same Mealy machine type/shape of macT
:
mealy ::
(HiddenClockResetEnable dom, NFDataX s) =>
(s -> i -> (s, o)) ->
s ->
(Signal dom i -> Signal dom o)
mealy f initS = ...
The complete sequential MAC circuit can now be specified as:
mac inp = mealy macT 0 inp
Where the first argument of mealy
is our macT
function, and the second argument is the initial state, in this case 0.
We can see it is functioning correctly in our interpreter:
>>> simulateN @System 4 mac [(1,1),(2,2),(3,3),(4,4)]
[0,1,5,14]
Where we simulate our sequential circuit over a list of input samples and take the first 4 output samples. We have now completed our first sequential circuit and have made an initial confirmation that it is working as expected.
Generating VHDL
We are now almost at the point that we can create actual hardware, in the form of a VHDL netlist, from our sequential circuit specification.
The first thing we have to do is create a function called topEntity
and ensure that it has a monomorphic type.
In our case that means that we have to give it an explicit type annotation.
It might not always be needed, you can always check the type with the :t
command and see if the function is monomorphic:
topEntity ::
Clock System ->
Reset System ->
Enable System ->
Signal System (Signed 9, Signed 9) ->
Signal System (Signed 9)
topEntity = exposeClockResetEnable mac
Which makes our circuit work on 9-bit signed integers.
Including the above definition, our complete MAC.hs
should now have the following content:
module MAC where
import Clash.Prelude
ma acc (x, y) = acc + x * y
macT acc (x, y) = (acc', o)
where
acc' = ma acc (x, y)
o = acc
mac xy = mealy macT 0 xy
topEntity ::
Clock System ->
Reset System ->
Enable System ->
Signal System (Signed 9, Signed 9) ->
Signal System (Signed 9)
topEntity = exposeClockResetEnable mac
The topEntity
function is the starting point for the Clash compiler to transform your circuit description into a VHDL netlist.
It must meet the following restrictions in order for the Clash compiler to work:
- It must be completely monomorphic
- It must be completely first-order
- Although not strictly necessary, it is recommended to expose
Hidden
clock and reset arguments, as it makes user-controlled name assignment in the generated HDL easier to do.
Our topEntity
meets those restrictions, and so we can convert it successfully to VHDL by executing the :vhdl
command in the interpreter.
This will create a directory called vhdl
, which contains a directory called MAC
, which ultimately contains all the generated VHDL files.
You can now load these files into your favorite VHDL synthesis tool, marking topentity.vhdl
as the file containing the top level entity.
Circuit test bench
There are multiple reasons as to why you might want to create a so-called test bench for the generated HDL:
- You want to compare post-synthesis / post-place&route behavior to that of the behavior of the original generated HDL.
- You need representative stimuli for your dynamic power calculations.
- You want to verify that the HDL output of the Clash compiler has the same behavior as the Haskell/Clash specification.
For these purposes, you can have the Clash compiler generate a test bench. In order for the Clash compiler to do this you need to do one of the following:
- Create a function called
testBench
in the root module. - Annotate your test bench function with a
TestBench
annotation.
For example, you can test the earlier defined topEntity by:
import Clash.Explicit.Testbench
topEntity ::
Clock System ->
Reset System ->
Enable System ->
Signal System (Signed 9, Signed 9) ->
Signal System (Signed 9)
topEntity = exposeClockResetEnable mac
testBench :: Signal System Bool
testBench = done
where
testInput =
stimuliGenerator
clk
rst
$(listToVecTH [(1, 1) :: (Signed 9, Signed 9), (2, 2), (3, 3), (4, 4)])
expectOutput =
outputVerifier clk rst $(listToVecTH [0 :: Signed 9, 1, 5, 14, 14, 14, 14])
done = expectOutput (topEntity clk rst en testInput)
en = enableGen
clk = tbSystemClockGen (not <$> done)
rst = systemResetGen
This will create a stimulus generator that creates the same inputs as we used earlier for the simulation of the circuit, and creates an output verifier that compares against the results we got from our earlier simulation. We can even simulate the behavior of the testBench:
>>> sampleN 8 testBench
[False,False,False,False,False
cycle(<Clock: System>): 5, outputVerifier
expected value: 14, not equal to actual value: 30
,False
cycle(<Clock: System>): 6, outputVerifier
expected value: 14, not equal to actual value: 46
,False
cycle(<Clock: System>): 7, outputVerifier
expected value: 14, not equal to actual value: 62
,False]
We can see that for the first 4 samples, everything is working as expected, after which warnings are being reported.
The reason is that stimuliGenerator
will keep on producing the last sample, (4,4), while the outputVerifier'
will keep on expecting the last sample, 14.
In the VHDL test bench these errors will not show, as the global clock will be stopped after 4 ticks.
You should now again run :vhdl
in the interpreter; this time the compiler will take a bit longer to generate all the circuits.
Inside the ./vhdl/MAC
directory you will now also find a testbench
subdirectory containing all the vhdl
files for the test bench.
After compilation is finished you load all the files in your favorite VHDL simulation tool.
Once all files are loaded into the VHDL simulator, run the simulation on the testbench
entity.
On questasim / modelsim: doing a run -all
will finish once the output verifier will assert its output to true
.
The generated test bench, modulo the clock signal generator(s), is completely synthesizable.
This means that if you want to test your circuit on an FPGA, you will only have to replace the clock signal generator(s) by actual clock sources, such as an onboard PLL.
Generating Verilog and SystemVerilog
Aside from being able to generate VHDL, the Clash compiler can also generate Verilog and SystemVerilog.
You can repeat the previous two parts of the tutorial, but instead of executing the :vhdl
command, you execute the :verilog
or :sytemverilog
command in the interpreter.
This will create a directory called verilog
, respectively systemverilog
, which contains a directory called MAC
, which ultimately contains all the generated Verilog and SystemVerilog files.
Verilog files end in the file extension .v
, while SystemVerilog files end in the file extension .sv
.
This concludes the tutorial for "Your first circuit".