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 testing

  • Signal.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

  1. Separate interfaces from implementations

  2. Use modules for logical grouping

  3. Keep create functions pure and deterministic

  4. 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

  1. Write unit tests for all combinational logic

  2. Use property-based testing with hardcaml_verify

  3. Test edge cases and boundary conditions

  4. Include waveform verification in tests

Naming Conventions

  • Use snake_case for signal names

  • Prefix with i_ for inputs, o_ for outputs

  • Use 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