diff --git a/default.nix b/default.nix index f6f42cb1..507bd265 100644 --- a/default.nix +++ b/default.nix @@ -39,7 +39,7 @@ yosys-eqy, yosys-ghdl, yosys-f4pga-sdc, - # PIP + # Python click, cloup, pyyaml, @@ -52,6 +52,7 @@ psutil, pytestCheckHook, pyfakefs, + lef-parser, system, }: buildPythonPackage rec { @@ -118,6 +119,7 @@ buildPythonPackage rec { ioplace-parser psutil klayout-pymod + lef-parser ] ++ includedTools; diff --git a/flake.lock b/flake.lock index 5f67682e..8530c872 100644 --- a/flake.lock +++ b/flake.lock @@ -13,6 +13,25 @@ "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" } }, + "lef-parser": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "narHash": "sha256-+7IcvzC5nh+qawyTnpo6J15PcUtSrjk/ftKoa1IGCzY=", + "owner": "efabless", + "repo": "lef_parser", + "rev": "e8d9f5e04dfd3da7afe4fd7e42d694e4e37962db", + "type": "github" + }, + "original": { + "owner": "efabless", + "repo": "lef_parser", + "type": "github" + } + }, "nixpkgs": { "locked": { "narHash": "sha256-C36QmoJd5tdQ5R9MC1jM7fBkZW9zBUqbUCsgwS6j4QU=", @@ -31,6 +50,7 @@ "root": { "inputs": { "flake-compat": "flake-compat", + "lef-parser": "lef-parser", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index aeb0aa3e..6069a935 100644 --- a/flake.nix +++ b/flake.nix @@ -23,12 +23,16 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs/nixos-23.11; + lef-parser.url = github:efabless/lef_parser; flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; }; + + inputs.lef-parser.inputs.nixpkgs.follows = "nixpkgs"; outputs = { self, nixpkgs, + lef-parser, ... }: { # Helper functions @@ -44,6 +48,7 @@ inherit system; overlays = [ (import ./nix/overlay.nix) + (new: old: { lef-parser = lef-parser.outputs.packages."${system}".default; }) ]; }) ); diff --git a/openlane/common/toolbox.py b/openlane/common/toolbox.py index 97bc34e2..c3dc70ff 100644 --- a/openlane/common/toolbox.py +++ b/openlane/common/toolbox.py @@ -36,9 +36,9 @@ ) import libparse +import lef_parser from deprecated.sphinx import deprecated - from .misc import mkdirp from .types import Path from .metrics import aggregate_metrics @@ -61,8 +61,9 @@ def __init__(self, tmp_dir: str) -> None: # "openlane_run/tmp" created in their PWD because of the global toolbox self.tmp_dir = tmp_dir - self.remove_cells_from_lib = lru_cache(16, True)(self.remove_cells_from_lib) # type: ignore - self.create_blackbox_model = lru_cache(16, True)(self.create_blackbox_model) # type: ignore + self.remove_cells_from_lib = lru_cache(32, True)(self.remove_cells_from_lib) # type: ignore + self.create_blackbox_model = lru_cache(32, True)(self.create_blackbox_model) # type: ignore + self.header_from_lef = lru_cache(32, True)(self.header_from_lef) # type: ignore @deprecated( version="2.0.0b1", @@ -427,6 +428,60 @@ class State(IntEnum): return out_paths + def header_from_lef( + self, + input_lef: str, + power_define: str, + ): + def get_decls(macro: lef_parser.Macro): + pg_decls = [] + other_decls = [] + pg_pins = [ + port + for port in macro.ports.values() + if port.kind in ["POWER", "GROUND"] + ] + other_pins = [ + port + for port in macro.ports.values() + if port.kind not in ["POWER", "GROUND"] + ] + for info in pg_pins: + bus_postfix = "" + if info.msb is not None: + bus_postfix = f"[{info.msb}:{info.lsb}]" + pg_decls.append(f"{info.direction.lower()}{bus_postfix} {info.name}") + for info in other_pins: + bus_postfix = "" + if info.msb is not None: + bus_postfix = f"[{info.msb}:{info.lsb}]" + other_decls.append(f"{info.direction.lower()}{bus_postfix} {info.name}") + return (pg_decls, other_decls) + + mkdirp(self.tmp_dir) + basename = os.path.splitext(os.path.basename(input_lef))[0] + out_path = os.path.join(self.tmp_dir, f"{basename}-{uuid.uuid4().hex}.bb.v") + with open(out_path, "w") as f: + lef = lef_parser.parse(input_lef) + for macro in lef.macros.values(): + pg_decls, other_decls = get_decls(macro) + print("// Auto-generated by OpenLane", file=f) + print(f"module {macro.name}(", file=f) + print(f"`ifdef {power_define}", file=f) + last_pos = f.tell() + for decl in pg_decls: + print(f" {decl}", file=f, end="") + last_pos = f.tell() + print(",", file=f) + print("`endif", file=f) + for decl in other_decls: + print(f" {decl}", file=f, end="") + last_pos = f.tell() + print(",", file=f) + f.seek(last_pos) # Overwrite ,\n + print("\n);\nendmodule", file=f) + return out_path + def create_blackbox_model( self, input_models: Union[frozenset, Tuple[str, ...]], diff --git a/openlane/flows/classic.py b/openlane/flows/classic.py index 48fd9ec7..277863e5 100644 --- a/openlane/flows/classic.py +++ b/openlane/flows/classic.py @@ -226,7 +226,7 @@ class Classic(SequentialFlow): OpenROAD.CheckMacroInstances, OpenROAD.STAPrePNR, OpenROAD.Floorplan, - Odb.CheckMacroAntennaProperties, + Misc.CheckMacroAntennaProperties, Odb.SetPowerConnections, Odb.ManualMacroPlacement, OpenROAD.CutRows, @@ -272,7 +272,7 @@ class Classic(SequentialFlow): Magic.StreamOut, KLayout.StreamOut, Magic.WriteLEF, - Odb.CheckDesignAntennaProperties, + Misc.CheckDesignAntennaProperties, KLayout.XOR, Checker.XOR, Magic.DRC, diff --git a/openlane/steps/misc.py b/openlane/steps/misc.py index 4d54a8d4..fdb9c411 100644 --- a/openlane/steps/misc.py +++ b/openlane/steps/misc.py @@ -13,6 +13,9 @@ # limitations under the License. import os from typing import Tuple +from io import TextIOWrapper + +import lef_parser from .step import ViewsUpdate, MetricsUpdate, Step from ..common import Path @@ -158,3 +161,120 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: with open(report_file, "w") as f: print(f"{antenna_report}\n{lvs_report}\n{drc_report}", file=f) return {}, {} + + +def get_macro_antenna_info(macro: lef_parser.Macro, f: TextIOWrapper): + inout_pins = [] + input_pins = [] + output_pins = [] + for pin in macro.pins.values(): + if pin.kind in ["POWER", "GROUND", "ANALOG"]: + continue + if pin.direction == "INOUT" and ( + pin.antennaDiffArea is None and pin.antennaGateArea is None + ): + inout_pins.append(pin.name) + elif pin.direction == "INPUT" and pin.antennaGateArea is None: + input_pins.append(pin.name) + elif pin.direction == "OUTPUT" and pin.antennaDiffArea is None: + output_pins.append(pin.name) + if len(inout_pins) + len(input_pins) + len(output_pins): + print(f"* {macro.name}", file=f) + if inout_pins: + print( + " * INOUT pin(s) without antenna gate information nor antenna diffusion information:", + file=f, + ) + for pin_name in inout_pins: + print(f" * {pin_name}", file=f) + if input_pins: + print( + " * INPUT pin(s) without antenna gate information:", + file=f, + ) + for pin_name in input_pins: + print(f" * {pin_name}", file=f) + if output_pins: + print( + " * OUTPUT pin(s) without antenna diffusion information:", + file=f, + ) + for pin_name in input_pins: + print(f" * {pin_name}, file=f") + return True + return False + + +@Step.factory.register() +class CheckMacroAntennaProperties(Step): + """ + Sanity-checks the LEF files of input macros for antenna information: + * Antenna Gate Area for Inputs + * Antenna Diffusion Area for Inouts + * Either or Both for Inouts + + If a pin is missing this information, estimates for the antenna effect and + diode insertion may not be accurate. However, it may also be missing + because the pin(s) in question are not internally connected to anything. + """ + + id = "Misc.CheckMacroAntennaProperties" + name = "Check Antenna Properties of Macros Pins in Their LEF Views" + inputs = [] + outputs = [] + + def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: + missing_values_found = False + log = self.get_log_path() + with open(log, "w") as f: + for macro in self.toolbox.get_macro_views(self.config, DesignFormat.LEF): + lef = lef_parser.parse(macro) + for lef_macro in lef.macros.values(): + missing_values_found = ( + missing_values_found or get_macro_antenna_info(lef_macro, f) + ) + if not missing_values_found: + print("* No macros found with missing antenna information.", file=f) + else: + self.warn( + f"One or more macros have missing antenna information on their pin(s): {os.path.relpath(log)}" + ) + return {}, {} + + +@Step.factory.register() +class CheckDesignAntennaProperties(Step): + """ + Sanity-checks the output LEF for antenna information: + * Antenna Gate Area for Inputs + * Antenna Diffusion Area for Inouts + * Either or Both for Inouts + + If a pin is missing this information, designs instantiating this Macro + may get bad estimates for the antenna effect and + diode insertion may not be accurate. However, it may also be missing + because the pin(s) in question are not internally connected to anything. + """ + + id = "Misc.CheckDesignAntennaProperties" + name = "Check Antenna Properties of the Design's LEF view" + inputs = [DesignFormat.LEF] + outputs = [] + + def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: + lef = lef_parser.parse(str(state_in[DesignFormat.LEF])) + log = self.get_log_path() + with open(log, "w") as f: + missing_values_found = get_macro_antenna_info( + lef.macros[self.config["DESIGN_NAME"]], f + ) + if not missing_values_found: + print( + "* Design LEF successfully generated with antenna information.", + file=f, + ) + else: + self.warn( + f"Generated LEF for the design is missing antenna information on some pins: {os.path.relpath(log)}" + ) + return {}, {} diff --git a/openlane/steps/verilator.py b/openlane/steps/verilator.py index cbcd0807..2c4bade5 100644 --- a/openlane/steps/verilator.py +++ b/openlane/steps/verilator.py @@ -90,6 +90,7 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: blackboxes = [] model_list: List[str] = [] + lefs: List[str] = [] model_set: Set[str] = set() if cell_verilog_models := self.config["CELL_VERILOG_MODELS"]: @@ -106,13 +107,16 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: DesignFormat.VERILOG_HEADER, DesignFormat.POWERED_NETLIST, DesignFormat.NETLIST, + DesignFormat.LEF, ], ) for view, format in macro_views: + str_view = str(view) if format == DesignFormat.VERILOG_HEADER: blackboxes.append(str(view)) + elif format == DesignFormat.LEF: + lefs.append(str_view) else: - str_view = str(view) if str_view not in model_set: model_set.add(str_view) model_list.append(str_view) @@ -139,6 +143,11 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: ) blackboxes.append(bb_path) + for lef in lefs: + blackboxes.append( + self.toolbox.header_from_lef(lef, self.config["VERILOG_POWER_DEFINE"]) + ) + vlt_file = os.path.join(self.step_dir, "_deps.vlt") with open(vlt_file, "w") as f: f.write("`verilator_config\n") diff --git a/openlane/steps/yosys.py b/openlane/steps/yosys.py index 8a8f5045..d0102f4e 100644 --- a/openlane/steps/yosys.py +++ b/openlane/steps/yosys.py @@ -156,6 +156,7 @@ def _generate_read_deps( DesignFormat.POWERED_NETLIST, DesignFormat.NETLIST, DesignFormat.LIB, + DesignFormat.LEF, ] if power_defines else [ @@ -163,6 +164,7 @@ def _generate_read_deps( DesignFormat.NETLIST, DesignFormat.POWERED_NETLIST, DesignFormat.LIB, + DesignFormat.LEF, ] ) for view, format in toolbox.get_macro_views_by_priority(config, format_list): @@ -171,6 +173,11 @@ def _generate_read_deps( commands += ( f"read_liberty -lib -ignore_miss_dir -setattr blackbox {view_escaped}\n" ) + elif format == DesignFormat.LEF: + lef_header = toolbox.header_from_lef( + str(view), config.get("VERILOG_POWER_DEFINE", "") + ) + commands += f"read_verilog -sv -lib {TclUtils.escape(lef_header)}\n" else: commands += f"read_verilog -sv -lib {TclUtils.join(verilog_include_args)} {view_escaped}\n" diff --git a/requirements.txt b/requirements.txt index 7b8d004b..a3d6d163 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ libparse>=0.3.1,<1 psutil>=5.9.0 httpx>=0.22.0, <0.28 ioplace_parser~=0.1.0 +lef-parser~=0.1.0 klayout>=0.28.17.post1,<0.29.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index dcc3fe41..5cd58735 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,7 +3,6 @@ wheel # lint black>=23,<24 -black[jupyter] flake8>=4 flake8-no-implicit-concat==0.3.3 flake8-pytest-style