Hardening parameterized modules (uniquify)#
Note
Beta feature. Uniquify (Uniquified) is new; its API and
generated output may change in a future release.
The hardened-module tutorial hardened a single, unparameterized module and instantiated it in a parent – packaging the macro, blackboxing the RTL and injecting the library all by hand. This tutorial tackles the case that flow cannot: a parameterized module instantiated with several different parameter values.
How this differs from the hardened-module tutorial
The hardened-module tutorial teaches the underlying mechanism – harden a module, blackbox it, inject the macro – by hand for one fixed module. If you have not read it, start there.
This tutorial is about the problem that mechanism alone cannot solve. A
hardened macro is a fixed netlist with no parameters, so a post-synthesis
block named heartbeat no longer has an N for a parent’s
heartbeat #(.N(8)) to bind to – and the parent uses N = 8, 24 and
48. Uniquified resolves this automatically: it discovers which
parameter values are actually used, generates a hardenable variant for each
plus a wrapper that restores the original interface, and drives the whole
harden-and-inject flow for every variant at once.
Given a design and the names of the parameterized modules to harden,
Uniquified:
enumerates every concrete parameter combination the modules are actually instantiated with (by elaborating the design with slang),
generates, for each combination, a parameter-free variant to harden (e.g.
heartbeat__N8) plus a parameterized wrapper that keeps the original module’s name and interface and dispatches to the matching variant,registers filesets for those sources on the design, then lets you build each variant into a macro and wire up the wrappers and macros into a parent
ASICproject.
This mirrors how lambdalib maps an abstract cell onto hardened
implementations, but generated automatically from how the modules are used.
Note
Unparameterized modules are handled too. A module with no overridable
parameters is already hardenable, so no wrapper is generated for it – it is
hardened directly and blackboxed at wireup, i.e. Uniquified does
exactly what the hardened-module tutorial does by
hand. A single Uniquified can therefore harden a mix of
parameterized and unparameterized modules in one shot.
All of the code below is contained in the example script. To inspect what uniquify generates (no EDA tools required):
smake main
and to run the full flow (requires yosys and OpenROAD):
smake harden
The design#
The parent instantiates a parameterized heartbeat counter with several
distinct widths (N), plus a prescaler. Note that N=24 is used twice
– uniquify hardens it only once.
// Parent design. It depends on the parameterized `heartbeat` and `prescaler`
// modules (each defined in its own file / SiliconCompiler Design) and
// instantiates them with several distinct parameter values.
module heartbeat_top (
input clk,
input nreset,
output out_fast,
output out_slow,
output out_slow2,
output out_slowest,
output [3:0] cnt_a,
output [7:0] cnt_b
);
// Four heartbeat instances, three distinct parameterizations (N=24 merges).
heartbeat #(
.N(8)
) u_fast (
.clk(clk),
.nreset(nreset),
.out(out_fast)
);
heartbeat #(
.N(24)
) u_slow (
.clk(clk),
.nreset(nreset),
.out(out_slow)
);
heartbeat #(
.N(24)
) u_slow2 (
.clk(clk),
.nreset(nreset),
.out(out_slow2)
);
heartbeat #(
.N(48)
) u_slowest (
.clk(clk),
.nreset(nreset),
.out(out_slowest)
);
// Two prescaler parameterizations.
prescaler #(
.W(4)
) u_ps4 (
.clk(clk),
.nreset(nreset),
.count(cnt_a)
);
prescaler #(
.W(8)
) u_ps8 (
.clk(clk),
.nreset(nreset),
.count(cnt_b)
);
endmodule
// A parameterized module, instantiated with several distinct N values by the
// parent. Hardening it requires knowing which concrete N values are actually
// used -- that is what the uniquify enumeration answers.
module heartbeat #(
parameter N = 8
) (
input clk,
input nreset,
output reg out
);
reg [N-1:0] counter_reg;
always @(posedge clk or negedge nreset) begin
if (!nreset) begin
counter_reg <= {(N) {1'b0}};
out <= 1'b0;
end else begin
counter_reg <= counter_reg + 1'b1;
out <= (counter_reg == {(N) {1'b1}});
end
end
endmodule
Step 1: Define the modules and the parent#
Each parameterized module to be hardened must be its own reusable
Design (uniquify aliases the module’s RTL to a generated wrapper, so it
needs a design to alias). The parent depends on their RTL so it elaborates.
with self.active_fileset("rtl"), self.active_dataroot(_DATAROOT):
self.set_topmodule("heartbeat")
self.add_file("heartbeat.v")
class Prescaler(Design):
"""Parameterized `prescaler` module (its own reusable design)."""
def __init__(self):
super().__init__("prescaler")
self.set_dataroot(_DATAROOT, __file__)
with self.active_fileset("rtl"), self.active_dataroot(_DATAROOT):
self.set_topmodule("prescaler")
self.add_file("prescaler.v")
class HeartbeatTop(Design):
"""Parent design instantiating the parameterized submodules."""
def __init__(self):
with self.active_fileset("rtl"), self.active_dataroot(_DATAROOT):
self.set_topmodule("heartbeat_top")
# The parent depends on the submodule RTL so it elaborates.
self.add_file("heartbeat_top.v")
self.add_depfileset(Heartbeat(), depfileset="rtl", fileset="rtl")
self.add_depfileset(Prescaler(), depfileset="rtl", fileset="rtl")
def uniquify():
"""Set up uniquification of the target modules on a fresh parent design.
Step 2: Construct the Uniquified helper#
Pass the parent design (or a Project) and the module names to
uniquify. Construction enumerates the parameter combinations and generates the
wrappers/variants in memory – it writes nothing to disk and runs no tools –
and registers the new filesets on the design.
_DATAROOT = "uniquify"
filesets are registered on the design, but nothing is hardened yet.
"""
return Uniquified(HeartbeatTop(), ["heartbeat", "prescaler"])
def _configure_freepdk45(project):
"""Configure a variant/parent ASIC run for FreePDK45."""
The filesets registered on the design are:
rtl.<module>.wrapper– the parameterized wrapper (keeps the module’s name and interface), one per module.rtl.hardened.<variant>– one parameter-free variant per combination, the top that gets hardened into a macro.rtl.wrapper– an aggregate that pulls every per-module wrapper at once.
Step 3: Inspect what was generated#
The helper exposes its results as plain state. The main function prints the
variants and fileset names, then writes the sources so you can read a wrapper:
print(f"'{module}' -> {len(variants)} variant(s): {variants}")
print()
print("wrapper filesets: ", uq.wrapper_filesets)
print("hardened filesets: ", uq.hardened_filesets)
# Materialize the generated sources (construction is disk-side-effect free),
# then show one wrapper: it keeps the module name + parameters and dispatches
# to the hardened variants.
uq.write()
with open(os.path.join(uq.outdir, "heartbeat.wrapper.v")) as fobj:
print("\n" + "=" * 64 + "\nGenerated wrapper for 'heartbeat':\n")
print(fobj.read())
if __name__ == "__main__":
main()
For this design that reports three heartbeat variants (N = 8, 24, 48 –
the duplicate N=24 merged) and two prescaler variants:
'heartbeat' -> 3 variant(s): ['heartbeat__N24', 'heartbeat__N48', 'heartbeat__N8']
'prescaler' -> 2 variant(s): ['prescaler__W4', 'prescaler__W8']
The generated wrapper keeps the heartbeat name and its N parameter, and
uses a generate block to select the matching hardened variant. If a design
ever requests a parameter combination that was not hardened, elaboration fails
loudly via $error rather than silently building the wrong thing:
module heartbeat #(
parameter N = 8
) (
input clk,
input nreset,
output out
);
generate
if ((N == 24)) begin : g_heartbeat__N24
heartbeat__N24 u_impl (.clk(clk), .nreset(nreset), .out(out));
end
else if ((N == 48)) begin : g_heartbeat__N48
heartbeat__N48 u_impl (.clk(clk), .nreset(nreset), .out(out));
end
else if ((N == 8)) begin : g_heartbeat__N8
heartbeat__N8 u_impl (.clk(clk), .nreset(nreset), .out(out));
end
else begin : g_invalid
$error("no hardened variant of heartbeat exists for the requested parameters");
end
endgenerate
endmodule
Step 4: Harden the variants#
Uniquified.build() hardens the selected variants (all of them by default)
into StdCellLibrary macros. Pass a target – a SiliconCompiler
target callback fn(project) that configures the PDK, flow and constraints on
each variant’s ASIC run – exactly as you would for a normal build:
freepdk45_demo(project)
project.constraint.area.set_density(5.0)
# The demo PDK has no power grid; skip IR-drop analysis.
for task in OpenROADPSMParameter.find_task(project):
task.set_openroad_psmenable(False)
def harden():
"""Full flow: harden every variant and build the parent with the macros.
project.add_fileset("rtl")
_configure_freepdk45(project)
Each variant is built under its own job (jobname=<variant>) beneath the
helper’s libdir, and the resulting macro is persisted so a later run reuses
it. build returns a {variant: StdCellLibrary} mapping. Pass
parallel=True to harden the variants concurrently, or macros="heartbeat"
/ macros="heartbeat__N*" to rebuild a subset.
Step 5: Wire up the parent#
Finally, build the parent. This is where the mechanism from the
hardened-module tutorial – alias the module’s RTL,
inject the macro with add_asiclib – is applied, but you do not write it out
by hand: Uniquified.wireup() does it for every variant at once. It aliases
each module’s RTL to its generated wrapper (so synthesis elaborates the
dispatch instead of the original parameterized RTL, the one twist beyond the
hardened-module flow) and injects all the hardened macros:
"""
uq = uniquify()
uq.build(target=_configure_freepdk45, parallel=True)
project = ASIC(uq.design)
project.add_fileset("rtl")
_configure_freepdk45(project)
uq.wireup(project)
project.run()
project.summary()
def main():
uq = uniquify()
Note
wireup requires every used variant to have a built (or loaded) macro, and
raises otherwise. The generated wrapper has a branch for each enumerated
variant, so an unbuilt-but-used variant still matches its own branch (it does
not reach the $error default) – what fails is the missing hardened
implementation. The $error branch only guards parameter combinations that
were never enumerated at all. Call Uniquified.build() first, or
Uniquified.load_macros() to reuse macros from a previous run.
Conclusion#
Starting from a parent that instantiates a parameterized module with several different parameter values, uniquify:
discovered the concrete parameterizations in use,
generated a hardenable variant for each and a parameterized wrapper to dispatch to them,
hardened the variants into reusable macros, and
wired the wrappers and macros into the parent build.
See the Uniquify API for the full class reference.