Functional hardware
Programming FPGA using Haskell
Here is very short introduction to functional programming language Haskel and functional reasoning. For this we will use CλaSH compiler, that is extension of Haskell language being able to convert large set of Haskell code in either VHLD, Verilog or SystemVerilog for now. Installation instructions are available in hackage doc.
If you need just quick peek in examples, there is source code available of all examples presented here and is compiled in VHLD, Verilog and SystemVerilog. Code is automatically generated, but can be used as quick check of what kind of circuit code describes and what to expect from CλaSH compiler.
Premise is that circuits described functional way are considered simpler to reason about, compose and reuse. We are able to describe topology trough functional composition on abstract way without sacrificing about reasoning, how may registries are in use and where logic gates are. This approach allows describing similar if not same circuits as with Verilog and VHDL using new and fresh approach allowing composing more ambitious synchronous digital circuits.
Descriptions is mathematical in way that is trying to use tools from mathematical theory about composition. It is common to see relations described with mathematical laws.
Function application
Before running coding we should explain how calling functions in Haskell works.
In most languages if function takes 2 arguments x
and y
it is called
like f(x,y)
where equivalent for this in Haskell is f\ x\ y
. This
come with advantage because Haskell supports partially applied functions
where only one out of two arguments is applied. Lets say that signature
of f
is Int -> Int -> Bool
. We can interpret this as - it takes two
integers x
an y
and it returns boolean. When only first argument x
is applied it returns function whit hremaining signature. When applying
on function Int -> Int -> Bool
first Int
remaining is Int -> Bool
.
That is function that takes Int
and it returns Bool
.
-- function of 2 Int, that telse if
-- 1st argument is greater then 2nd
greater :: Int -> Int -> Bool
greater x y = x > y
-- takes y and tels whatever it is greater than 2
isgt2 :: Int -> Bool
isgt2 y = greater 2 y
or less verbose as partially applied
isgt2 :: Int -> Bool
isgt2 = greater 2
One way to think about functions in Haskell is that each function takes
only one argument and it returns one value that can be function.
Signature of greater
can be seen as
greater :: Int -> ( Int -> Bool )
. So it takes an Int
and it returns
function Int -> Bool
. That is function that takes Int
and returns
Bool
.
In Haskell every function returns something. Pure functions, does not execute any action, but just transform input data and return relevant result. Pure functions, has important property that they always returns same output for same input and so they don’t have access to some external time changing data or internal state. In Haskell there is strong force to separate effects from pure functions.
Function composition
Functional programming is about composition. Here it is example of signatures of 2 functions.
funcA2B :: a -> b
funcB2C :: b -> c
funcA2B
is function that takes type a
and returns b
, one example
is that it takes Int
and returns Bool
.funcB2C
takes b
and it
returns c
. For example it takes Bool
and returns Bool
. Having this
we are able to create function
funcA2C :: a -> c
by composing together funcA2B
and funcB2C
to get funcA2C
.
For composing two functions we need function
composeFunc :: (b->c) -> (a->b) -> a -> c
composeFunc f g x = f (g x)
First we apply x
on g
and then we apply result on f
.
Moving back to composeFunc
it takes 3 arguments. First two are
functions, f
with signature b->c
and g
with signature a->b
, 3rd
param x
is of type a
. Function returns type c
and is same type
that f
returns. Looking differently it takes two functions that is
b->c
and a->b
and it returns function a -> c
.
For example
minus1 x = x - 1
largeNum x = x > 3
superLargeNum = composeFunc largeNum minus1
superLargeNum
is function composed by largeNum
and minus1
.Type of
each function is well defined, but we can either define it manually or
compiler can automatically deduce it.
-- simplified types
minus1 :: a -> a
largeNum :: a -> Bool
superLargeNum :: a -> Bool
-- actual types
minus1 :: Num a => a -> a
largeNum :: (Num a, Ord a) => a -> Bool
superLargeNum :: (Num a, Ord a) => a -> Bool
We can read this, that function superLargeNum
transforms type a
into
Bool
. Type a
has to be defined for class Num
and Ord
. Num
is
class that has defined function (-)
as minus operator. (among others)
and class Ord
has defined (>)
.
composeFunc
is commonly used and in Haskell it is defined as operator
(.)
.
(.) = composeFunc
Using this, one can alternatively define superLargeNum
as
superLargeNum = largeNum . minus1
-- or
superLargeNum = (> 3) . (- 1)
-- or
superLargeNum x = (x - 1) > 3
Operator .
comes from function composition as defined in mathematics f
∘ g . Here we have showed implemention of .
as in composeFunc
, but
sometimes instead of implementation we get only laws. For function (.)
holds.
If id
is identity function that returns argument
id :: a -> a
id x = x
then function composition laws for ∘ are
id . f == f . id == f
f . g . h == (f . g) . h == f . (g . h)
Instead of having function implementation available we sometimes have available laws that function obeys. It helps reasoning about how function behaves without knowing what it does. Laws also goes hand to hand with implementation.
Functor
Lets take for example type in CλaSH called Signal a
. This represent
type a
changing with clock. Signal
is concrete example of functor
and a
is type embedded in Signal
.
Other than function composition of Signal a
there is different
composition available for functor.
funcA2B :: a -> b
signalA :: Signal a
If we have function of type a -> b
and Signal a
, than functor
enables us to get Signal b
fmap :: (a -> b) -> Signal a -> Signal b
Functor obeys two laws
fmap id == id
fmap (p . q) == (fmap p) . (fmap q)
Or equivalently when applying signal.
fmap id signal == signal
fmap (p . q) signal == fmap p (fmap q signal)
Knowing this we can aswer next question. Does fmap
over Signal
uses
any registries as for example D flip-flop?
Answer is, if that would be case, neither of laws would hold. Form first
law it would imply that on left we have signal delayed by 1 clock, and
on right non delayed signal and they are not same. Second law would
imply delay by 1 clock on left where right side made 2 delays, each by
one fmap
calls.
To generate core we define function topEntity
. Generic type a
should
be concrete and in this case we will use Signed 7
what is 7 bit signed
integer.
topEntity :: Signal (Signed 7) -> Signal Bool
topEntity signal = fmap superLargeNum signal
Executing command fmap can be used as operator. That is <$>
.
Alternatively we can implement topEntity also as
topEntity signal = superLargeNum <$> signal
-- or point free version
topEntity = fmap superLargeNum
-- or using fmap as operator
topEntity = superLargeNum `fmap` signal
It is hard to tell witch version is most common among haskellers, but we can be sure, that due to laws all of them behaves same.
Compiling this exact into Verilog generates code as expected.
Mealy machinery
For generating output based on previous stored value we can use
mealy
function. Lets implement integrator using mealy machine. With integrator
we have in mind function that takes Signal a
and produces Signal a
and is representing sum of all previous values including current.
integrator signal = mealy mf 0 signal
where
mf state input = (nextstate,output)
where
nextstate = state + input
output = nextstate
mealy
is function that takes 3 arguments. First parameter is a function on how
to generate next state from state and signal value. Signature of this
function is s -> i -> (s,o)
. In our code that is function called mf
.
mf
is defined in where
block for local visibility so can not be used
out of
integrator
. What this mf
function does it takes 2 arguments state
and input
and it returns tuple (nextstate,output)
. Next clock cycle
nextstate
will be passed again in mf
as argument state
. 2nd
argument of integrator 0
is initial state value. Generated core will
provide reset signal that sets state to initial value.
If we now need to create FPGA core, we need to provide topEntity
. Here
it is full source instantiated with Fixed point arithmetics. We
arbitrarily choose SFixed 2 8
, This is 10 bit signed fixed point
number. It is representing rational numbers from -2 <= x < 2.
module Integrator where
import CLaSH.Prelude
integrator signal = mealy mf 0 signal
where
mf state input = (nextstate,output)
where
nextstate = state + input
output = nextstate
topEntity :: Signal (SFixed 2 8) -> Signal (SFixed 2 8)
topEntity = integrator
integrator
can be used in other modules, to build larger and more
ambitious circuits. For example to make integrator of integrated signal
so that output of first integrator is feed in second one.
intInt = integrator . integrator
Vector
Vec
is sized vector with compile type defined size holding elements of same
type. It is similar to tuple, except that all elements are of same type.
Vec 5 (Signed 7)
is type that holds 5 elements of type Signed 7
what
is 7 bit signed number. Vec
is also an functor.
For example
vs :: Vec 6 (Signed 9)
vs = 1 :> 2 :> 3 :> 4 :> 5 :> 6 :> Nil
vb :: Vec 6 Bool
vb = fmap (> 3) vs -- that is <False,False,False,True,True,True>
Operator :>
is right-associativity operator that prepends vector on
right with value on left. Nil
is empty vector and is used, because
:>
expects vector on right side.
Size of Vec
is defined at compile time and can be generic. Lets take
look on one of many functions from Vec
libraray that is
replicate
and has prototype
replicate :: SNat n -> a -> Vec n a
-- "replicate n a" returns a vector that has `n` copies of `a`.
Vec 5 a
is not same as Vec 6 a
and it means that replicate
returns
different type based on argument n
. For this to work, n
is of type
SNat x
and is different type for each x. Value that has type
Signed 5
as 5 bit signed value can be -2 or 7, where value d3
that
has type SNat 3
represents number 3 as unique type.
Moving average
Moving average is simple low pass FIR filter expressed as sum of last
n
values. We will deliberately skip division in our example, so we
will in reality talk about moving sum.
Here is straight forward and naive option to describe moving average
using
mealy
.
movingAvarage n signal = mealy mf (replicate n 0) signal
where
mf state input = (nexts,output)
where
nexts = input :> init state
output = fold (+) nexts
First argument n
is number of accumulated value is also size of Vec
used as internal state storage. Type of internal state is deduced as
Vec n a
and is deduced from type of initial state value
replicate n 0
that returns Vec
that returns n
copies of 0.
init :: Vec (n + 1) a -> Vec n a
is function that takes Vec
with size n+1
and returns all except last
element that is Vec
of size n
.
nexts
expressed as next state is vector right shifted by previous
value and perpended :>
with current clock input
value. Operation
works as fifo shift register.
fold
is function best expressed with same picture as in
manuals
From signature we can conclude that
fold :: (a -> a -> a) -> Vec (n + 1) a -> a
takes 2 arguments. First
is function of type (a -> a -> a)
an second argument is of type
Vec (n + 1) a
. Function is than applied over vector in tree like
structure, enabling minimum latency.
Function f from out case is function (+)
(in haskell operator in
brakets is used as function). and has prototype a -> a -> a
and is
applied over state
.
There is better
Lets try some optimization of moving average from previous section. Observing totals we can conclude we don’t need that many adders. Total output is changed at each clock only by value that enters fifo from one side and value that pops out of fifo on other. At each clock we need to subtract value that pops out and add value that enters from total. This comes at expanse of 2 extra sized registers (previous solution can be reduced by 1 sized register) but requires only 2 sized adders.
movingAvarage2 n signal = mealy mf ((replicate n 0),0) signal
where
mf (vec,total) input = ((nextVec,nextTot),output)
where
output = total + input - (last vec)
nextVec = input :> init vec
nextTot = output
mf
has at this point signature (Vec n a,a) -> a -> ((Vec n a,a),a)
.
Internal state type is tuple where first value is vec
used as fifo and
total accumulated in fifp.
vecand
totalare unpacked from tuple in input argument, so we are able to access this value, without unpacking them in body. Equivalent, but more verbose
mf`
implemetaion is sketched as
mf state input = ...
where
vec = fst state -- first element of tuple
total = snd state -- second element of tuple
output = ...
Circuit composition
Great power of composition comes from separating logic into reusable
parts and then composing them together in larger blocks. Function (.)
and fold
are good examples we have used so far. Now lets try something
similar, by designing feedback loop.
+---+ +---+
input -->-| f |-->-| g |-->-+-->-- output
+---+ +---+ |
| ∨
∧ +---+ |
+----<-| h |-<--+
+---+
First lets figure out prototypes of each function f
, g
and h
.
f :: Signal a -> Signal b -> Signal c
g :: Signal c -> Signal d
h :: Signal d -> Signal b
and feedback loop can be written directly from circuit diagram as
feedbackLoop :: (Signal a -> Signal b -> Signal c)
-> (Signal c -> Signal d)
-> (Signal d -> Signal b)
-> Signal a
-> Signal d
feedbackLoop f g h input = out where
out = g fout
fout = f input (h out)
If user has available functions that works over primitive values like
Signed
instead Signal (Signed 16)
for example, he can use fold
.
For example if
g :: Signed 16 -> Signed 16
Then gs
working over signal would be constructed from g
gs :: Signal (Signed 16) -> Signal (Signed 16)
gs = fmap g
-- or more verbose
gs sig = fmap g sig
For function over 2 input arguments we can use library function liftA2
liftA2 :: (a -> b -> c) -> functor a -> functor b -> functor c
Where functor
is in our example Signal
For example to get function
minus (-)
to operate over Signal
we need to apply liftA2
.
signalMinus :: Signal a -> Signal a -> Signal a
signalMinus = liftA2 (-)
To construct negative feedback looking like
+---+
input -->-( - )-->-| g |-->-+-->-- output
| +---+ |
∧ ∨
+--------<------+
Using feedbackLoop
,
negativeFeedback :: Num a =>
(Signal a -> Signal a) ->
Signal a ->
Signal a
negativeFeedback g = feedbackLoop (liftA2 (-)) g id
We have replaced h
by id
and f
argument is liftA2 (-)
There is subtle issue with this and in some cases this circuit can not
be synthesized. That is because we have feedback loop, so we have made
sure that there is delay in g function. If g
function is for example
id
or fmap id
so it works like wire this circuit is not stable any
more. To overcome this, we have to introduce delay in feedback. Function
h
from previous diagram, becomes register 0
instead of id
.
function
negativeFeedback :: Num a => (Signal a -> Signal a) -> Signal a -> Signal a
negativeFeedback g = feedbackLoop (liftA2 (-)) g (register 0)
register
:: a -> Signal a -> Signal
a
is a function, that takes inital value, that outputs this value before
remaining of stream. register
is available in CλaSH libraray, but can
be implemented using mealy
as
register initialValue signal = mealy mf initialValue
where
mf prevS input = (input,prevS)
What is even more surprising is that it can be done even vice versa.
That is mealey
can be expressed in terms of register
. For details
check examples on
github
Conclusion
There are lot of features that were no covered. But are invaluable during development
- repl, (read eval print loop) that allows interactive development, type deduction
- ability to simulate in repl or compile in CPU native executable
- testebenches that generates VHDL / Verilog / Systemverilog and can be used directly in IDE
- full featured library, (ram and rom access and initalisation, multiple clock domains, type safe delays … )
From my and experience of many it takes lot of time and practice to grasp new programming language, what Haskell for those not familiar with functional programming certainly is. Reader with interest will be able to find lot of free resources available online for all levels of experience with open and active community.