diff --git a/precheck/pin_check.py b/precheck/pin_check.py index 1618ff3..9c2ba31 100644 --- a/precheck/pin_check.py +++ b/precheck/pin_check.py @@ -98,8 +98,13 @@ def parsefp3(value: str): return int(mul) -def pin_check(gds: str, lef: str, template_def: str, toplevel: str): +def pin_check(gds: str, lef: str, template_def: str, toplevel: str, uses_3v3: bool): logging.info("Running pin check...") + logging.info(f"* gds: {gds}") + logging.info(f"* lef: {lef}") + logging.info(f"* template_def: {template_def}") + logging.info(f"* toplevel: {toplevel}") + logging.info(f"* uses_3v3: {uses_3v3}") # parse pins from template def # def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html @@ -174,8 +179,13 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str): layer_re = re.compile(r"LAYER (\S+) ;") rect_re = re.compile(r"RECT (\S+) (\S+) (\S+) (\S+) ;") + power_pins = ["VGND", "VDPWR"] + if uses_3v3: + power_pins.append("VAPWR") + compat_pins = {"VPWR": "VDPWR"} + macro_active = False - pins_expected = set(def_pins).union({"VPWR", "VGND"}) + pins_expected = set(def_pins).union(power_pins).union(compat_pins) pins_seen = set() current_pin = None pin_rects = 0 @@ -259,6 +269,16 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str): ports.append((layer, lx, by, rx, ty)) lef_ports[current_pin] = ports + for old_pin, new_pin in compat_pins.items(): + if old_pin in lef_ports: + if new_pin in lef_ports: + logging.error(f"Both {old_pin} and {new_pin} ports appear in {lef}") + lef_errors += 1 + else: + lef_ports[new_pin] = [] + lef_ports[new_pin].extend(lef_ports[old_pin]) + del lef_ports[old_pin] + for current_pin in def_pins: if current_pin not in lef_ports: logging.error(f"Pin {current_pin} not found in {lef}") @@ -280,21 +300,13 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str): ) lef_errors += 1 - pin_widths = {} - for current_pin in ("VPWR", "VGND"): + for current_pin in power_pins: 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 + width = rx - lx if layer != "met4": logging.error( f"Port {current_pin} has wrong layer in {lef}: {layer} != met4" @@ -305,14 +317,14 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str): f"Port {current_pin} has too small width in {lef}: {width/1000} < 1.2 um" ) lef_errors += 1 - if width > 2000: + if by > 10000: logging.error( - f"Port {current_pin} has too large width in {lef}: {width/1000} > 2.0 um" + f"Port {current_pin} is too far from bottom edge of module: {by/1000} > 10 um" ) lef_errors += 1 - if height < die_height * 0.95: + if die_height - ty > 10000: logging.error( - f"Port {current_pin} has too small height in {lef}: {height/1000} < {die_height*0.95/1000} um" + f"Port {current_pin} is too far from top edge of module: {(die_height-ty)/1000} > 10 um" ) lef_errors += 1 if lx < 0 or rx > die_width or by < 0 or ty > die_height: @@ -321,18 +333,6 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str): ) 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 for overlapping pins for (pin1, rects1), (pin2, rects2) in combinations(sorted(lef_ports.items()), 2): diff --git a/precheck/precheck.py b/precheck/precheck.py index da6a1f8..e93b5d6 100644 --- a/precheck/precheck.py +++ b/precheck/precheck.py @@ -160,8 +160,16 @@ def main(): 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 + uses_3v3 = bool(yaml_data.get("project", {}).get("uses_3v3", False)) + if uses_3v3 and not is_analog: + raise PrecheckFailure( + "Projects with 3v3 power need at least one analog pin" + ) if is_analog: - template_def = f"../def/analog/tt_analog_{tiles}.def" + if uses_3v3: + template_def = f"../def/analog/tt_analog_{tiles}_3v3.def" + else: + template_def = f"../def/analog/tt_analog_{tiles}.def" else: template_def = f"../def/tt_block_{tiles}_pg.def" logging.info(f"using def template {template_def}") @@ -181,7 +189,10 @@ 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)], + [ + "Pin check", + lambda: pin_check(args.gds, lef, template_def, top_module, uses_3v3), + ], ] testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks") diff --git a/precheck/test_precheck.py b/precheck/test_precheck.py index ba540a3..da983c7 100644 --- a/precheck/test_precheck.py +++ b/precheck/test_precheck.py @@ -107,50 +107,69 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory): return str(gds_file) +class PortRect: + def __init__( + self, layer: str, bottom_left: tuple[int, int], top_right: tuple[int, int] + ): + self.layer = layer + self.lx, self.by = bottom_left + self.rx, self.ty = top_right + + +class CompoundPort: + def __init__(self, name: str, rects: list[PortRect]): + self.name = name + self.port_use = "ground" if name == "VGND" else "power" + self.port_class = "bidirectional" + self.rects = rects + + +class SimplePort(CompoundPort): + def __init__(self, name: str, *rect_args: list): + super().__init__(name, [PortRect(*rect_args)]) + + def generate_analog_example( tcl_file: str, gds_file: str, lef_file: str, toplevel: str, - vpwr_layer: str, - vpwr_boxes: str, - vgnd_layer: str, - vgnd_boxes: str, + extra_ports: list[CompoundPort], ): with open(tcl_file, "w") as f: - f.write( - textwrap.dedent( + + def tcl_append(s): + f.write(textwrap.dedent(s)) + + tcl_append( + f""" + def read ../def/analog/tt_analog_1x2.def + cellname rename tt_um_template {toplevel} + """ + ) + for port in extra_ports: + for rect in port.rects: + tcl_append( + f""" + box {rect.lx} {rect.by} {rect.rx} {rect.ty} + paint {rect.layer} + label {port.name} FreeSans {rect.layer} + """ + ) + tcl_append( f""" - def read ../def/analog/tt_analog_1x2.def - cellname rename tt_um_template {toplevel} - - # VPWR - foreach vpwr_box {{ {vpwr_boxes} }} {{ - box {{*}}$vpwr_box - paint {vpwr_layer} - label VPWR FreeSans {vpwr_layer} - }} - port VPWR makeall n - port VPWR use power - port VPWR class bidirectional + port {port.name} makeall n + port {port.name} use {port.port_use} + port {port.name} class {port.port_class} port conn n s e w - - # VGND - foreach vgnd_box {{ {vgnd_boxes} }} {{ - box {{*}}$vgnd_box - paint {vgnd_layer} - label VGND FreeSans {vgnd_layer} - }} - port VGND makeall n - port VGND use ground - port VGND class bidirectional - port conn n s e w - - # Export - gds write {gds_file} - lef write {lef_file} """ ) + tcl_append( + f""" + # Export + gds write {gds_file} + lef write {lef_file} + """ ) magic = subprocess.run( @@ -179,10 +198,30 @@ def gds_lef_analog_example(tmp_path_factory: pytest.TempPathFactory): str(gds_file), str(lef_file), "TEST_analog_example", - "met4", - "{100 500 250 22076}", - "met4", - "{4900 500 5050 22076}", + [ + SimplePort("VDPWR", "met4", (100, 500), (250, 22076)), + SimplePort("VGND", "met4", (4900, 500), (5050, 22076)), + ], + ) + return str(gds_file), str(lef_file) + + +@pytest.fixture(scope="session") +def gds_lef_analog_power_compat(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS and LEF using the 1x2 analog template, with VPWR instead of VDPWR.""" + tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_power_compat.tcl" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_power_compat.gds" + lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_power_compat.lef" + + generate_analog_example( + str(tcl_file), + str(gds_file), + str(lef_file), + "TEST_analog_power_compat", + [ + SimplePort("VPWR", "met4", (100, 500), (250, 22076)), + SimplePort("VGND", "met4", (4900, 500), (5050, 22076)), + ], ) return str(gds_file), str(lef_file) @@ -199,10 +238,10 @@ def gds_lef_analog_wrong_vgnd(tmp_path_factory: pytest.TempPathFactory): str(gds_file), str(lef_file), "TEST_analog_wrong_vgnd", - "met4", - "{100 500 250 22076}", - "met3", - "{4900 500 5250 12076}", + [ + SimplePort("VDPWR", "met4", (100, 500), (250, 22076)), + SimplePort("VGND", "met3", (4900, 500), (5250, 12076)), + ], ) return str(gds_file), str(lef_file) @@ -219,10 +258,10 @@ def gds_lef_analog_overlapping_vgnd(tmp_path_factory: pytest.TempPathFactory): str(gds_file), str(lef_file), "TEST_analog_overlapping_vgnd", - "met4", - "{100 500 250 22076}", - "met4", - "{349 20 549 22504}", + [ + SimplePort("VDPWR", "met4", (100, 500), (250, 22076)), + SimplePort("VGND", "met4", (3000, 20), (3200, 22504)), + ], ) return str(gds_file), str(lef_file) @@ -230,19 +269,46 @@ def gds_lef_analog_overlapping_vgnd(tmp_path_factory: pytest.TempPathFactory): @pytest.fixture(scope="session") def gds_lef_analog_compound_vgnd(tmp_path_factory: pytest.TempPathFactory): """Creates a GDS and LEF using the 1x2 analog template, with VGND consisting of two rectangles.""" - 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" + tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_compound_vgnd.tcl" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_compound_vgnd.gds" + lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_compound_vgnd.lef" generate_analog_example( str(tcl_file), str(gds_file), str(lef_file), "TEST_analog_compound_vgnd", - "met4", - "{100 500 250 22076}", - "met4", - "{4900 500 5050 12076} {4900 12000 5050 22076}", + [ + SimplePort("VDPWR", "met4", (100, 500), (250, 22076)), + CompoundPort( + "VGND", + [ + PortRect("met4", (4900, 500), (5050, 12076)), + PortRect("met4", (4900, 12000), (5050, 22076)), + ], + ), + ], + ) + return str(gds_file), str(lef_file) + + +@pytest.fixture(scope="session") +def gds_lef_analog_example_3v3(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_3v3.tcl" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example_3v3.gds" + lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example_3v3.lef" + + generate_analog_example( + str(tcl_file), + str(gds_file), + str(lef_file), + "TEST_analog_example_3v3", + [ + SimplePort("VDPWR", "met4", (100, 500), (250, 22076)), + SimplePort("VAPWR", "met4", (2500, 500), (2650, 22076)), + SimplePort("VGND", "met4", (4900, 500), (5050, 22076)), + ], ) return str(gds_file), str(lef_file) @@ -317,6 +383,18 @@ def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]): lef_file, "../def/analog/tt_analog_1x2.def", "TEST_analog_example", + False, + ) + + +def test_pin_analog_power_compat(gds_lef_analog_power_compat: tuple[str, str]): + gds_file, lef_file = gds_lef_analog_power_compat + precheck.pin_check( + gds_file, + lef_file, + "../def/analog/tt_analog_1x2.def", + "TEST_analog_power_compat", + False, ) @@ -331,6 +409,7 @@ def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]): lef_file, "../def/analog/tt_analog_1x2.def", "TEST_analog_wrong_vgnd", + False, ) @@ -345,6 +424,7 @@ def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str, lef_file, "../def/analog/tt_analog_1x2.def", "TEST_analog_overlapping_vgnd", + False, ) @@ -355,4 +435,46 @@ def test_pin_analog_compound_vgnd(gds_lef_analog_compound_vgnd: tuple[str, str]) lef_file, "../def/analog/tt_analog_1x2.def", "TEST_analog_compound_vgnd", + False, ) + + +def test_pin_analog_example_3v3(gds_lef_analog_example_3v3: tuple[str, str]): + gds_file, lef_file = gds_lef_analog_example_3v3 + precheck.pin_check( + gds_file, + lef_file, + "../def/analog/tt_analog_1x2.def", + "TEST_analog_example_3v3", + True, + ) + + +def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]): + with pytest.raises( + precheck.PrecheckFailure, + match="Some ports are missing or have wrong dimensions", + ): + gds_file, lef_file = gds_lef_analog_example + precheck.pin_check( + gds_file, + lef_file, + "../def/analog/tt_analog_1x2.def", + "TEST_analog_example", + True, + ) + + +def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]): + with pytest.raises( + precheck.PrecheckFailure, + match="Some ports are missing or have wrong dimensions", + ): + gds_file, lef_file = gds_lef_analog_example_3v3 + precheck.pin_check( + gds_file, + lef_file, + "../def/analog/tt_analog_1x2.def", + "TEST_analog_example_3v3", + False, + ) diff --git a/project.py b/project.py index 48965d5..913eb32 100644 --- a/project.py +++ b/project.py @@ -187,6 +187,19 @@ class Project: os.unlink(json_file) module_ports = ports["modules"][top]["ports"] + if "VPWR" in module_ports: + if "VDPWR" in module_ports: + logging.error( + f"{self} top module '{top}' uses both VPWR and VDPWR ports" + ) + exit(1) + else: + logging.info( + f"{self} top module '{top}' uses VPWR port, substituting VDPWR" + ) + module_ports["VDPWR"] = module_ports["VPWR"] + del module_ports["VPWR"] + required_ports = [ ["input", "clk", 1], ["input", "ena", 1], @@ -197,15 +210,19 @@ class Project: ["output", "uio_out", 8], ["output", "uo_out", 8], ] - if self.is_analog_design(): + if self.info.is_analog: required_ports += [ ["inout", "ua", 8], ] if include_power_ports: required_ports += [ ["input", "VGND", 1], - ["input", "VPWR", 1], + ["input", "VDPWR", 1], ] + if self.info.uses_3v3: + required_ports += [ + ["input", "VAPWR", 1], + ] for direction, port, bits in required_ports: if port not in module_ports: logging.error(f"{self} port '{port}' missing from top module ('{top}')") @@ -248,7 +265,7 @@ class Project: def get_project_type_string(self): if self.is_wokwi(): return f"[Wokwi]({self.get_wokwi_url()})" - elif self.is_analog_design(): + elif self.info.is_analog: return "Analog" else: return "HDL" @@ -306,9 +323,6 @@ class Project: commit_info = self.read_commit_info_json() return commit_info["workflow_url"] - def is_analog_design(self) -> bool: - return len(self.info.pinout.ua) > 0 - def get_workflow_url(self): GITHUB_SERVER_URL = os.getenv("GITHUB_SERVER_URL") GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") @@ -612,7 +626,8 @@ class Project: } for i, desc in enumerate(self.info.pinout.ua) ], - "is_analog": bool(self.info.pinout.ua), + "is_analog": self.info.is_analog, + "uses_3v3": self.info.uses_3v3, } ) diff --git a/project_info.py b/project_info.py index fdb27b6..3e8e494 100644 --- a/project_info.py +++ b/project_info.py @@ -101,6 +101,13 @@ class ProjectInfo: "Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6" ) self.analog_pins: int = analog_pins + self.is_analog: bool = self.analog_pins > 0 + + self.uses_3v3: bool = project_section.get("uses_3v3", False) + if self.uses_3v3 and not self.is_analog: + raise ProjectYamlError( + "Projects with 3v3 power need at least one analog pin" + ) language = project_section.get("language") if language is None: