How do I…?#

A task-oriented reference: the shortest correct way to do common things. Feel free to suggest new entries.

For questions rather than method calls – what a target is, whether the public server is confidential, why a run failed – see the Frequently Asked Questions.

Entries are grouped by what you are trying to do, roughly in the order people need them.

Set up a build#

Create a design object#

from siliconcompiler import Design
design = Design('<design>')

Dataroot: register a new source of files#

design.set_dataroot("<name>", "<path>", tag="<version>")

The path may be a local directory, a git URL, or an archive URL. tag applies to remote sources only, and is a git commit, branch, or tag. See dataroot.

Dataroot relative to my current file#

design.set_dataroot('<name>', __file__)

Create an ASIC project object#

from siliconcompiler import ASIC
project = ASIC(design)

Add files to a fileset#

Group a design’s files into named filesets and register them against a dataroot so the paths survive being run from elsewhere. The fileset and filetype are inferred from the extension:

design.set_dataroot("mydesign", __file__)

with design.active_dataroot("mydesign"), design.active_fileset("rtl"):
    design.set_topmodule("mydesign")
    design.add_file("rtl/mydesign.v")        # -> fileset "rtl", type verilog

with design.active_dataroot("mydesign"), design.active_fileset("sdc"):
    design.add_file("constraints/mydesign.sdc")

with design.active_dataroot("mydesign"), design.active_fileset("testbench"):
    design.add_file("tb/mydesign_tb.sv")

Pass filetype= explicitly when the extension does not imply it. A design may carry as many filesets as you like – only the ones you activate are compiled.

Activate filesets#

Choose which of the design’s filesets this run compiles:

project.add_fileset(["rtl", "sdc"])     # ignores "testbench"

This is how one design serves several flows: an ASIC build activates rtl and sdc, while a simulation activates rtl and testbench.

Run a compilation#

project.run()

See what happened#

Display my layout#

project.show()

Display a previous run from the command line#

sc-show -design <name>

Change the logging level, or quieten the output#

Two different things control how much you see.

SiliconCompiler’s own messages come from a standard Python logger, so set its level directly. There is no option for this:

project.logger.setLevel("DEBUG")     # or "INFO" (the default), "WARNING", "ERROR"

Tool output is separate. It is summarised by default; set [option,quiet] to suppress it entirely, or unset it to see everything the tool prints:

project.option.set_quiet(True)                              # whole run
project.option.set_quiet(True, step="synthesis", index="0")  # one node

Either way the full tool log is always written to <step>.log in the node directory – see Directory structures.

Use the manifest from a previous run#

from siliconcompiler import Project

project = Project.from_manifest("build/<design>/<jobname>/<design>.pkg.json")

Control a run#

Run a build script’s targets without editing it#

A build script usually holds more than one thing you might want to do – lint, synthesize, run the full flow, sweep a parameter. Writing each as a function and editing the bottom of the file to pick one gets old quickly.

smake runs those functions by name. Point it at a make.py and every top-level function becomes a target, with its arguments turned into command-line switches and its docstring into help text:

smake --help                        # what this script can do
smake syn                           # call syn()
smake asic --fileset rtl.memory     # call asic(fileset="rtl.memory")

Nothing special is needed in the script – these are ordinary functions:

def syn(fileset: str = "rtl", pdk: str = "freepdk45"):
    """Synthesis only, the quickest check."""
    ...

Functions whose names start with _ are helpers, not targets, and are left out of --help. smake -C <dir> runs a script in another directory, and smake -f <file> uses a file that is not called make.py.

This is why several examples ship a make.py rather than a script with one entry point: examples/heartbeat/make.py exposes ten targets covering every project type. python3 make.py still works, but runs whatever the __main__ block chooses – usually one of the targets, not the one you meant.

Run only part of the flow#

A re-run resumes by default: nodes that already completed are reused, so restarting from a step re-runs it and everything downstream.

project.option.add_from("synthesis")    # start here, reusing earlier results
project.option.add_to("synthesis")      # and stop here

[option,from] and [option,to] take step names only, not indices – a deliberate choice to keep them simple. To drop individual (step, index) nodes, use [option,prune]:

project.option.add_prune(("floorplan.init", "0"))

Start a fresh run#

project.option.set_clean(True)

Start a fresh run and keep the old one#

project.option.set_clean(True)
project.option.set_jobincr(True)

Start a fresh run using the previous run information#

project.option.set_clean(True)
project.option.set_jobincr(True)
project.option.add_from('floorplan')

Control how much of the machine a run uses#

Two job-wide limits, both defaulting to the number of available CPU cores:

project.option.scheduler.set_maxnodes(4)     # concurrent nodes in the job
project.option.scheduler.set_maxthreads(8)   # threads available to each task

[option,scheduler,maxnodes] bounds how many flowgraph nodes run at once; [option,scheduler,maxthreads] bounds each task’s own threading. On a machine you are still using for other work, lowering both is usually what you want – otherwise a wide flow will happily take every core.

To override the thread count for one task rather than the whole job:

from siliconcompiler.tools.yosys.syn_asic import ASICSynthesis

ASICSynthesis.find_task(project).set_threads(4, step="synthesis", index="0")

Build directory#

project.option.set_builddir("/path/to/build")

See Directory structures for what a run writes there.

Cache directory#

project.option.set_cachedir("/path/to/cache")

Defaults to ~/.sc/cache; see the SC home directory.

Preserve options across sessions#

Options such as scheduler information can be preserved across sessions:

project.option.write_defaults()

Check my setup before running#

project.check_manifest()

Extend SiliconCompiler#

Set up a new tool#

See Tools

Set up a new flow#

See Flows

Set up a new PDK#

See PDKs

Set up a new library#

See Libraries

Set up a new target#

See Targets

Reuse a block as a hardened macro#

Use a macro I already have#

Warning

Adding a .lef to your design’s own fileset does not work:

design.add_file("mymacro.lef", fileset="rtl")   # wrong

The parent will synthesize the macro’s RTL anyway, and place-and-route then fails with LEF master ... not found. A macro is a library, not a source file.

Package the views into a StdCellLibrary:

from siliconcompiler import StdCellLibrary

macro = StdCellLibrary("mymacro")
macro.set_dataroot("macro", __file__)
macro.add_asic_pdk("skywater130")            # must match the parent's PDK

with macro.active_dataroot("macro"), macro.active_fileset("models.physical"):
    macro.add_file("mymacro.lef")            # abstract view for place & route
    macro.add_file("mymacro.gds")            # layout, merged into the final GDS
    macro.add_asic_aprfileset()

with macro.active_dataroot("macro"), macro.active_fileset("models.timing.typical"):
    macro.add_file("mymacro_typical.lib")    # timing view
    macro.add_asic_libcornerfileset("typical", "nldm")

Then, in the parent project, do two things – both are required:

# MyMacro() is the Design holding the macro's RTL -- the same sources the
# parent would otherwise synthesize, and what the alias tells it to skip.
project.add_alias(MyMacro(), "rtl", None, None)        # 1. blackbox the RTL
project.add_asiclib(macro)                             # 2. inject the views

# Macros need room. Too small a die and the placer cannot fit them.
project.constraint.area.set_diearea_rectangle(250, 250, coremargin=10)

Without the alias the parent re-synthesizes the block instead of instantiating the hardened version; without the library the tools have no physical or timing view of it.

Instantiating a hardened module works through this end to end, including producing the macro from a first build.

Harden a parameterized module so I can reuse it as a macro#

A hardened macro has no parameters, so a parameterized module cannot be hardened directly. Use Uniquified, which generates a parameter-free variant per used parameter combination plus a wrapper that dispatches to them. See the uniquify tutorial and the Uniquify API.

from siliconcompiler import ASIC
from siliconcompiler.targets import freepdk45_demo
from siliconcompiler.tools.slang.utils.macro import Uniquified

# parent_design: your Design that instantiates the parameterized module.
uq = Uniquified(parent_design, ["mymodule"])
uq.build(target=freepdk45_demo)   # harden every used parameterization

project = ASIC(parent_design)
project.add_fileset("rtl")
freepdk45_demo(project)
uq.wireup(project)                # alias wrappers + inject macros

Find out which parameter values my module is instantiated with#

Construct Uniquified (construction only elaborates and generates in memory – no disk writes, no tools) and read its state:

uq = Uniquified(parent_design, ["mymodule"])
print(uq.variants)   # {'mymodule': ['mymodule__N8', 'mymodule__N16']}

Rebuild only some hardened variants#

Pass macros to Uniquified.build() as a variant name, a module name, or a glob; add rebuild=True to force a rebuild even if a cached macro exists.

uq.build(target=freepdk45_demo, macros="mymodule__N8", rebuild=True)

Wrap a design in an IO pad ring#

Implementing an IO pad ring walks through this end to end, and examples/padring is the working design it builds. In short, three pieces are involved:

  1. The ring in RTL. Depend on lambdalib’s pad ring generator rather than naming technology cells, and describe the cell order per side in a CELLMAP. lambdalib installs with lambdapdk, so it is already present:

    from lambdalib.padring import Padring
    ...
    self.add_depfileset(Padring(), "rtl")
    
  2. A top level whose ports are the pads. Name them for what they carry, and declare the direction each is really used in – the ring’s cells are all bidirectional, but a pin on the finished part is not, and only the supplies are genuinely inout. Underneath, the logic talks to the ring through din/dout/oen/ie and a technology configuration bus per side, never touching a pad cell, which is what keeps it portable between technologies.

  3. Physical construction in TCL, attached to the tasks that read it. These are task variables, not schema keys:

    InitFloorplanTask.find_task(project).add_openroad_padringfileset("padring.sky130")
    PowerGridTask.find_task(project).add_openroad_powergridfileset("mydesign", "pdn.sky130")
    for task in APRTask.find_task(project):
        task.add_openroad_globalconnectfileset("mydesign", "globalconns.sky130")
    

    Note that the pad ring hook takes only a fileset name while the other two also take the library that owns the fileset.

The TCL itself places pads into IO rows, then corners, then fill, then calls connect_by_abutment so the supply and configuration signals pass between neighbouring cells, then adds bond pads. The order matters. Use -connect_to_pads on the core power rings so the core is fed from the ring.

These are OpenROAD commands rather than SiliconCompiler ones, so their arguments are documented upstream: pad for place_pads, place_corners, place_io_fill, connect_by_abutment and place_bondpad, and pdn for define_pdn_grid, add_pdn_ring, add_pdn_stripe and add_global_connection.

A die with a ring is usually pad limited: its size is set by how many pads must fit around the edge, not by the logic inside.

Schema internals#

Avoid rebuilding an expensive object (such as a PDK) many times#

If a schema object is a pure function of its construction arguments, base it on CachedSchema. Instances are built once per unique (hashable) set of arguments, and the shared, frozen instance is returned on subsequent constructions. This is useful for heavy objects, like PDKs, that would otherwise be re-created dozens of times while loading a target.

from siliconcompiler import PDK
from siliconcompiler.schema import CachedSchema

class MyPDK(PDK, CachedSchema):
    def __init__(self):
        super().__init__("mypdk")
        # ... expensive schema population ...

MyPDK() is MyPDK()   # True -- same shared instance, built only once

The shared instance is frozen: calling set, add, unset, remove, or using EditableSchema on it raises a SchemaFrozenError. This protects the shared object from accidental modification.

Get a modifiable version of a frozen (cached) object#

Use copy(). A copy is always mutable and fully independent of the shared instance, so you are free to modify it. Objects reloaded from a manifest (for example, inside a run) are likewise mutable.

my_pdk = MyPDK()          # frozen, shared
local = my_pdk.copy()     # mutable, independent
local.set("pdk", "foundry", "virtual")

To modify a frozen object in place (for example, to write resolved file paths or hashes back into a shared object during a run), use the _thaw context manager, which restores the frozen state on exit:

with my_pdk._thaw():
    my_pdk.set(*keypath, hashes, field="filehash")

Warning

_thaw() is internal API – the leading underscore is not decorative. It exists for SiliconCompiler’s own run machinery, carries no compatibility guarantee, and mutating a shared instance affects every holder of it. copy() is the supported answer; reach for _thaw() only when you genuinely need the mutation to be visible through the shared object.