6.2 Serial Multipler

May 15, 2026 · View on GitHub

We now show a complete example which implements a bit-serial unsigned multiplier. We will both implement the design and show how to test it.

Algorithm

Given two input numbers of bit-widths N and M, a bit serial multiplier will produce a result of size M+N every M clock cycles.

Given two input values a and b, we consider each bit of b in turn and form a partial product.

if b[i] then (a << i) else 0

This then gets added to a running sum. Let's start with a simple OCaml implementation of this algorithm.

# open Base
# let rec umul a b =
  if b=0 then 0
  else
    let partial_product = if (b land 1) = 1 then a else 0 in
    partial_product + umul (a lsl 1) (b lsr 1)
val umul : int -> int -> int = <fun>
# umul 3 5
- : int = 15
# umul 100 99
- : int = 9900

Note that we do not explicitly track the iteration number. Rather we detect when to stop by shifting b down on each iteration and checking if it is 0. Similarly, the partial product term a << i is computed by shifting up by one each iteration. A similar set of tricks will be used to define an efficient hardware implementation.

Before we turn fully to the hardware implementation, let's try to port the above code to the Hardcaml Bits type.

# open Hardcaml
# open Hardcaml.Bits
# let rec umul a b =
  if to_unsigned_int b = 0 then zero (width a)
  else
    let partial_product = mux2 b.:[0,0] a (zero (width a)) in
    partial_product +: umul (sll a ~by:1) (srl b ~by:1)
val umul : t -> t -> t = <fun>
# to_unsigned_int (umul (of_unsigned_int ~width:2 3) (of_unsigned_int ~width:3 5))
- : int = 3

The reason this hasn't worked is we are computing the running sum and partial product terms using the bit-width of a. We need to also include the width of b.

# let umul a b = umul (uresize a ~width:(width a + width b)) b
val umul : t -> t -> t = <fun>
# to_unsigned_int (umul (of_unsigned_int ~width:2 3) (of_unsigned_int ~width:3 5))
- : int = 15
# to_unsigned_int (umul (of_unsigned_int ~width:7 100) (of_unsigned_int ~width:7 99))
- : int = 9900

Hardware implementation

We start by implementing functions for the partial product and running sum.

# open Hardcaml.Signal
# let partial_product a b0 =
    mux2 b0 a (zero (width a))
val partial_product : Type.t -> Type.t -> Type.t = <fun>
# let running_sum first sum a b0 =
    let sum = mux2 first (zero (width sum)) sum in
    ue sum +: (ue (partial_product a b0))
val running_sum : Type.t -> Type.t -> Type.t -> Type.t -> Type.t = <fun>

The running_sum function takes a new argument called first. This is used to indicate when we are processing bit 0 of b and clears the initial sum to 0.

The inputs to the addition in running_sum are of width a and are zero-extended by the ue function to produce a result of width a + 1. Didn't we need more precision than this in the Bits implementation? We will avoid this by outputting a fully computed bit at each iteration.

# let running_sum_reg spec first a b0 =
    let sum_w = wire (width a) -- "running_sum" in
    let running_sum = running_sum first sum_w a b0  -- "running_sum_next" in
    (* Split the sum into it's least significant bit and the rest. *)
    let running_sum_bit_out = lsb running_sum in
    let running_sum = msbs running_sum in
    (* Register the sum *)
    let sum = reg spec ~enable:vdd running_sum in
    sum_w <-- sum;
    sum, running_sum_bit_out
val running_sum_reg :
  Reg_spec.t -> Type.t -> Type.t -> Type.t -> Type.t * Type.t = <fun>

We also need to store the computed bits in a register.

# let computed_bits spec width bit =
   reg_fb spec ~width ~f:(fun d -> bit @: msbs d)
val computed_bits : Reg_spec.t -> int -> Type.t -> Type.t = <fun>

The final implementation just needs to put these functions together.

# let umul_sequential clock first a b_width b0 =
    let spec = Reg_spec.create ~clock () in
    let running_sum, computed_bit = running_sum_reg spec first a b0 in
    let computed_bits = computed_bits spec b_width computed_bit in
    running_sum @: computed_bits
val umul_sequential : Type.t -> Type.t -> Type.t -> int -> Type.t -> Type.t =
  <fun>

Testbench

We will now test our multiplier. First, let's create the required Circuit.t.

# let create_circuit a_width b_width =
    let clock = input "clock" 1 in
    let first = input "first" 1 in
    let a = input "a" a_width in
    let b0 = input "b0" 1 in
    let result = umul_sequential clock first a b_width b0 in
    Circuit.create_exn ~name:"umul" [ output "result" result ]
val create_circuit : int -> int -> Circuit.t = <fun>

We can now create a simulation and waveform and get a handle on the input and output ports.

# module Waveform = Hardcaml_waveterm.Waveform
module Waveform = Hardcaml_waveterm.Waveform
# let create_sim circuit =
    let sim = Cyclesim.create ~config:Cyclesim.Config.trace_all circuit in
    let waves, sim = Cyclesim.Waveform.create sim in
    let first = Cyclesim.in_port sim "first" in
    let a = Cyclesim.in_port sim "a" in
    let b0 = Cyclesim.in_port sim "b0" in
    let result = Cyclesim.out_port sim "result" in
    waves, sim, first, a, b0, result
val create_sim :
  Circuit.t ->
  Waveform.t * (Cyclesim.Port_list.t, Cyclesim.Port_list.t) Cyclesim.t *
  Binary.t ref * Binary.t ref * Binary.t ref * Binary.t ref = <fun>

The following testbench will take a and b and create a circuit adapted to their bit widths. It will then perform width b iterations and return the final result.

# let test a_in b_in =
    let open Bits in
    let waves, sim, first, a, b0, result =
      create_circuit (width a_in) (width b_in)
      |> create_sim
    in
    let step iteration =
      first := if iteration=0 then vdd else gnd;
      b0 := b_in.:[iteration,iteration];
      Cyclesim.cycle sim;
    in
    a := a_in;
    for i=0 to width b_in - 1 do
      step i
    done;
    (* grab the result and perform 1 more cycle so we can see the result in the waveform *)
    let result = !result in
    Cyclesim.cycle sim;
    waves, result
val test : Binary.t -> Binary.t -> Waveform.t * Binary.t = <fun>

Let's test our running examples of multiplying 3*5 and 100*99.

let waves, result = test (Bits.of_unsigned_int ~width:2 3) (Bits.of_unsigned_int ~width:3 5);;
# Stdio.printf "%i" (Bits.to_unsigned_int result)
15
- : unit = ()
# Waveform.print waves
┌Signals────────┐┌Waves──────────────────────────────────────────────┐
│clock          ││┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌──│
│               ││    └───┘   └───┘   └───┘   └───┘   └───┘   └───┘  │
│               ││────────────────────────────────                   │
│a              ││ 3                                                 │
│               ││────────────────────────────────                   │
│b0             ││────────┐       ┌───────────────                   │
│               ││        └───────┘                                  │
│first          ││────────┐                                          │
│               ││        └───────────────────────                   │
│               ││────────┬───────┬───────┬───────                   │
│result         ││ 00     │0C     │06     │0F                        │
│               ││────────┴───────┴───────┴───────                   │
│gnd            ││                                                   │
│               ││────────────────────────────────                   │
│               ││────────┬───────┬───────┬───────                   │
│running_sum    ││ 0      │1      │0      │1                         │
│               ││────────┴───────┴───────┴───────                   │
│               ││────────┬───────┬───────┬───────                   │
│running_sum_nex││ 3      │1      │3      │4                         │
│               ││────────┴───────┴───────┴───────                   │
└───────────────┘└───────────────────────────────────────────────────┘
- : unit = ()
# let _, result = test (Bits.of_unsigned_int ~width:7 100) (Bits.of_unsigned_int ~width:7 99)
val result : Binary.t = 10011010101100
# Stdio.printf "%i" (Bits.to_unsigned_int result)
9900
- : unit = ()