The signature Comb.S is the combinatorial API of HardCaml and defines the basic arithmetic, logical, comparison etc vector operations.
| constants | `const constb consti vdd gnd zero one ones ...` |
| bitwise | `(&:) (|:) (^:) (~:)` |
| arithmetic | `(+:) (-:) (*:) (*+) negate` |
| comparison | `(<:) (<=:) (>:) (>=:) (<+) (<=+) (>+) (>=+) (==:) (<>:)` |
| shift | `sll srl sra log_shift` |
| resize | `uresize sresize ue se` |
| selection | `select select_e msb lsb msbs lsbs split bits` |
| multiplexors/roms | `mux mux2 mux_init` |
| concatenation | `concat concat_e (@:) repeat` |
| other | `width input output ...` |
There are a few general rules followed by the API
: ie (+:) for addition+ symbol is appended for the signed variant rather than a :. If you dont like these rules, see the TypedMath modules for dealing with signed/unsigned vectors and the required zero/sign extension.. added ie (+:.). This indicates that the second operand is given as an integer and the appropriate constant is inferred ie a +:. 1 becomes a +: (consti (width a) 1).An important point to note is that HardCaml RTL designs are essentially graphs with defined inputs and outputs.
Everything else in the graph is a node which takes signals as input and produces one or more signals as output.
Circuits are constructed by providing a list of outputs. The graph is then traversed from the outputs to find all connected nodes and the inputs.
Time for some examples. We will use the shallow embedding so we can see exactly what is happening. The code is equally relevant to the deep embedding, however.
In [1]:
open HardCaml.Bits.Comb.IntbitsList
Every signal may be queried for it's size in bits using the width function.
In [2]:
width (const "12'd0")
Out[2]:
This means it is possible to design circuits that can take any width of argument and adapt themselves internally. This leads to generic designs often with little effort.
There are a number of functions to help creating constants. The most basic one is constb. It takes a string which must consist of 0 or 1 characters only. Note that the most significant bit is the left most (first) character.
In [3]:
let a = constb "0101"
Out[3]:
This results in the list [0; 1; 0; 1]. Again, the most significant bit is the left most (head) element of list.
The consti function is probably the most convienient of all the constant constructing functions.
In [4]:
let b = consti 4 3
Out[4]:
The first parameter is the vector width. The second parameter is it's value. Variants for int32 and int64 are called consti32 and consti64.
Constants can also be generated from strings of hex characters.
In [5]:
consthu 4 "e"
Out[5]:
The most general function for creating constants is called simply const. It takes a string with an embedded width, type and value. It's format is very similar to that of Verilog style constants.
"<width>'<type><value>"
In [6]:
const "4'b0001", const "5'd-2", const "6'hf"
Out[6]:
The following types are valid: b binary, d decimal, h hexidecimal. The types may also be capitalised. This causes sign extension if the value given is shorted than the given width.
In [7]:
const "6'hf", const "6'Hf"
Out[7]:
In [8]:
const "2'b1", const "2'B1"
Out[8]:
The two predefined functions vdd and gnd are commonly used to represent a single bit with value 1/true and 0/false respectively.
HardCaml is strict about what it allows you to pass to operators.
In [9]:
consti 8 2 +: consti 7 3
Out[9]:
The width of the operands must be the same, as is the case for the constants a and b defined earlier.
In [10]:
a +: b
Out[10]:
For addition (and subtraction) the width of the result is the same as the operands. In some cases this is not what is wanted ie overflow.
In [11]:
let a, b = const "111", const "001"
let c = a +: b
Out[11]:
Out[11]:
In this case we could use the ue function to increase the size of the operands by 1 bit.
In [12]:
ue a +: ue b
Out[12]:
If we know that our operands are specifically meant to represent, say, signed vectors we could write a generic addition function as follows.
In [13]:
let add_signed a b =
let w = max (width a) (width b) + 1 in
sresize a w +: sresize b w
Out[13]:
In [14]:
add_signed (consti 8 22) (consti 6 (-23))
Out[14]:
In fact this sort of properly defined arithmetic is common enough that it is provided by the Signed and Unsigned modules (: TypedMath).
In [15]:
let a, b = Signed.(of_signal (consti 8 22), of_signal (consti 6 (-23)))
Out[15]:
In [16]:
Signed.(to_signal (a +: b))
Out[16]:
Multipliers are one of the few operators which do not care about vector sizes.
In [17]:
consti 2 2 *: consti 3 3
Out[17]:
That being said it does care about signs and there is a seperate signed version of the operator.
In [18]:
consti 2 2 *+ consti 3 3
Out[18]:
The bitwise operators behave similarly to addition and subtraction - operand and result are all the same width.
In [19]:
let a,b = consti 2 1, consti 2 3
Out[19]:
In [20]:
a ^: b
Out[20]:
In [21]:
a |: b
Out[21]:
In [22]:
a &: b
Out[22]:
The operands of the comparison operators must be the same width and lead to a 1 bit result
In [23]:
a <: b
Out[23]:
In [24]:
a >: b
Out[24]:
In [25]:
a ==: b
Out[25]:
Comparison requires a seperate set of operators to deal with signed/unsigned operations. Like multiplication the signed variants are appended with a +.
In [26]:
a <+ b
Out[26]:
The mux function implements a multiplexer and is a very common function in HardCaml. It takes the place of if and (simple) match expressions and is also used to generate ROMs.
In [27]:
mux (consti 2 1) [gnd;vdd;gnd;gnd]
Out[27]:
The (unsigned) value of the first argument selects the appropriate value from the list given as the second argument. This corresponds to a verilog case statement.
case (index)
0: q <= a;
1: q <= b;
2: q <= c;
...
let q = mux index [a;b;c;...]
If there are more data elements in the list than can be reached by the index an exception is raised. If there are fewer then the last element is repeated - it is treated as though it is the default element in a verilog case statement.
When the list given to mux only consists of constants this effectively generates a ROM.
You can extract parts of a vector with the select function.
In [28]:
let a = const "001100"
Out[28]:
In [29]:
select a 3 2
Out[29]:
The hardcaml syntax extension implements a short cut for this as it's a very common operation: signal.[ expr : expr ]
In [30]:
a.[3:2]
Out[30]:
There are a number of predefined selection operations for common cases, in particular msb and lsb
In [31]:
lsb (const "00001")
Out[31]:
In [32]:
msb (const "01111")
Out[32]:
Vectors are concatenated with the concat function or (@:) operator.
In [33]:
a @: b
Out[33]:
In [34]:
concat [a;b;a]
Out[34]:
Concatention is msb first - the left most bit of the left most (head) element of the list passed to concat is the msb of the result.
A single vector can be concatented with itself using the repeat function.
In [35]:
repeat (const "01") 3
Out[35]:
Selection and concatention are how resizing is performed. For example to extend a signed vector by 3 bits
In [36]:
let signed_extend_by_3bits v = repeat (msb v) 3 @: v
Out[36]:
In [37]:
signed_extend_by_3bits (const "100")
Out[37]:
Similarly we can reverse a vector
In [38]:
let rev v = concat (List.rev (bits v))
Out[38]:
In [39]:
rev (const "10")
Out[39]:
The shift functions are a convenience built upon selection and concatenation.
In [40]:
sra (const "100") 1
Out[40]:
The log_shift function uses the basic shift functions and generates a barrel shifter.
In [41]:
log_shift sll (consti 4 1) (consti 2 2)
Out[41]:
Although the difference is perhaps not obvious here, the point is that the final argument to log_shift is a signal rather than an integer and in a hardware circuit it could shift by different amounts on differet clock cycles.
The tree function is a good example of using OCaml to create more complex functions from the simple primtives provided by HardCaml. The idea to that given a list of signals we want to apply some operator between then all. For example
In [42]:
let x = Array.init 8 (fun i -> consti 8 i) |> Array.to_list
Out[42]:
Lets say we want to sum this list of 8 signals
In [43]:
List.fold_left (+:) (zero 8) x
Out[43]:
Indeed there is a Comb.S function for this as well
In [44]:
reduce (+:) x
Out[44]:
The problem with both these approaches is that the result hardware will have a critical path through 7 adders.
(a + (b + (c + (d + (e + (f + (g + h)))))))
What we really want is for the brackets to be arranged thus;
(((a + b) + (c + d)) + ((e + f) + (g + h)))
This has the same total number of adders except the critical path is now through only 3 adders.
This is exactly what the tree function does for us.
In [45]:
tree 2 (function [a] -> a | [a;b] -> a +: b | _ -> failwith "wont happen") x
Out[45]:
There is a small complication in above definition due to the fact that tree will build trees with an arbitrary number of branches. We can simplify the above with the reduce function.
In [46]:
tree 2 (reduce (+:)) x
Out[46]:
In [47]:
tree 4 (reduce (+:)) x
Out[47]:
This ability becomes especially useful when mapping to FPGAs with either 4 or 6 input luts. Consider a xor tree based reduction for 6 input LUT FPGAs
tree 6 (reduce (^:)) x
It is frankly disturbing how this is able to build more optimal hardware than $50,000 commerical synthesizers...