Merge pull request #115 from TinyTapeout/tech-refactor

Refactor to separate PDK/tech-specific code
This commit is contained in:
Uri Shaked
2025-09-14 14:51:28 +03:00
committed by GitHub
59 changed files with 314 additions and 340 deletions
+1 -1
View File
@@ -34,4 +34,4 @@ jobs:
- name: Run mypy
run: |
mypy cells.py config.py configure.py documentation.py git_utils.py markdown_utils.py project.py project_info.py rom.py shuttle.py --check-untyped-defs
mypy *.py --check-untyped-defs
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
run: pip install -r requirements.txt
- name: Install Sky130 PDK
uses: TinyTapeout/volare-action@v2
uses: TinyTapeout/ciel-action@v1
with:
pdk_name: sky130
pdk_version: ${{ env.PDK_VERSION }}
-44
View File
@@ -1,44 +0,0 @@
import json
import os
from typing import Dict, List, Optional, TypedDict
class Port(TypedDict):
kind: str
name: str
direction: str
description: str
class Sky130Cell(TypedDict):
description: str
file_prefix: str
library: str
name: str
parameters: List[str]
ports: List[Port]
type: str
verilog_name: str
equation: Optional[str]
class IHPCell(TypedDict):
description: str
doc_name: str
doc_ref: int
Sky130Cells = Dict[str, Sky130Cell]
IHPCells = Dict[str, IHPCell]
def load_sky130_cells() -> Sky130Cells:
script_dir = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(script_dir, "cells.json")) as fh:
return json.load(fh)
def load_ihp_cells() -> IHPCells:
script_dir = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(script_dir, "ihp/cells.json")) as fh:
return json.load(fh)
+3 -1
View File
@@ -1,5 +1,7 @@
from typing import TypedDict
from tech import TechName
class Config(TypedDict):
"""TypedDict for Tiny Tapeout's config.yaml file."""
@@ -11,4 +13,4 @@ class Config(TypedDict):
top_level_macro: str
powered_netlists: bool
no_power_gating: bool
pdk: str
pdk: TechName
+2 -2
View File
@@ -21,7 +21,7 @@ class TechDef(TypedDict):
tech: Dict[str, TechDef] = {
"sky130": {
"sky130A": {
"PRBOUNDARY_LAYER": 235, # prBoundary
"PRBOUNDARY_DATATYPE": 4, # .boundary
"LOGO_LAYER": 71, # Metal4
@@ -149,7 +149,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TT logo generator")
parser.add_argument(
"--pdk", type=str, default="sky130", choices=["sky130", "ihp-sg13g2"]
"--pdk", type=str, default="sky130A", choices=["sky130A", "ihp-sg13g2"]
)
parser.add_argument("--top", action="store_true")
parser.add_argument("--bottom", action="store_true")
+12 -11
View File
@@ -293,9 +293,9 @@ def main():
else:
raise PrecheckFailure("Layout file extension is neither .gds nor .gds.br")
if args.tech not in tech_names:
raise PrecheckFailure(f"Invalid tech: {args.tech}")
tech = args.tech
if tech not in tech_names:
raise PrecheckFailure(f"Invalid tech: {tech}")
yaml_dir = os.path.dirname(args.gds)
while not os.path.exists(f"{yaml_dir}/info.yaml"):
@@ -317,15 +317,16 @@ def main():
pinout = yaml_data.get("pinout", {})
if uses_3v3 and not is_analog:
raise PrecheckFailure("Projects with 3v3 power need at least one analog pin")
def_root = f"../tech/{tech}/def"
if is_analog:
if uses_3v3:
template_def = f"../def/analog/tt_analog_{tiles}_3v3.def"
template_def = f"{def_root}/analog/tt_analog_{tiles}_3v3.def"
else:
template_def = f"../def/analog/tt_analog_{tiles}.def"
template_def = f"{def_root}/analog/tt_analog_{tiles}.def"
elif tech == "ihp-sg13g2":
template_def = f"../ihp/def/tt_block_{tiles}_pgvdd.def"
template_def = f"{def_root}/tt_block_{tiles}_pgvdd.def"
else:
template_def = f"../def/tt_block_{tiles}_pg.def"
template_def = f"{def_root}/tt_block_{tiles}_pg.def"
logging.info(f"using def template {template_def}")
lef_file = gds_stem + ".lef"
@@ -335,22 +336,22 @@ def main():
{
"name": "Magic DRC",
"check": lambda: magic_drc(gds_file, top_module),
"tech": "sky130",
"tech": "sky130A",
},
{
"name": "KLayout FEOL",
"check": lambda: klayout_drc(gds_file, "feol"),
"tech": "sky130",
"tech": "sky130A",
},
{
"name": "KLayout BEOL",
"check": lambda: klayout_drc(gds_file, "beol"),
"tech": "sky130",
"tech": "sky130A",
},
{
"name": "KLayout offgrid",
"check": lambda: klayout_drc(gds_file, "offgrid"),
"tech": "sky130",
"tech": "sky130A",
},
{
"name": "KLayout pin label overlapping drawing",
@@ -380,7 +381,7 @@ def main():
{
"name": "Power pin check",
"check": lambda: power_pin_check(verilog_file, lef_file, uses_3v3),
"tech": "sky130",
"tech": "sky130A",
},
{"name": "Layer check", "check": lambda: layer_check(gds_file, tech)},
{"name": "Cell name check", "check": lambda: cell_name_check(gds_file)},
+11 -11
View File
@@ -1,5 +1,5 @@
# fmt: off
valid_layers_sky130 = {
valid_layers_sky130A = {
(10, 0), (11, 0), (11, 20), (11, 44), (17, 0), (18, 20), (20, 0), (21, 0),
(22, 0), (22, 20), (22, 21), (22, 22), (22, 23), (22, 24), (23, 0), (23, 28),
(25, 0), (25, 42), (25, 43), (25, 44), (26, 20), (26, 21), (26, 22), (27, 0),
@@ -176,24 +176,24 @@ valid_layers_ihp_sg13g2 = [
]
# layers used for the analog pin check
layer_map_sky130 = {
layer_map_sky130A = {
"met4": (71, 20),
"via3": (70, 44),
}
def analog_pin_pos_sky130(pin_number: int, uses_3v3: bool):
def analog_pin_pos_sky130A(pin_number: int, uses_3v3: bool):
return 151.81 - 19.32 * pin_number - (15.64 if uses_3v3 else 0)
valid_layers = {"sky130": valid_layers_sky130, "ihp-sg13g2": valid_layers_ihp_sg13g2}
layer_map = {"sky130": layer_map_sky130}
analog_pin_pos = {"sky130": analog_pin_pos_sky130}
lyp_filename = {"sky130": "sky130A.lyp", "ihp-sg13g2": "sg13g2.lyp"}
valid_layers = {"sky130A": valid_layers_sky130A, "ihp-sg13g2": valid_layers_ihp_sg13g2}
layer_map = {"sky130A": layer_map_sky130A}
analog_pin_pos = {"sky130A": analog_pin_pos_sky130A}
lyp_filename = {"sky130A": "sky130A.lyp", "ihp-sg13g2": "sg13g2.lyp"}
# TODO: read layer numbers from lyp file
valid_lef_port_layers = {
"sky130": {
"sky130A": {
"met1.pin": (68, 16),
"met2.pin": (69, 16),
"met3.pin": (70, 16),
@@ -208,7 +208,7 @@ valid_lef_port_layers = {
},
}
forbidden_layers = {
"sky130": ["met5.drawing", "met5.pin", "met5.label"],
"sky130A": ["met5.drawing", "met5.pin", "met5.label"],
"ihp-sg13g2": [
"TopMetal1.drawing",
"TopMetal1.pin",
@@ -219,8 +219,8 @@ forbidden_layers = {
],
}
power_pins_layer = {
"sky130": "met4",
"sky130A": "met4",
"ihp-sg13g2": "Metal5",
}
tech_names = ["sky130", "ihp-sg13g2"]
tech_names = ["sky130A", "ihp-sg13g2"]
+28 -28
View File
@@ -251,7 +251,7 @@ def generate_analog_example(
tcl_append(
f"""
def read ../def/analog/tt_analog_1x2.def
def read ../tech/sky130A/def/analog/tt_analog_1x2.def
cellname rename tt_um_template {toplevel}
"""
)
@@ -477,14 +477,14 @@ def test_klayout_beol_fail(gds_fail_met1_poly: str):
def test_klayout_checks_pass(gds_valid: str):
precheck.klayout_checks(gds_valid, "TEST_valid", "sky130")
precheck.klayout_checks(gds_valid, "TEST_valid", "sky130A")
def test_klayout_checks_fail_metal5(gds_fail_metal5_poly: str):
with pytest.raises(
precheck.PrecheckFailure, match=r"Forbidden layer met5\.drawing found in .+"
):
precheck.klayout_checks(gds_fail_metal5_poly, "TEST_met5_error", "sky130")
precheck.klayout_checks(gds_fail_metal5_poly, "TEST_met5_error", "sky130A")
def test_klayout_checks_fail_pr_boundary(gds_no_pr_boundary: str):
@@ -492,7 +492,7 @@ def test_klayout_checks_fail_pr_boundary(gds_no_pr_boundary: str):
precheck.PrecheckFailure,
match=r"prBoundary.boundary \(235/4\) layer not found in .+",
):
precheck.klayout_checks(gds_no_pr_boundary, "TEST_no_prboundary", "sky130")
precheck.klayout_checks(gds_no_pr_boundary, "TEST_no_prboundary", "sky130A")
def test_klayout_top_module_name(gds_invalid_macro_name: str):
@@ -501,7 +501,7 @@ def test_klayout_top_module_name(gds_invalid_macro_name: str):
match="Top macro name mismatch: expected TEST_invalid_macro_name, got wrong_name",
):
precheck.klayout_checks(
gds_invalid_macro_name, "TEST_invalid_macro_name", "sky130"
gds_invalid_macro_name, "TEST_invalid_macro_name", "sky130A"
)
@@ -516,7 +516,7 @@ def test_klayout_zero_area_drc_fail(gds_zero_area: str):
def test_shapes_outside_area(gds_shapes_outside_area: str):
with pytest.raises(precheck.PrecheckFailure, match="Shapes outside project area"):
precheck.boundary_check(gds_shapes_outside_area, "sky130")
precheck.boundary_check(gds_shapes_outside_area, "sky130A")
def test_wrong_power_pins_1(verilog_lef_wrong_power_pins: tuple[str, str]):
@@ -533,7 +533,7 @@ def test_wrong_power_pins_2(verilog_lef_wrong_power_pins: tuple[str, str]):
def test_invalid_layer(gds_invalid_layer: str):
with pytest.raises(precheck.PrecheckFailure, match="Invalid layers in GDS"):
precheck.layer_check(gds_invalid_layer, "sky130")
precheck.layer_check(gds_invalid_layer, "sky130A")
def test_invalid_cell_name(gds_invalid_cell_name: str):
@@ -557,10 +557,10 @@ def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_example",
False,
"sky130",
"sky130A",
)
@@ -569,10 +569,10 @@ def test_pin_analog_power_compat(gds_lef_analog_power_compat: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_power_compat",
False,
"sky130",
"sky130A",
)
@@ -585,10 +585,10 @@ def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_wrong_vgnd",
False,
"sky130",
"sky130A",
)
@@ -601,10 +601,10 @@ def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str,
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_overlapping_vgnd",
False,
"sky130",
"sky130A",
)
@@ -613,10 +613,10 @@ def test_pin_analog_compound_vgnd(gds_lef_analog_compound_vgnd: tuple[str, str])
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_compound_vgnd",
False,
"sky130",
"sky130A",
)
@@ -625,10 +625,10 @@ def test_pin_analog_example_3v3(gds_lef_analog_example_3v3: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3",
True,
"sky130",
"sky130A",
)
@@ -641,10 +641,10 @@ def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_example",
True,
"sky130",
"sky130A",
)
@@ -657,17 +657,17 @@ def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]):
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"../tech/sky130A/def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3",
False,
"sky130",
"sky130A",
)
def test_analog_exact_pins(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example
precheck.analog_pin_check(
gds_file, "sky130", True, False, 2, {"ua[0]": "x", "ua[1]": "x"}
gds_file, "sky130A", True, False, 2, {"ua[0]": "x", "ua[1]": "x"}
)
@@ -678,7 +678,7 @@ def test_analog_less_pins(gds_lef_analog_pin_example: tuple[str, str]):
match="Analog pin `ua\\[1\\]` is connected to some metal but `analog_pins` is set to 1 .*",
):
precheck.analog_pin_check(
gds_file, "sky130", True, False, 1, {"ua[0]": "x", "ua[1]": "x"}
gds_file, "sky130A", True, False, 1, {"ua[0]": "x", "ua[1]": "x"}
)
@@ -689,7 +689,7 @@ def test_analog_more_pins(gds_lef_analog_pin_example: tuple[str, str]):
match="Analog pin `ua\\[2\\]` is not connected to any adjacent metal but `analog_pins` is set to 3 .*",
):
precheck.analog_pin_check(
gds_file, "sky130", True, False, 3, {"ua[0]": "x", "ua[1]": "x"}
gds_file, "sky130A", True, False, 3, {"ua[0]": "x", "ua[1]": "x"}
)
@@ -699,7 +699,7 @@ def test_analog_less_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
precheck.PrecheckFailure,
match="Analog pin `ua\\[1\\]` is connected to some metal but the description of `ua\\[1\\]` .*",
):
precheck.analog_pin_check(gds_file, "sky130", True, False, 2, {"ua[0]": "x"})
precheck.analog_pin_check(gds_file, "sky130A", True, False, 2, {"ua[0]": "x"})
def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
@@ -710,7 +710,7 @@ def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
):
precheck.analog_pin_check(
gds_file,
"sky130",
"sky130A",
True,
False,
2,
+36 -134
View File
@@ -12,27 +12,14 @@ import typing
import cairosvg # type: ignore
import chevron
import gdstk # type: ignore
import volare
import yaml
from git.repo import Repo
import git_utils
from cells import Sky130Cell, load_ihp_cells, load_sky130_cells
from config_utils import read_config, write_config
from markdown_utils import limit_markdown_headings
from project_info import ProjectInfo, ProjectYamlError
_SKY130_CELL_URL_FMT = "https://skywater-pdk.readthedocs.io/en/main/contents/libraries/sky130_fd_sc_hd/cells/{name}/README.html"
_IHP_CELL_URL_FMT = "https://raw.githubusercontent.com/IHP-GmbH/IHP-Open-PDK/refs/heads/main/ihp-sg13g2/libs.ref/sg13g2_stdcell/doc/sg13g2_stdcell_typ_1p20V_25C.pdf#{ref}"
def _sky130_cell_url(cell_name):
return _SKY130_CELL_URL_FMT.format(name=cell_name)
def _ihp_cell_url(doc_ref):
return _IHP_CELL_URL_FMT.format(ref=doc_ref)
from tech import TechName, tech_map
PINOUT_KEYS = [
"ui[0]",
@@ -76,11 +63,12 @@ class Project:
index: int,
git_url: str,
local_dir: str,
pdk: str,
pdk: TechName,
is_user_project: bool,
):
self.git_url = git_url
self.ihp = pdk == "ihp-sg13g2"
self.pdk = pdk
self.tech = tech_map[pdk]
self.index = index
self.local_dir = os.path.realpath(local_dir)
self.is_user_project = is_user_project
@@ -357,10 +345,7 @@ class Project:
return os.path.join(self.local_dir, f"{self.info.top_module}.v")
def get_tile_sizes(self):
if self.ihp:
tile_sizes_yaml = "ihp/tile_sizes.yaml"
else:
tile_sizes_yaml = "tile_sizes.yaml"
tile_sizes_yaml = f"tech/{self.pdk}/tile_sizes.yaml"
with open(
os.path.join(os.path.dirname(__file__), tile_sizes_yaml), "r"
) as stream:
@@ -487,10 +472,10 @@ class Project:
tiles = self.info.tiles
tile_sizes = self.get_tile_sizes()
die_area = tile_sizes[tiles]
if self.ihp:
def_template = f"dir::../tt/ihp/def/tt_block_{tiles}_pgvdd.def"
else:
def_template = f"dir::../tt/def/tt_block_{tiles}_pg.def"
def_suffix = self.tech.def_suffix
def_template = (
f"dir::../tt/tech/{self.pdk}/def/tt_block_{tiles}_{def_suffix}.def"
)
config = {
"DESIGN_NAME": self.info.top_module,
"VERILOG_FILES": [f"dir::{src}" for src in self.sources],
@@ -522,7 +507,7 @@ class Project:
os.makedirs("runs/wokwi", exist_ok=True)
arg_progress = "--hide-progress-bar" if "CI" in os.environ else ""
arg_pdk_root = '--pdk-root "$PDK_ROOT"' if "PDK_ROOT" in os.environ else ""
arg_pdk = "--manual-pdk --pdk ihp-sg13g2" if self.ihp else ""
arg_pdk = self.tech.librelane_pdk_args
harden_cmd = f"python -m librelane {arg_pdk_root} --docker-no-tty --dockerized {arg_pdk_root} {arg_pdk} --run-tag wokwi --force-run-dir runs/wokwi {arg_progress} src/config_merged.json"
env = os.environ.copy()
logging.debug(harden_cmd)
@@ -546,27 +531,13 @@ class Project:
)
f.write("\n")
with open("runs/wokwi/FLOW_VERSION", "w") as f:
resolved_json_path = "runs/wokwi/resolved.json"
config = json.load(open(os.path.join(self.local_dir, resolved_json_path)))
librelane_version = config["meta"]["librelane_version"]
if self.ihp:
pdk_repo = Repo(env["PDK_ROOT"])
pdk_repo_name = list(pdk_repo.remotes[0].urls)[0].split("/")[-1]
pdk_commit = pdk_repo.commit().hexsha
pdk_sources = f"{pdk_repo_name} {pdk_commit}"
else:
pdk_sources_file = os.path.join(
config["PDK_ROOT"], config["PDK"], "SOURCES"
)
pdk_sources = open(pdk_sources_file).read()
open("runs/wokwi/FLOW_VERSION", "w").write(f"LibreLane {librelane_version}\n")
open("runs/wokwi/PDK_SOURCES", "w").write(pdk_sources)
if not self.ihp:
volare.enable(
volare.get_volare_home(), # uses PDK_ROOT if set, ~/.volare otherwise
{"sky130A": "sky130"}[config["PDK"]],
pdk_sources.split()[1],
)
f.write(f"LibreLane {librelane_version}\n")
with open("runs/wokwi/PDK_SOURCES", "w") as f:
f.write(self.tech.read_pdk_version(config["PDK_ROOT"]))
os.chdir(cwd)
@@ -727,26 +698,7 @@ class Project:
top_cells = self.get_final_gds_top_cells()
# Remove label layers (i.e. delete text), then generate SVG:
sky130_label_layers = [
(64, 59), # 64/59 - pwell.label
(64, 5), # 64/5 - nwell.label
(67, 5), # 67/5 - li1.label
(68, 5), # 68/5 - met1.label
(69, 5), # 69/5 - met2.label
(70, 5), # 70/5 - met3.label
(71, 5), # 71/5 - met4.label
]
ihp_label_layers = [
(8, 1), # 8/1 - Metal1.label
(8, 25), # 8/25 - Metal1.text
(67, 25), # 67/25 - Metal5.text
(126, 25), # 126/25 - TopMetal1.text
]
if self.ihp:
label_layers = ihp_label_layers
else:
label_layers = sky130_label_layers
label_layers = self.tech.label_layers
top_cells.filter(label_layers)
for subcell in top_cells.dependencies(True):
subcell.filter(label_layers)
@@ -777,31 +729,9 @@ class Project:
p.stderr.decode().strip()
)
)
# Remove more layers that are hardly visible anyway:
sky130_buried_layers = [
(64, 16), # 64/16 - nwell.pin
(65, 44), # 65/44 - tap.drawing
(68, 16), # 68/16 - met1.pin
(68, 44), # 68/44 - via.drawing
(81, 4), # 81/4 - areaid.standardc
(70, 20), # 70/20 - met3.drawing
# Important:
# (68,20), # 68/20 - met1.drawing
# Less important, but keep for now:
# (69,20), # 69/20 - met2.drawing
]
ihp_buried_layers = [
(6, 0), # 6/0 - Cont.drawing
(8, 2), # 8/2 - Metal1.pin
(19, 0), # 19/0 - Via1.drawing
(51, 0), # 51/0 - HeatTrans.drawing
(189, 4), # 189/4 - prBoundary.boundary
]
if self.ihp:
buried_layers = ihp_buried_layers
else:
buried_layers = sky130_buried_layers
# Remove more layers that are hardly visible anyway:
buried_layers = self.tech.buried_layers
top_cells.filter(buried_layers)
for subcell in top_cells.dependencies(True):
subcell.filter(buried_layers)
@@ -822,6 +752,7 @@ class Project:
p.stderr.decode().strip()
)
)
# Fall back to cairosvg, and since we're doing that, might as well use the original full-detail SVG anyway:
cairosvg.svg2png(url=svg, write_to=png)
@@ -869,10 +800,7 @@ class Project:
if "WIDTHLABEL" not in line:
warnings.append(line.strip())
if self.ihp:
tt_corner = "nom_typ_1p20V_25C"
else:
tt_corner = "nom_tt_025C_1v80"
tt_corner = self.tech.tt_corner
sta_report_glob = f"runs/wokwi/*-openroad-stapostpnr/{tt_corner}/checks.rpt"
sta_report = glob.glob(os.path.join(self.local_dir, sta_report_glob))[0]
with open(os.path.join(self.local_dir, sta_report)) as f:
@@ -911,23 +839,11 @@ class Project:
Categories = typing.TypedDict(
"Categories", {"categories": typing.List[str], "map": typing.Dict[str, int]}
)
if self.ihp:
categories_file = "ihp/categories.json"
else:
categories_file = "categories.json"
categories_file = f"tech/{self.pdk}/categories.json"
with open(os.path.join(SCRIPT_DIR, categories_file)) as fh:
categories: Categories = json.load(fh)
if self.ihp:
ihp_defs = load_ihp_cells()
else:
sky130_defs = load_sky130_cells()
def _cell_url(name):
if self.ihp:
return _ihp_cell_url(ihp_defs[name]["doc_ref"])
else:
return _sky130_cell_url(name)
cell_defs = self.tech.load_cell_definitions()
# print all used cells, sorted by frequency
total = 0
@@ -941,12 +857,13 @@ class Project:
):
if count > 0:
total += count
cell_link = _cell_url(name)
if self.ihp:
description = ihp_defs[name]["description"]
else:
description = sky130_defs[name]["description"]
cell_def = cell_defs.get(name)
if cell_def is not None:
cell_link = cell_def["url"]
description = cell_def["description"]
print(f"| [{name}]({cell_link}) | {description} |{count} |")
else:
print(f"| {name} | (unknown) | {count} |")
print(f"| | Total | {total} |")
@@ -978,9 +895,14 @@ class Project:
for cat_name, cat_dict in sorted(
by_category.items(), key=lambda x: x[1]["count"], reverse=True
):
cell_links = [
f"[{name}]({_cell_url(name)})" for name in cat_dict["examples"]
]
cell_links = []
for name in cat_dict["examples"]:
cell_def = cell_defs.get(name)
if cell_def is not None:
cell_links.append(f"[{name}]({cell_def['url']})")
else:
cell_links.append(name)
print(f'|{cat_name} | {" ".join(cell_links)} | {cat_dict["count"]}|')
print(f"## {total} total cells (excluding fill and tap cells)")
@@ -1005,15 +927,9 @@ class Project:
def get_cell_counts_from_gl(self):
cell_count: typing.Dict[str, int] = {}
total = 0
if self.ihp:
cell_re = r"sg13g2_(?P<cell_name>\S+)_(?P<cell_drive>\d+)"
else:
cell_re = (
r"sky130_(?P<cell_lib>\S+)__(?P<cell_name>\S+)_(?P<cell_drive>\d+)"
)
with open(self.get_gl_path()) as fh:
for line in fh.readlines():
m = re.search(cell_re, line)
m = re.search(self.tech.cell_regexp, line)
if m is not None:
total += 1
gd = m.groupdict()
@@ -1025,17 +941,3 @@ class Project:
except KeyError:
cell_count[cell_name] = 1
return cell_count
# Load all the json defs and combine into one big dict, keyed by cellname
# Needs access to the PDK
def create_defs(self):
# replace this with a find
json_files = glob.glob("sky130_fd_sc_hd/latest/cells/*/definition.json")
definitions: typing.Dict[str, Sky130Cell] = {}
for json_file in json_files:
with open(json_file) as fh:
definition: Sky130Cell = json.load(fh)
definitions[definition["name"]] = definition
with open("cells.json", "w") as fh:
json.dump(definitions, fh)
-1
View File
@@ -17,7 +17,6 @@ dependencies = [
"python-frontmatter",
"PyYAML",
"requests",
"volare",
"yowasp-yosys",
]
+5 -38
View File
@@ -1,11 +1,9 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile
#
anyio==4.10.0
# via httpx
brotli==1.1.0
# via tt-support-tools (pyproject.toml)
cairocffi==1.7.1
@@ -13,10 +11,7 @@ cairocffi==1.7.1
cairosvg==2.8.2
# via tt-support-tools (pyproject.toml)
certifi==2025.8.3
# via
# httpcore
# httpx
# requests
# via requests
cffi==1.17.1
# via cairocffi
cfgv==3.4.0
@@ -26,9 +21,7 @@ charset-normalizer==3.4.2
chevron==0.14.0
# via tt-support-tools (pyproject.toml)
click==8.2.1
# via
# volare
# yowasp-yosys
# via yowasp-yosys
cocotb==1.9.2
# via tt-support-tools (pyproject.toml)
cssselect2==0.8.0
@@ -47,29 +40,16 @@ gitdb==4.0.12
# via gitpython
gitpython==3.1.45
# via tt-support-tools (pyproject.toml)
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via volare
identify==2.6.12
# via pre-commit
idna==3.10
# via
# anyio
# httpx
# requests
# via requests
importlib-resources==6.5.2
# via wasmtime
iniconfig==2.1.0
# via pytest
klayout==0.29.12
# via tt-support-tools (pyproject.toml)
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
mistune==3.1.3
# via tt-support-tools (pyproject.toml)
nodeenv==1.9.1
@@ -80,8 +60,6 @@ numpy==1.26.4
# tt-support-tools (pyproject.toml)
packaging==25.0
# via pytest
pcpp==1.30
# via volare
pillow==11.3.0
# via cairosvg
platformdirs==4.3.8
@@ -95,9 +73,7 @@ pre-commit==4.2.0
pycparser==2.22
# via cffi
pygments==2.19.2
# via
# pytest
# rich
# via pytest
pytest==8.4.1
# via tt-support-tools (pyproject.toml)
python-frontmatter==1.1.0
@@ -107,15 +83,10 @@ pyyaml==6.0.2
# pre-commit
# python-frontmatter
# tt-support-tools (pyproject.toml)
# volare
requests==2.32.4
# via tt-support-tools (pyproject.toml)
rich==13.9.4
# via volare
smmap==5.0.2
# via gitdb
sniffio==1.3.1
# via anyio
tinycss2==1.4.0
# via
# cairosvg
@@ -124,8 +95,6 @@ urllib3==2.5.0
# via requests
virtualenv==20.33.1
# via pre-commit
volare==0.20.6
# via tt-support-tools (pyproject.toml)
wasmtime==35.0.0
# via yowasp-runtime
webencodings==0.5.1
@@ -136,5 +105,3 @@ yowasp-runtime==1.78
# via yowasp-yosys
yowasp-yosys==0.55.0.0.post944
# via tt-support-tools (pyproject.toml)
zstandard==0.23.0
# via volare
+14 -5
View File
@@ -3,8 +3,6 @@
"VERILOG_FILES": "dir::tt_um_chip_rom.v",
"FP_SIZING": "absolute",
"DIE_AREA": [0, 0, 161.00, 111.52],
"FP_DEF_TEMPLATE": "ref::$DESIGN_DIR/../def/tt_block_1x1_pg.def",
"FP_IO_HLENGTH": 2,
"FP_IO_VLENGTH": 2,
@@ -13,9 +11,8 @@
"CLOCK_PERIOD": 20,
"CLOCK_PORT": "clk",
"//": "don't use power rings or met5",
"//": "don't use power rings",
"DESIGN_IS_CORE": false,
"RT_MAX_LAYER": "met4",
"//": "save some time",
"RUN_KLAYOUT_XOR": false,
@@ -24,5 +21,17 @@
"TOP_MARGIN_MULT": 1,
"BOTTOM_MARGIN_MULT": 1,
"LEFT_MARGIN_MULT": 6,
"RIGHT_MARGIN_MULT": 6
"RIGHT_MARGIN_MULT": 6,
"pdk::sky130A": {
"DIE_AREA": [0, 0, 161.0, 111.52],
"FP_DEF_TEMPLATE": "ref::$DESIGN_DIR/../tech/sky130A/def/tt_block_1x1_pg.def",
"RT_MAX_LAYER": "met4"
},
"pdk::ihp-sg13g2": {
"DIE_AREA": [0, 0, 202.08, 154.98],
"FP_DEF_TEMPLATE": "ref::$DESIGN_DIR/../tech/ihp-sg13g2/def/tt_block_1x1_pgvdd.def",
"RT_MAX_LAYER": "Metal5"
}
}
-28
View File
@@ -1,28 +0,0 @@
{
"DESIGN_NAME": "tt_um_chip_rom",
"VERILOG_FILES": "dir::tt_um_chip_rom.v",
"FP_SIZING": "absolute",
"DIE_AREA": [0, 0, 202.08, 154.98],
"FP_DEF_TEMPLATE": "ref::$DESIGN_DIR/../ihp/def/tt_block_1x1_pgvdd.def",
"FP_IO_HLENGTH": 2,
"FP_IO_VLENGTH": 2,
"//": "period is in ns, so 20ns == 50mHz",
"CLOCK_PERIOD": 20,
"CLOCK_PORT": "clk",
"//": "don't use power rings or TopMetal",
"DESIGN_IS_CORE": false,
"RT_MAX_LAYER": "Metal5",
"//": "save some time",
"RUN_KLAYOUT_XOR": false,
"//": "reduce wasted space",
"TOP_MARGIN_MULT": 1,
"BOTTOM_MARGIN_MULT": 1,
"LEFT_MARGIN_MULT": 6,
"RIGHT_MARGIN_MULT": 6
}
+15 -29
View File
@@ -13,27 +13,7 @@ import yaml
from config import Config
from project import Project
from shuttle_index import ShuttleIndex, ShuttleIndexLayout, ShuttleIndexProject
# Power gate macros for each PDK. We need to copy them to the multiplexer directory:
SHUTTLE_PG_MACROS = {
"sky130": [
"tt_pg_1v8_hp_1",
"tt_pg_1v8_hp_2",
"tt_pg_1v8_hp_4",
"tt_pg_1v8_ll_1",
"tt_pg_1v8_ll_2",
"tt_pg_1v8_ll_4",
"tt_pg_3v3_2",
],
"ihp-sg13g2": [
"tt_pg_1v5_hp_1",
"tt_pg_1v5_hp_2",
"tt_pg_1v5_hp_4",
"tt_pg_1v5_ll_1",
"tt_pg_1v5_ll_2",
"tt_pg_1v5_ll_4",
],
}
from tech import tech_map
def copy_print(src: str, dest: str):
@@ -76,7 +56,13 @@ class ShuttleConfig:
self.projects = projects
self.script_dir = os.path.dirname(os.path.realpath(__file__))
self.modules_yaml_name = modules_yaml_name
self.mux_config_yaml_name = os.environ.get("TT_CONFIG", "sky130.yaml")
pdk = config.get("pdk")
if pdk is None:
raise ValueError("PDK is not specified in the configuration.")
self.tech = tech_map[pdk]
self.mux_config_yaml_name = os.environ.get(
"TT_CONFIG", self.tech.mux_config_yaml_name
)
self.tt_top_macro = config.get("top_level_macro", "openframe_project_wrapper")
self.read_mux_config_file()
@@ -283,17 +269,17 @@ class ShuttleConfig:
)
# Copy power gate / analog switch macros:
for pdk, pg_macros in SHUTTLE_PG_MACROS.items():
for pg_macro in pg_macros:
self.copy_mux_macro(f"pg/{pdk}/{pg_macro}", pg_macro)
self.copy_mux_macro("asw/sky130/tt_asw_3v3", "tt_asw_3v3")
for mux_macro in self.tech.mux_macros:
self.copy_mux_macro(mux_macro, os.path.basename(mux_macro))
# Copy logo & shuttle ID
self.copy_logo_macro("tt_logo_top")
self.copy_logo_macro("tt_logo_bottom")
if self.config.get("pdk") == "ihp-sg13g2":
self.copy_logo_macro("tt_logo_corner", source_dir="tt/ihp")
for logo_macro in self.tech.extra_logo_macros:
self.copy_logo_macro(
os.path.basename(logo_macro),
source_dir=os.path.join("tt", os.path.dirname(logo_macro)),
)
def copy_final_results(self):
macros = ["tt_um_chip_rom", "tt_ctrl", "tt_mux", "tt_top"]
+139
View File
@@ -0,0 +1,139 @@
import json
import os
from typing import Dict, List, Literal, Protocol, Tuple, TypedDict
from git.repo import Repo
TechName = Literal["sky130A", "ihp-sg13g2"]
class CellDefinition(TypedDict):
url: str
description: str
class Tech(Protocol):
def_suffix: str
librelane_pdk_args: str
tt_corner: str
cell_regexp: str
""" These layers will be removed from the SVG render of the layout """
label_layers: List[Tuple[int, int]]
""" These layers are hardly visible and will also be removed from the SVG render of the layout """
buried_layers: List[Tuple[int, int]]
""" Default name of the mux config YAML file. """
mux_config_yaml_name: str
""" Lists all of the analog switch / power gate macros that need to be copied to the tt_top directory. """
mux_macros: List[str]
""" Lists all of the extra logo macros that need to be copied to the tt_top directory. """
extra_logo_macros: List[str]
def read_pdk_version(self, pdk_root: str) -> str:
raise NotImplementedError()
def load_cell_definitions(self) -> Dict[str, CellDefinition]:
raise NotImplementedError()
class Sky130Tech(Tech):
def_suffix = "pg"
librelane_pdk_args = ""
tt_corner = "nom_tt_025C_1v80"
cell_regexp = r"sky130_(?P<cell_lib>\S+)__(?P<cell_name>\S+)_(?P<cell_drive>\d+)"
label_layers = [
(64, 59), # pwell.label
(64, 5), # nwell.label
(67, 5), # li1.label
(68, 5), # met1.label
(69, 5), # met2.label
(70, 5), # met3.label
(71, 5), # met4.label
]
buried_layers = [
(64, 16), # nwell.pin
(65, 44), # tap.drawing
(68, 16), # met1.pin
(68, 44), # via.drawing
(81, 4), # areaid.standardc
(70, 20), # met3.drawing
]
mux_config_yaml_name = "sky130.yaml"
mux_macros = [
"pg/sky130/tt_pg_1v8_hp_1",
"pg/sky130/tt_pg_1v8_hp_2",
"pg/sky130/tt_pg_1v8_hp_4",
"pg/sky130/tt_pg_1v8_ll_1",
"pg/sky130/tt_pg_1v8_ll_2",
"pg/sky130/tt_pg_1v8_ll_4",
"pg/sky130/tt_pg_3v3_2",
"asw/sky130/tt_asw_3v3",
]
extra_logo_macros = []
def read_pdk_version(self, pdk_root: str) -> str:
pdk_sources_file = os.path.join(pdk_root, "sky130A", "SOURCES")
with open(pdk_sources_file) as f:
return f.read()
def load_cell_definitions(self) -> Dict[str, CellDefinition]:
URL_FORMAT = "https://skywater-pdk.readthedocs.io/en/main/contents/libraries/sky130_fd_sc_hd/cells/{name}/README.html"
script_dir = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(script_dir, "tech/sky130A/cells.json")) as fh:
cells = json.load(fh)
for name, cell in cells.items():
cell["url"] = URL_FORMAT.format(name=name)
return cells
class IHPTech(Tech):
def_suffix = "pgvdd"
librelane_pdk_args = "--manual-pdk --pdk ihp-sg13g2"
tt_corner = "nom_typ_1p20V_25C"
cell_regexp = r"sg13g2_(?P<cell_name>\S+)_(?P<cell_drive>\d+)"
label_layers = [
(8, 1), # Metal1.label
(8, 25), # Metal1.text
(67, 25), # Metal5.text
(126, 25), # TopMetal1.text
]
buried_layers = [
(6, 0), # Cont.drawing
(8, 2), # Metal1.pin
(19, 0), # Via1.drawing
(51, 0), # HeatTrans.drawing
(189, 4), # prBoundary.boundary
]
mux_config_yaml_name = "ihp-sg13g2.yaml"
mux_macros = [
"pg/ihp-sg13g2/tt_pg_1v5_hp_1",
"pg/ihp-sg13g2/tt_pg_1v5_hp_2",
"pg/ihp-sg13g2/tt_pg_1v5_hp_4",
"pg/ihp-sg13g2/tt_pg_1v5_ll_1",
"pg/ihp-sg13g2/tt_pg_1v5_ll_2",
"pg/ihp-sg13g2/tt_pg_1v5_ll_4",
]
extra_logo_macros = [
"tech/ihp-sg13g2/tt_logo_corner",
]
def read_pdk_version(self, pdk_root: str) -> str:
pdk_repo = Repo(pdk_root)
pdk_repo_name = list(pdk_repo.remotes[0].urls)[0].split("/")[-1]
pdk_commit = pdk_repo.commit().hexsha
return f"{pdk_repo_name} {pdk_commit}"
def load_cell_definitions(self) -> Dict[str, CellDefinition]:
URL_FORMAT = "https://raw.githubusercontent.com/IHP-GmbH/IHP-Open-PDK/refs/heads/main/ihp-sg13g2/libs.ref/sg13g2_stdcell/doc/sg13g2_stdcell_typ_1p20V_25C.pdf#{ref}"
script_dir = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(script_dir, "tech/ihp-sg13g2/cells.json")) as fh:
cells = json.load(fh)
for cell in cells.values():
cell["url"] = URL_FORMAT.format(ref=cell["doc_ref"])
return cells
tech_map: dict[TechName, Tech] = {
"sky130A": Sky130Tech(),
"ihp-sg13g2": IHPTech(),
}
+40
View File
@@ -0,0 +1,40 @@
import glob
import json
import os
from typing import Dict, List, Optional, TypedDict
class Port(TypedDict):
kind: str
name: str
direction: str
description: str
class Sky130Cell(TypedDict):
description: str
file_prefix: str
library: str
name: str
parameters: List[str]
ports: List[Port]
type: str
verilog_name: str
equation: Optional[str]
def create_cell_defs():
json_files = glob.glob("sky130_fd_sc_hd/latest/cells/*/definition.json")
definitions: Dict[str, Sky130Cell] = {}
for json_file in json_files:
with open(json_file) as fh:
definition: Sky130Cell = json.load(fh)
definitions[definition["name"]] = definition
script_dir = os.path.dirname(os.path.realpath(__file__))
with open(f"{script_dir}/cells.json", "w") as fh:
json.dump(definitions, fh)
if __name__ == "__main__":
create_cell_defs()
@@ -1,10 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Tiny Tapeout LTD
# Copyright (c) 2024-2025 Tiny Tapeout LTD
# Author: Uri Shaked
# Description: This script initializes a new Magic project for an analog design on Tiny Tapeout.
# Important: before running this script, download the the .def file from
# https://raw.githubusercontent.com/TinyTapeout/tt-support-tools/tt08/def/analog/tt_analog_1x2.def
# https://raw.githubusercontent.com/TinyTapeout/tt-support-tools/main/tech/sky130A/def/analog/tt_analog_1x2.def
# Change the settings below to match your design:
# ------------------------------------------------
+2 -1
View File
@@ -4,6 +4,7 @@ import logging
import sys
from project import Project
from tech import TechName
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TT setup")
@@ -140,7 +141,7 @@ if __name__ == "__main__":
ch.setFormatter(log_format)
log.addHandler(ch)
pdk = "ihp-sg13g2" if args.ihp else "sky130A"
pdk: TechName = "ihp-sg13g2" if args.ihp else "sky130A"
project = Project(0, "unknown", args.project_dir, pdk, is_user_project=True)
project.post_clone_setup()