Designing FPGA Circuits in Lava 1 Satnam Singh Department of Computing Science University of Glasgow Glasgow, UK Mary Sheeran Department of Computer Science Chalmers University of Technology Gothenberg, Sweden [email protected] [email protected] Introduction This paper explores the potential of the lazy functional programming language Haskell for the specification, synthesis and verification of digital circuits. Circuits are described using combinators that combine behaviour (allowing circuit simulation, symbolic evaluation and partial evaluation) and layout (allowing the generation of output suitable for entry into VLSI CAD tools). We call the software that provides this functionality Lava. Note that Lava is not a new language: it is simply a collection of Haskell modules. Circuits described in Lava can be transformed into a format that can be using with the commercial propositional logic theorem prover NP-Tools for hardware verification. In recent years many more circuit design projects are undertaken using hardware description languages like VHDL (mandated for US military projects and based on Ada) or in Verilog (a popular rival to VHDL with C inspired syntax). Describing hardware symbolically rather than pictorially affords many advantages, including the opportunity to use programming language constructs like repetition, hierarchy and data abstraction to describe circuits in a high level manner and to capture common patterns of computation. For example, it is difficult to express a generic n-bit adder composed of n full-adder cells in a picture, but it is relatively easy to describe this using programming constructs supported in VHDL: entity adder is generic (n : integer) ; port (a, b: in bit_vector (1 to n) ; sum : out bit_vector (1 to n)) ; end entity adder ; architecture structural of adder is signal c : bit_vector (0 to n) ; begin c(0) <= ‘0’ ; FAs : for i in 1 to n generate FAi : fullAdder port map (a(i), b(i), sum(i), c(i-1), c(i)) ; end generate FAs ; end architecture structural ; Figure 1: Structural VHDL description of an n-bit adder The interface of the adder component is shown on the left. This description is parameterised on the size of the adder (the number of bits to add n). The adder circuit has two inputs a and b which are bit vectors of length n and the adder circuit has one output sum which is the n-bit sum of a and b. The right hand side shows one implementation which assumes the availability of a full-adder circuit. This implementation creates n instances of a full-adder circuit and wires them together by signal-name association (the normal method of circuit composition in VHDL). The fourth and fifth arguments of the fulladder cell are the carry in and carry out values, which are represented with a local signal declaration. The VHDL language is based on the Ada programming language. But why not just use a conventional programming language like Ada for hardware description? The trend has been to develop hardware specific languages to capture the concurrent nature of hardware; to allow the specification of multiple implementations for one interface; and to focus on the structural representation of circuits. We argue that it should be possible to use conventional programming languages for the description of hardware since concurrency, multiple implementations and sufficiently powerful data-types are features of many languages. In particular, the structural description of circuits corresponds to just a graph (the components are the nodes and the wires are the vertices), and structural VHDL can be viewed as a graph description language. The standard hardware design interchange format EDIF is just a LISP-style graph description. The VHDL style of circuit composition is rather clumsy and it is difficult to understand how components are connected together. Many researchers have found the notion of function composition very appropriate for describing how two circuits are composed. By modelling each circuit as a time-varying function from inputs to outputs, one can compose circuits without mentioning intermediate signal names. One can also write higher order combinators that take circuits as arguments and compose them in interesting ways e.g. a row of circuits all composed together. Much research work has been reported in this area by Steve Johnson (Daisy)[3], Mary Sheeran and Geraint Jones (µFP and Ruby)[8], John O’Donnell (Hydra)[6] and Miriam Lesser (HML)[5]. One of the early hardware description languages to attract industrial interest was ELLA which is functional. This paper builds on this existing base of work, but shows how far we can go using just Haskell. A major weakness of almost all hardware description languages is their inability to succinctly describe geometry (or layout) Designing FPGA Circuits in Lava 2 as well as structure and behaviour. Even if there is a way to describe the location or orientation of a component, it is often impossible to describe transformations on circuits such as translation and rotation. The usual method for finding a layout for a given graph is to employ an automatic place and route system that typically uses simulated annealing to find out where to place the nodes and then how to connected them together. This is a lengthy and very computationally intensive task, and often the layout or routing delivered is very sub-optimal. In the Lava system circuits can be composed using higher order combinators (functions) that take circuits as parameters and return circuits as results. The first class status of circuits in Lava permits the definition of powerful circuit transformation functions that may compose or lay out circuits in interesting ways. In standard hardware description languages circuits cannot be passed as parameters and there is no standard way of specifying layout (and even if there were, there is no convenient way of transforming circuit layouts). The authors are particularly concerned with the design of circuits for realisation on Field Programmable Logic Arrays (FPGAs). FPGAs are Lego-like chips which comprise of a grid of processing cells which are nearest neighbour as well as being connected to other cells via some hierarchical routing system as shown in Figure 2(a). 2 2 1 AND AND 1 0 XOR XOR 0 0 (a) OR 1 (b) 2 3 0 AND DFF 1 2 3 (c) Figure 2: (a) FPGA grid (b) one configuration of six cells (c) the same cells reconfigured An interesting feature of FPGAs is that the configuration of each cell can be rapidly changed, allowing circuits to be modified as they run. At one instant the bottom 12 cells of the FPGA may have the configuration shown in Figure 2(b), but very rapidly these same cells can be reprogrammed to assume the circuit shown in Figure 2(c). The ability to dynamically reconfigure circuits as they run is a powerful design technique. However current hardware description languages are geared towards the description of static graphs that represent conventional circuits in standard VLSI technologies that do not allow the circuit to change. We will show how the technique of partial evaluation provides a suitable framework for describing an important class of dynamically reconfigurable circuits. This represents one of the major advantages of using a functional hardware description language like Haskell instead of VHDL which provides no support for dynamic reconfiguration. FPGAs have quite a small grid of cells, with only very simple processing cells (2-input gates, multiplexers and flipflops). The FPGA chip used in this paper is the Xilinx XC6216 which has a grid of 64 by 64 cells which can realise circuits containing around 16,000 gates. To make the best use of the small number of cells available, one has to be very careful about how circuits are placed and routed. A key advantage of the approach that we present is that we employ combinators that specify layout and behaviour, so we can calculate the position of each cell. This allows the designer to express a good architecture (layout) for his design that makes the best use of the FPGAs facilities without being a hostage to the automatic place and route system over which he has very little control. Finally, it is very difficult to transform or reason about VHDL circuit descriptions. Lava provides functions for outputing Haskell circuits in VHDL or EDIF. Leaf cells can also be imported from structural VHDL descriptions. This reflects one of our design goals: that Lava should not be a closed system but instead should be used alongside real VLSI CAD software. The system can take cells designed using conventional techniques, and then use the power of a functional language to compose these cells in interesting ways and then deliver the result in VHDL which can be pushed back into the standard design flow for digital circuits. Designing FPGA Circuits in Lava 2 3 A Trivial Lava Circuit First we present an absolutely trivial circuit in the Lava system and demonstrate how Lava can be used to describe, simulate, symbolically evaluate and translate this circuit for use with FPGA CAD tools. The remaining sections introduce more realistic circuit designs in Lava. Many real circuits have been designed in Lava, including a reconfigurable multiplier [4], several kinds of adders (carry look-ahead, carry skip) and finite impulse filters (with multiply-accumulate units). However, to concentrate on the language support provided by Lava we shall only present small circuits in this paper. Circuits descriptions in Lava are overloaded using the Haskell class system. Consider one of the simplest circuits that can be described in Lava, a NAND gate built out of an AND gate and an inverter (INV). To allow comparison with VHDL we show a typical VHDL description of a NAND gate built out of two simpler gates: entity nandGate is port (signal a, b : in bit ; c : out bit) ; end entity nandGate ; library xilinx ; use xilinx.xc6200.aall ; architecture structural of nandGate is signal intermediate_signal : bit ; begin and_gate_1 : and2 port map (a, b, intermediate_signal) ; inv_gate_1 : inv port map (intermediate_signal, c) ; end architecture structural ; The entity declaration declares an interface or “specification” for the NAND gate design. It is possible to give multiple implementations (called architectures) for the same entity. The architecture shown above is called “structural” and shows how to build a NAND gate by wiring together an instance of an AND2 gate and an inverter. Notice how signal name association is used to compose smaller circuits to form larger circuits. This VHDL description can be simulated with any VHDL simulator that conforms to the IEEE standard for VHDL-93. It can also be automatically translated into a programming stream for an FPGA which will configure a portion of the FPGA to realise a NAND gate. The Haskell text for this circuit is shown below using the Lava system: > > > > > > > module NandGate where import Lava nandGate_A :: Circuit state => (Bit, Bit) -> STrans state Bit nandGate_A (a, b) = do intermediate_signal <- and2 (a, b) result_signal <- inv intermediate_signal return result_signal All lines in a literate Haskell script are comments unless they start with a ‘>’ symbol. The NAND gate design resides in a module called NandGate, and this module imports definitions from the Lava module (which is itself made up of a large number of modules). First the type of the NAND gate design is given after the double colons “::”. The type states that the definition of the circuit is overloaded on the type ‘state’. This circuit can be executed for any type belonging to the class Circuit. We shall specify different states for the circuit, depending on how we wish to interpret the circuit description. Examples of states include state types suitable for simulation, symbolic evaluation, layout generation or VHDL output. We call this technique non-standard interpretation [9]. The portion of the type that reads Circuit state => is called the context clause for the type that follows. In this case, the type says that this circuit takes a pair of bits as input and delivers a bit as the output of a stateful computation. State based calculations in the pure lazy functional language Haskell are best expressed using monads which help to sequence calculations and implicitly plumb state information. Our circuit descriptions are cast in the state transformer (STrans) monad which is overloaded with the state type. This is a rather heavyweight type scheme for describing circuits compared to the equivalent VHDL, but this power also buys us many advantages. In normal lazy functional language expressions one can assume very little about the order in which expressions are evaluated (the lack of side effects means that the compiler or run-time system can choose between many evaluation orders as it sees fit). The monadic expressions that follow the do are executed in the order shown, with the result of each cal- Designing FPGA Circuits in Lava 4 culation bound to the variable or pattern to the left hand side of the <-. The state is implicitly singly threaded through these sequenced calculations, and calculations that only operate on the state without returning a result are written without the <- binding. A circuit with input of type a and an output of type b has its type written as Circuit state => a -> STrans state b in Lava. We shall say a “a circuit of result type b” and implicitly assume that the result is in the state transformer monad. The definition of the NAND gate design names the two bits a and b by pattern matching and then a sequence of operations follow the do command which create an AND gate and an inverter (the do syntax is used to combine monadic calculations). First an instance of an AND gate (named and2 in the Lava system) is created and supplied a and b as its inputs. The result of this AND gate is bound to the variable intermediate_signal. The signal is then used as the input to an instance of an inverter whose output is bound to the signal result_signal. The result of running the NAND gate circuit is the value of the result_signal i.e. the NAND of a and b. This style of description has little to recommend itself over the equivalent VHDL. However, we shall see that it is possible to transform this description to lay out the nand gate as we wish. Alternatively, we can give a more concise description using higher order circuit combinators. One very useful combinator is serial composition. This combinator is similar to function composition in mathematics or in functional languages. The Lava serial composition combinator is written as an infix operator with the symbols >->. The symbols indicate that the data flow is from left to right and that the composed circuit is laid out flat with the intermediate signals connected together. The type of the serial composition combinator is: > (>->) :: Circuit state => (a -> STrans state b) -> (b -> STrans state c) -> a -> > (a -> STrans state c) This type is overloaded on the type of state (which must belong to the Circuit class). The combinator takes two circuits and one data value as inputs and returns a circuit. The first circuit maps an input of type a to a result of type b. The second circuit must map a circuit that takes an input of type b to a result of type c. The third argument must be of type a. The serial composition combinator simply runs the first circuit with the value a, and then feeds the result of this into the second circuit. The output of the composed circuit is the output of the second circuit. The serial composition combinator may be defined as: > (>->) circuit1 circuit2 input1 > = do input2 <- circuit1 input1 > result <- circuit2 input2 > return result The actual definition is somewhat more complicated because it also specifies the layout. This layout combinator ensures that circuit2 is laid out to the right of circuit1. Note that one combinator is used to specify how to compose behaviour and layout. Serial composition is an example of a higher order combinator which takes two circuits as arguments and returns a circuit as a result. This kind of combinator cannot be expressed in a conventional language like VHDL. Using the serial composition combinator we can respecify the NAND gate design as: > nandGate (a, b) = (and2 >-> inv) (a, b) It is somewhat more pleasing to eliminate the (a, b) from both sides: > nandGate = and2 >-> inv. The combinator style of circuit description avoids naming signals, especially internal signals. VHDL descriptions are often complicated by the need to name all internal signals, since signal name association is the only glue available for composing circuits. Lava provides richer kinds of glue which include signal name association as well as combinators. Many combinators have useful algebraic properties that allow us to rewrite our cicuits to make them smaller or faster. A basic property of the serial composition combinator is that: (a >-> b) >-> c 3 = a -> (b >-> c) Lava with Hugs The Lava software can be compiled to produce a binary with any Haskell compiler that accepts version 1.3 of the Designing FPGA Circuits in Lava 5 language. Alternatively, the system can be loaded up in the Hugs Haskell interpreter and the user can interactively execute circuits. Lava provides a function simulate which takes a circuit, names for the arguments and a list of values to be applied in sequence to the inputs of the circuit and returns the corresponding sequence of values computed at the output of the circuit. The text below shows the output from a Hugs session where the user asks for the exhaustive simulation of the NAND gate design. ? simulate nandGate (a, b) [(L,L), (L,H), (H,L), (H,H)] 1 1 1 0 It is also possible to symbolically evaluate a circuit: ? symbolic_eval nandGate (a,b) ~(a & b) Lava provides support for reading in structural VHDL and for generating VHDL from Lava descriptions, making it suitable for use in the standard hardware design flow. For example, combinator based description of the NAND gate design can be automatically translated into VHDL by using the writeCircuitAsVHDL function: ? writeCircuitAsVHDL "nandgate" c (nandGate (a, b)) This generates the following VHDL which includes layout hints about the positions of the AND gate and the inverter. entity nandgate is port ( signal a : signal b : signal c : ) ; end entity nandgate in bit ; in bit ; out bit ; library xilinx ; use xilinx.xc6200.all ; architecture generated of nandgate is signal const0 : bit ; signal const1 : bit ; signal v : bit_vector(0 to 1) ; begin gnd0 : gnd port map (const0) ; vcc1 : vcc port map (const1) ; block_nst_1 : block attribute relative_location of inst_1 : label is (0, 0) ; begin inst_1 : and2 port map (a, b, v(0)) ; end block block_nst_1 ; block_nst_2 : block attribute relative_location of inst_2 : label is (1, 0) ; begin inst_2 : inv port map (v(0), c) ; end block block_nst_2 ; end architecture generated ; This description can be simulated or translated into a form that can be placed and routed on an FPGA. The Lava system provides another translator that converts a Lava circuit into an EDIF description of the same circuit. EDIF is a design interchange format used by many VLSI CAD tools, including the Xilinx tools for the XC6200 FPGAs. Clearly, the Lava description of even a simple NAND gate is far more elegant than the corresponding VHDL. The VHDL code has to talk about the internal nets, and the composition of the AND gate and the inverter is not obvious. The position of each gate has to be stated explicitly. The internal signals and cell location is implicit in the Lava description. The layout of the nandgate on a XC6216 FPGA is shown in Figure 3. This picture was produced by feeding the generated EDIF into the Xilinx XACTstep 6000 software. The figure shows the bottom left-hand corner of a 64 by 64 cell FPGA. The cell in the bottom left corner [at position (0,0)] is configured to be an AND gate. The cell at position (1,0) is configured to be an inverter. A light coloured wire shows that the output of the AND gate is connected to the input of the inverter. The blocks outwith the main grid are I/O blocks and pads which are used to communicate signals over the chip pins. Designing FPGA Circuits in Lava 6 Figure 3: The FPGA layout of the NAND gate 4 Binary Adders in Lava Another module shipped with Lava contains several binary adder circuits. The binary adders in this paper are made up of full-adder circuits laid out vertically with the least significant bit at the bottom. A horizontal adder can be made by rotating a vertical adder. A straight forward way of making an adder is to use two half adders and a multiplexor. A half adder can be described with the following Lava code in which the relative locations of the AND and XOR gates are explicitly specified. > halfAdd2 :: Circuit state => (Bit, Bit) -> STrans state (Bit, Bit) > halfAdd2 (a, b) > = do partCarry <- at (0,1) (and2 (a, b)) > partSum <- at (0,0) (xor2 (a, b)) > return (partCarry, partSum) The at combinator takes a displacement (a pair of integers) and a fully applied circuit (a circuit with all its inputs) and returns the same circuit except translated by the given displacement (x displacement and y displacement respectively). The half adder circuits supplies both the AND gate and the OR gate with same inputs and returns a result pair containing the part carry and the part sum. A more elegant description that avoids the explicit mention of layout coordinates can be cast using a combinator style description. This description uses the parallel composition combinator <|> which places one circuit below another. In this case, an AND gate is placed below an XOR gate. > halfAdd3 :: Circuit state => (Bit, Bit) -> STrans state (Bit, Bit) > halfAdd3 (a, b) = (and2 <|> xor2) ((a, b), (a, b)) The duplicated input to this circuit is formed simply by making a pair in which both elements are (a, b). The parallel composition combinator takes the first element of the pair and uses it as the input to the first (lower circuit). It also takes the second element of the pair and uses it as the input to the second (upper) circuit. The result of the parallel composition operator is a pair in which the first element is drawn from the lower circuit and the second element is drawn from the upper circuit. The type of the parallel composition operator is: > (<|>) :: Circuit state => (a -> STrans state b) -> (c -> STrans state d) -> (a, c) -> > STrans state (b, d) An easily proved and useful property of serial and parallel composition is: (a <|> b) >-> (c <|> d) = (a >-> c) <|> (b >-> d) Wiring combinations themselves are worthy of expression as circuits. Unfortunately, in many hardware description languages, circuits that are composed of just wires cannot be expressed (e.g. VHDL or EDIF). In Lava, wiring circuits has the same status as circuits containing components. For example, a circuit that duplicates its input to form Designing FPGA Circuits in Lava 7 a pair (with two identical elements) is defined in the Lava prelude as: > mdup :: Monad m => a -> m (a, a) > mdup a = return (a, a) Informally, this declaration takes a value of type a as input and returns a value of type pair (both elements of type a) as its result. Using this combinator we can cast an even more elegant data-flow style description of the half adder: > halfAdd :: Circuit state => (Bit, Bit) -> STrans state (Bit, Bit) > halfAdd = mdup >-> (and2 <|> xor2) A full adder can now be build using two half adders and an OR gate. This description explicitly places the subcircuits. A combinator style description is possible, but the data-flow in this circuit is too awkward to make such a description much more readable then the version shown below. > big_FA :: Circuit state => ((Bit, Bit), In Bit) -> STrans state (Bit, Bit) > big_FA ((a,b), carryIn) > = do (partCarry,partSum) <- at (0,0) (halfAdd (a, b)) > (partCarry2,sum) <- at (1,0) (halfAdd (partSum, carryIn)) > carryOut <- at (2,0) (or2 (partCarry, partCarry2)) > return (carryOut,sum) The layout produced by this full adder description is shown in Figure 1 (a). Note how the second full adder that contained cells at (0,0) and (0,1) has been translated to locations (1,0) and (1,1) by the at combinator. (a) (b) Figure 4: (a) A big full adder cell; (b) a compact full adder cell The XC6200 FPGAs can implement a 2-to-1 multiplexor in one cell: sum (a, b, carryin) = a ⊕ b ⊕ carryin carryout (a, b, carryin) = if a ⊕ b then carryin else b which affords a much more compact realisation of the full adder circuit: > compact_FA :: Circuit state => ((Bit, Bit), In Bit) -> STrans state (Bit, Bit) > compact_FA ((a,b), carryIn) > = do partSum <- at (0,0) (xor2 (a, b)) > sumOut <- at (1,0) (xor2 (partSum, carryIn)) > carryOut <- at (1,1) (mux2 (partSum, a, carryIn)) > return (carryOut, sumOut) The layout produced by the compact full adder is shown in Figure 4 (b). An ripple-carry adder is now described using a combinator to lay out the full adders in a column. The XC6200 Designing FPGA Circuits in Lava 8 architecture makes it easy for software to access the cells in a column of the FPGA, which encourages some designs to be vertically orientated. Rather than repeating the code for making a ripple-carry adder using the fat and compact adders, we shall abstract over the full adder circuit itself by making it a higher order parameter to the vertical ripplecarry circuit. > vertical_adder :: Circuit state => > (((Bit, Bit), Bit) -> STrans state (Bit, Bit)) -> > Int -> ([Bit], [Bit]) -> STrans state [Bit] > vertical_adder full_adder n (aBits, bBits) > = do let zs = zip aBits bBits > (cout, sums) <- column n full_adder (zs, L) > return (sums++[cout]) The first argument to this higher order circuit is a full adder circuit. Any circuit that maps a tuple of the type ((Bit, Bit), Bit) to a result of type (Bit, Bit) can be passed as an argument. Furthermore, one would hope that only circuits that behaved as full adders were supplied, although the vertical_adder declaration does check this property. The vertical ripple-carry adder is also parameterised on the number of bits to add and it assumed that the lists aBits and bBits contain n elements. The type identifies that aBits is a list of bits (written [Bit]) but the Haskell type system does not specify the length of a list type. The declaration of vertical_adder joins the two lists of bits to be added pairwise (using the Haskell list processing function zip) and binds the result to zs. Then a column of n full adders is made using the column combinator, which also plumbs the carry out of one full adder into the carry in of the full adder above. The declaration of the column combinator is essentially: > column :: Circuit c => > Int -> ((a,b) -> STrans c (b,d)) -> ([a],b) -> > STrans c (b,[d]) > column 0 f ([],b) = return (b,[]) > column n f ((a:as), b) > = do (intermediate_signal, c) <- f (a,b) > (last_signal, cs) <- column (n-1) f (as, intermediate_signal) > return (last_signal, c:cs) The column combinator knows nothing about full adders; all it expects of its argument circuit is that it has a type of the shape (_, _) -> STrans state (_, _) i.e. that it be a pair to pair circuit. The plumbing works by taking the first element of the result pair and feeding it in as the second element of the circuit above.The column combinator takes a list of input values and the initial value to feed into the first cell as its last argument. The result of making a column of full adders is to return a pair containing the carry out and a list of bits that form the sum of aBits and bBits. The adder circuit returns a result of size n+1 by adding the carry bit onto the top of the sum bits. The higher order vertical adder circuit can be specialised to two 4-bit adders: > big_adder_8 = vertical_adder big_FA 8 > compact_adder_8 = vertical_adder compact_FA 8 The layout for the 4-bit adder based on the big full adder cell is shown in Figure 5 (a); part (b) shows the layout for the vertical adder using the compact full adder cell. Figure 5 (c) shows how the automatic place and route CAD tools would have laid out the adder in part (a) in the absence of the layout information inferred from the combinators. This layout in part (c) is not a very desirable form e.g. it is not easy to juxtapose with a vertically orientated register. Our experience has shown that for larger circuits, the automatic place and route tools either fail to find a placement or they fail to route a circuit. Using Lava, a designer can choose to specify the layout of part of a design. The designer can use layout combinators to influence the floor plan of the design, and by choosing a good layout one can be more certain that the automatic routing tool will find a good route. Currently Lava only gives control over layout and does not allows the designer to specify the route of a wire. Designing FPGA Circuits in Lava (a) 9 (b) (c) Figure 5: (a) Ripple-carry adder based on big full adder; (b) based on a compact full adder; and (c) the layout without using any information from combinators In this section we presented two circuits that have different implementations but should have the same behaviour. Program verification techniques could be used to prove that these two full adder circuits behave identically. For such a small circuit, exhaustive verification could also be used. Alternatively, one could use proved correct program transformation to derive one full adder from the other. Formal transformation and verification techniques are more suitable for use in Lava than VHDL because Lava has a much simpler semantics. Lava provides another approach for verifying that two circuits are the same by generating input for the commercial model checker NP-Tools. Using a form of symbolic evaluation, we automatically produce boolean formulae from circuit descriptions. These formulae are then input to the commercial propositional logic theorem prover NP-Tools. We can either ask if two circuits are equivalent (have the same behaviour) or we can ask if a circuit satisfies a requirement, which must also be expressed in propositional logic. Here, we have found it useful to have Haskell available for constructing these question formulae. The fact that NP-Tools also implements arithmetic on finite domains allows us to verify across levels of abstraction, something which is vital in large scale examples. The screen shot in Figure 6 shows the NPTools application at work on this rather easy example, which can prove equivalent immediately. The success of the verification is declared by the text “Valid” in the bottom left hand corner of the main window. The NP-Tools software can verify that the two adder designs are equivalent in less than a second. Designing FPGA Circuits in Lava 10 Figure 6: Formal verification of the two full adder designs 5 A Sequential Example: A Synchronous Counter Sequential circuits can be built using a D-type positive edge triggered flip flop. The type of the D-type flip-flop is: > fdc :: Circuit state => Bit -> (Bit, Bit, But) -> STrans Bit > fdc initial_value (d, clk, clr) = ... { primitive cell } This flip flop corresponds to the primitive flip-flop available in each cell on the XC6200 FPGA, has an asynchronous clear signal (clr) and it updates its state (by reading in d) on the rising edge of the clock (clk) (otherwise it retains its state value). The first argument specifies the initial value of the flip-flop and must be given statically (at compile type). Lava provides a toggle flip-flop which just inverts its state on every clock tick. This circuit generates the output sequence {0, 1, 0, 1, ...}. Sequential circuits with feedback can be expressed by using the Lava loop combinator: > mloop :: Circuit state => (a -> STrans state a) -> STrans state a This mloop combinator takes a circuit whose input and output types are the same and returns the same circuit with the output wires connected to the input wires. As an example of mloop consider the toggle flip-flop as defined in the Lava prelude: > ftc :: Circuit state => (Bit, Bit) -> STrans state Bit > ftc (t, clk) > = mloop (\ previousState > -> do invPreviousState <- at (0,0) $ xor2 (t, previousState) > nextState <- at (0,0) $ fd0 (invPreviousState, clk) > return nextState > ) where fd0 is a version of fdc specialised with the initial value 0. A lambda abstraction is used to define an “inline” function that takes the previous state, inverts it and then feeds in into a flip-flop. The mloop combinator joins the signals previousState and nextState, making sure they get realised with the same wire. It is possible to realise a flip-flop and an inverter in one cell, which is why both of these circuits have the same location. Here the functional composition operator $ has been used to avoid writing extra brackets. Writing f (g a b) is the same as writing f $ g a b where f and g are functions. Designing FPGA Circuits in Lava 11 A synchronous counter stage can be built in the standard way: > counterStage :: Circuit a => > Bit -> (Bit, Bit) -> STrans a (Bit, Bit) > counterStage clk (lastStage, lastLastStage) > = do t <- at (0,0) (and2 (lastStage, lastLastStage)) > output <- at (1,0) (ftc (t, clk)) > return (output, t) This can be composed vertically using a variant of column: > synchronousCounter :: Circuit a => Int -> Bit -> STrans a [Bit] > synchronousCounter size clk = column_of size (counterStage clk) (H,H) This counter can be simulated, symbolically evaluated or converted into VHDL. The layout of this counter is shown in Figure 7. This also shows the pin through which the clock signal arrives. The automatic place and route tools would not have nicely laid out the counter in this fashion: the layout combinators have allowed us to capture vertically replicated cells without explicitly mentioning cell locations. Figure 7: Layout of the synchronous counter. 6 Lava Implementation Lava is designed around a systematic approach for building VLSI CAD tools using the technique of non-standard interpretation [9]. The basic circuits are parameterised on the type of data flow through the ‘wires’. If the data along the wires is bits, then the Haskell class system is used to bind a simulation interpretation to the basic gates. If it is symbols then a symbolic evaluation interpretation is used. Overloading in Haskell is controlled by a class system rather than being ad hoc like in C++. The class Circuit contains the signatures of overloaded circuits (or member functions) that are said to belong to the Circuit class. A part of the Circuit class looks like: > class Circuit state where > inv :: Bit -> STrans state Bit > and2, or2, xor2 :: (Bit, Bit) -> STrans Bit > mux2 :: (Bit, Bit, Bit) -> STrans state Bit For each type of state that we wish to interpret our circuit we must make an instance of the Circuit class. One of the simplest interpretations is the simulation interpretation in which no state information is recorded. In this case, an instance of Circuit is created with the empty type and the simulation definitions of the member functions is given: Designing FPGA Circuits in Lava 12 > instance Circuit () where > inv L = H > inv H = L > ... To define symbolic simulation we declare a suitable type Symbol for representing symbolic expessions. We then create an instance of Circuit at the Symbol type that builds up a symbol tree for the circuit. > data Symbol = Low | High | Var String | Inv Symbol | And2 Symbol Symbol deriving (Eq, Show) > instance Circuit Symbol where > inv a = Inv a > and2 (a,b) = And2 (a, b) > ... Thus when one writes nandGate it is not known exactly what code should be executed for this circuit, hence the need for a context clauses. However, once an argument is supplied the Haskell class system can dispatch the appropriate function. For example: ? exec nandGate (L, H) H ? exec nandGate (Var “a”, “b”) Inv (And2 (Var “a”, Var “b”)) Note that the same description has multiple interpretations, depending on the type of the data supplied to the circuit. Basic interpretations can be combined to form powerful composite interpretations. This provides a systematic approach to building CAD tools and avoids re-specification of the circuit in different notations. 7 Summary Lava is not a new language, but rather a system that demonstrates that Haskell, used in a particular style, makes a good hardware description language. It allows elegant specification of requirements at a high level of abstraction. It allows compact readable descriptions of circuits at various levels of abstraction, and these descriptions can be translated automatically into VHDL or EDIF code. So there is a link to real circuits. We consider this to be very important. All of the circuits in this paper and many others have actually been realised on FPGAs. In addition, the Lava system allows a circuit description to be analysed in many different ways. For example, symbolic evaluation often allows the user to quickly examine a design; it can be seen as an alternative to user-guided proof in certain cases. An adaption of symbolic evaluation gives us access to automatic verification of gate-level circuits using the NP-Tools system. Performance analysis (such as timing analysis) and testability analysis can also be implemented in this way. Lava is still under development at the time of writing. It is used to design circuits on several projects, including the hardware design of a PostScript accelerator chip and the description of circuits that are dynamically reconfigured at run-time by using partial evaluation. It has been particularly effective as a rapid proto-typing tool, allowing circuits to be designed and laid out far more quickly than was possible using VHDL and layout pragmas. The Lava website is at http://www.dcs.gla.ac.uk/~satnam/lava. 8 Acknowledgements We would like to acknowledge the support of Xilinx Development Corp. for the provision of FPGA hardware, CAD tools and support for the development of the Lava interface to their tools. Loggikonsult provided the NPTools software which allowed us to formally verify Lava circuits. Logikkonsult also supported Mary Sheeran’s work on the formal verification of circuits described in Haskell. The design of Lava was heavily influenced by the Hawk system developed at the Oregon Graduate Institute by John Launchbury and John Mathews. This work is part funded by the United Kingdom EPSRC grants GR/K82055 and GR/L38530. 9 Note to Referees Due to space constraints we have not been able to include a derivation of a reconfigurable multiplier from its specification. This example illustrates design by formal transformation in Lava and if the referees agree to a 15 page paper we are willing to compact some of the earlier sections to make way for the multiplier derivation. Designing FPGA Circuits in Lava 13 References [1] M. Fourman. Formal Design in Formal Design Methods for VLSI Design, ed. J. Stanstrup, North Holland, 1992. [2] F. K. Hanna. Implementation of the Veritas Design Logic in IFIP Conf. on Theorem Provers in Circuit Design, 1992. [3] S. D. Johnson. Applicative Programming and Digital Design, 11th ACM Symp. on Principles of Programming Languages (POPL), 1984. [4] T. Kean, B. New, B. Slous. A Multiplier for the XC6200. Sixth International Workshop on Field Programmable Logic and Applications. Darmstadt, 1996. [5] Y. Li and Miriam Lesser. HML: An innovative Hardware Design Language and Its Translation to VHDL. Proc. CHDL’95, 1995. [6] John O’Donnell. Hydra: Hardware descriptions in a functional language using recursion equations and higher order combining forms, The Fusion of Hardware Design and Verification, G. J. Milne (ed), North-Holland, 1988. [7] John O’Donnell. Bidirectional fold and scan. Functional Programming: Glasgow 1993, Springer, 1994. [8] M. Sheeran, G. Jones. Circuit Design in Ruby . Formal Methods for VLSI Design, J. Stanstrup, North Holland, 1992. [9] S. Singh. Circuit Analysis by Non-Standard Interpretation. IFIP Transactions on Designing Correct Circuits, J. Staunstrup, R. Sharp (Eds), North-Holland, 1992.
© Copyright 2026 Paperzz