Skip to content

Commit

Permalink
support for mesh powerstrap (#845)
Browse files Browse the repository at this point in the history
* support for mesh powerstrap

* added documentation for mesh

* merged mesh-strap with master

* mesh pattern with more descriptions

* updated mesh with sensible power utilization
  • Loading branch information
yolowiz authored Apr 11, 2024
1 parent e491b4a commit 1f3611f
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 47 deletions.
6 changes: 6 additions & 0 deletions doc/Hammer-Use/Hammer-APIs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``_<layer name>`` 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.
Expand Down
Binary file added doc/Hammer-Use/mesh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/Hammer-Use/mesh_description.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/Hammer-Use/mesh_no_space.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions hammer/config/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions hammer/config/defaults_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
26 changes: 20 additions & 6 deletions hammer/vlsi/hammer_vlsi_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.).
Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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 ...
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
205 changes: 164 additions & 41 deletions tests/test_power_straps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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"]}],
Expand All @@ -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
Expand Down Expand Up @@ -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 """
Expand Down

0 comments on commit 1f3611f

Please sign in to comment.