Cycle Accurate Simulation

I'll start with a short demonstration of the simulator followed by some of the internal details.

First of all we need something to simulate. To keep things simple it'll be an basic up-down counter.

To start we define the input and output interfaces to the counter using the syntax extension.


In [1]:
module Up_down_counter_in = interface
    clear[1] up[1] down[1]
end
module Up_down_counter_out = interface
    count[8]
end


Out[1]:
module Up_down_counter_in :
  sig
    type 'a t = { clear : 'a; up : 'a; down : 'a; }
    val t : (bytes * int) t
    val map : ('a -> 'b) -> 'a t -> 'b t
    val map2 : ('a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
    val to_list : 'a t -> 'a list
  end
Out[1]:
module Up_down_counter_out :
  sig
    type 'a t = { count : 'a; }
    val t : (bytes * int) t
    val map : ('a -> 'b) -> 'a t -> 'b t
    val map2 : ('a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
    val to_list : 'a t -> 'a list
  end

Now the design itself


In [2]:
open HardCaml
open Signal.Types
open Signal.Comb
open Signal.Seq

(* basic logic for the counter.  
   increment when up=1, decrement when down=1 *)
let up_down_counter clear up down width = 
    reg_fb 
        { r_sync with reg_clear = clear } 
        (up ^: down) width
        (fun d -> mux2 up (d +:. 1) (d -:. 1))

(* wrap with interfaces *)
let up_down_counter_if i =
    let width = snd Up_down_counter_out.(t.count) in
    let count = Up_down_counter_in.(up_down_counter i.clear i.up i.down width) in
    Up_down_counter_out.( { count } )


Out[2]:
val up_down_counter : t -> t -> t -> int -> t = <fun>
Out[2]:
val up_down_counter_if : t Up_down_counter_in.t -> t Up_down_counter_out.t =
  <fun>

The main logic is in the up_down_counter function. This is then wrapped up with the interfaces using the up_down_counter_if. This little bit of extra work here pays dividends in the next step.


In [3]:
module B = Bits.Comb.IntbitsList
module Builder = Interface.Gen(B)(Up_down_counter_in)(Up_down_counter_out)


Out[3]:
module B = HardCaml.Bits.Comb.IntbitsList
Out[3]:
module Builder :
  sig
    val make :
      bytes ->
      (t Up_down_counter_in.t -> t Up_down_counter_out.t) ->
      Circuit.t * B.t Cyclesim.Api.cyclesim * B.t ref Up_down_counter_in.t *
      B.t ref Up_down_counter_out.t * B.t ref Up_down_counter_out.t
  end

The circuit interfaces have been plugged into the interface machinery from which we can build the simulator.


In [4]:
let circ,sim,i,o,_ = Builder.make "up_down_counter" up_down_counter_if


Out[4]:
val circ : Circuit.t =
  {HardCaml.Circuit.circ_name = "up_down_counter"; circ_id_to_sig = <abstr>;
   circ_inputs =
    [Signal_wire ({s_id = 37L; s_names = ["down"]; s_width = 1; s_deps = []},
      {contents = Signal_empty});
     Signal_wire
      ({s_id = 39L; s_names = ["clear"]; s_width = 1; s_deps = []},
      {contents = Signal_empty});
     Signal_wire ({s_id = 3L; s_names = ["clock"]; s_width = 1; s_deps = []},
      {contents = Signal_empty});
     Signal_wire ({s_id = 38L; s_names = ["up"]; s_width = 1; s_deps = []},
      {contents = Signal_empty})];
   circ_outputs =
    [Signal_wire
      ({s_id = 50L; s_names = ["count"]; s_width = 8; s_deps = []},
      {contents =
        Signal_reg
         ({s_id = 44L; s_names = []; s_width = 8;
           s_deps =
            [Signal_wire
              ({s_id = 41L; s_names = []; s_width = 8; s_deps = []},
              {contents =
                Signal_op
                 ({s_id = 49L; s_names = []; s_width = 8;
                   s_deps =
                    [Signal_wire
                      ({s_id = 38L; s_names = ["up"]; s_width = 1;
                        s_deps = []},
                      {contents = Signal_empty});
                     Signal_op
                      ({s_id = 46L; s_names = []; s_width = 8;
                        s_deps =
                         [<cycle>;
                          Signal_const
                           ({s_id = 45L; s_names = []; s_width = 8;
                             s_deps = []},
                           "00000001")]},
                      Signal_sub);
                     Signal_op
                      ({s_id = 48L; s_names = []; s_width = 8;
                        s_deps =
                         [<cycle>;
                          Signal_const
                           ({s_id = 47L; s_names = []; s_width = 8;
                             s_deps = []},
                           "00000001")]},
                      Signal_add)]},
                 Signal_mux)});
             Signal_wire
              ({s_id = 3L; s_names = ["clock"]; s_width = 1; s_deps = []},
              {contents = Signal_empty});
             Signal_empty;
             Signal_const
              ({s_id = 43L; s_names = []; s_width = 8; s_deps = []},
              "00000000");
             Signal_const
              ({s_id = 1L; s_names = ["vdd"]; s_width = 1; s_deps = []}, "1");
             Signal_wire
              ({s_id = 39L; s_names = ["clear"]; s_width = 1; s_deps = []},
              {contents = Signal_empty});
             Signal_const
              ({s_id = 42L; s_names = []; s_width = 8; s_deps = []},
              "00000000");
             Signal_const
              ({s_id = 1L; s_names = ["vdd"]; s_width = 1; s_deps = []}, "1");
             Signal_op
              ({s_id = 40L; s_names = []; s_width = 1;
                s_deps =
                 [Signal_wire
                   ({s_id = 38L; s_names = ["up"]; s_width = 1; s_deps = []},
                   {contents = Signal_empty});
                  Signal_wire
                   ({s_id = 37L; s_names = ["down"]; s_width = 1;
                     s_deps = []},
                   {contents = Signal_empty})]},
              Signal_xor)]},
         {reg_clock =
           Signal_wire
            ({s_id = 3L; s_names = ["clock"]; s_width = 1; s_deps = []},
            {contents = Signal_empty});
          reg_clock_level =
           Signal_const
            ({s_id = 1L; s_names = ["vdd"]; s_width = 1; s_deps = []}, "1");
          reg_reset = Signal_empty;
          reg_reset_level =
           Signal_const
            ({s_id = 1L; s_names = ["vdd"]; s_width = 1; s_deps = []}, "1");
          reg_reset_value =
           Signal_const
            ({s_id = 43L; s_names = []; s_width = 8; s_deps = []},
            "00000000");
          reg_clear =
           Signal_wire
            ({s_id = 39L; s_names = ["clear"]; s_width = 1; s_deps = []},
            {contents = Signal_empty});
          reg_clear_level =
           Signal_const
            ({s_id = 1L; s_names = ["vdd"]; s_width = 1; s_deps = []}, "1");
          reg_clear_value =
           Signal_const
            ({s_id = 42L; s_names = []; s_width = 8; s_deps = []},
            "00000000");
          reg_enable =
           Signal_op
            ({s_id = 40L; s_names = []; s_width = 1;
              s_deps =
               [Signal_wire
                 ({s_id = 38L; s_names = ["up"]; s_width = 1; s_deps = []},
                 {contents = Signal_empty});
                Signal_wire
                 ({s_id = 37L; s_names = ["down"]; s_width = 1; s_deps = []},
                 {contents = Signal_empty})]},
            Signal_xor)})})];
   circ_fanout = <abstr>; circ_fanin = <abstr>}
val sim : B.t Cyclesim.Api.cyclesim =
  {HardCaml.Cyclesim.Api.sim_in_ports =
    [("down", {contents = [0]}); ("clear", {contents = [0]});
     ("clock", {contents = [0]}); ("up", {contents = [0]})];
   sim_out_ports = [("count", {contents = [0; 0; 0; 0; 0; 0; 0; 0]})];
   sim_out_ports_next = [("count", {contents = [0; 0; 0; 0; 0; 0; 0; 0]})];
   sim_internal_ports = [("vdd", {contents = [0]})]; sim_reset = <fun>;
   sim_cycle_check = <fun>; sim_cycle_comb0 = <fun>; sim_cycle_seq = <fun>;
   sim_cycle_comb1 = <fun>}
val i : B.t ref Up_down_counter_in.t =
  {Up_down_counter_in.clear = {contents = [0]}; up = {contents = [0]};
   down = {contents = [0]}}
val o : B.t ref Up_down_counter_out.t =
  {Up_down_counter_out.count = {contents = [0; 0; 0; 0; 0; 0; 0; 0]}}

We now have the circuit, simulator and ports. Time to simulate.


In [5]:
module S = Cyclesim.Api

let test sim = 
    let open Up_down_counter_in in
    let open Up_down_counter_out in
    let cycle() = 
        S.cycle sim;
        Printf.printf "%i\n" (B.to_int !(o.count))
    in
    S.reset sim;
    i.clear := B.vdd;
    cycle();
    i.clear := B.gnd;
    i.up := B.vdd;
    i.down := B.gnd;
    cycle();
    cycle();
    cycle();
    i.up := B.gnd;
    i.down := B.vdd;
    cycle();
    i.up := B.vdd;
    i.down := B.vdd;
    cycle();
    i.up := B.gnd;
    i.down := B.vdd;
    cycle();
    cycle();
    
    Printf.printf "done.\n"


Out[5]:
module S = HardCaml.Cyclesim.Api
Out[5]:
val test : 'a S.cyclesim -> unit = <fun>

In [6]:
test sim


0
0
1
2
3
2
2
1
done.
Out[6]:
- : unit = ()

Printing a trace is all fine and dandy. But we can do better.


In [7]:
(* theres a div in a cell below which we intend to render to *)
let div = Js.Opt.get (Dom_html.document##getElementById(Js.string "waves")) 
    (fun () -> raise Not_found)
    
let sim, wave = HardCamlJS.Wave.wrap sim


Out[7]:
val div : Dom_html.element Js.t = <abstr>
Out[7]:
val sim : HardCamlJS.Wave.B.t S.cyclesim =
  {HardCaml.Cyclesim.Api.sim_in_ports =
    [("down", {contents = [1]}); ("clear", {contents = [0]});
     ("clock", {contents = [0]}); ("up", {contents = [0]})];
   sim_out_ports = [("count", {contents = [0; 0; 0; 0; 0; 0; 0; 1]})];
   sim_out_ports_next = [("count", {contents = [0; 0; 0; 0; 0; 0; 0; 0]})];
   sim_internal_ports = [("vdd", {contents = [1]})]; sim_reset = <fun>;
   sim_cycle_check = <fun>; sim_cycle_comb0 = <fun>; sim_cycle_seq = <fun>;
   sim_cycle_comb1 = <fun>}
val wave : HardCamlJS.Wave.wave array =
  [|{HardCamlJS.Wave.name = "down"; nbits = 1;
     data = {HardCamlJS.Wave.len = 0; data = [||]}};
    {HardCamlJS.Wave.name = "clear"; nbits = 1;
     data = {HardCamlJS.Wave.len = 0; data = [||]}};
    {HardCamlJS.Wave.name = "clock"; nbits = 1;
     data = {HardCamlJS.Wave.len = 0; data = [||]}};
    {HardCamlJS.Wave.name = "up"; nbits = 1;
     data = {HardCamlJS.Wave.len = 0; data = [||]}};
    {HardCamlJS.Wave.name = "count"; nbits = 8;
     data = {HardCamlJS.Wave.len = 0; data = [||]}}|]

In [8]:
test sim


0
0
1
2
3
2
2
1
done.
Out[8]:
- : unit = ()

In [9]:
HardCamlJS.Wave.Gui.mk_wave_table div 400 20 wave


Out[9]:
- : unit = ()

Interfaces

HardCaml can be used without interfaces, though it does require some extra work. The main issue is this; when I write something like

let up_down_counter clear up down = ...

in my head I know what the interface looks like, but from HardCaml's point of view those useful names don't really exist. When you need to manipulate the interface to a circuit (both in simulation and when writing our RTL) you need some way to get those names back and this is done with strings.

let up = input "up" 1 in
let down = input "down" 1 in
let count = up_down_counter ... in
let count = output "count" count in
...

the whole reason we need inputs and output is precisely to associate names to the signals.

When working with the low level simulator APIs we have a similar issue.

let sim = ....
let up = S.find_in_port sim "up" in
let count = S.find_out_port sim "down" in

For small examples it isn't too bad but after a while it becomes annoying.

For the price of defining interfaces and wrapping your circuits with them this can be done automatically.