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

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.