5.4 Simulating with Interfaces
March 30, 2026 ยท View on GitHub
With standard Hardcaml simulations we are forced to manage input and output ports manually using the strings. Interfaces allow us to automate much of this work and are particularly useful as circuits get more complex.
The following is a common pattern for specifying a hardware circuit. It consists of an input and output interface, and a function over these interfaces which constructs the logic.
open Base
open Hardcaml
(* Input interface. *)
module I = struct
type 'a t =
{ clock : 'a
; foo : 'a [@bits 8]
; bar : 'a [@bits 8]
}
[@@deriving hardcaml]
end
(* Output interface. *)
module O = struct
type 'a t =
{ baz : 'a [@bits 8]
; baz_delayed : 'a [@bits 8]
}
[@@deriving hardcaml]
end
let create (i : Signal.t I.t) : Signal.t O.t =
let open Signal in
let spec = Reg_spec.create ~clock:i.clock () in
let baz = i.foo +: i.bar in
let baz_delayed = reg ~enable:vdd spec baz in
{ O. baz ; baz_delayed }
;;
To simulate this, we can use the
Cyclesim.With_interface
functor. This will automatically create a circuit with the input and output ports
labeled, build a simulator, and then construct input and output records for driving the
simulator. All this and we never have to worry about the underlying string names of ports.
# let create_sim () =
let module Sim = Cyclesim.With_interface(I)(O) in
Sim.create create
;;
val create_sim : unit -> (Bits.t ref I.t, Bits.t ref O.t) Cyclesim.t = <fun>
Notice the type signature of (_, _) Cyclesim.t. The parametric type arguments to
Cyclesim.t
encode the types returned when retrieving the inputs and output values from the simulator
object.
Driving the inputs and reading outputs can be performed via the convenience of record fields.
# let run_sim () =
let sim = create_sim () in
let inputs = Cyclesim.inputs sim in
let outputs = Cyclesim.outputs sim in
let print_outputs () =
Stdio.print_s (
[%sexp_of: int O.t]
(O.map outputs ~f:(fun p -> Bits.to_unsigned_int !p)))
in
inputs.foo := Bits.of_unsigned_int ~width:8 1;
inputs.bar := Bits.of_unsigned_int ~width:8 2;
Cyclesim.cycle sim;
print_outputs ();
inputs.foo := Bits.of_unsigned_int ~width:8 7;
inputs.bar := Bits.of_unsigned_int ~width:8 9;
Cyclesim.cycle sim;
print_outputs ();
;;
val run_sim : unit -> unit = <fun>
# run_sim ()
((baz 3) (baz_delayed 3))
((baz 16) (baz_delayed 16))
- : unit = ()