mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-02 20:26:10 +01:00
feat(precheck): add pin checks
Checks for missing or invalid pins, wrong pin dimensions or layers.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import gdstk
|
||||
from precheck_failure import PrecheckFailure
|
||||
|
||||
|
||||
def parsefp3(value: str):
|
||||
# parse fixed-point numbers with 3 digits after the decimal point
|
||||
# e.g. '20.470' to 20470
|
||||
ip, fp = value.split(".")
|
||||
mul = ip + fp[:3].rjust(3, "0")
|
||||
return int(mul)
|
||||
|
||||
|
||||
def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
|
||||
logging.info("Running pin check...")
|
||||
|
||||
# parse pins from template def
|
||||
|
||||
diearea_re = re.compile(r"DIEAREA \( (\S+) (\S+) \) \( (\S+) (\S+) \) ;")
|
||||
pins_re = re.compile(r"PINS (\d+) ;")
|
||||
pin_re = re.compile(r" *- (\S+) \+ NET (\S+) \+ DIRECTION (\S+) \+ USE (\S+)")
|
||||
layer_re = re.compile(r" *\+ LAYER (\S+) \( (\S+) (\S+) \) \( (\S+) (\S+) \)")
|
||||
placed_re = re.compile(r" *\+ PLACED \( (\S+) (\S+) \) (\S+) ;")
|
||||
|
||||
def_pins = {}
|
||||
die_width = 0
|
||||
die_height = 0
|
||||
|
||||
with open(template_def) as f:
|
||||
for line in f:
|
||||
if line.startswith("DIEAREA "):
|
||||
match = diearea_re.match(line)
|
||||
lx, by, rx, ty = map(int, match.groups())
|
||||
if (lx, by) != (0, 0):
|
||||
raise PrecheckFailure("Wrong die origin in template DEF")
|
||||
die_width = rx
|
||||
die_height = ty
|
||||
elif line.startswith("PINS "):
|
||||
match = pins_re.match(line)
|
||||
pin_count = int(match.group(1))
|
||||
break
|
||||
|
||||
for i in range(pin_count):
|
||||
line = next(f)
|
||||
match = pin_re.match(line)
|
||||
pin_name, net_name, direction, use = match.groups()
|
||||
if pin_name != net_name:
|
||||
raise PrecheckFailure(
|
||||
"Inconsistent pin name and net name in template DEF"
|
||||
)
|
||||
|
||||
line = next(f)
|
||||
if not line.strip().startswith("+ PORT"):
|
||||
raise PrecheckFailure("Unexpected token in template DEF")
|
||||
|
||||
line = next(f)
|
||||
match = layer_re.match(line)
|
||||
layer, lx, by, rx, ty = match.groups()
|
||||
lx, by, rx, ty = map(int, (lx, by, rx, ty))
|
||||
|
||||
line = next(f)
|
||||
match = placed_re.match(line)
|
||||
ox, oy, direction = match.groups()
|
||||
ox, oy = map(int, (ox, oy))
|
||||
|
||||
if pin_name in def_pins:
|
||||
raise PrecheckFailure("Duplicate pin in template DEF")
|
||||
|
||||
def_pins[pin_name] = (layer, ox + lx, oy + by, ox + rx, oy + ty)
|
||||
|
||||
line = next(f)
|
||||
if not line.startswith("END PINS"):
|
||||
raise PrecheckFailure("Unexpected token in template DEF")
|
||||
|
||||
# parse pins from user lef
|
||||
|
||||
origin_re = re.compile(r"ORIGIN (\S+) (\S+) ;")
|
||||
size_re = re.compile(r"SIZE (\S+) BY (\S+) ;")
|
||||
layer_re = re.compile(r"LAYER (\S+) ;")
|
||||
rect_re = re.compile(r"RECT (\S+) (\S+) (\S+) (\S+) ;")
|
||||
|
||||
macro_active = False
|
||||
pins_expected = set(def_pins).union({"VPWR", "VGND"})
|
||||
pins_seen = set()
|
||||
current_pin = None
|
||||
pin_rects = 0
|
||||
lef_errors = 0
|
||||
lef_ports = {}
|
||||
|
||||
with open(lef) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("MACRO "):
|
||||
if line == "MACRO " + toplevel:
|
||||
macro_active = True
|
||||
else:
|
||||
macro_active = False
|
||||
elif macro_active:
|
||||
if line.startswith("ORIGIN "):
|
||||
match = origin_re.match(line)
|
||||
lx, by = map(parsefp3, match.groups())
|
||||
if lx != 0 or by != 0:
|
||||
raise PrecheckFailure("Wrong die origin in LEF")
|
||||
elif line.startswith("SIZE "):
|
||||
match = size_re.match(line)
|
||||
rx, ty = map(parsefp3, match.groups())
|
||||
if (rx, ty) != (die_width, die_height):
|
||||
raise PrecheckFailure(
|
||||
f"Inconsistent die area between LEF and template DEF: ({rx}, {ty}) != ({die_width}, {die_height})"
|
||||
)
|
||||
elif line.startswith("PIN "):
|
||||
if current_pin is not None:
|
||||
raise PrecheckFailure("Unexpected token in LEF")
|
||||
current_pin = line.removeprefix("PIN ")
|
||||
pins_seen.add(current_pin)
|
||||
if current_pin not in pins_expected:
|
||||
logging.error(f"Unexpected pin {current_pin} in {lef}")
|
||||
lef_errors += 1
|
||||
pin_rects = 0
|
||||
elif line == "PORT":
|
||||
if current_pin is None:
|
||||
raise PrecheckFailure("Unexpected token in LEF")
|
||||
line = next(f).strip()
|
||||
while line.startswith("LAYER "):
|
||||
match = layer_re.match(line)
|
||||
layer = match.group(1)
|
||||
line = next(f).strip()
|
||||
while line.startswith("RECT "):
|
||||
match = rect_re.match(line)
|
||||
lx, by, rx, ty = map(parsefp3, match.groups())
|
||||
pin_rects += 1
|
||||
if current_pin not in lef_ports:
|
||||
lef_ports[current_pin] = []
|
||||
lef_ports[current_pin].append((layer, lx, by, rx, ty))
|
||||
line = next(f).strip()
|
||||
if line != "END":
|
||||
raise PrecheckFailure("Unexpected token in LEF")
|
||||
elif current_pin is not None and line.startswith("END " + current_pin):
|
||||
if pin_rects < 1:
|
||||
logging.error(f"No ports for pin {current_pin} in {lef}")
|
||||
lef_errors += 1
|
||||
current_pin = None
|
||||
|
||||
for current_pin in def_pins:
|
||||
if current_pin not in lef_ports:
|
||||
logging.error(f"Pin {current_pin} not found in {lef}")
|
||||
lef_errors += 1
|
||||
else:
|
||||
if len(lef_ports[current_pin]) > 1:
|
||||
logging.error(f"Too many rectangles for pin {current_pin} in {lef}")
|
||||
lef_errors += 1
|
||||
lef_layer, *lef_rect = lef_ports[current_pin][0]
|
||||
def_layer, *def_rect = def_pins[current_pin]
|
||||
if lef_layer != def_layer:
|
||||
logging.error(
|
||||
f"Port {current_pin} on layer {lef_layer} in {lef} but on layer {def_layer} in {template_def}"
|
||||
)
|
||||
lef_errors += 1
|
||||
elif lef_rect != def_rect:
|
||||
logging.error(
|
||||
f"Port {current_pin} has different dimensions in {lef} and {template_def}"
|
||||
)
|
||||
lef_errors += 1
|
||||
|
||||
pin_widths = {}
|
||||
for current_pin in ("VPWR", "VGND"):
|
||||
if current_pin not in lef_ports:
|
||||
logging.error(f"Pin {current_pin} not found in {lef}")
|
||||
lef_errors += 1
|
||||
else:
|
||||
for layer, lx, by, rx, ty in lef_ports[current_pin]:
|
||||
width, height = rx - lx, ty - by
|
||||
if current_pin in pin_widths:
|
||||
if pin_widths[current_pin] != width:
|
||||
logging.error(
|
||||
f"Multiple {current_pin} rectangles with different widths in {lef}: {pin_widths[current_pin]/1000} != {width/1000} um"
|
||||
)
|
||||
lef_errors += 1
|
||||
pin_widths[current_pin] = width
|
||||
if layer != "met4":
|
||||
logging.error(
|
||||
f"Port {current_pin} has wrong layer in {lef}: {layer} != met4"
|
||||
)
|
||||
lef_errors += 1
|
||||
if width < 1200:
|
||||
logging.error(
|
||||
f"Port {current_pin} has too small width in {lef}: {width/1000} < 1.2 um"
|
||||
)
|
||||
lef_errors += 1
|
||||
if width > 2000:
|
||||
logging.error(
|
||||
f"Port {current_pin} has too large width in {lef}: {width/1000} > 2.0 um"
|
||||
)
|
||||
lef_errors += 1
|
||||
if height < die_height * 0.95:
|
||||
logging.error(
|
||||
f"Port {current_pin} has too small height in {lef}: {height/1000} < {die_height*0.95/1000} um"
|
||||
)
|
||||
lef_errors += 1
|
||||
if lx < 0 or rx > die_width or by < 0 or ty > die_height:
|
||||
logging.error(
|
||||
f"Port {current_pin} not entirely within project area in {lef}"
|
||||
)
|
||||
lef_errors += 1
|
||||
|
||||
if (
|
||||
"VPWR" in pin_widths
|
||||
and "VGND" in pin_widths
|
||||
and pin_widths["VPWR"] != pin_widths["VGND"]
|
||||
):
|
||||
vpwr_width = pin_widths["VPWR"]
|
||||
vgnd_width = pin_widths["VGND"]
|
||||
logging.error(
|
||||
f"VPWR and VGND have different widths in {lef}: {vpwr_width/1000} != {vgnd_width/1000} um"
|
||||
)
|
||||
lef_errors += 1
|
||||
|
||||
# check gds for the ports being present
|
||||
|
||||
lib = gdstk.read_gds(gds)
|
||||
top = [cell for cell in lib.top_level() if cell.name == toplevel]
|
||||
if not top:
|
||||
raise PrecheckFailure("Wrong cell at GDS top-level")
|
||||
top = top[0]
|
||||
|
||||
gds_layers = {
|
||||
"met1.pin": (68, 16),
|
||||
"met2.pin": (69, 16),
|
||||
"met3.pin": (70, 16),
|
||||
"met4.pin": (71, 16),
|
||||
}
|
||||
|
||||
gds_errors = 0
|
||||
for current_pin, lef_rects in sorted(lef_ports.items()):
|
||||
for layer, lx, by, rx, ty in lef_rects:
|
||||
assert layer + ".pin" in gds_layers, "Unexpected port layer in LEF"
|
||||
pin_layer = gds_layers[layer + ".pin"]
|
||||
|
||||
pin_ok = False
|
||||
for poly in top.polygons:
|
||||
poly_layer = (poly.layer, poly.datatype)
|
||||
if poly.contain_all(
|
||||
((lx + 1) / 1000, (by + 1) / 1000),
|
||||
((rx - 1) / 1000, (by + 1) / 1000),
|
||||
((lx + 1) / 1000, (ty - 1) / 1000),
|
||||
((rx - 1) / 1000, (ty - 1) / 1000),
|
||||
):
|
||||
if poly_layer == pin_layer:
|
||||
pin_ok = True
|
||||
|
||||
if not pin_ok:
|
||||
logging.error(
|
||||
f"Port {current_pin} missing from layer {layer}.pin in {gds}"
|
||||
)
|
||||
gds_errors += 1
|
||||
|
||||
if lef_errors > 0 or gds_errors > 0:
|
||||
err_list = []
|
||||
if lef_errors > 0:
|
||||
err_list.append(f"{lef_errors} LEF error" + ("s" if lef_errors > 1 else ""))
|
||||
if gds_errors > 0:
|
||||
err_list.append(f"{gds_errors} GDS error" + ("s" if gds_errors > 1 else ""))
|
||||
err_desc = " and ".join(err_list)
|
||||
raise PrecheckFailure(
|
||||
f"Some ports are missing or have wrong dimensions, see {err_desc} above"
|
||||
)
|
||||
+26
-4
@@ -10,7 +10,10 @@ import xml.etree.ElementTree as ET
|
||||
import gdstk
|
||||
import klayout.db as pya
|
||||
import klayout.rdb as rdb
|
||||
import yaml
|
||||
from klayout_tools import parse_lyp_layers
|
||||
from pin_check import pin_check
|
||||
from precheck_failure import PrecheckFailure
|
||||
|
||||
PDK_ROOT = os.getenv("PDK_ROOT")
|
||||
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
|
||||
@@ -22,10 +25,6 @@ if not PDK_ROOT:
|
||||
exit(1)
|
||||
|
||||
|
||||
class PrecheckFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def has_sky130_devices(gds: str):
|
||||
for cell_name in gdstk.read_rawcells(gds):
|
||||
if cell_name.startswith("sky130_fd_"):
|
||||
@@ -131,6 +130,8 @@ def klayout_checks(gds: str):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--gds", required=True)
|
||||
parser.add_argument("--lef", required=False)
|
||||
parser.add_argument("--template-def", required=False)
|
||||
parser.add_argument("--top-module", required=False)
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||
@@ -141,6 +142,26 @@ def main():
|
||||
else:
|
||||
top_module = os.path.splitext(os.path.basename(args.gds))[0]
|
||||
|
||||
if args.lef:
|
||||
lef = args.lef
|
||||
else:
|
||||
lef = os.path.splitext(args.gds)[0] + ".lef"
|
||||
|
||||
if args.template_def:
|
||||
template_def = args.template_def
|
||||
else:
|
||||
yaml_dir = os.path.dirname(os.path.dirname(args.gds))
|
||||
yaml_file = f"{yaml_dir}/info.yaml"
|
||||
yaml_data = yaml.safe_load(open(yaml_file))
|
||||
logging.info("info.yaml data:" + str(yaml_data))
|
||||
tiles = yaml_data.get("project", {}).get("tiles", "1x1")
|
||||
is_analog = yaml_data.get("project", {}).get("analog_pins", 0) > 0
|
||||
if is_analog:
|
||||
template_def = f"../def/analog/tt_block_{tiles}_pg_ana.def"
|
||||
else:
|
||||
template_def = f"../def/tt_block_{tiles}_pg.def"
|
||||
logging.info(f"using def template {template_def}")
|
||||
|
||||
checks = [
|
||||
["Magic DRC", lambda: magic_drc(args.gds, top_module)],
|
||||
["KLayout FEOL", lambda: klayout_drc(args.gds, "feol")],
|
||||
@@ -156,6 +177,7 @@ def main():
|
||||
],
|
||||
["KLayout zero area", lambda: klayout_zero_area(args.gds)],
|
||||
["KLayout Checks", lambda: klayout_checks(args.gds)],
|
||||
["Pin check", lambda: pin_check(args.gds, lef, template_def, top_module)],
|
||||
]
|
||||
|
||||
testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
class PrecheckFailure(Exception):
|
||||
pass
|
||||
@@ -1,3 +1,4 @@
|
||||
klayout==0.29.0
|
||||
gdstk==0.9.50
|
||||
pytest==7.4.3
|
||||
PyYAML==6.0.1
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import klayout.db as pya
|
||||
import klayout_tools
|
||||
@@ -105,6 +107,102 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory):
|
||||
return str(gds_file)
|
||||
|
||||
|
||||
def generate_analog_example(
|
||||
tcl_file: str,
|
||||
gds_file: str,
|
||||
lef_file: str,
|
||||
toplevel: str,
|
||||
vpwr_layer: str,
|
||||
vpwr_box: str,
|
||||
vgnd_layer: str,
|
||||
vgnd_box: str,
|
||||
):
|
||||
with open(tcl_file, "w") as f:
|
||||
f.write(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
def read ../def/analog/tt_block_1x2_pg_ana.def
|
||||
cellname rename tt_um_template {toplevel}
|
||||
|
||||
# VPWR
|
||||
box {vpwr_box}
|
||||
paint {vpwr_layer}
|
||||
label VPWR FreeSans {vpwr_layer}
|
||||
port VPWR make n
|
||||
port VPWR use power
|
||||
port VPWR class bidirectional
|
||||
port conn n s e w
|
||||
|
||||
# VGND
|
||||
box {vgnd_box}
|
||||
paint {vgnd_layer}
|
||||
label VGND FreeSans {vgnd_layer}
|
||||
port VGND make n
|
||||
port VGND use ground
|
||||
port VGND class bidirectional
|
||||
port conn n s e w
|
||||
|
||||
# Export
|
||||
gds write {gds_file}
|
||||
lef write {lef_file} -pinonly
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
magic = subprocess.run(
|
||||
[
|
||||
"magic",
|
||||
"-noconsole",
|
||||
"-dnull",
|
||||
"-rcfile",
|
||||
f"{PDK_ROOT}/{PDK_NAME}/libs.tech/magic/{PDK_NAME}.magicrc",
|
||||
tcl_file,
|
||||
],
|
||||
)
|
||||
|
||||
assert magic.returncode == 0
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def gds_lef_analog_example(tmp_path_factory: pytest.TempPathFactory):
|
||||
"""Creates a GDS and LEF using the 1x2 analog template."""
|
||||
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_example.tcl"
|
||||
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example.gds"
|
||||
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example.lef"
|
||||
|
||||
generate_analog_example(
|
||||
str(tcl_file),
|
||||
str(gds_file),
|
||||
str(lef_file),
|
||||
"TEST_analog_example",
|
||||
"met4",
|
||||
"100 500 250 22076",
|
||||
"met4",
|
||||
"4900 500 5050 22076",
|
||||
)
|
||||
return str(gds_file), str(lef_file)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def gds_lef_analog_wrong_vgnd(tmp_path_factory: pytest.TempPathFactory):
|
||||
"""Creates a GDS and LEF using the 1x2 analog template, with wrong VGND layer & dimensions."""
|
||||
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_wrong_vgnd.tcl"
|
||||
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_wrong_vgnd.gds"
|
||||
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_wrong_vgnd.lef"
|
||||
|
||||
generate_analog_example(
|
||||
str(tcl_file),
|
||||
str(gds_file),
|
||||
str(lef_file),
|
||||
"TEST_analog_wrong_vgnd",
|
||||
"met4",
|
||||
"100 500 250 22076",
|
||||
"met3",
|
||||
"4900 500 5250 12076",
|
||||
)
|
||||
return str(gds_file), str(lef_file)
|
||||
|
||||
|
||||
def test_magic_drc_pass(gds_valid: str):
|
||||
precheck.magic_drc(gds_valid, "TEST_valid")
|
||||
|
||||
@@ -166,3 +264,27 @@ def test_klayout_zero_area_drc_pass(gds_valid: str):
|
||||
def test_klayout_zero_area_drc_fail(gds_zero_area: str):
|
||||
with pytest.raises(precheck.PrecheckFailure, match="Klayout zero_area failed"):
|
||||
precheck.klayout_zero_area(gds_zero_area)
|
||||
|
||||
|
||||
def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
|
||||
gds_file, lef_file = gds_lef_analog_example
|
||||
precheck.pin_check(
|
||||
gds_file,
|
||||
lef_file,
|
||||
"../def/analog/tt_block_1x2_pg_ana.def",
|
||||
"TEST_analog_example",
|
||||
)
|
||||
|
||||
|
||||
def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
|
||||
with pytest.raises(
|
||||
precheck.PrecheckFailure,
|
||||
match="Some ports are missing or have wrong dimensions",
|
||||
):
|
||||
gds_file, lef_file = gds_lef_analog_wrong_vgnd
|
||||
precheck.pin_check(
|
||||
gds_file,
|
||||
lef_file,
|
||||
"../def/analog/tt_block_1x2_pg_ana.def",
|
||||
"TEST_analog_wrong_vgnd",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user