diff --git a/doc/Hammer-Use/Hammer-APIs.rst b/doc/Hammer-Use/Hammer-APIs.rst index 78be4bf75..16a768e78 100644 --- a/doc/Hammer-Use/Hammer-APIs.rst +++ b/doc/Hammer-Use/Hammer-APIs.rst @@ -87,6 +87,12 @@ For track spacing > 0 and effective power utilization < 100%, powerstraps are ro The currently supported API supports power strap generation by tracks, which auto-calculates power strap width, spacing, set-to-set distance, and offsets based on basic DRC rules specified in the technology Stackup object. The basic pieces of information needed are the desired track utilization per strap and overall power strap density. Different values can be specified on a layer-by-layer basis by appending ``_`` to the end of the desired option. +For mesh pattern, the following diagram shows how the sizings of a mesh-patterned strap is derived from user inputs. + +.. image:: mesh_no_space.png + +Notice that for a mesh pattern, users do not need to specify the layer track spacing, since that should not matter for a regular mesh pattern. + Special Cells ------------- Special cells are specified in the technology's JSON, but are exposed to provide lists of cells needed for certain steps, such as for fill, well taps, and more. Synthesis and place-and-route tool plugins can grab the appropriate type of special cell for the relevant steps. diff --git a/doc/Hammer-Use/mesh.png b/doc/Hammer-Use/mesh.png new file mode 100755 index 000000000..e4151f1fb Binary files /dev/null and b/doc/Hammer-Use/mesh.png differ diff --git a/doc/Hammer-Use/mesh_description.png b/doc/Hammer-Use/mesh_description.png new file mode 100755 index 000000000..c2e0ab89d Binary files /dev/null and b/doc/Hammer-Use/mesh_description.png differ diff --git a/doc/Hammer-Use/mesh_no_space.png b/doc/Hammer-Use/mesh_no_space.png new file mode 100755 index 000000000..348a6e177 Binary files /dev/null and b/doc/Hammer-Use/mesh_no_space.png differ diff --git a/hammer/config/defaults.yml b/hammer/config/defaults.yml index f0c86151f..1bacbe229 100644 --- a/hammer/config/defaults.yml +++ b/hammer/config/defaults.yml @@ -632,6 +632,9 @@ par: power_nets: [] # List of power nets to generate straps for. If empty, generates straps for all nets in vlsi.inputs.supplies # type: List[str] + pattern: "coupled" # existing default powerstrap pattern (e.g. ----VDD-VSS----VDD-VSS----VDD-VSS----) + + # DRC settings drc.inputs: # DRC settings diff --git a/hammer/config/defaults_types.yml b/hammer/config/defaults_types.yml index a2ad9c90f..4bce0845a 100644 --- a/hammer/config/defaults_types.yml +++ b/hammer/config/defaults_types.yml @@ -332,6 +332,7 @@ par: # type: list[str] strap_layers: list[str] + # Indicates if Hammer should generate straps for the stdcell rail layer # type: bool generate_rail_layer: bool @@ -344,6 +345,11 @@ par: # type: list[str] power_nets: list[str] + # Powerstrap patterns: e.g. "coupled" or "mesh" + # type: str + pattern: str + + # DRC settings drc.inputs: # Top RTL module. diff --git a/hammer/vlsi/hammer_vlsi_impl.py b/hammer/vlsi/hammer_vlsi_impl.py index 56552b7d5..7d92e1e74 100644 --- a/hammer/vlsi/hammer_vlsi_impl.py +++ b/hammer/vlsi/hammer_vlsi_impl.py @@ -673,7 +673,9 @@ def get_weight(supply_name: str) -> int: raise NotImplementedError("Power strap generation method %s is not implemented" % method) - def specify_power_straps_by_tracks(self, layer_name: str, bottom_via_layer: str, blockage_spacing: Decimal, track_pitch: int, track_width: int, track_spacing: int, track_start: int, track_offset: Decimal, bbox: Optional[List[Decimal]], nets: List[str], add_pins: bool, layer_is_all_power: bool, antenna_trim_shape: str) -> List[str]: + + def specify_power_straps_by_tracks(self, layer_name: str, bottom_via_layer: str, blockage_spacing: Decimal, track_pitch: int, track_width: int, track_spacing: int, track_start: int, track_offset: Decimal, bbox: Optional[List[Decimal]], nets: List[str], add_pins: bool, layer_is_all_power: bool, antenna_trim_shape: str, pattern: str) -> List[str]: + """ Generate a list of TCL commands that will create power straps on a given layer by specifying the desired track consumption. This method assumes that power straps are built bottom-up, starting with standard cell rails. @@ -683,7 +685,7 @@ def specify_power_straps_by_tracks(self, layer_name: str, bottom_via_layer: str, :param blockage_spacing: The minimum spacing between the end of a strap and the beginning of a macro or blockage. :param track_pitch: The integer pitch between groups of power straps (i.e. from left edge of strap A to the next left edge of strap A) in units of the routing pitch. :param track_width: The desired number of routing tracks to consume by a single power strap. - :param track_spacing: The desired number of USABLE routing tracks between power straps. It is recommended to leave this at 0 except to fix DRC issues. + :param track_spacing: The desired number of USABLE routing tracks between power straps (e.g. between VDD and VSS). It is recommended to leave this at 0 except to fix DRC issues. :param track_start: The index of the first track to start using for power straps relative to the bounding box. :param bbox: The optional (2N)-point bounding box of the area to generate straps. By default the entire core area is used. :param nets: A list of power nets to create (e.g. ["VDD", "VSS"], ["VDDA", "VSS", "VDDB"], ... etc.). @@ -698,6 +700,9 @@ def specify_power_straps_by_tracks(self, layer_name: str, bottom_via_layer: str, width = Decimal(0) spacing = Decimal(0) strap_offset = Decimal(0) + # Force unit spacing for correct power utilization to reuse twt + if pattern == 'mesh': + track_spacing = 1 # just for sizing power-straps using twt if track_spacing == 0: # An all-power (100% utilization) layer results in us wanting to do a uniform strap pattern, so we can just calculate the # maximum width and minimum spacing from the desired pitch, instead of using TWWT. @@ -710,6 +715,9 @@ def specify_power_straps_by_tracks(self, layer_name: str, bottom_via_layer: str, else: width, spacing, strap_start = layer.get_width_spacing_start_twt(track_width, logger=self.logger.context(layer_name)) spacing = 2*spacing + (track_spacing - 1) * layer.pitch + layer.min_width + if pattern == "mesh": + spacing = pitch / 2 - width + offset = track_offset + track_start * layer.pitch + strap_start assert width > Decimal(0), "Width must be greater than zero. You probably have a malformed tech plugin on layer {}.".format(layer_name) assert spacing > Decimal(0), "Spacing must be greater than zero. You probably have a malformed tech plugin on layer {}.".format(layer_name) @@ -762,7 +770,8 @@ def specify_all_power_straps_by_tracks(self, layer_names: List[str], bottom_via_ assert layer.index > last.index, "Must build power straps bottom-up" if last.direction == layer.direction: raise ValueError("Layers {a} and {b} run in the same direction, but have no power straps between them.".format(a=last.name, b=layer.name)) - + + pattern = self._get_by_tracks_metal_setting("pattern", layer_name) blockage_spacing = coerce_to_grid(float(self._get_by_tracks_metal_setting("blockage_spacing", layer_name)), layer.grid_unit) track_width = int(self._get_by_tracks_metal_setting("track_width", layer_name)) track_spacing = int(self._get_by_tracks_metal_setting("track_spacing", layer_name)) @@ -771,7 +780,6 @@ def specify_all_power_straps_by_tracks(self, layer_names: List[str], bottom_via_ track_offset = Decimal(str(self._get_by_tracks_metal_setting("track_offset", layer_name))) antenna_trim_shape = self._get_by_tracks_metal_setting("antenna_trim_shape", layer_name) offset = layer.offset # TODO this is relaxable if we can auto-recalculate this based on hierarchical setting - add_pins = layer_name in pin_layers # For multiple domains, we'll stripe them like this: # 2:1 : A A B A A B ... @@ -785,7 +793,9 @@ def specify_all_power_straps_by_tracks(self, layer_names: List[str], bottom_via_ nets = [ground_net, power_nets[i]] group_offset = offset + track_offset + track_pitch * i * layer.pitch group_pitch = sum_weights * track_pitch - output.extend(self.specify_power_straps_by_tracks(layer_name, last.name, blockage_spacing, group_pitch, track_width, track_spacing, track_start, group_offset, bbox, nets, add_pins, layer_is_all_power, antenna_trim_shape)) + + output.extend(self.specify_power_straps_by_tracks(layer_name, last.name, blockage_spacing, group_pitch, track_width, track_spacing, track_start, group_offset, bbox, nets, add_pins, layer_is_all_power, antenna_trim_shape, pattern)) + last = layer self._dump_power_straps_for_hardmacros() @@ -1040,13 +1050,17 @@ def _get_by_tracks_track_pitch(self, layer_name: str) -> int: track_width = int(self._get_by_tracks_metal_setting("track_width", layer_name)) track_spacing = int(self._get_by_tracks_metal_setting("track_spacing", layer_name)) power_utilization = float(self._get_by_tracks_metal_setting("power_utilization", layer_name)) + pattern = self._get_by_tracks_metal_setting("pattern", layer_name) assert power_utilization > 0.0 assert power_utilization <= 1.0 # Calculate how many tracks we consume # This strategy uses pairs of power and ground - consumed_tracks = 2 * track_width + track_spacing + if pattern == 'mesh': + consumed_tracks = 2 * track_width + else: + consumed_tracks = 2 * track_width + track_spacing return round(consumed_tracks / power_utilization) @abstractmethod diff --git a/tests/test_power_straps.py b/tests/test_power_straps.py index 507e8a8d4..825d38c7e 100644 --- a/tests/test_power_straps.py +++ b/tests/test_power_straps.py @@ -69,23 +69,9 @@ def add_stackup_and_site(in_dict: Dict[str, Any]) -> Dict[str, Any]: assert driver.load_par_tool() yield PowerStrapsTestContext(temp_dir=tech_dir_base, driver=driver) - def simple_straps_options() -> Dict[str, Any]: # TODO clean this up a bit - strap_layers = ["M4", "M5", "M6", "M7", "M8"] - pin_layers = ["M7", "M8"] - track_width = 4 - track_width_M7 = 8 - track_width_M8 = 10 - track_spacing = 0 - track_spacing_M6 = 1 - power_utilization = 0.2 - power_utilization_M8 = 1.0 - track_start_M5 = 1 - track_offset_M5 = 1.2 - bottom_via_layer = "rail" - # VSS comes before VDD nets = ["VSS", "VDD"] @@ -99,32 +85,59 @@ def simple_straps_options() -> Dict[str, Any]: "par.power_straps_mode": "generate", "par.generate_power_straps_method": "by_tracks", "par.generate_power_straps_options.by_tracks": { - "strap_layers": strap_layers, - "pin_layers": pin_layers, - "track_width": track_width, - "track_width_M7": track_width_M7, - "track_width_M8": track_width_M8, - "track_spacing": track_spacing, - "track_spacing_M6": track_spacing_M6, - "power_utilization": power_utilization, - "power_utilization_M8": power_utilization_M8, - "track_start_M5": track_start_M5, - "track_offset_M5": track_offset_M5, - "bottom_via_layer": bottom_via_layer + + "strap_layers": ["M4", "M5", "M6", "M7", "M8"], + "pin_layers": ["M7", "M8"], + "track_width": 4, + "track_width_M7": 8, + "track_width_M8": 10, + "track_spacing": 0, + "track_spacing_M6": 1, + "power_utilization": 0.2, + "power_utilization_M8": 1.0, + "track_start_M5": 1, + "track_offset_M5": 1.2, + "bottom_via_layer": "rail" } } return straps_options +def mesh_straps_options() -> Dict[str, Any]: + # TODO clean this up a bit -def multiple_domains_straps_options() -> Dict[str, Any]: - strap_layers = ["M4", "M5", "M8"] - pin_layers = ["M8"] - track_width = 8 - track_spacing = 0 - power_utilization = 0.2 - power_utilization_M8 = 1.0 - bottom_via_layer = "rail" + # VSS comes before VDD + nets = ["VSS", "VDD"] + straps_options = { + "vlsi.inputs.supplies": { + "power": [{"name": "VDD", "pins": ["VDD"]}], + "ground": [{"name": "VSS", "pins": ["VSS"]}], + "VDD": "1.00 V", + "GND": "0 V" + }, + "par.power_straps_mode": "generate", + "par.generate_power_straps_method": "by_tracks", + "par.generate_power_straps_options.by_tracks": { + "strap_layers": ["M4", "M5", "M6", "M7", "M8"], + "pin_layers": ["M7", "M8"], + "track_width": 4, + "track_width_M7": 8, + "track_width_M8": 10, + "track_spacing": 0, + "track_spacing_M6": 1, + "power_utilization": 0.2, + "power_utilization_M8": 1.0, + "track_start_M5": 1, + "track_offset_M5": 1.2, + "pattern": 'coupled', + "pattern_M4": 'mesh', + "track_spacing_M4": 1, + "bottom_via_layer": "rail" + } + } + return straps_options + +def multiple_domains_straps_options() -> Dict[str, Any]: straps_options = { "vlsi.inputs.supplies": { "power": [{"name": "VDD", "pins": ["VDD"]}, {"name": "VDD2", "pins": ["VDD2"]}], @@ -135,13 +148,13 @@ def multiple_domains_straps_options() -> Dict[str, Any]: "par.power_straps_mode": "generate", "par.generate_power_straps_method": "by_tracks", "par.generate_power_straps_options.by_tracks": { - "strap_layers": strap_layers, - "pin_layers": pin_layers, - "track_width": track_width, - "track_spacing": track_spacing, - "power_utilization": power_utilization, - "power_utilization_M8": power_utilization_M8, - "bottom_via_layer": bottom_via_layer + "strap_layers": ["M4", "M5", "M8"], + "pin_layers": ["M8"], + "track_width": 8, + "track_spacing": 0, + "power_utilization": 0.2, + "power_utilization_M8": 1.0, + "bottom_via_layer": "rail" } } return straps_options @@ -251,7 +264,117 @@ def test_simple_by_tracks_power_straps(self, power_straps_test_context) -> None: assert strap_pitch == required_pitch else: assert False, "Got the wrong layer_name: {}".format(layer_name) + + @pytest.mark.parametrize("straps_options, tech_name", [(mesh_straps_options(), "mesh")]) + def test_mesh_power_straps(self, power_straps_test_context) -> None: + """ Creates simple power straps using the by_tracks method """ + c = power_straps_test_context + success, par_output = c.driver.run_par() + assert success + par_tool = c.driver.par_tool + # It's surpringly annoying to import mockpar.MockPlaceAndRoute, which is the class + # that contains the parse_mock_power_straps_file() method, so we're just ignoring + # that particular part of this + assert isinstance(par_tool, hammer_vlsi.HammerPlaceAndRouteTool) + stackup = par_tool.get_stackup() + parsed_out = par_tool.parse_mock_power_straps_file() # type: ignore + entries = cast(List[Dict[str, Any]], parsed_out) + + # TODO: this is copied, avoid that + strap_layers = ["M4", "M5", "M6", "M7", "M8"] + pin_layers = ["M7", "M8"] + track_width = 4 + track_spacing_M4 = 1 + pattern_M4 = 'mesh' + track_width_M7 = 8 + track_width_M8 = 10 + track_spacing = 0 + track_spacing_M6 = 1 + power_utilization = 0.2 + power_utilization_M8 = 1.0 + track_start_M5 = 1 + track_offset_M5 = 1.2 + nets = ["VSS", "VDD"] + + for entry in entries: + print(entry) + c.logger.debug("Power strap entry:" + str(entry)) + layer_name = entry["layer_name"] + if layer_name == "M1": + # Standard cell rails + assert entry["tap_cell_name"] == "FakeTapCell" + assert entry["bbox"] == [] + assert entry["nets"] == nets + continue + + strap_width = Decimal(entry["width"]) + strap_spacing = Decimal(entry["spacing"]) + strap_pitch = Decimal(entry["pitch"]) + strap_offset = Decimal(entry["offset"]) + metal = stackup.get_metal(layer_name) + min_width = metal.min_width + group_track_pitch = strap_pitch / metal.pitch + track_offset = Decimal(str(track_offset_M5)) if layer_name == "M5" else Decimal(0) + track_start = Decimal(str(track_start_M5)) if layer_name == "M5" else Decimal(0) + used_tracks = round(Decimal(strap_offset + strap_width + strap_spacing + strap_width + strap_offset - 2 * (track_offset + track_start * metal.pitch)) / metal.pitch) - 1 + if layer_name == "M4": + assert entry["bbox"] == [] + assert entry["nets"] == nets + # TODO more tests in a future PR + assert strap_spacing == strap_pitch / 2 - strap_width + assert strap_pitch / metal.pitch == track_width * 2 / power_utilization + elif layer_name == "M5": + assert entry["bbox"] == [] + assert entry["nets"] == nets + # Check that the requested tracks equals the used tracks + requested_tracks = track_width * 2 + track_spacing + assert used_tracks == requested_tracks + # Spacing should be at least the min spacing + min_spacing = metal.get_spacing_for_width(strap_width) + assert strap_spacing >= min_spacing + # TODO more tests in a future PR + assert strap_pitch / metal.pitch == (track_width * 2 + track_spacing) / power_utilization + elif layer_name == "M6": + assert entry["bbox"] == [] + assert entry["nets"] == nets + # This is a sanity check that we didn't accidentally change something up above + assert track_spacing_M6 == 1 + # We should be able to fit a track in between the stripes because track_spacing_M6 == 1 + wire_to_strap_spacing = (strap_spacing - min_width) / 2 + min_spacing = metal.get_spacing_for_width(strap_width) + assert wire_to_strap_spacing >= min_spacing + # Check that the requested tracks equals the used tracks + requested_tracks = track_width * 2 + track_spacing_M6 + assert used_tracks == requested_tracks + # Spacing should be at least the min spacing + min_spacing = metal.get_spacing_for_width(strap_width) + assert wire_to_strap_spacing >= min_spacing + # TODO more tests in a future PR + elif layer_name == "M7": + assert entry["bbox"] == [] + assert entry["nets"] == nets + # TODO more tests in a future PR + elif layer_name == "M8": + other_spacing = strap_pitch - (2 * strap_width) - strap_spacing + # Track spacing should be 0 + assert track_spacing == 0 + # Test that the power straps are symmetric + assert other_spacing == strap_spacing + # Spacing should be at least the min spacing + min_spacing = metal.get_spacing_for_width(strap_width) + assert other_spacing >= min_spacing + # Test that a slightly larger strap would be a DRC violation + new_spacing = metal.get_spacing_for_width(strap_width + metal.grid_unit) + new_pitch = (strap_width + metal.grid_unit + new_spacing) * 2 + assert strap_pitch < new_pitch + # Test that the pitch does consume the right number of tracks + required_pitch = Decimal(track_width_M8 * 2) * metal.pitch + # 100% power utilzation should produce straps that consume 2*strap_width + strap_spacing tracks + assert strap_pitch == required_pitch + else: + assert False, "Got the wrong layer_name: {}".format(layer_name) + @pytest.mark.parametrize("straps_options, tech_name", [(multiple_domains_straps_options(), "multiple_domains")]) def test_multiple_domains(self, power_straps_test_context) -> None: """ Tests multiple power domains """