Cheatsheet / Reference
TLDR of Hardcaml
Installation and Setup
Package Installation
opam install hardcaml ppx_hardcaml hardcaml_waveterm
opam install hardcaml_c hardcaml_verilator hardcaml_step_testbench
Project Structure
project/
βββ src/
β βββ interfaces.ml (* Interface definitions *)
β βββ components/ (* Reusable components *)
β βββ top.ml (* Top-level module *)
β βββ utils.ml (* Utility functions *)
βββ test/
β βββ unit_tests.ml (* Unit tests *)
β βββ integration_tests.ml
βββ bin/
βββ main.ml (* RTL Code Generation *)
Core Concepts and Fundamentals
What is Hardcaml?
Hardcaml is an embedded hardware design domain-specific language (DSL) implemented in OCaml by Jane Street. It provides low-level control over hardware while abstracting away tedious aspects of traditional HDLs like Verilog/VHDL.
Key Design Principles
Full Control DSL: Direct instantiation of hardware primitives (registers, memories, multiplexers)
Type Safety: OCaml's type system prevents connection errors and width mismatches
Fast Elaboration: Circuit construction happens entirely within OCaml at elaboration time
Embedded Approach: No secondary abstraction layersβpurely OCaml
Graph-Based Representation
Hardcaml represents circuits as directed graphs where:
Nodes = hardware operations (registers, logic gates, arithmetic)
Edges = signal connections
Cycles = supported via wires for feedback loops
Signal Types and Operations
Primary Signal Type
type Signal.t (* Fundamental building block *)
(* Signal creation *)
let a = Signal.of_int ~width:8 42 (* 8-bit constant *)
let b = Signal.input "data_in" 16 (* 16-bit input port *)
let c = Signal.wire 4 (* 4-bit wire *)
(* Width operations *)
let width_of_a = Signal.width a (* Query width *)
let resized = Signal.uresize a ~width:10 (* Resize to 10 bits *)
Bits vs Signals
Bits.t
: Concrete values for simulation and testingSignal.t
: Abstract representation for circuit description
Basic Operations
(* Arithmetic *)
let sum = a +: b (* Addition (same width) *)
let sum_const = a +:. 10 (* Signal + constant *)
let diff = a -: b (* Subtraction *)
let prod = a *: b (* Multiplication *)
(* Signed vs unsigned *)
let signed_sum = a +:+ b (* Signed addition *)
let unsigned_sum = a +: b (* Unsigned addition *)
(* Bitwise operations *)
let and_result = a &: b (* Bitwise AND *)
let or_result = a |: b (* Bitwise OR *)
let xor_result = a ^: b (* Bitwise XOR *)
let not_result = ~: a (* Bitwise NOT *)
(* Comparisons *)
let eq = a ==: b (* Equality *)
let lt = a <: b (* Less than *)
let gte = a >=: b (* Greater than or equal *)
(* Multiplexers *)
let mux_result = Signal.mux2 select a b (* if select then a else b *)
let mux_multi = Signal.mux select_signal [a; b; c; d]
(* Bit selection *)
let upper_bits = Signal.select a ~hi:7 ~lo:4 (* Select bits 7-4 *)
let bit_slice = a.:(7, 4) (* Infix syntax *)
Module Definitions and Interfaces
Interface Pattern (ppx_deriving_hardcaml)
module I = struct
type 'a t = {
clock : 'a;
reset : 'a;
enable : 'a;
data : 'a; [@bits 8]
} [@@deriving sexp_of, hardcaml]
end
module O = struct
type 'a t = {
ready : 'a;
result : 'a; [@bits 16]
} [@@deriving sexp_of, hardcaml]
end
let create (inputs : Signal.t I.t) : Signal.t O.t =
let { I.clock; enable; data } = inputs in
let processed_data = data +: (Signal.of_int ~width:8 1) in
let result = Signal.reg (Signal.Reg_spec.create ~clock ()) enable processed_data in
{ O.ready = enable; result }
Interface Attributes
type 'a t = {
signal : 'a; [@bits 6] (* Signal width *)
signal_list : 'a list; [@length 2] (* List length *)
signal_array : 'a array; [@length 3] [@bits 7] (* Array with bit width *)
named_signal : 'a [@rtlname "custom_name"] (* Custom RTL name *)
} [@@deriving sexp_of, hardcaml]
Parameterized Modules with Functors
module type Config = sig
val data_width : int
val addr_width : int
end
module Make (C : Config) = struct
module I = struct
type 'a t = {
data : 'a; [@bits C.data_width]
addr : 'a; [@bits C.addr_width]
} [@@deriving sexp_of, hardcaml]
end
let create inputs = (* implementation *)
end
Combinational Logic Constructs
Adder Example
module Adder = struct
module I = struct
type 'a t = {
a : 'a; [@bits 8]
b : 'a; [@bits 8]
cin : 'a;
} [@@deriving sexp_of, hardcaml]
end
module O = struct
type 'a t = {
sum : 'a; [@bits 8]
cout : 'a;
} [@@deriving sexp_of, hardcaml]
end
let create (i : _ I.t) : _ O.t =
let result = i.a +: i.b +: (zero 7 @: i.cin) in
{ O.sum = select result ~high:7 ~low:0;
cout = select result ~high:8 ~low:8 }
end
Tree Operations
(* Tree adder for multiple inputs *)
let tree_adder inputs =
Signal.tree ~arity:2 ~f:(+:) inputs
(* Tree reduction *)
let tree_and inputs =
Signal.tree ~arity:2 ~f:(&:) inputs
Sequential Logic (Registers, Memories, State Machines)
Register Creation
(* Register specification *)
let reg_spec =
Signal.Reg_spec.create
~clock
~reset
~clear
()
(* Basic register *)
let reg_out = Signal.reg reg_spec enable data_in
(* Register with feedback *)
let counter = Signal.reg_fb reg_spec ~enable ~w:8 (fun d -> d +:. 1)
Memory Primitives
(* Basic memory *)
let memory_out = Signal.memory size ~write_port ~read_address
(* RAM with read/write ports *)
let ram_out = Signal.ram_wbr
~write_port:{ write_clock; write_address; write_data; write_enable }
~read_port:{ read_clock; read_address; read_enable }
size
(* Multi-port memory *)
let memory = multiport_memory ~clock
~write_ports:[{
write_clock = clk;
write_enable = we;
write_address = addr;
write_data = data;
}]
State Machines with Always DSL
module States = struct
type t =
| Idle
| Processing
| Done
[@@deriving sexp_of, compare, enumerate]
end
let create_state_machine (inputs : Signal.t I.t) =
let open Signal.Always in
let sm = Always.State_machine.create (module States) ~enable:inputs.enable in
let counter = Always.Variable.reg ~width:8 spec in
compile [
sm.switch [
Idle, [
when_ inputs.start [
sm.set_next Processing;
counter <-- zero 8;
];
];
Processing, [
counter <-- (counter.value +:. 1);
when_ (counter.value ==:. 100) [
sm.set_next Done;
];
];
Done, [
sm.set_next Idle;
];
]
]
Simulation and Testing
Basic Simulation
(* Create simulator *)
let sim = Cyclesim.create (Circuit.create_exn ~name:"test" outputs)
(* Drive inputs and simulate *)
let inputs = Cyclesim.inputs sim
let outputs = Cyclesim.outputs sim
inputs.clock := Bits.vdd;
inputs.data := Bits.of_int ~width:8 42;
Cyclesim.cycle sim;
let result = !(outputs.result)
Expect Test with Waveforms
let%expect_test "counter test" =
let sim = create_counter_sim () in
let waves, sim = Waveform.create sim in
(* simulation steps *)
for i = 0 to 5 do
Cyclesim.cycle sim
done;
Waveform.print ~display_height:12 ~display_width:70 waves;
[%expect {|
βSignalsββββββββββWavesβββββββββββββββββββββββββββββββββββββββββββββββ
βclock βββββββ βββββ βββββ βββββ βββββ βββββ ββββ
βenable ββ βββββ βββββ βββββ βββββ βββββ βββββ β
βcount 3 βββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ
β ββ 000 β 001 β 010 β 011 β 100 β 101 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|}]
High-Performance Simulation Backends
(* Hardcaml_verilator - Ultra-high speed *)
let verilator_sim = Hardcaml_verilator.create circuit
(* Hardcaml_c - C-based simulation *)
let c_sim = Hardcaml_c.create circuit
(* All use same API as standard Cyclesim *)
Synthesis and Compilation
RTL Generation
(* Verilog generation *)
let () =
let circuit = Circuit.create_exn ~name:"my_design" Top.create in
Hardcaml.Rtl.Verilog.print ~output_file:"design.v" circuit
(* VHDL generation *)
let () =
let circuit = Circuit.create_exn ~name:"my_design" Top.create in
Hardcaml.Rtl.Vhdl.print ~output_file:"design.vhd" circuit
Circuit Creation
let circuit =
let module Circuit = Hardcaml.Circuit.With_interface(Top.I)(Top.O) in
Circuit.create_exn ~name:"top_level" Top.create
Common Patterns and Idioms
Pipeline Pattern
let pipeline_stage ~spec input =
let stage1 = reg spec ~enable:vdd (process_stage1 input) in
let stage2 = reg spec ~enable:vdd (process_stage2 stage1) in
let stage3 = reg spec ~enable:vdd (process_stage3 stage2) in
stage3
Counter Pattern
let counter ~clock ~clear ~enable count_to =
let reg_spec = Signal.Reg_spec.create ~clock ~clear () in
let counter_reg = Signal.wire (Signal.width count_to) in
let counter_next = Signal.mux2
(counter_reg ==: count_to)
(Signal.of_int ~width:(Signal.width count_to) 0)
(counter_reg +:. 1) in
Signal.assign counter_reg (Signal.reg reg_spec enable counter_next);
counter_reg
FIFO Pattern
let fifo ~clock ~clear ~write_enable ~read_enable ~write_data ~depth =
let addr_width = Int.ceil_log2 depth in
let mem = Signal.memory depth ~write_port:{
write_clock = clock;
write_enable = write_enable;
write_address = write_addr;
write_data = write_data;
} in
mem ~read_address:read_addr
Error Handling and Debugging
Width Validation
(* Compile-time width checking *)
let safe_add a b =
if Signal.width a <> Signal.width b then
failwith "Width mismatch"
else
a +: b
Assertion-based Debugging
let counter ~spec ~enable ~clear =
let count = reg spec ~enable ~clear (count +:. 1) in
(* Add assertion for overflow detection *)
assert (count <:. 256);
count
Interactive Debugging
let debug_circuit () =
let sim = Cyclesim.create ~config:Cyclesim.Config.trace_all circuit in
let waves, sim = Waveform.create sim in
(* Run simulation *)
for i = 0 to 100 do
Cyclesim.cycle sim
done;
(* Launch interactive viewer *)
Hardcaml_waveterm_interactive.run ~wave_width:5 ~signals_width:30 waves
Performance Considerations
Memory Usage Optimization
(* Use appropriate memory primitives *)
let efficient_memory = Signal.memory ~size:1024 (* for inference *)
(* FIFO for streaming *)
let streaming_fifo = Signal.fifo ~depth:32 ~write_port ~read_port
Timing Optimization
(* Tree structures for critical paths *)
let tree_adder inputs =
Signal.tree ~arity:2 ~f:(+:) inputs
(* Pipeline critical paths *)
let pipelined_mult a b =
let spec = Signal.Reg_spec.create ~clock () in
let stage1 = Signal.reg spec ~enable:vdd (a *: b) in
let stage2 = Signal.reg spec ~enable:vdd (stage1 +:. offset) in
stage2
Code Examples for Major Concepts
FIR Filter
let fir_filter coeffs data_in ~clock ~enable =
let reg_spec = Signal.Reg_spec.create ~clock () in
let shift_reg = List.init (List.length coeffs) ~f:(fun i ->
Signal.reg reg_spec enable
(if i = 0 then data_in else List.nth_exn shift_reg (i-1))
) in
let products = List.map2_exn coeffs shift_reg ~f:(fun coeff data ->
Signal.of_int ~width:16 coeff *: data
) in
Signal.tree ~arity:2 products ~f:(+:)
Linear Feedback Shift Register (LFSR)
let lfsr ~width ~taps ~clock ~enable =
let reg_spec = Signal.Reg_spec.create ~clock () in
let state = Signal.wire width in
let feedback = List.fold taps ~init:(Signal.of_int ~width:1 0) ~f:(fun acc tap ->
acc ^: (Signal.select state ~hi:tap ~lo:tap)
) in
let next_state = Signal.concat_msb [Signal.select state ~hi:(width-2) ~lo:0; feedback] in
Signal.assign state (Signal.reg reg_spec enable next_state);
state
Quick Reference Syntax
Essential Operations
(* Constants *)
let zero_8 = Signal.of_int ~width:8 0
let ones_8 = Signal.of_int ~width:8 255
let vdd = Signal.vdd
let gnd = Signal.gnd
(* Arithmetic *)
let add = a +: b (* Addition *)
let sub = a -: b (* Subtraction *)
let mul = a *: b (* Multiplication *)
let add_const = a +:. 5 (* Add constant *)
(* Logical *)
let and_op = a &: b (* Bitwise AND *)
let or_op = a |: b (* Bitwise OR *)
let xor_op = a ^: b (* Bitwise XOR *)
let not_op = ~: a (* Bitwise NOT *)
(* Comparison *)
let eq = a ==: b (* Equal *)
let neq = a <>: b (* Not equal *)
let lt = a <: b (* Less than *)
let lte = a <=: b (* Less than or equal *)
let gt = a >: b (* Greater than *)
let gte = a >=: b (* Greater than or equal *)
(* Bit manipulation *)
let sel = a.:(7, 0) (* Select bits 7 to 0 *)
let concat = a @: b (* Concatenate *)
let select = Signal.select a ~hi:7 ~lo:0
Register and Memory
(* Register *)
let reg = Signal.reg spec ~enable data
(* Memory *)
let mem = Signal.memory size ~write_port ~read_address
(* State machine *)
let sm = Always.State_machine.create (module States) ~enable
Best Practices and Conventions
Code Organization
Separate interfaces from implementations
Use modules for logical grouping
Keep create functions pure and deterministic
Use descriptive names for signals and modules
Type Safety
(* Custom scalar types for different domains *)
module Types = struct
module Price = Types.Scalar(struct let width = 32 end)
module Quantity = Types.Scalar(struct let width = 16 end)
end
Testing Strategy
Write unit tests for all combinational logic
Use property-based testing with hardcaml_verify
Test edge cases and boundary conditions
Include waveform verification in tests
Naming Conventions
Use
snake_case
for signal namesPrefix with
i_
for inputs,o_
for outputsUse meaningful names that describe the signal's purpose
Advanced Features and Techniques
Hierarchical Design
let create ~scope (i : _ I.t) : _ O.t =
let module Hierarchy = Hierarchy.In_scope (I) (O) in
Hierarchy.hierarchical ~scope ~name:"module_name" (fun _scope ->
(* implementation *)
)
Custom Types
module Price_in_usd = struct
type 'a t = 'a [@@bits 32] [@@deriving sexp_of, hardcaml]
let create = Signal.of_int ~width:32
let is_positive price = price >:. 0
end
Verification and Formal Methods
(* SAT-based verification *)
let verify_adder_properties () =
let verify_commutative =
Verify.prove (fun a b -> (a +: b) ==: (b +: a)) in
let verify_associative =
Verify.prove (fun a b c -> (a +: (b +: c)) ==: ((a +: b) +: c)) in
(* Results in SAT/UNSAT *)
Ecosystem Libraries
Core Libraries
hardcaml_circuits: Arbiters, multipliers, sorting networks, DSP
hardcaml_fixed_point: Fixed-point arithmetic with rounding/overflow control
hardcaml_verify: SAT-based formal verification tools
Simulation Libraries
hardcaml_verilator: Ultra-high speed simulation using Verilator
hardcaml_c: C-based simulation for improved performance
hardcaml_waveterm: ASCII waveform visualization
hardcaml_step_testbench: Monadic testbench API
Xilinx Libraries
hardcaml_xilinx: Xilinx-specific FPGA components
hardcaml_xilinx_components: Tool to generate OCaml from Xilinx libraries
Integration Libraries
hardcaml_of_verilog: Import Verilog designs into Hardcaml
Last updated