6.5 Binary Search

July 10, 2026 · View on GitHub

Binary Search

Binary search allows us to find an element in a sorted array of length N in log2(N) steps.

It starts by considering the full range of the array 0..N-1 and testing the mid-point at N/2. If the element is found we are done. Otherwise if the element at the midpoint is greater than the element we are looking for we repeat with the range set to 0..N/2. If it is less we use the range N/2..N. This continues until the element is found, or the range becomes empty.

Designing the Hardware Interface

The elements of the array we search must contain a key value by which it is sorted and that the binary search algorithm can use.

In Ocaml we could represent each element as:

module type Data = sig 
    type t
    
    val key : t -> int
end

In Hardcaml we can take a similar approach.

module type Data = sig
  include Interface.S

  val key : Signal.t t -> Signal.t
end

The hardware design also needs to know the size of the array we are searching and the number of bits in the key. Thus we define the following Config type.

module type Config = sig
  module Data : Data

  val log_size : int
  val key_size : int
end

The hardware design will be fully parameterized by Config by implementing it within a functor.

module Make(Config : Config) = struct
 (* ... hardware implemented here ... *)
end

The interface to the design is specified as follows

  module Data_with_valid = With_valid.Wrap.Make (Config.Data)

  module I = struct
    type 'a t =
      { clocking : 'a Clocking.t (* Clock and clear. *)
      ; start : 'a (* Pulsed to start search. *)
      ; d : 'a Config.Data.t (* Data read at `address`. *)
      ; find_key : 'a [@bits Config.key_size] (* Key we are looking for. *)
      }
    [@@deriving hardcaml]
  end

  module O = struct
    type 'a t =
      { done_ : 'a (* Low while searching. *)
      ; index : 'a [@bits Config.log_size] (* Index where key was found. *)
      ; q : 'a Data_with_valid.t (* Data where key was found. *)
      ; address : 'a [@bits Config.log_size] (* Address into search array. *)
      }
    [@@deriving hardcaml]
  end

The interface consists of the following logical parts.

Clocking

Clocking is an interface which groups together the clock and clear signals.

Control

start is used to begin the search and done_ indicates when a search is in progress.

find_key is the key we are searching for and must be provided coincident with start and be held during the search operation.

Array access

address is the current index we are searching the array at and d is the data at that address.

Output result

index is the address at which the element was found.

q is the data we found. The type of q is Data_with_valid which was defined as:

  module Data_with_valid = With_valid.Wrap.Make (Config.Data)

With_valid is common pattern in Hardcaml code. It wraps a signal or, as in this case, an Interface with an additional valid bit. If valid is high the search succeeded and if it is low the search failed (the element we were looking for did not exist in the input array).

Implementing the Search

  module State = struct
    type t =
      | Start
      | Loop
    [@@deriving sexp_of, compare ~localize, enumerate]
  end

  let create scope (i : _ I.t) =
    let%hw.Always.State_machine sm =
      Always.State_machine.create (module State) (Clocking.to_spec i.clocking)
    in
    (* search range *)
    let%hw_var low = Clocking.Var.reg i.clocking ~width:(Config.log_size + 1) in
    let%hw_var mid = Clocking.Var.reg i.clocking ~width:(Config.log_size + 1) in
    let%hw_var high = Clocking.Var.reg i.clocking ~width:(Config.log_size + 1) in
    (* results *)
    let index = Clocking.Var.reg i.clocking ~width:Config.log_size in
    let q = Data_with_valid.Of_always.reg (Clocking.to_spec i.clocking) in
    let%hw key = Config.Data.key i.d in
    Always.(
      compile
        [ sm.switch
            [ ( Start
              , [ low <--. 0
                ; mid <--. Int.pow 2 Config.log_size / 2
                ; high <--. Int.pow 2 Config.log_size
                ; when_ i.start [ q.valid <-- gnd; sm.set_next Loop ]
                ] )
            ; ( Loop
              , [ (* success *)
                  if_
                    (i.find_key ==: key)
                    [ q.valid <-- vdd
                    ; index <-- lsbs mid.value
                    ; Config.Data.Of_always.assign q.value i.d
                    ; sm.set_next Start
                    ]
                  (* failure *)
                  @@ elif (mid.value ==: low.value) [ q.valid <-- gnd; sm.set_next Start ]
                  (* search left half *)
                  @@ elif
                       (i.find_key <: key)
                       [ high <-- mid.value
                       ; mid <-- low.value +: srl (mid.value -: low.value) ~by:1
                       ]
                  @@ (* search right half *)
                  [ low <-- mid.value
                  ; mid <-- mid.value +: srl (high.value -: mid.value) ~by:1
                  ]
                ] )
            ]
        ]);
    { O.done_ = sm.is Start
    ; index = index.value
    ; q = Data_with_valid.Of_always.value q
    ; address = lsbs mid.value
    }
  ;;

When start is applied we set the initial range and the state machine transitions to the Loop state. The mid point is output as address and the current element is checked:

  • if we found the key the search succeeds. q.valid is set high and the index and data are output.
  • otherwise, if the mid point equals the low point, the search range is 0 and the search fails. q.valid is set low.
  • otherwise, if the search key is less than the current key, search the left half.
  • otherwise, (if the search key is greater then the current key) search the right half.

Testing

Adding the search array

To make the testbench simpler to implement we are going to create a top level architecture which include the search array memory.

The interface to this design has a write port for loading the search data along with the start and find_key values. The output is the same as before except we don't output address and wire this directly into the memory.

The implementation just instantiates the search array memory and binary search module.

module Make_with_memory (Config : Config) = struct
  module Binary_search = Make (Config)

  module I = struct
    type 'a t =
      { clocking : 'a Clocking.t
      ; write_enable : 'a
      ; write_data : 'a Config.Data.t
      ; write_address : 'a [@bits Config.log_size]
      ; start : 'a
      ; find_key : 'a [@bits Config.key_size]
      }
    [@@deriving hardcaml]
  end

  module O = struct
    type 'a t =
      { done_ : 'a
      ; index : 'a [@bits Config.log_size]
      ; q : 'a Binary_search.Data_with_valid.t
      }
    [@@deriving hardcaml]
  end

  let create scope (i : _ I.t) =
    let read_address = wire Config.log_size in
    let q =
      multiport_memory
        (Int.pow 2 Config.log_size)
        ~write_ports:
          [| { write_clock = i.clocking.clock
             ; write_address = i.write_address
             ; write_enable = i.write_enable
             ; write_data = Config.Data.Of_signal.pack i.write_data
             }
          |]
        ~read_addresses:[| read_address |]
    in
    let binary_search =
      Binary_search.create
        scope
        { Binary_search.I.clocking = i.clocking
        ; start = i.start
        ; d = Config.Data.Of_signal.unpack q.(0)
        ; find_key = i.find_key
        }
    in
    read_address <-- binary_search.address;
    { O.done_ = binary_search.done_; index = binary_search.index; q = binary_search.q }
  ;;
end

Writing the testbench

Configuring the testbench

Define the Data interface we will search over and the size of the array to search.

module Data = struct
  let key_size = 6

  type 'a t =
    { key : 'a [@bits key_size]
    ; data : 'a [@bits 32]
    }
  [@@deriving hardcaml]

  let key t = t.key
end

module Config = struct
  module Data = Data

  let key_size = Data.key_size
  let log_size = 4
end

The array size will be 16 elements. The key is 6 bits so ranges from 0 to 63.

Test functions

The testbench is implemented by the following functions.

  • clear_core - clear (reset) the registers.
  • load_inputs - load the array to search.
  • run_core - start the search, wait for it to complete then return the results.
let clear_core (sim : Sim.t) =
  let inputs = Cyclesim.inputs sim in
  inputs.clocking.clear <--. 1;
  Cyclesim.cycle sim;
  inputs.clocking.clear <--. 0
;;

let load_inputs (sim : Sim.t) (input_data : int Data.t array) =
  let inputs = Cyclesim.inputs sim in
  inputs.write_enable <--. 1;
  for i = 0 to Int.pow 2 Config.log_size - 1 do
    inputs.write_address <--. i;
    inputs.write_data.key <--. input_data.(i).key;
    inputs.write_data.data <--. input_data.(i).data;
    Cyclesim.cycle sim
  done
;;

let run_core (sim : Sim.t) find_key =
  let inputs = Cyclesim.inputs sim in
  let outputs = Cyclesim.outputs sim in
  (* Start the search. *)
  inputs.start <--. 1;
  Cyclesim.cycle sim;
  inputs.start <--. 0;
  inputs.find_key <--. find_key;
  (* Wait for it to finish. Don't let it take more than 20 cycles. *)
  let timeout = ref 0 in
  while (not (Bits.to_bool !(outputs.done_))) && !timeout < 20 do
    Cyclesim.cycle sim;
    Int.incr timeout
  done;
  Cyclesim.cycle sim;
  (* Collect and return results. *)
  let found = Bits.to_bool !(outputs.q.valid) in
  if found
  then (
    let index = Bits.to_unsigned_int !(outputs.index) in
    let data = Bits.to_unsigned_int !(outputs.q.value.data) in
    let key = Bits.to_unsigned_int !(outputs.q.value.key) in
    (* Sanity check - make sure the key we found was actually the key we were searching
       for. *)
    if key <> find_key
    then
      raise_s
        [%message
          "Found wrong key" (find_key : int) (key : int) (index : int) (data : int)];
    Some (index, data))
  else None
;;

Testbench

let validate ~(input_data : int Data.t array) ~find_key ~result =
  let key_is_in_input_data =
    match Array.find input_data ~f:(fun { key; _ } -> key = find_key) with
    | None -> false
    | _ -> true
  in
  let raise_search_failed () =
    raise_s
      [%message
        "Search failed"
          (find_key : int)
          (key_is_in_input_data : bool)
          (result : (int * int) option)
          (input_data : int Config.Data.t array)]
  in
  match result, key_is_in_input_data with
  | None, true ->
    (* The hardware didn't find the element but it was there. *)
    raise_search_failed ()
  | Some _, false ->
    (* The hardware found an element but it wasn't there. *)
    raise_search_failed ()
  | None, false ->
    (* Nothing to find. *)
    ()
  | Some (index, data), true ->
    (* We found the element - check the key and data are as expected. *)
    if input_data.(index).key <> find_key || input_data.(index).data <> data
    then raise_search_failed ()
;;

let test ~waves ~input_data ~find_key =
  let open Hardcaml_waveterm in
  (* Create the simulator. *)
  let scope = Scope.create ~auto_label_hierarchical_ports:true ~flatten_design:true () in
  let sim = Sim.create ~config:Cyclesim.Config.trace_all (Binary_search.create scope) in
  (* Optionally trace a waveform. *)
  let waves, sim = Cyclesim.Waveform.create_if ~enabled:waves sim in
  (* Run the testbench. *)
  clear_core sim;
  load_inputs sim input_data;
  let result = run_core sim find_key in
  (* Optionally show the waveform. *)
  Option.iter
    waves
    ~f:(Waveform.expect_exact ~wave_width:3 ~start_cycle:16 ~display_rules);
  (* Check the results are valid. *)
  validate ~input_data ~find_key ~result
;;

Randomized testing

We can now perform some randomized testing. We create an array of random Data elements and sort it. We then search for some random keys in the array. If it doesn't raise all is well.

let%expect_test "random tests" =
  for _ = 1 to 10 do
    let input_data =
      Array.init (Int.pow 2 Config.log_size) ~f:(fun _ ->
        { Config.Data.key = random_key (); data = random_data () })
    in
    Array.sort input_data ~compare:(fun { key; _ } { key = key1; _ } ->
      Int.compare key key1);
    for _ = 1 to 20 do
      let find_key = random_key () in
      test ~waves:false ~input_data ~find_key
    done
  done;
  [%expect {| |}]
;;

Waveform

let%expect_test "debug waveform" =
  let input_data =
    Array.init (Int.pow 2 Config.log_size) ~f:(fun i -> { Config.Data.key = i; data = i })
  in
  let find_key = 3 in
  test ~waves:true ~input_data ~find_key;
  [%expect_exact
    {|
┌Signals───────────┐┌Waves───────────────────────────────────────────────────────────────┐
│clock             ││┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───│
│                  ││    └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   │
│clear             ││                                                                    │
│                  ││────────────────────────────────────────────────────────            │
│write_enable      ││────────────────────────────────────────────────────────            │
│                  ││                                                                    │
│                  ││────────────────────────────────────────────────────────            │
│write_data$key    ││ 15                                                                 │
│                  ││────────────────────────────────────────────────────────            │
│                  ││────────────────────────────────────────────────────────            │
│write_data$data   ││ 15                                                                 │
│                  ││────────────────────────────────────────────────────────            │
│                  ││────────────────────────────────────────────────────────            │
│write_address     ││ 15                                                                 │
│                  ││────────────────────────────────────────────────────────            │
│start             ││        ┌───────┐                                                   │
│                  ││────────┘       └───────────────────────────────────────            │
│                  ││────────────────┬───────────────────────────────────────            │
│find_key          ││ 0              │3                                                  │
│                  ││────────────────┴───────────────────────────────────────            │
│done_             ││────────────────┐                               ┌───────            │
│                  ││                └───────────────────────────────┘                   │
│                  ││────────────────────────────────────────────────┬───────            │
│index             ││ 0                                              │3                  │
│                  ││────────────────────────────────────────────────┴───────            │
│q$valid           ││                                                ┌───────            │
│                  ││────────────────────────────────────────────────┘                   │
│                  ││────────────────────────────────────────────────┬───────            │
│q$value$key       ││ 0                                              │3                  │
│                  ││────────────────────────────────────────────────┴───────            │
│                  ││────────────────────────────────────────────────┬───────            │
│q$value$data      ││ 0                                              │3                  │
│                  ││────────────────────────────────────────────────┴───────            │
│                  ││────────────────────────────────────────┬───────────────            │
│low               ││ 0                                      │2                          │
│                  ││────────────────────────────────────────┴───────────────            │
│                  ││────────────────────────┬───────┬───────┬───────────────            │
│mid               ││ 8                      │4      │2      │3                          │
│                  ││────────────────────────┴───────┴───────┴───────────────            │
│                  ││────────────────────────┬───────┬───────────────────────            │
│high              ││ 16                     │8      │4                                  │
│                  ││────────────────────────┴───────┴───────────────────────            │
│                  ││────────────────┬───────────────────────────────┬───────            │
│sm                ││ Start          │Loop                           │Start              │
│                  ││────────────────┴───────────────────────────────┴───────            │
└──────────────────┘└────────────────────────────────────────────────────────────────────┘
c55f6d1c162be347d7d0654ae22a7310
|}]
;;