Running Flow A on Your Own SV Design

This page is a build-from-nothing walkthrough: environment setup through place-and-route results, using the shared scripts and templates under flow-a/ in this repository, unmodified, on any SystemVerilog design of your own.

Important

Implemented and validated (on custom_myfir — a sequential, clocked FIR filter, multi-file since it uses a custom Radix-4 Modified Booth signed multiplier internally rather than a plain * operator; see examples/custom_myfir/): RTL simulation, synthesis, post-synthesis gate-level simulation, pre-route PPA, place & route, post-route PPA, post-route SDF-back-annotated gate-level simulation, GDSII generation (gen_gds.sh, via KLayout’s stream_out.py script), DRC, LVS, antenna checks and multi-corner timing. The signoff steps, the timing-margin variables and the reference results are documented in Flow A: Signoff and Results.

GDSII generation requires a separate one-time klayout Python-bindings venv beyond the apt-installed KLayout GUI (see Getting Started) — the GUI package alone does not expose the pya module the stream-out script needs. Activity-driven (VCD-based) power reporting is a separate step; see Real-activity power (optional, recommended) below.

The core idea

Every script in flow-a/scripts/ (synth.tcl, load_constraints.tcl, ppa_report.tcl, pnr.tcl, post_route.tcl, multicorner.tcl, the two power_activity*.tcl, gen_gds.sh, drc.sh, lvs.sh with its helpers, and the run_flow.sh/sweep.sh/metrics scripts) is identical across every design and should be copied in unmodified. The only script that changes per design is scripts/yosys_common.tcl (plus the per-design SDC and Makefile config block). If you find yourself editing one of the shared scripts to onboard a design, the change almost certainly belongs in yosys_common.tcl instead.

File

Changes per design?

Source

scripts/yosys_common.tcl

Yes — this is the file you write

flow-a/templates/yosys_common.tcl.example

Makefile

Only the top config block (DESIGN, CORE_NAME, TOP, TB, DUT_INST, VERSION) — see below

flow-a/templates/Makefile.example

<design>.core

Yes — the single source of truth for sources (no separate manifest or file lists)

flow-a/templates/design.core.example

constraints/<design>.sdc

Only if the design has a clock

flow-a/templates/constraints.sdc.example

scripts/synth.tcl, load_constraints.tcl, ppa_report.tcl, pnr.tcl, post_route.tcl, multicorner.tcl, power_activity_postsynth.tcl, power_activity.tcl, gen_gds.sh, drc.sh, lvs.sh, lvs_prep.py, drop_cells.py, rdb_summary.py, run_flow.sh, sweep.sh, sweep_table.py, collect_metrics.py, tech.tcl, tech_query.tcl, tech/

No — copy verbatim (all PDK-specific values are in tech/<TECH>.tcl; see PDK Portability (e.g. Retargeting to 55 nm))

flow-a/scripts/

Makefile

Makefile wraps every stage below into a single make <target> (sim, synth, gls, ppa, pnr, postroute, sdf, power, gds, drc, lvs, signoff, metrics, plus check/tech-check/clean/help/all) so you don’t need to remember or copy-paste the raw fusesoc/openroad/ iverilog/./scripts/gen_gds.sh invocations documented in each numbered section below — those sections remain the authoritative reference for what each make target actually runs under the hood, and for running a stage manually if you need to.

Copy flow-a/templates/Makefile.example to the project root as Makefile and fill in its six-line config block to match yosys_common.tcl — run make check to verify the two stay consistent (along with tool/PDK/KLayout-bindings availability). make gds needs the klayout Python venv active in the same shell (see GDS Generation), so it’s deliberately excluded from make all; make drc and make lvs build the GDS first and are excluded for the same reason. Run them separately once the rest of the flow has passed (see Flow A: Signoff and Results).

PDK selection and checking

The Makefile reads every PDK-specific value from the technology file scripts/tech/$(TECH).tcl (TECH defaults to sky130hd; override per call with make TECH=<name> <target> or with export TECH=<name>). It queries that file through scripts/tech_query.tcl, so tclsh must be installed (sudo apt install tcl).

make tech-check verifies the active technology on its own: every required variable is set, every referenced PDK file exists, and the tech_tapcell/tech_pdn procs are defined. It prints one MISS line per problem and fails if any are found. Run it after installing the PDK or after writing a new tech file (see PDK Portability (e.g. Retargeting to 55 nm)); make check runs the same check as part of its wider tool and PDK verification.

make tech-check                  # default technology (sky130hd)
make TECH=<name> tech-check      # a new technology file

Project layout

<design_name>/
├── Makefile
├── <design_name>.core
├── rtl/*.sv
├── tb/*.sv
├── scripts/
│   ├── yosys_common.tcl            (the file you write)
│   ├── synth.tcl                   (copied from flow-a/scripts/)
│   ├── load_constraints.tcl        (reads the SDC)
│   ├── ppa_report.tcl
│   ├── pnr.tcl
│   ├── post_route.tcl              (post-route timing/power/SDF)
│   ├── multicorner.tcl
│   ├── power_activity_postsynth.tcl
│   ├── power_activity.tcl
│   ├── gen_gds.sh
│   ├── drc.sh, lvs.sh, lvs_prep.py, drop_cells.py, rdb_summary.py
│   ├── run_flow.sh, sweep.sh, sweep_table.py, collect_metrics.py
│   ├── tech.tcl, tech_query.tcl
│   └── tech/sky130hd.tcl           (all PDK-specific names)
└── constraints/<design_name>.sdc   (clocked designs only)
mkdir -p <design_name>/{rtl,tb,scripts,constraints}
cd <design_name>
cp -r <repo>/flow-a/scripts/* scripts/
cp <repo>/flow-a/templates/yosys_common.tcl.example scripts/yosys_common.tcl
cp <repo>/flow-a/templates/Makefile.example Makefile
cp <repo>/flow-a/templates/design.core.example <design_name>.core
chmod +x scripts/*.sh

1. <design_name>.core — declare your sources

The FuseSoC .core file is the only place the source list lives. Simulation and synthesis both build their file lists from its filesets, so there is nothing to generate and nothing to keep in sync:

CAPI=2:
name: thesis:flowA:<design_name>:0.1.0
description: <one-line description>

filesets:
  rtl:
    files:
      - rtl/leaf_module.sv     # leaf modules first, by convention;
      - rtl/top_module.sv      # read_slang doesn't strictly require it
    file_type: systemVerilogSource

  tb:
    files:
      - tb/testbench.sv
    file_type: systemVerilogSource

targets:
  sim:
    default_tool: icarus
    filesets: [rtl, tb]
    toplevel: <testbench_module_name>
    tools:
      icarus:
        iverilog_options: [-g2012]

  synth:
    default_tool: yosys
    filesets: [rtl]            # the testbench never reaches synthesis
    toplevel: <top_design>
    tools:
      yosys:
        arch: generic          # unused: synth.tcl is a full custom template
        output_format: verilog
        output_name: <proj_name>_mapped.v
        yosys_template: ../../../scripts/synth.tcl

Adding or removing a source file is a one-line edit to the rtl fileset; FuseSoC tracks the individual files, so edits are picked up on the next fusesoc run.

2. scripts/yosys_common.tcl — the file you actually write

Find your own values first:

grep -n "^module" rtl/*.sv
# identifies every module; the one instantiating the others is
# top_design — NOT necessarily the .core file's name

grep -n "input\|output" rtl/<top_module_file>.sv
# sort into in_ports (every input except a clock), out_ports, and
# clk_port (leave "" if the design is purely combinational)
set common_dir   [file dirname [file normalize [info script]]]
set project_root [file normalize $common_dir/..]

set top_design   <top_module_name>
set proj_name    <design_name>
set build_name   thesis_flowA_<design_name>_<version>
    ;# deterministic from the .core file's CAPI2 name field:
    ;# "thesis:flowA:<design_name>:0.1.0" -> "thesis_flowA_<design_name>_0.1.0"

set synth_dir       $project_root/build/${build_name}/synth-yosys
set mapped_netlist  $synth_dir/${proj_name}_mapped.v

set clk_port         ""      ;# leave "" for a purely combinational design
set budget_ns        5.0     ;# combinational: flat max-delay budget;
                             ;# clocked: starting guess only - the
                             ;# period lives in constraints/<design>.sdc
set clk_uncertainty  0.07

set in_ports   {a b ...}
set out_ports  {p ...}

set io_delay   0.5
set out_load   0.05

set fp_utilization  40
set fp_aspect_ratio 1
set fp_core_space   2
;# the standard-cell site (fp_site) is PDK-specific: it is set in
;# scripts/tech/<TECH>.tcl, not here

3. RTL simulation

The sim target defined in the core file (step 1) compiles the rtl and tb filesets together with Icarus:

conda activate x-heep
source ~/thesis/tools/oss-cad-suite/environment

fusesoc --cores-root . run --target=sim thesis:flowA:<design_name>

Confirm a clean pass before moving on.

4. Synthesis

The synth target (step 1) is already in the core file. Its yosys_template is scripts/synth.tcl (flow-a/scripts/synth.tcl — copy verbatim; summarised here for reference). Edalize writes the target’s fileset into edalize_yosys_procs.tcl as a read_files proc; synth.tcl reuses that list but passes it to Yosys’s read_slang — a real SystemVerilog elaborator (also known as yosys-slang / sv-elab) that compiles the whole design together, correctly handling multi-file, multi-package designs in one call, rather than the fragile per-file-in-order read_verilog -sv approach:

source ../../../scripts/yosys_common.tcl   ;# per-design values
source ../../../scripts/tech.tcl           ;# $liberty etc. for $TECH
source edalize_yosys_procs.tcl             ;# sources from the .core file

# ... collect the file list from read_files into $slang_files ...

yosys plugin -i slang
yosys read_slang {*}$slang_files --top $top --allow-use-before-declare

yosys hierarchy -top $top
yosys proc
yosys opt
yosys memory
yosys techmap
yosys opt

yosys dfflibmap -liberty $liberty
yosys abc -liberty $liberty
yosys opt_clean

yosys write_verilog -noattr ${proj_name}_mapped.v
exec sed -i {s/ signed / /g} ${proj_name}_mapped.v

The final sed line strips the signed qualifier Yosys still emits on some ports — required before OpenSTA/OpenROAD can read this netlist, and already handled by the script, not a separate manual step.

fusesoc --cores-root . run --target=synth thesis:flowA:<design_name>

Sanity checks before trusting the result:

grep -A 60 "ABC RESULTS" build/${build_name}/synth-yosys/yosys.log
# expect real, varied Sky130 cell counts, not empty/trivial

grep -c "VPWR" build/${build_name}/synth-yosys/<design_name>_mapped.v
# expect 0 unless you deliberately synthesized with power pins

See Troubleshooting if plugin -i slang fails to load, or if read_slang rejects --ignore-unknown-modules/--compat-mode on a newer build.

Synthesis options: excluded cells and the ABC timing target

synth.tcl expands the tech file’s dont_use_cells patterns against the cell LEF and passes the resulting names to dfflibmap and abc (-dont_use), so synthesis never picks cells the flow excludes (probe_p_8, probec_p_8, lpflow_* for Sky130 HD). OpenROAD excludes the same list with set_dont_use.

abc_target in yosys_common.tcl (or ABC_D in the environment, or scripts/run_flow.sh --abc) selects the ABC mapping mode: none is plain abc -liberty; auto uses the SDC clock period minus abc_margin_ps as -D target with the SDC’s driving cell and load as ABC constraints; a number is an explicit target in ps. auto and numeric targets read set_driving_cell from the SDC, so a combinational design without an SDC must stay on none.

5. Post-synthesis gate-level simulation

mkdir -p build/${build_name}/gls-icarus
cd build/${build_name}/gls-icarus
cp ../../../tb/<testbench>.sv <testbench>_gls.sv

Before compiling, edit the copy (never the original RTL testbench):

  1. Remove any parameter override on the DUT instantiation — synthesis flattens parameters, so the mapped netlist’s module no longer declares them.

  2. Remove any hierarchical debug references into internal submodule signals (dut.u_sub.signal) — these fail to bind against a gate-level netlist even when the design is correct.

# cell models for the active PDK come from the tech file (TECH, default sky130hd)
iverilog -g2012 -DFUNCTIONAL -DUNIT_DELAY=#1 \
  -o gls_sim.out \
  $(tclsh scripts/tech_query.tcl cell_verilog) \
  ../../../build/${build_name}/synth-yosys/<design_name>_mapped.v \
  <testbench>_gls.sv

vvp gls_sim.out

Confirm the same pass as step 3.

6. Pre-route PPA

flow-a/scripts/ppa_report.tcl — copy verbatim; branches on whether clk_port is set:

source scripts/yosys_common.tcl

source scripts/tech.tcl      ;# PDK names, from scripts/tech/$TECH.tcl
read_lef     $tech_lef
read_lef     $cell_lef
read_liberty $liberty

read_verilog $mapped_netlist
link_design  $top_design

source scripts/load_constraints.tcl   ;# reads constraints/<design>.sdc
                                      ;# (clocked) or applies set_max_delay
                                      ;# $budget_ns (combinational)

report_design_area
report_checks -path_delay max > $synth_dir/timing_fmax.rpt
report_worst_slack -max
report_power > $synth_dir/power_fmax.rpt
openroad -no_init -exit scripts/ppa_report.tcl

Note

report_power here has no real placement/parasitics or real switching activity behind it — treat it as provisional. See Real-activity power (optional, recommended) below.

Finding the real critical-path delay

Works identically for clocked and combinational designs:

  1. Run with the default tight clock period (constraints/<design>.sdc for a clocked design, budget_ns for a combinational one) — worst slack max comes back negative (VIOLATED).

  2. Read the “data arrival time” line at the bottom of timing_fmax.rpt — that is the real critical-path delay, exact, no bisection needed.

  3. Set the clock period in constraints/<design>.sdc (and TS in the testbench; make check verifies they match) — or budget_ns for a combinational design — to roughly double that value before place & route, for post-route parasitic margin.

Read the report’s Startpoint/Endpoint and per-cell breakdown too, not just the slack number — it shows where the critical path actually is (which pipeline stage, or which input port is disproportionately loaded), which matters more for interpreting results than the single pass/fail number.

7. constraints/<design_name>.sdc — clocked designs only

Skip this entirely if clk_port is "" — load_constraints.tcl applies set_max_delay $budget_ns directly for combinational designs.

create_clock -name <clk_port> -period <period> [get_ports <clk_port>]
set_clock_uncertainty <uncertainty> [get_clocks <clk_port>]
set_input_delay  <io_delay> -clock <clk_port> [get_ports {<in_ports>}]
set_output_delay <io_delay> -clock <clk_port> [get_ports {<out_ports>}]
set_driving_cell -lib_cell <cell> -pin <pin> [get_ports {<in_ports>}]
set_load <pf> [get_ports {<out_ports>}]

This SDC is the single source of truth for timing: every OpenROAD script reads it through scripts/load_constraints.tcl. The delays deliberately have no -max so input/output hold is checked too, and the testbench must launch its inputs at the same offset as the SDC input delay (see Flow A: Signoff and Results).

8. Place & route

flow-a/scripts/pnr.tcl — copy verbatim. Run headless only (-gui is unsafe — detailed_route is known to segfault inside OpenROAD’s GUI process):

openroad -no_init -exit scripts/pnr.tcl

Reading the results:

  • worst slack min = INF is correct, not an error, for any design with zero flip-flops (no hold paths exist to check).

  • Post-route delay vs. the pre-route estimate can go either direction — compare explicitly rather than assuming parasitics always add delay; a small, compact design can see real parasitics add less delay than a zero-load pre-route model assumed.

make pnr also runs scripts/post_route.tcl afterwards (seconds, repeatable with make postroute): it reloads the routed netlist, reads the SPEF, and writes the timing and power reports and the SDF — reports made inside pnr.tcl itself would ignore the extracted wires. make multicorner re-times the routed design at ss/tt/ff with the matching min/nom/max SPEF (the three SPEFs are written by pnr.tcl). The timing-margin variables SETUP_MARGIN, HOLD_MARGIN, ANT_MARGIN and PNR_LIB, the optional make pnr-ss (repair against the ss library, not part of make all) and the repair/antenna steps are described in Flow A: Signoff and Results; the stages of pnr.tcl one by one, with the design choices, are in Place and Route: Stage by Stage.

Output: <design_name>_placed.def (routed, despite the name), <design_name>_route.v, <design_name>_route.cdl, <design_name>_route.sdf, <design_name>_route.spef, <design_name>_antenna*.rpt, plus timing_postroute*.rpt and power_postroute.rpt.

GDS generation

See GDS Generation for full detail (one-time KLayout Python-bindings venv setup, the oss-cad-suite PATH-shadowing gotcha, PDK file locations, and how to verify the output). Quick version:

source ~/klayout-venv/bin/activate     # NOT oss-cad-suite/environment
make gds                               # or: ./scripts/gen_gds.sh

Output: <proj_name>.gds in the project root, generated from <proj_name>_placed.def — despite the name, that DEF already contains full routing, since write_def runs after detailed_route in pnr.tcl above.

9. Post-route SDF-back-annotated simulation

cd build/${build_name}/gls-icarus
cp ../../../tb/<testbench>.sv <testbench>_sdf.sv

Apply the same two fixes as step 5, plus an $sdf_annotate call right after the DUT instantiation:

initial begin
    $sdf_annotate("../../../<design_name>_route.sdf", dut);
end

Important

Increase every settling delay in the testbench before compiling. A small delay like #1 between driving inputs and sampling outputs was sized for a zero-delay functional model — use at least 2x the real post-route critical-path delay (found in step 6/8’s reports) as the new settling window. Skipping this produces X-propagation garbage in the output (including corrupted string output from a ternary comparison against an unresolved X value) that looks like a functional bug but is a testbench-timing bug. See Troubleshooting for the full pattern, including the distinct clocked-design variant of this same class of issue.

iverilog -g2012 -gspecify -ginterconnect -o gls_sdf_sim.out \
  $(tclsh scripts/tech_query.tcl cell_verilog) \
  ../../../<design_name>_route.v \
  <testbench>_sdf.sv

vvp gls_sdf_sim.out -sdf-verbose 2>&1 | tee run.log

Both -gspecify and -ginterconnect are required (unlike step 5): -gspecify is disabled by default in Icarus, and without it $sdf_annotate has no ModPath objects to attach delays to — every cell fails with “Unable to match ModPath” and the sim can silently report a false-positive pass with zero real delay applied. Do not define -DFUNCTIONAL here — it bypasses the specify blocks this stage needs delays attached to.

Confirm the same pass as every prior stage — this is the final correctness gate before this design’s PPA numbers are trustworthy for comparison against the other three flows.

Known gotchas

See Troubleshooting for the full list — in particular, both a multi-file/read_slang-specific section and the original single-file/clocked-design section apply depending on your design.