Common FAQs
in-depth insights into Hardcaml
Hardcaml FAQ & Guide
What is a Signal in Hardcaml?
In Hardcaml, a Signal represents a wire or register in hardware. It is the core datatype used to describe combinational and sequential logic in your design. Think of it as a symbolic representation of a bitvector that can be wired to other logic β like inputs, outputs, gates, or flip-flops.
Key points about Signals:
A
Signal.t
is not like a regular value (int or bool) - it's a symbolic expression representing a circuit connectionIt will later be turned into actual hardware (e.g., in Verilog or VHDL)
During circuit construction, a signal is not the actual number, it's a symbolic expression
What's the difference between a Wire and a Register?
Wire:
A wire is a combinational connection β it carries a signal instantly from one logic gate to another
No memory: A wire does not store anything
Instant: Its value is updated immediately based on the logic driving it
Used in: Data paths, logic gates, connections between modules
Register:
A register is a sequential element β it stores data across clock cycles
Has memory: It holds its value until it is explicitly updated
Synchronous: Updates its value only on a clock edge (typically rising edge)
Used in: State machines, counters, pipelines, buffering
Summary Table:
Memory
β No
β Yes
Updates
Instantly (combinational)
On clock edge (sequential)
Typical Use
Logic routing, gates
State holding, counters
Control
No control (passive)
Controlled by clock/reset
Signal Operations and Creation
What can a Signal hold?
A Signal.t
can hold a symbolic bit-vector expression β it represents a sequence of bits (0s and 1s) of a given width.
Signal can hold:
Bit Vectors - Basic type representing N-bit wide wire or register
Combinational Expressions - Composed logic like adders, muxes, gates
Constants - Created using
Signal.of_int
,Signal.of_string
,Signal.gnd
(zero),Signal.vdd
(one)Registered Values (State) - With a
Reg_spec
, a signal can hold state
Signal cannot hold:
Actual runtime values during circuit construction
Pointers, objects, data structures
Mutable values (signals are immutable)
How do I create signals from values?
(* From integer *)
Signal.of_int ~width:4 3 (* 4-bit signal with value 3 *)
(* From binary string *)
Signal.of_string "1010" (* 4-bit signal from string *)
(* Constants *)
Signal.gnd (* zero *)
Signal.vdd (* one *)
Why use of_string "01"
instead of just 1
?
of_string "01"
instead of just 1
?In Hardcaml, you're building hardware descriptions, not just doing computations. You're working with bitvectors that have:
A width (how many bits)
A bit-level value (e.g., "01", "10")
OCaml integers like 1
and 2
are not Signal.t
values β they don't carry bit width, binary shape, or signal expression info.
(* This would fail with a type error *)
adder 1 2 (* β Passing int, but adder expects Signal.t *)
(* This works *)
adder (of_string "01") (of_string "10") (* β
*)
Understanding Signal vs Bits
What's the difference between Signal and Bits?
This is crucial to understand:
What it is
Abstract representation of a circuit
Concrete bit-level values
When used
At circuit definition time
At simulation time
Mutable?
No (pure & immutable)
Yes (in simulation)
Example use
Describing combinational logic, registers, muxes
Running testbenches, printing outputs
OCaml type
Signal.t
Bits.t
Analogy
Verilog wire
, reg
Runtime logic values
Signal describes the structure:
let sum = Signal.(a +: b) (* Describes an adder in the circuit *)
Bits represents actual values:
let a = Bits.of_int ~width:4 3 (* 4-bit value: 0011 *)
let b = Bits.of_int ~width:4 4 (* 4-bit value: 0100 *)
let sum = Bits.(a +: b) (* sum = 7 = 0111 *)
Binary Arithmetic
How many numbers can I represent with N bits?
With N bits, you can represent 2^N distinct numbers.
For 8 bits:
Unsigned: 0 to 255 (256 values total)
Signed (two's complement): -128 to 127 (256 values total)
Binary conversion table (0-10)
0
0000
1
0001
2
0010
3
0011
4
0100
5
0101
6
0110
7
0111
8
1000
9
1001
10
1010
Working with Registers
What is a Reg_spec?
A Reg_spec
(register specification) is a configuration describing how a register should behave with respect to clock, reset, and optionally enable.
let spec = Reg_spec.create ~clock:clk ~clear:clr ()
What it contains:
clock
: When the register triggers (clock edge)clear
: Optional reset signalenable
: Optional enable signal (only updates when enabled)
How does reg_fb work?
reg_fb
creates a "register with feedback" β a register whose next value depends on its current value.
let counter = Signal.reg_fb spec ~enable:Signal.vdd ~width:8 ~f:(fun d -> Signal.(d +:. 1))
Components:
spec
: Contains clock/reset configuration~enable:Signal.vdd
: Always enabled (updates every cycle)~width:8
: 8-bit register~f:(fun d -> d +:. 1)
: Next value = current value + 1
Why do we need wires for feedback?
In OCaml, you can't refer to a variable before it's defined (no forward references). But in hardware, feedback loops are common:
(* This won't compile - q is undefined! *)
let q = Signal.reg spec ~enable (f q) (* β *)
Solution: Use a wire as a placeholder:
let reg_fb spec ~enable ~w f =
let d = Signal.wire w in (* placeholder *)
let q = Signal.reg spec ~enable (f d) in
Signal.(d <-- q); (* connect the loop *)
q
What does "enable" do in a register?
The enable
signal controls whether the register updates on the clock edge:
If
enable = 1
: Register loads new value on clock edgeIf
enable = 0
: Register holds its previous value
Example behavior:
1
1
0
1
2
1
1
2
3
0
2
2 (holds)
4
1
2
3
Circuit Building Functions
How does select work?
select
extracts a slice (range of bits) from a wider signal.
val select : Signal.t -> int -> int -> Signal.t
Example:
let x = Signal.of_string "10110011" (* 8-bit signal *)
let y = Signal.select x 5 2 (* Extract bits [5:2] *)
(* Result: "1100" *)
Important notes:
Uses inclusive indexing:
select s 5 2
returns 4 bits: s[5], s[4], s[3], s[2]Indexing is from LSB = 0
If high < low, you'll get a runtime error
What is mux?
mux
builds a hardware multiplexer - it selects one signal from a list based on a selector:
val mux : Signal.t -> Signal.t list -> Signal.t
Example:
mux sel [a; b; c; d]
(* If sel = 0, returns a
If sel = 1, returns b
If sel = 2, returns c
If sel = 3, returns d *)
What is mux2?
mux2
is a convenience function for 2-way multiplexing:
mux2 sel a b
(* Returns: a if sel = 0, b if sel = 1 *)
It's equivalent to mux sel [a; b]
but cleaner for binary choices.
How does concatenation (@:) work?
The @:
operator concatenates bit vectors:
a @: b (* a becomes MSB side, b becomes LSB side *)
Example with JK flip-flop:
j @: k (* Creates 2-bit vector {j, k} *)
0
0
00
0
0
1
01
1
1
0
10
2
1
1
11
3
Memory Operations
How does multiport_memory work?
multiport_memory
creates a RAM block with multiple read and write ports.
let q = Signal.multiport_memory
256 (* 256 entries *)
~write_ports:[|write_port|] (* Array of write ports *)
~read_addresses:[|read_address|] (* Array of read addresses *)
Write Port structure:
type 'a Write_port.t = {
write_enable : 'a; (* Enable writing *)
address : 'a; (* Where to write *)
data : 'a; (* What to write *)
}
Read behavior:
Returns an array of read data signals
q.(0)
is the data from the first read addressCan be synchronous (1 cycle delay) or asynchronous
What do the |...| symbols mean?
The [| ... |]
syntax creates an OCaml array (not a list):
[| 1; 2; 3 |] (* int array *)
[1; 2; 3] (* int list - different! *)
Arrays are:
Fixed size
Mutable
Accessed via
arr.(i)
PPX and Interfaces
What does [@@deriving hardcaml] do?
When you use [@@deriving hardcaml]
on an interface type:
type 'a t = {
a : 'a [@bits 8];
b : 'a [@bits 7];
} [@@deriving hardcaml]
It generates many utility functions:
Generated functions:
make
- Create signal bundleslet inputs = I.make Signal.input (* Creates: { a = input "a" 8; b = input "b" 7 } *)
map
- Transform each fieldlet incremented = I.map inputs ~f:(fun x -> x +:. 1)
map2
- Combine two interfaces field by fieldlet sum = I.map2 inputs1 inputs2 ~f:(+:)
to_bits
/of_bits
- Convert between structured records and flat bitvectorsport_names_and_widths
- Get metadataI.port_names_and_widths (* [("a", 8); ("b", 7)] *)
Always DSL
What is Always.Variable?
Always.Variable
is used within the Always DSL to describe sequential logic in an imperative style (like Verilog):
let counter = Always.Variable.create ~width:8 in
Always.build ~clock (fun () ->
if_ (enable) [
Variable.set counter (Variable.value counter +:. one 8)
]
)
Key operations:
.create ~width
- Makes a new variable/register.set
- Assigns to it (like<=
in Verilog).value
- Reads the current signal value
Simulation
Why do we dereference with (!) in simulation?
In Hardcaml simulation, signals are stored as mutable references (ref
):
(* To read an output *)
let value = !(outputs.dout)
(* To write an input *)
inputs.clock := Bits.vdd
Why refs?
Simulation signals change every clock cycle
Need mutable containers to hold/update values
Standard OCaml pattern:
:=
to write,!
to read
What does Cyclesim.cycle do?
Cyclesim.cycle sim
advances the simulation by one clock cycle:
Evaluates all combinational logic
Updates all registers on clock edge
Propagates new values through the circuit
How do I view signal output?
To see human-readable signal values:
(* For Bits.t *)
Bits.to_string x (* Binary string: "1010" *)
Bits.to_int x (* Decimal: 10 *)
(* For Signal.t - must simulate first *)
let sim = Cyclesim.create circuit
let value = !(sim.outputs.some_signal)
Bits.to_string value
Common Patterns
Building a Counter
let counter =
reg_fb spec
~enable:vdd
~width:8
~f:(fun d -> d +:. 1)
This creates an 8-bit counter that:
Increments every cycle (always enabled)
Wraps from 255 to 0
Can be cleared via the
spec
Creating a State Machine
let state = Variable.create ~width:2 in
Always.build ~clock (fun () ->
switch (Variable.value state) [
0, [ (* State 0 actions *) ];
1, [ (* State 1 actions *) ];
2, [ (* State 2 actions *) ];
3, [ (* State 3 actions *) ];
]
)
JK Flip-Flop Pattern
fun q -> mux (j @: k) [q; gnd; vdd; ~:q]
This implements JK flip-flop logic:
J=0, K=0: Hold (q)
J=0, K=1: Reset (0)
J=1, K=0: Set (1)
J=1, K=1: Toggle (~q)
Debugging Tips
Understanding Signal Tree Output
When you see verbose output like:
Hardcaml__.Signal__type.Op2 { op = Signal_add; ... }
This is the internal representation of your circuit graph. It shows:
Node type (Op2 = binary operation)
Operation (Signal_add = addition)
Arguments (the inputs to the operation)
Common Errors
"Unbound module Bits"
Use
Hardcaml.Bits
or addopen Hardcaml
Type errors with signals
Remember: Signal.t for circuit building, Bits.t for simulation
Can't mix regular OCaml ints with signals
Forward reference errors
Use
Signal.wire
for feedback loopsConnect wires after creating dependent signals
Looking through the document again, I believe I've covered all the major topics, but let me add a few more sections that were in the original document to ensure completeness:
Additional Topics
What is MSBs?
MSB stands for Most Significant Bit β the bit that represents the highest value in a binary number. msbs
typically refers to a function or slice that returns the top (leftmost) bits of a signal.
Example:
(* For an 8-bit signal *)
let x = Signal.of_int ~width:8 0b11010110
(* Get top 4 bits *)
let top4 = Signal.select x 7 4 (* β 1101 *)
(* Custom helper for MSBs *)
let msbs x n = Signal.select x (Signal.width x - 1) (Signal.width x - n)
let top2 = msbs x 2 (* β 11 *)
Use cases:
Determining the sign bit in signed arithmetic
Address decoding (using MSBs to select memory banks)
Control logic where upper bits determine branches
What is dout?
dout
is just a naming convention β short for data out β typically used to label the output signal of a hardware module. It's not a keyword.
Example:
module O = struct
type 'a t = { dout : 'a } [@@deriving hardcaml]
end
let create (inputs : _ I.t) : _ O.t =
{ dout = some_signal }
Common uses:
In a counter: the current count value
In an ALU: the result of an operation
In a memory: the value read from RAM
Array Indexing Syntax .(i)
The .(i)
syntax is OCaml's array indexing:
arr.(0) (* Access first element *)
arr.(i) (* Access i-th element *)
In multiport_memory context:
let q = Signal.multiport_memory 256 ~write_ports ~read_addresses
(* q is an array of read data signals *)
q.(0) (* Data from first read port *)
q.(1) (* Data from second read port *)
Understanding Internal Signal Representations
When you see output like:
Bytes.of_string "\002\000\000\000\000\000\000\000\003\000\000\000\000\000\000\000"
This is the internal binary encoding of a Bits.t
value. It's not meant for human readability. To view it properly:
(* Convert to readable format *)
Bits.to_string result (* Binary string *)
Bits.to_int result (* Decimal integer *)
Step-by-Step Counter Operation
Here's exactly how a counter increments with clock cycles and clear:
Counter definition:
let counter = reg_fb spec ~enable:vdd ~width:8 ~f:(fun d -> d +:. 1)
Simulation trace:
0
0
00000000
0
Start value
1
0
00000001
1
+1
2
0
00000010
2
+1
3
0
00000011
3
+1
4
1
00000000
0
Cleared to 0
5
0
00000001
1
Counting resumes
ASCII Waveform:
Cycle : 0 1 2 3 4 5 6 7 8 9
clear : βββββ βββββββββββββββββ
β β
βββββββββββββββββ
counter : 0 1 2 3 0 1 2 3 4 5
βββββ¬ββββ¬ββββ¬ββββ βββββ¬ββββ¬ββββ¬ββββ
Simulation Step Function Pattern
The step
function in testbenches encapsulates a single simulation cycle:
let step ~clear ~incr =
inputs.clear := if clear=1 then Bits.vdd else Bits.gnd;
inputs.incr := if incr=1 then Bits.vdd else Bits.gnd;
Stdio.printf "dout='%s'\n" (Bits.to_string !(outputs.dout));
Cyclesim.cycle sim
What it does:
Sets input signals based on parameters
Prints current output value (before the clock)
Advances simulation by one clock cycle
Why use it:
Cleaner than repeating code
Easy to parameterize tests
Better for loops and automated testing
Signal Constants
Common signal constants you'll use:
Signal.gnd
- Logic 0Signal.vdd
- Logic 1 (VDD = supply voltage)Signal.zero width
- Zero of specific widthSignal.ones width
- All ones of specific width
Read and Write Port Details
Write Port Fields:
write_enable
: When high, enables writing on clock edgeaddress
: Target memory locationdata
: Value to write
Read Port Behavior:
Asynchronous: Data updates immediately when address changes
Synchronous: Data updates on next clock cycle (typical for FPGA RAM)
Example usage:
(* Write to address 3 *)
wr.write_enable <-- vdd;
wr.address <-- of_int ~width:4 3;
wr.data <-- of_int ~width:8 42;
(* Read from address 3 *)
rd.address <-- of_int ~width:4 3;
(* rd.data will show the value after appropriate delay *)
Last updated