Instantiating a hardened module in a design#

Hardened modules allow predictable timing, efficient placement, and scalable hierarchical ASIC design.

This tutorial demonstrates how to use Silicon Compiler to harden a Verilog module as a reusable macro, and then instantiate it multiple times in another module by:

  1. Setup design

  2. Synthesizing, placing, and routing mod_and using Silicon Compiler

  3. Packaging the resulting layout and timing models from mod_and as a hard macro

  4. Running the ASIC flow on module top

The module hardened here (mod_and) has no parameters. This tutorial does each step by hand so the mechanism is explicit.

See also

If your module is parameterized and instantiated with several different parameter values, a hardened macro (which has no parameters) cannot bind to those instantiations directly. The uniquify tutorial shows how Uniquified automates this whole flow – discovering the parameter values in use, generating a parameter-free hardenable variant for each parameterization plus one dispatching wrapper per module, and building and wiring them into the parent for you.

All these steps are contained in the python script.

To run this script from its containing folder:

smake build_top

or to use docker:

docker run --rm -v "$(pwd):/sc_work" \
    ghcr.io/siliconcompiler/sc_runner:latest \
    python3 make.py

The image output will appear in: ./build/top/job0/write.gds/0/outputs/top.png

Environment Setup#

Import the modules to be used:

from siliconcompiler import Design, ASIC, StdCellLibrary
from siliconcompiler.targets import skywater130_demo

Files used#

Child Module (and.v)#

and.v#
module mod_and (
    input  wire       clk,
    input  wire [7:0] a,
    input  wire [7:0] b,
    output reg  [7:0] y
);

    always @(posedge clk) begin
        y <= a & b;
    end

endmodule

Parent Module (top.v)#

top.v#
module top (
    input  wire       clk,
    input  wire [7:0] a1,
    input  wire [7:0] b1,
    input  wire [7:0] a2,
    input  wire [7:0] b2,
    output wire [7:0] y
);

    wire [7:0] d1;
    wire [7:0] d2;

    mod_and D1 (
        .clk(clk),
        .a  (a1),
        .b  (b1),
        .y  (d1)
    );
    mod_and D2 (
        .clk(clk),
        .a  (a2),
        .b  (b2),
        .y  (d2)
    );
    mod_and Y (
        .clk(clk),
        .a  (d1),
        .b  (d2),
        .y  (y)
    );
endmodule

Step 1: Setup design#

The following code will instantiate the module: mod_and and top:

Child Module (and.v)#

Design setup for mod_and#
class And(Design):
    """
    Defines the child module (an AND gate).
    Inheriting from Design allows us to encapsulate file paths and module names.
    """
    def __init__(self):
        super().__init__("and")
        # Set the root directory for relative paths to this file's location
        self.set_dataroot("local", __file__)
        self.set_topmodule("mod_and", fileset="rtl")
        self.add_file("and.v", fileset="rtl")

Parent Module (top.v)#

Design setup for top#
class Top(Design):
    """
    Defines the parent module (Top) which contains instances of the And gate.
    """
    def __init__(self):
        super().__init__("top")
        self.set_dataroot("local", __file__)
        self.set_topmodule("top", fileset="rtl")
        self.add_file("top.v", fileset="rtl")

        # Logically link 'And' so the linter/elaborator knows definitions exist.
        # Note: In the build_top() function, we will override this for physical implementation.
        self.add_depfileset(And(), depfileset="rtl", fileset="rtl")

Dependency graph from top:

../../_images/depgraph.png

Dependency graph from Top().write_depgraph#

Step 2: Synthesize, Place & Route module mod_and#

The following code will build module mod_and with the default ASIC flow. This generates the physical layout required for the macro.

def build_and() -> StdCellLibrary:
    """
    Hardens module 'And' and packages it as a library.

    Returns:
        StdCellLibrary: An object containing paths to the generated LEF, GDS, and LIB files.
    """

    project = ASIC(And())
    project.add_fileset("rtl")
    skywater130_demo(project)

    # Run the standard ASIC flow (Syn -> Floorplan -> Place -> Route -> Export)
    project.run()
    project.summary()

    # Create a reproducible snapshot of the build
    project.snapshot()
../../_images/and.png

Step 3: Packaging the resulting layout and timing models from mod_and as a hard macro#

Once the flow is complete, we must package the results into a StdCellLibrary. This object tells the tools where to find the physical views (LEF/GDS) and timing views (LIB) required by the parent design. We gather the results from the previous build step and register them into the library object

    # --- Packaging the Macro ---
    # Create a new library object to represent this hardened block
    library = StdCellLibrary("module_and")

    # Copy PDK name from the build project
    library.add_asic_pdk(project.get("asic", "pdk"))

    # Register Physical Views (GDS and Abstract)
    with library.active_fileset("models.physical"):
        # LEF: Abstract view for placement and routing in the parent
        library.add_file(project.find_result("lef", step="write.views"))
        # GDS: Actual layout for final merge
        library.add_file(project.find_result("gds", step="write.gds"))
        # Include necessary setup files for APR tools (like OpenROAD)
        library.add_asic_aprfileset()

    # Register Timing Views (Liberty files) for all available corners
    # This ensures the parent flow can perform timing analysis on the macro
    for corner in ("slow", "typical", "fast"):
        with library.active_fileset(f"models.timing.{corner}"):
            library.add_file(project.find_result(f"{corner}.lib", step="write.views"))
            library.add_asic_libcornerfileset(corner, "nldm")

    return library

Step 4: Running the ASIC flow on module top#

Finally, we configure the flow for the top module. Critical steps here include:

  1. Blackboxing (Alias): We use add_alias to prevent the tool from synthesizing the RTL of And. We want to use the pre-hardened version, not re-synthesize it.

  2. Library Injection: We use add_asiclib to provide the LEF and LIB files we packaged in Step 3.

def build_top(size: int = 250, margin: int = 10):
    """
    Builds the top-level module using the pre-built 'And' library.
    """

    project = ASIC(Top())
    project.add_fileset('rtl')

    # --- Hierarchical Configuration ---
    # 1. 'add_alias': Tells the tool "When you see module 'And', do not compile its RTL."
    #    This effectively turns 'And' into a blackbox during synthesis.
    project.add_alias(And(), "rtl", None, None)

    # 2. 'add_asiclib': Injects the pre-built library (LEF/LIB) we created in build_and().
    #    The APR tool will use the LEF for placement and the LIB for timing.
    project.add_asiclib(build_and())

    # Load target technology
    skywater130_demo(project)

    # --- Constraints ---
    # Setting explicit die area is critical for macros.
    # If the core is too small, the placer may fail to fit the child macros.
    project.constraint.area.set_diearea_rectangle(size, size, coremargin=margin)

    project.run()
    project.summary()
    project.snapshot()

Note

Setting the core and die area explicitly is crucial for macro placement. If the area is too small, the floorplan may fail to insert the macros.

    project.constraint.area.set_diearea_rectangle(size, size, coremargin=margin)
../../_images/top.png

Conclusion#

This tutorial demonstrates how to perform a basic modular hierarchical ASIC design flow in Silicon Compiler by:

  • Hardening a leaf module mod_and

  • Exporting the layout and timing views from mod_and as a custom library module_and

  • Instantiating module_and in the parent module top

This approach enables scalable chip design with reusable hardened blocks.