2.4 RTL Generation
May 15, 2026 ยท View on GitHub
You can convert a
Hardcaml Circuit
to either Verilog or VHDL.
The following is a trivial example.
let circuit = Circuit.create_exn ~name:"test" [ output "b" (input "a" 1) ]
# let () = Rtl.print Verilog circuit
module test (
a,
b
);
input a;
output b;
assign b = a;
endmodule
# let () = Rtl.print Vhdl circuit
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity test is
port (
a : in std_logic;
b : out std_logic
);
end entity;
architecture rtl of test is
begin
b <= a;
end architecture;
We also provide basic Systemverilog support - the generated RTL is basically the same as Verilog mode except we use the Systemverilog reserved words to correctly perform name mangling.
Instantiations
In Hardcaml, a Circuit corresponds to a single module in Verilog or entity in
VHDL. Circuits can contain Instantiations, which reference some other module or entity
within a hierarchical design.
The Hardcaml RTL generator is aware of the Instantiations and can recursively generate
the RTL for them, if they are provided in a Circuit_database.
A Circuit_database stores the implementation of Circuits that can be instantiated. It
is essentially a mapping between circuit names and their implementations.
If a circuit implementation is not found in the Circuit_database, Hardcaml will still
generate the appropriate instantiation in the RTL output. This allows for integration with
external modules (such as vendor IP) that will be added later in the design flow or for
module hierarchy to be described in Hardcaml.
Printing Circuits
The Rtl.print function will output the RTL for a design (and optionally any
instantiations defined through a Circuit_database) to stdout.
It is simple and usable in most cases.
Low Level Control
The RTL API provides a way for the Hardcaml to discover the full design hierarchy and for the user to decide how to write it out.
It starts with the create function.
# Rtl.create
- : ?database:Hardcaml.Circuit_database.t ->
?config:Hardcaml__Rtl_config.t ->
Rtl.Language.t -> Circuit.t list -> Rtl.Hierarchical_circuits.t list
= <fun>
database- theCircuit_databasewhich contains implementations ofInstantiationsfound within the hierarchy ofCircuits.config- Some configuration options for how to generate the RTL.Rtl.Langauge.t- either Verilog or VHDL.Circuit.t list- a list of top-level circuits to generate.
create returns a list Hierarchical_circuits which represent the design hierarchy.
Hierarchical circuits
The Hierarchical_circuits returned by create can be factored into the full design
hierarchy using subcircuits and top_level_circuits. These both return a list of
Circuit_instances which contain functions to output a module implementation.
It should be noted that if a module is instantiated in multiple places it will still only
be represented once within the Hierarchical_circuits design hierarchy.
Blackboxes
A blackbox is a module or entity which describes just its interface and does not include its implementation. The RTL generator can create black boxes if required.
Outputting RTL
Hardcaml predefines 4 ways to output the hierarchy of modules.
full_hierarchy- generate full rtl for everything, recursively. This is the most common option and whatRtl.printuses.top_levels_only- only generate the given top levels and do not recuse into the hierarchy.top_levels_and_blackboxes- the top levels will be generated along with blackboxes for all modules in the hierarchy.top_levels_as_blackboxes- the top levels will be generated as blackboxes.
Each of these functions returns a Rope.t
Ropes
Ropes are used by Hardcaml to generate code and are a fancy type of string. All you need
to know is they can be converted to a standard string with Rope.to_string.
Example
let inner1 () = Circuit.create_exn ~name:"inner1" [output "b" (input "a" 1)]
let inner2 () =
let x = input "x" 1 in
let inst = Instantiation.create ~name:"inner1" ~inputs:["a", x] ~outputs:["b", 1] () in
Circuit.create_exn ~name:"inner2" [output "y" (Instantiation.output inst "b")]
let top () =
let s = input "s" 1 in
let inst = Instantiation.create ~name:"inner2" ~inputs:["x", s] ~outputs:["y", 1] () in
Circuit.create_exn ~name:"top" [output "t" (Instantiation.output inst "y")]
This creates a hierarchy where top instantiates inner2 which in turn instantiates
inner1.
We now need to create a Circuit_database containing the inner circuits.
let database = Circuit_database.create ();;
Circuit_database.insert database (inner1 ());;
Circuit_database.insert database (inner2 ());;
Now we can print the RTL for top in various ways
let rtl = Rtl.create ~database Verilog [ top() ]
# Rtl.top_levels_and_blackboxes rtl |> Rope.to_string |> Stdio.print_endline;;
module inner1 (
a,
b
);
input a;
output b;
endmodule
module inner2 (
x,
y
);
input x;
output y;
endmodule
module top (
s,
t
);
input s;
output t;
wire signal_inst;
wire signal_wire;
inner2
the_inner2
( .x(s),
.y(signal_inst) );
assign signal_wire = signal_inst;
assign t = signal_wire;
endmodule
- : unit = ()
# Rtl.top_levels_as_blackboxes rtl |> Rope.to_string |> Stdio.print_endline;;
module top (
s,
t
);
input s;
output t;
endmodule
- : unit = ()
Find inner2 within the hierarchy and print that directly.
# let inner2 =
Rtl.Hierarchical_circuits.subcircuits rtl
|> List.find_exn ~f:(fun sub -> String.equal (Rtl.Circuit_instance.module_name sub) "inner2")
|> Rtl.Circuit_instance.rtl
|> Rope.to_string
|> Stdio.print_endline
module inner2 (
x,
y
);
input x;
output y;
wire signal_inst;
wire signal_wire;
inner1
the_inner1
( .a(x),
.b(signal_inst) );
assign signal_wire = signal_inst;
assign y = signal_wire;
endmodule
val inner2 : unit = ()