From 97e65e74f6465980fb383c6fc876ab1157f717b1 Mon Sep 17 00:00:00 2001 From: htfab Date: Tue, 30 Apr 2024 19:36:02 +0200 Subject: [PATCH] feat(precheck): add pin checks Checks for missing or invalid pins, wrong pin dimensions or layers. --- precheck/pin_check.py | 268 +++++++++++++++++++++++++++++++++++ precheck/precheck.py | 30 +++- precheck/precheck_failure.py | 2 + precheck/requirements.txt | 1 + precheck/test_precheck.py | 122 ++++++++++++++++ 5 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 precheck/pin_check.py create mode 100644 precheck/precheck_failure.py diff --git a/precheck/pin_check.py b/precheck/pin_check.py new file mode 100644 index 0000000..41f7950 --- /dev/null +++ b/precheck/pin_check.py @@ -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" + ) diff --git a/precheck/precheck.py b/precheck/precheck.py index 60b734b..9e8f923 100644 --- a/precheck/precheck.py +++ b/precheck/precheck.py @@ -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") diff --git a/precheck/precheck_failure.py b/precheck/precheck_failure.py new file mode 100644 index 0000000..ed13ca8 --- /dev/null +++ b/precheck/precheck_failure.py @@ -0,0 +1,2 @@ +class PrecheckFailure(Exception): + pass diff --git a/precheck/requirements.txt b/precheck/requirements.txt index a857acb..8222ced 100644 --- a/precheck/requirements.txt +++ b/precheck/requirements.txt @@ -1,3 +1,4 @@ klayout==0.29.0 gdstk==0.9.50 pytest==7.4.3 +PyYAML==6.0.1 diff --git a/precheck/test_precheck.py b/precheck/test_precheck.py index 65e360c..feb74fa 100644 --- a/precheck/test_precheck.py +++ b/precheck/test_precheck.py @@ -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", + )