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 connection

  • It 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:

Feature
Wire
Register

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:

  1. Bit Vectors - Basic type representing N-bit wide wire or register

  2. Combinational Expressions - Composed logic like adders, muxes, gates

  3. Constants - Created using Signal.of_int, Signal.of_string, Signal.gnd (zero), Signal.vdd (one)

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

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:

Concept
Signal πŸ—οΈ (Static structure)
Bits 🎭 (Dynamic values)

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)

Decimal
Binary

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 signal

  • enable: 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 edge

  • If enable = 0: Register holds its previous value

Example behavior:

Clock Cycle
enable
Register Before
Register After

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} *)
j
k
j @: k (binary)
Decimal

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 address

  • Can 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:

  1. make - Create signal bundles

    let inputs = I.make Signal.input
    (* Creates: { a = input "a" 8; b = input "b" 7 } *)
  2. map - Transform each field

    let incremented = I.map inputs ~f:(fun x -> x +:. 1)
  3. map2 - Combine two interfaces field by field

    let sum = I.map2 inputs1 inputs2 ~f:(+:)
  4. to_bits/of_bits - Convert between structured records and flat bitvectors

  5. port_names_and_widths - Get metadata

    I.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:

  1. Evaluates all combinational logic

  2. Updates all registers on clock edge

  3. 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 add open 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 loops

  • Connect 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:

Cycle
clear
counter (binary)
counter (decimal)
Notes

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:

  1. Sets input signals based on parameters

  2. Prints current output value (before the clock)

  3. 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 0

  • Signal.vdd - Logic 1 (VDD = supply voltage)

  • Signal.zero width - Zero of specific width

  • Signal.ones width - All ones of specific width

Read and Write Port Details

Write Port Fields:

  • write_enable: When high, enables writing on clock edge

  • address: Target memory location

  • data: 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