librelane 2.4.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of librelane might be problematic. Click here for more details.
- librelane/__init__.py +38 -0
- librelane/__main__.py +470 -0
- librelane/__version__.py +43 -0
- librelane/common/__init__.py +61 -0
- librelane/common/cli.py +75 -0
- librelane/common/drc.py +245 -0
- librelane/common/generic_dict.py +319 -0
- librelane/common/metrics/__init__.py +35 -0
- librelane/common/metrics/__main__.py +413 -0
- librelane/common/metrics/library.py +354 -0
- librelane/common/metrics/metric.py +186 -0
- librelane/common/metrics/util.py +279 -0
- librelane/common/misc.py +402 -0
- librelane/common/ring_buffer.py +63 -0
- librelane/common/tcl.py +80 -0
- librelane/common/toolbox.py +549 -0
- librelane/common/tpe.py +41 -0
- librelane/common/types.py +117 -0
- librelane/config/__init__.py +32 -0
- librelane/config/__main__.py +158 -0
- librelane/config/config.py +1025 -0
- librelane/config/flow.py +490 -0
- librelane/config/pdk_compat.py +255 -0
- librelane/config/preprocessor.py +464 -0
- librelane/config/removals.py +45 -0
- librelane/config/variable.py +722 -0
- librelane/container.py +264 -0
- librelane/env_info.py +306 -0
- librelane/examples/spm/config.yaml +33 -0
- librelane/examples/spm/pin_order.cfg +14 -0
- librelane/examples/spm/src/impl.sdc +73 -0
- librelane/examples/spm/src/signoff.sdc +68 -0
- librelane/examples/spm/src/spm.v +73 -0
- librelane/examples/spm/verify/spm_tb.v +106 -0
- librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
- librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
- librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
- librelane/examples/spm-user_project_wrapper/config.json +13 -0
- librelane/examples/spm-user_project_wrapper/defines.v +66 -0
- librelane/examples/spm-user_project_wrapper/template.def +7656 -0
- librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
- librelane/flows/__init__.py +24 -0
- librelane/flows/builtins.py +18 -0
- librelane/flows/classic.py +330 -0
- librelane/flows/cli.py +463 -0
- librelane/flows/flow.py +985 -0
- librelane/flows/misc.py +71 -0
- librelane/flows/optimizing.py +179 -0
- librelane/flows/sequential.py +367 -0
- librelane/flows/synth_explore.py +173 -0
- librelane/logging/__init__.py +40 -0
- librelane/logging/logger.py +323 -0
- librelane/open_pdks_rev +1 -0
- librelane/plugins.py +21 -0
- librelane/py.typed +0 -0
- librelane/scripts/base.sdc +80 -0
- librelane/scripts/klayout/Readme.md +2 -0
- librelane/scripts/klayout/open_design.py +63 -0
- librelane/scripts/klayout/render.py +121 -0
- librelane/scripts/klayout/stream_out.py +176 -0
- librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
- librelane/scripts/klayout/xor.drc +120 -0
- librelane/scripts/magic/Readme.md +1 -0
- librelane/scripts/magic/common/read.tcl +114 -0
- librelane/scripts/magic/def/antenna_check.tcl +35 -0
- librelane/scripts/magic/def/mag.tcl +19 -0
- librelane/scripts/magic/def/mag_gds.tcl +81 -0
- librelane/scripts/magic/drc.tcl +79 -0
- librelane/scripts/magic/extract_spice.tcl +98 -0
- librelane/scripts/magic/gds/drc_batch.tcl +74 -0
- librelane/scripts/magic/gds/erase_box.tcl +32 -0
- librelane/scripts/magic/gds/extras_mag.tcl +47 -0
- librelane/scripts/magic/gds/mag_with_pointers.tcl +32 -0
- librelane/scripts/magic/get_bbox.tcl +11 -0
- librelane/scripts/magic/lef/extras_maglef.tcl +63 -0
- librelane/scripts/magic/lef/maglef.tcl +27 -0
- librelane/scripts/magic/lef.tcl +57 -0
- librelane/scripts/magic/open.tcl +28 -0
- librelane/scripts/magic/wrapper.tcl +19 -0
- librelane/scripts/netgen/setup.tcl +28 -0
- librelane/scripts/odbpy/apply_def_template.py +49 -0
- librelane/scripts/odbpy/cell_frequency.py +107 -0
- librelane/scripts/odbpy/check_antenna_properties.py +116 -0
- librelane/scripts/odbpy/contextualize.py +109 -0
- librelane/scripts/odbpy/defutil.py +574 -0
- librelane/scripts/odbpy/diodes.py +373 -0
- librelane/scripts/odbpy/disconnected_pins.py +305 -0
- librelane/scripts/odbpy/exception_codes.py +17 -0
- librelane/scripts/odbpy/filter_unannotated.py +100 -0
- librelane/scripts/odbpy/io_place.py +482 -0
- librelane/scripts/odbpy/label_macro_pins.py +277 -0
- librelane/scripts/odbpy/lefutil.py +97 -0
- librelane/scripts/odbpy/placers.py +162 -0
- librelane/scripts/odbpy/power_utils.py +395 -0
- librelane/scripts/odbpy/random_place.py +57 -0
- librelane/scripts/odbpy/reader.py +246 -0
- librelane/scripts/odbpy/remove_buffers.py +173 -0
- librelane/scripts/odbpy/snap_to_grid.py +57 -0
- librelane/scripts/odbpy/wire_lengths.py +93 -0
- librelane/scripts/openroad/antenna_check.tcl +20 -0
- librelane/scripts/openroad/antenna_repair.tcl +31 -0
- librelane/scripts/openroad/basic_mp.tcl +24 -0
- librelane/scripts/openroad/buffer_list.tcl +10 -0
- librelane/scripts/openroad/common/dpl.tcl +24 -0
- librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
- librelane/scripts/openroad/common/grt.tcl +32 -0
- librelane/scripts/openroad/common/io.tcl +476 -0
- librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
- librelane/scripts/openroad/common/resizer.tcl +103 -0
- librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
- librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
- librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
- librelane/scripts/openroad/common/set_rc.tcl +75 -0
- librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
- librelane/scripts/openroad/cts.tcl +80 -0
- librelane/scripts/openroad/cut_rows.tcl +24 -0
- librelane/scripts/openroad/dpl.tcl +24 -0
- librelane/scripts/openroad/drt.tcl +37 -0
- librelane/scripts/openroad/fill.tcl +30 -0
- librelane/scripts/openroad/floorplan.tcl +145 -0
- librelane/scripts/openroad/gpl.tcl +88 -0
- librelane/scripts/openroad/grt.tcl +30 -0
- librelane/scripts/openroad/gui.tcl +15 -0
- librelane/scripts/openroad/insert_buffer.tcl +127 -0
- librelane/scripts/openroad/ioplacer.tcl +67 -0
- librelane/scripts/openroad/irdrop.tcl +51 -0
- librelane/scripts/openroad/pdn.tcl +52 -0
- librelane/scripts/openroad/rcx.tcl +32 -0
- librelane/scripts/openroad/repair_design.tcl +70 -0
- librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
- librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
- librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
- librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
- librelane/scripts/openroad/sta/corner.tcl +393 -0
- librelane/scripts/openroad/tapcell.tcl +25 -0
- librelane/scripts/openroad/write_views.tcl +27 -0
- librelane/scripts/pyosys/construct_abc_script.py +177 -0
- librelane/scripts/pyosys/json_header.py +84 -0
- librelane/scripts/pyosys/synthesize.py +493 -0
- librelane/scripts/pyosys/ys_common.py +153 -0
- librelane/scripts/tclsh/hello.tcl +1 -0
- librelane/state/__init__.py +24 -0
- librelane/state/__main__.py +61 -0
- librelane/state/design_format.py +180 -0
- librelane/state/state.py +351 -0
- librelane/steps/__init__.py +61 -0
- librelane/steps/__main__.py +511 -0
- librelane/steps/checker.py +637 -0
- librelane/steps/common_variables.py +340 -0
- librelane/steps/cvc_rv.py +169 -0
- librelane/steps/klayout.py +509 -0
- librelane/steps/magic.py +566 -0
- librelane/steps/misc.py +160 -0
- librelane/steps/netgen.py +253 -0
- librelane/steps/odb.py +955 -0
- librelane/steps/openroad.py +2433 -0
- librelane/steps/openroad_alerts.py +102 -0
- librelane/steps/pyosys.py +629 -0
- librelane/steps/step.py +1547 -0
- librelane/steps/tclstep.py +288 -0
- librelane/steps/verilator.py +222 -0
- librelane/steps/yosys.py +371 -0
- librelane-2.4.0.dev0.dist-info/METADATA +151 -0
- librelane-2.4.0.dev0.dist-info/RECORD +166 -0
- librelane-2.4.0.dev0.dist-info/WHEEL +4 -0
- librelane-2.4.0.dev0.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,2433 @@
|
|
|
1
|
+
# Copyright 2023 Efabless Corporation
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import io
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
import tempfile
|
|
19
|
+
import functools
|
|
20
|
+
import subprocess
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from math import inf
|
|
23
|
+
from glob import glob
|
|
24
|
+
from decimal import Decimal
|
|
25
|
+
from base64 import b64encode
|
|
26
|
+
from abc import abstractmethod
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
29
|
+
from typing import (
|
|
30
|
+
Any,
|
|
31
|
+
List,
|
|
32
|
+
Dict,
|
|
33
|
+
Literal,
|
|
34
|
+
Set,
|
|
35
|
+
Tuple,
|
|
36
|
+
Optional,
|
|
37
|
+
Union,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
import yaml
|
|
42
|
+
import rich
|
|
43
|
+
import rich.table
|
|
44
|
+
|
|
45
|
+
from .step import (
|
|
46
|
+
CompositeStep,
|
|
47
|
+
DefaultOutputProcessor,
|
|
48
|
+
StepError,
|
|
49
|
+
ViewsUpdate,
|
|
50
|
+
MetricsUpdate,
|
|
51
|
+
Step,
|
|
52
|
+
StepException,
|
|
53
|
+
)
|
|
54
|
+
from .openroad_alerts import (
|
|
55
|
+
OpenROADAlert,
|
|
56
|
+
OpenROADOutputProcessor,
|
|
57
|
+
)
|
|
58
|
+
from .tclstep import TclStep
|
|
59
|
+
from .common_variables import (
|
|
60
|
+
io_layer_variables,
|
|
61
|
+
pdn_variables,
|
|
62
|
+
rsz_variables,
|
|
63
|
+
dpl_variables,
|
|
64
|
+
grt_variables,
|
|
65
|
+
routing_layer_variables,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
from ..config import Variable, Macro
|
|
69
|
+
from ..config.flow import option_variables
|
|
70
|
+
from ..state import State, DesignFormat
|
|
71
|
+
from ..logging import debug, info, verbose, console, options
|
|
72
|
+
from ..common import (
|
|
73
|
+
Path,
|
|
74
|
+
TclUtils,
|
|
75
|
+
get_script_dir,
|
|
76
|
+
mkdirp,
|
|
77
|
+
aggregate_metrics,
|
|
78
|
+
process_list_file,
|
|
79
|
+
_get_process_limit,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
EXAMPLE_INPUT = """
|
|
83
|
+
li1 X 0.23 0.46
|
|
84
|
+
li1 Y 0.17 0.34
|
|
85
|
+
met1 X 0.17 0.34
|
|
86
|
+
met1 Y 0.17 0.34
|
|
87
|
+
met2 X 0.23 0.46
|
|
88
|
+
met2 Y 0.23 0.46
|
|
89
|
+
met3 X 0.34 0.68
|
|
90
|
+
met3 Y 0.34 0.68
|
|
91
|
+
met4 X 0.46 0.92
|
|
92
|
+
met4 Y 0.46 0.92
|
|
93
|
+
met5 X 1.70 3.40
|
|
94
|
+
met5 Y 1.70 3.40
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def old_to_new_tracks(old_tracks: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
>>> old_to_new_tracks(EXAMPLE_INPUT)
|
|
101
|
+
'make_tracks li1 -x_offset 0.23 -x_pitch 0.46 -y_offset 0.17 -y_pitch 0.34\\nmake_tracks met1 -x_offset 0.17 -x_pitch 0.34 -y_offset 0.17 -y_pitch 0.34\\nmake_tracks met2 -x_offset 0.23 -x_pitch 0.46 -y_offset 0.23 -y_pitch 0.46\\nmake_tracks met3 -x_offset 0.34 -x_pitch 0.68 -y_offset 0.34 -y_pitch 0.68\\nmake_tracks met4 -x_offset 0.46 -x_pitch 0.92 -y_offset 0.46 -y_pitch 0.92\\nmake_tracks met5 -x_offset 1.70 -x_pitch 3.40 -y_offset 1.70 -y_pitch 3.40\\n'
|
|
102
|
+
"""
|
|
103
|
+
layers: Dict[str, Dict[str, Tuple[str, str]]] = {}
|
|
104
|
+
|
|
105
|
+
for line in old_tracks.splitlines():
|
|
106
|
+
if line.strip() == "":
|
|
107
|
+
continue
|
|
108
|
+
layer, cardinal, offset, pitch = line.split()
|
|
109
|
+
layers[layer] = layers.get(layer) or {}
|
|
110
|
+
layers[layer][cardinal] = (offset, pitch)
|
|
111
|
+
|
|
112
|
+
final_str = ""
|
|
113
|
+
for layer, data in layers.items():
|
|
114
|
+
x_offset, x_pitch = data["X"]
|
|
115
|
+
y_offset, y_pitch = data["Y"]
|
|
116
|
+
final_str += f"make_tracks {layer} -x_offset {x_offset} -x_pitch {x_pitch} -y_offset {y_offset} -y_pitch {y_pitch}\n"
|
|
117
|
+
|
|
118
|
+
return final_str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def pdn_macro_migrator(x):
|
|
122
|
+
if not isinstance(x, str):
|
|
123
|
+
return x
|
|
124
|
+
if "," in x:
|
|
125
|
+
return [el.strip() for el in x.split(",")]
|
|
126
|
+
else:
|
|
127
|
+
return [x.strip()]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@Step.factory.register()
|
|
131
|
+
class CheckSDCFiles(Step):
|
|
132
|
+
"""
|
|
133
|
+
Checks that the two variables used for SDC files by OpenROAD steps,
|
|
134
|
+
namely, ``PNR_SDC_FILE`` and ``SIGNOFF_SDC_FILE``, are explicitly set to
|
|
135
|
+
valid paths by the users, and emits a warning that the fallback will be
|
|
136
|
+
utilized otherwise.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
id = "OpenROAD.CheckSDCFiles"
|
|
140
|
+
name = "Check SDC Files"
|
|
141
|
+
inputs = []
|
|
142
|
+
outputs = []
|
|
143
|
+
|
|
144
|
+
config_vars = [
|
|
145
|
+
Variable(
|
|
146
|
+
"PNR_SDC_FILE",
|
|
147
|
+
Optional[Path],
|
|
148
|
+
"Specifies the SDC file used during all implementation (PnR) steps",
|
|
149
|
+
),
|
|
150
|
+
Variable(
|
|
151
|
+
"SIGNOFF_SDC_FILE",
|
|
152
|
+
Optional[Path],
|
|
153
|
+
"Specifies the SDC file for STA during signoff",
|
|
154
|
+
),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
158
|
+
default_sdc_file = [
|
|
159
|
+
var for var in option_variables if var.name == "FALLBACK_SDC_FILE"
|
|
160
|
+
][0]
|
|
161
|
+
assert default_sdc_file is not None
|
|
162
|
+
|
|
163
|
+
is_generic_fallback = default_sdc_file.default
|
|
164
|
+
fallback_descriptor = "generic" if is_generic_fallback else "user-defined"
|
|
165
|
+
if self.config["PNR_SDC_FILE"] is None:
|
|
166
|
+
self.warn(
|
|
167
|
+
f"'PNR_SDC_FILE' is not defined. Using {fallback_descriptor} fallback SDC for OpenROAD PnR steps."
|
|
168
|
+
)
|
|
169
|
+
if self.config["SIGNOFF_SDC_FILE"] is None:
|
|
170
|
+
self.warn(
|
|
171
|
+
f"'SIGNOFF_SDC_FILE' is not defined. Using {fallback_descriptor} fallback SDC for OpenROAD PnR steps."
|
|
172
|
+
)
|
|
173
|
+
return {}, {}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class OpenROADStep(TclStep):
|
|
177
|
+
inputs = [DesignFormat.ODB]
|
|
178
|
+
outputs = [
|
|
179
|
+
DesignFormat.ODB,
|
|
180
|
+
DesignFormat.DEF,
|
|
181
|
+
DesignFormat.SDC,
|
|
182
|
+
DesignFormat.NETLIST,
|
|
183
|
+
DesignFormat.POWERED_NETLIST,
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
output_processors = [OpenROADOutputProcessor, DefaultOutputProcessor]
|
|
187
|
+
|
|
188
|
+
config_vars = [
|
|
189
|
+
Variable(
|
|
190
|
+
"PDN_CONNECT_MACROS_TO_GRID",
|
|
191
|
+
bool,
|
|
192
|
+
"Enables the connection of macros to the top level power grid.",
|
|
193
|
+
default=True,
|
|
194
|
+
deprecated_names=["FP_PDN_ENABLE_MACROS_GRID"],
|
|
195
|
+
),
|
|
196
|
+
Variable(
|
|
197
|
+
"PDN_MACRO_CONNECTIONS",
|
|
198
|
+
Optional[List[str]],
|
|
199
|
+
"Specifies explicit power connections of internal macros to the top level power grid, in the format: regex matching macro instance names, power domain vdd and ground net names, and macro vdd and ground pin names `<instance_name_rx> <vdd_net> <gnd_net> <vdd_pin> <gnd_pin>`.",
|
|
200
|
+
deprecated_names=[("FP_PDN_MACRO_HOOKS", pdn_macro_migrator)],
|
|
201
|
+
),
|
|
202
|
+
Variable(
|
|
203
|
+
"PDN_ENABLE_GLOBAL_CONNECTIONS",
|
|
204
|
+
bool,
|
|
205
|
+
"Enables the creation of global connections in PDN generation.",
|
|
206
|
+
default=True,
|
|
207
|
+
deprecated_names=["FP_PDN_ENABLE_GLOBAL_CONNECTIONS"],
|
|
208
|
+
),
|
|
209
|
+
Variable(
|
|
210
|
+
"PNR_SDC_FILE",
|
|
211
|
+
Optional[Path],
|
|
212
|
+
"Specifies the SDC file used during all implementation (PnR) steps",
|
|
213
|
+
),
|
|
214
|
+
Variable(
|
|
215
|
+
"FP_DEF_TEMPLATE",
|
|
216
|
+
Optional[Path],
|
|
217
|
+
"Points to the DEF file to be used as a template.",
|
|
218
|
+
),
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
def get_script_path(self) -> str:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def on_alert(self, alert: OpenROADAlert) -> OpenROADAlert:
|
|
226
|
+
if alert.code in [
|
|
227
|
+
"ORD-0039", # .openroad ignored with -python
|
|
228
|
+
"ODB-0220", # lef parsing/NOWIREEXTENSIONATPIN statement is obsolete in version 5.6 or later.
|
|
229
|
+
"STA-1256", # table template \w+ not found
|
|
230
|
+
]:
|
|
231
|
+
return alert
|
|
232
|
+
if alert.cls == "error":
|
|
233
|
+
self.err(str(alert), extra={"key": alert.code})
|
|
234
|
+
elif alert.cls == "warning":
|
|
235
|
+
self.warn(str(alert), extra={"key": alert.code})
|
|
236
|
+
return alert
|
|
237
|
+
|
|
238
|
+
def prepare_env(self, env: dict, state: State) -> dict:
|
|
239
|
+
env = super().prepare_env(env, state)
|
|
240
|
+
|
|
241
|
+
lib_list = self.toolbox.filter_views(self.config, self.config["LIB"])
|
|
242
|
+
lib_list += self.toolbox.get_macro_views(self.config, DesignFormat.LIB)
|
|
243
|
+
|
|
244
|
+
env["_SDC_IN"] = self.config["PNR_SDC_FILE"] or self.config["FALLBACK_SDC_FILE"]
|
|
245
|
+
env["_PNR_LIBS"] = TclStep.value_to_tcl(lib_list)
|
|
246
|
+
env["_MACRO_LIBS"] = TclStep.value_to_tcl(
|
|
247
|
+
self.toolbox.get_macro_views(self.config, DesignFormat.LIB)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
excluded_cells: Set[str] = set(self.config["EXTRA_EXCLUDED_CELLS"] or [])
|
|
251
|
+
excluded_cells.update(process_list_file(self.config["PNR_EXCLUDED_CELL_FILE"]))
|
|
252
|
+
env["_PNR_EXCLUDED_CELLS"] = TclUtils.join(excluded_cells)
|
|
253
|
+
|
|
254
|
+
return env
|
|
255
|
+
|
|
256
|
+
def run(self, state_in, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
257
|
+
"""
|
|
258
|
+
The `run()` override for the OpenROADStep class handles two things:
|
|
259
|
+
|
|
260
|
+
1. Before the `super()` call: It creates a version of the lib file
|
|
261
|
+
minus cells that are known bad (i.e. those that fail DRC) and pass it on
|
|
262
|
+
in the environment variable `_PNR_LIBS`.
|
|
263
|
+
|
|
264
|
+
2. After the `super()` call: Processes the `or_metrics_out.json` file and
|
|
265
|
+
updates the State's `metrics` property with any new metrics in that object.
|
|
266
|
+
"""
|
|
267
|
+
kwargs, env = self.extract_env(kwargs)
|
|
268
|
+
env = self.prepare_env(env, state_in)
|
|
269
|
+
|
|
270
|
+
check = False
|
|
271
|
+
if "check" in kwargs:
|
|
272
|
+
check = kwargs.pop("check")
|
|
273
|
+
|
|
274
|
+
command = self.get_command()
|
|
275
|
+
|
|
276
|
+
subprocess_result = self.run_subprocess(
|
|
277
|
+
command,
|
|
278
|
+
env=env,
|
|
279
|
+
check=check,
|
|
280
|
+
**kwargs,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
generated_metrics = subprocess_result["generated_metrics"]
|
|
284
|
+
|
|
285
|
+
views_updates: ViewsUpdate = {}
|
|
286
|
+
for output in self.outputs:
|
|
287
|
+
if output.value.multiple:
|
|
288
|
+
# Too step-specific.
|
|
289
|
+
continue
|
|
290
|
+
path = Path(env[f"SAVE_{output.name}"])
|
|
291
|
+
if not path.exists():
|
|
292
|
+
continue
|
|
293
|
+
views_updates[output] = path
|
|
294
|
+
|
|
295
|
+
# 1. Parse warnings and errors
|
|
296
|
+
alerts = subprocess_result["openroad_alerts"]
|
|
297
|
+
if subprocess_result["returncode"] != 0:
|
|
298
|
+
error_strings = [str(alert) for alert in alerts if alert.cls == "error"]
|
|
299
|
+
if len(error_strings):
|
|
300
|
+
error_string = "\n".join(error_strings)
|
|
301
|
+
raise StepError(
|
|
302
|
+
f"{self.id} failed with the following errors:\n{error_string}"
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
raise StepException(
|
|
306
|
+
f"{self.id} failed unexpectedly. Please check the logs and file an issue."
|
|
307
|
+
)
|
|
308
|
+
# 2. Metrics
|
|
309
|
+
metrics_path = os.path.join(self.step_dir, "or_metrics_out.json")
|
|
310
|
+
if os.path.exists(metrics_path):
|
|
311
|
+
or_metrics_out = json.loads(open(metrics_path).read(), parse_float=Decimal)
|
|
312
|
+
for key, value in or_metrics_out.items():
|
|
313
|
+
if value == "Infinity":
|
|
314
|
+
or_metrics_out[key] = inf
|
|
315
|
+
elif value == "-Infinity":
|
|
316
|
+
or_metrics_out[key] = -inf
|
|
317
|
+
generated_metrics.update(or_metrics_out)
|
|
318
|
+
|
|
319
|
+
metric_updates_with_aggregates = aggregate_metrics(generated_metrics)
|
|
320
|
+
|
|
321
|
+
return views_updates, metric_updates_with_aggregates
|
|
322
|
+
|
|
323
|
+
def get_command(self) -> List[str]:
|
|
324
|
+
metrics_path = os.path.join(self.step_dir, "or_metrics_out.json")
|
|
325
|
+
return [
|
|
326
|
+
"openroad",
|
|
327
|
+
"-exit",
|
|
328
|
+
"-no_splash",
|
|
329
|
+
"-metrics",
|
|
330
|
+
metrics_path,
|
|
331
|
+
self.get_script_path(),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
def layout_preview(self) -> Optional[str]:
|
|
335
|
+
if self.state_out is None:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
state_in = self.state_in.result()
|
|
339
|
+
if self.state_out.get("def") == state_in.get("def"):
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
if image := self.toolbox.render_png(self.config, self.state_out):
|
|
343
|
+
image_encoded = b64encode(image).decode("utf8")
|
|
344
|
+
return f'<img src="data:image/png;base64,{image_encoded}" />'
|
|
345
|
+
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@Step.factory.register()
|
|
350
|
+
class STAMidPNR(OpenROADStep):
|
|
351
|
+
"""
|
|
352
|
+
Performs `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
|
|
353
|
+
using OpenROAD on an OpenROAD database, mid-PnR, with estimated values for
|
|
354
|
+
parasitics.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
id = "OpenROAD.STAMidPNR"
|
|
358
|
+
name = "STA (Mid-PnR)"
|
|
359
|
+
long_name = "Static Timing Analysis (Mid-PnR)"
|
|
360
|
+
|
|
361
|
+
inputs = [DesignFormat.ODB]
|
|
362
|
+
outputs = []
|
|
363
|
+
|
|
364
|
+
def get_script_path(self):
|
|
365
|
+
return os.path.join(get_script_dir(), "openroad", "sta", "corner.tcl")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class OpenSTAStep(OpenROADStep):
|
|
369
|
+
@dataclass(frozen=True)
|
|
370
|
+
class CornerFileList:
|
|
371
|
+
libs: Tuple[str, ...]
|
|
372
|
+
netlists: Tuple[str, ...]
|
|
373
|
+
spefs: Tuple[Tuple[str, str], ...]
|
|
374
|
+
extra_spefs_backcompat: Optional[Tuple[Tuple[str, str], ...]] = None
|
|
375
|
+
current_corner_spef: Optional[str] = None
|
|
376
|
+
|
|
377
|
+
def set_env(self, env: Dict[str, Any]):
|
|
378
|
+
env["_CURRENT_CORNER_LIBS"] = TclStep.value_to_tcl(self.libs)
|
|
379
|
+
env["_CURRENT_CORNER_NETLISTS"] = TclStep.value_to_tcl(self.netlists)
|
|
380
|
+
env["_CURRENT_CORNER_SPEFS"] = TclStep.value_to_tcl(self.spefs)
|
|
381
|
+
if self.extra_spefs_backcompat is not None:
|
|
382
|
+
env["_CURRENT_CORNER_EXTRA_SPEFS_BACKCOMPAT"] = TclStep.value_to_tcl(
|
|
383
|
+
self.extra_spefs_backcompat
|
|
384
|
+
)
|
|
385
|
+
if self.current_corner_spef is not None:
|
|
386
|
+
env["_CURRENT_SPEF_BY_CORNER"] = self.current_corner_spef
|
|
387
|
+
|
|
388
|
+
inputs = [DesignFormat.NETLIST]
|
|
389
|
+
|
|
390
|
+
def get_command(self) -> List[str]:
|
|
391
|
+
return ["sta", "-no_splash", "-exit", self.get_script_path()]
|
|
392
|
+
|
|
393
|
+
def layout_preview(self) -> Optional[str]:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
def _get_corner_files(
|
|
397
|
+
self: Step,
|
|
398
|
+
timing_corner: Optional[str] = None,
|
|
399
|
+
prioritize_nl: bool = False,
|
|
400
|
+
) -> Tuple[str, CornerFileList]:
|
|
401
|
+
(
|
|
402
|
+
timing_corner,
|
|
403
|
+
libs,
|
|
404
|
+
netlists,
|
|
405
|
+
spefs,
|
|
406
|
+
) = self.toolbox.get_timing_files_categorized(
|
|
407
|
+
self.config,
|
|
408
|
+
prioritize_nl=prioritize_nl,
|
|
409
|
+
timing_corner=timing_corner,
|
|
410
|
+
)
|
|
411
|
+
state_in = self.state_in.result()
|
|
412
|
+
|
|
413
|
+
name = timing_corner
|
|
414
|
+
current_corner_spef = None
|
|
415
|
+
input_spef_dict = state_in[DesignFormat.SPEF]
|
|
416
|
+
if input_spef_dict is not None:
|
|
417
|
+
if not isinstance(input_spef_dict, dict):
|
|
418
|
+
raise StepException(
|
|
419
|
+
"Malformed input state: value for 'spef' is not a dictionary"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
current_corner_spefs = self.toolbox.filter_views(
|
|
423
|
+
self.config, input_spef_dict, timing_corner
|
|
424
|
+
)
|
|
425
|
+
if len(current_corner_spefs) < 1:
|
|
426
|
+
raise StepException(
|
|
427
|
+
f"No SPEF file compatible with corner '{timing_corner}' found."
|
|
428
|
+
)
|
|
429
|
+
elif len(current_corner_spefs) > 1:
|
|
430
|
+
self.warn(
|
|
431
|
+
f"Multiple SPEF files compatible with corner '{timing_corner}' found. The first one encountered will be used."
|
|
432
|
+
)
|
|
433
|
+
current_corner_spef = str(current_corner_spefs[0])
|
|
434
|
+
|
|
435
|
+
extra_spefs_backcompat_raw = None
|
|
436
|
+
if extra_spef_list := self.config.get("EXTRA_SPEFS"):
|
|
437
|
+
extra_spefs_backcompat_raw = []
|
|
438
|
+
self.warn(
|
|
439
|
+
"The configuration variable 'EXTRA_SPEFS' is deprecated. It is recommended to use the new 'MACROS' configuration variable."
|
|
440
|
+
)
|
|
441
|
+
if len(extra_spef_list) % 4 != 0:
|
|
442
|
+
raise StepException(
|
|
443
|
+
"Invalid value for 'EXTRA_SPEFS': Element count not divisible by four. It is recommended that you migrate your configuration to use the new 'MACROS' configuration variable."
|
|
444
|
+
)
|
|
445
|
+
for i in range(len(extra_spef_list) // 4):
|
|
446
|
+
start = i * 4
|
|
447
|
+
module, min, nom, max = (
|
|
448
|
+
extra_spef_list[start],
|
|
449
|
+
extra_spef_list[start + 1],
|
|
450
|
+
extra_spef_list[start + 2],
|
|
451
|
+
extra_spef_list[start + 3],
|
|
452
|
+
)
|
|
453
|
+
mapping = {
|
|
454
|
+
"min_*": [min],
|
|
455
|
+
"nom_*": [nom],
|
|
456
|
+
"max_*": [max],
|
|
457
|
+
}
|
|
458
|
+
spef = str(
|
|
459
|
+
self.toolbox.filter_views(
|
|
460
|
+
self.config, mapping, timing_corner=timing_corner
|
|
461
|
+
)[0]
|
|
462
|
+
)
|
|
463
|
+
extra_spefs_backcompat_raw.append((module, spef))
|
|
464
|
+
|
|
465
|
+
extra_spefs_backcompat = None
|
|
466
|
+
if extra_spefs_backcompat_raw is not None:
|
|
467
|
+
extra_spefs_backcompat = tuple(extra_spefs_backcompat_raw)
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
name,
|
|
471
|
+
OpenSTAStep.CornerFileList(
|
|
472
|
+
libs=tuple([str(lib) for lib in libs]),
|
|
473
|
+
netlists=tuple([str(netlist) for netlist in netlists]),
|
|
474
|
+
spefs=tuple([(pair[0], str(pair[1])) for pair in spefs]),
|
|
475
|
+
extra_spefs_backcompat=extra_spefs_backcompat,
|
|
476
|
+
current_corner_spef=current_corner_spef,
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@Step.factory.register()
|
|
482
|
+
class CheckMacroInstances(OpenSTAStep):
|
|
483
|
+
"""
|
|
484
|
+
Checks if all macro instances declared in the configuration are, in fact,
|
|
485
|
+
in the design, emitting an error otherwise.
|
|
486
|
+
|
|
487
|
+
Nested macros (macros within macros) are supported provided netlist views
|
|
488
|
+
are available for the macro.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
id = "OpenROAD.CheckMacroInstances"
|
|
492
|
+
name = "Check Macro Instances"
|
|
493
|
+
outputs = []
|
|
494
|
+
|
|
495
|
+
config_vars = OpenROADStep.config_vars
|
|
496
|
+
|
|
497
|
+
def get_script_path(self):
|
|
498
|
+
return os.path.join(
|
|
499
|
+
get_script_dir(), "openroad", "sta", "check_macro_instances.tcl"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
503
|
+
kwargs, env = self.extract_env(kwargs)
|
|
504
|
+
macros: Optional[Dict[str, Macro]] = self.config["MACROS"]
|
|
505
|
+
if macros is None:
|
|
506
|
+
info("No macros found, skipping instance check…")
|
|
507
|
+
return {}, {}
|
|
508
|
+
|
|
509
|
+
macro_instance_pairs = []
|
|
510
|
+
for macro_name, data in macros.items():
|
|
511
|
+
for instance_name in data.instances:
|
|
512
|
+
macro_instance_pairs.append(instance_name)
|
|
513
|
+
macro_instance_pairs.append(macro_name)
|
|
514
|
+
|
|
515
|
+
env["_check_macro_instances"] = TclUtils.join(macro_instance_pairs)
|
|
516
|
+
|
|
517
|
+
corner_name, file_list = self._get_corner_files(prioritize_nl=True)
|
|
518
|
+
file_list.set_env(env)
|
|
519
|
+
env["_CURRENT_CORNER_NAME"] = corner_name
|
|
520
|
+
|
|
521
|
+
return super().run(state_in, env=env, **kwargs)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class MultiCornerSTA(OpenSTAStep):
|
|
525
|
+
outputs = [DesignFormat.SDF, DesignFormat.SDC]
|
|
526
|
+
|
|
527
|
+
config_vars = OpenSTAStep.config_vars + [
|
|
528
|
+
Variable(
|
|
529
|
+
"STA_MACRO_PRIORITIZE_NL",
|
|
530
|
+
bool,
|
|
531
|
+
"Prioritize the use of Netlists + SPEF files over LIB files if available for Macros. Useful if extraction was done using OpenROAD, where SPEF files are far more accurate.",
|
|
532
|
+
default=True,
|
|
533
|
+
),
|
|
534
|
+
Variable(
|
|
535
|
+
"STA_MAX_VIOLATOR_COUNT",
|
|
536
|
+
Optional[int],
|
|
537
|
+
"Maximum number of violators to list in violator_list.rpt",
|
|
538
|
+
),
|
|
539
|
+
Variable(
|
|
540
|
+
"EXTRA_SPEFS",
|
|
541
|
+
Optional[List[Union[str, Path]]],
|
|
542
|
+
"A variable that only exists for backwards compatibility with LibreLane <2.0.0 and should not be used by new designs.",
|
|
543
|
+
),
|
|
544
|
+
Variable(
|
|
545
|
+
"STA_THREADS",
|
|
546
|
+
Optional[int],
|
|
547
|
+
"The maximum number of STA corners to run in parallel. If unset, this will be equal to your machine's thread count.",
|
|
548
|
+
),
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
def get_script_path(self):
|
|
552
|
+
return os.path.join(get_script_dir(), "openroad", "sta", "corner.tcl")
|
|
553
|
+
|
|
554
|
+
def run_corner(
|
|
555
|
+
self,
|
|
556
|
+
state_in: State,
|
|
557
|
+
current_env: Dict[str, Any],
|
|
558
|
+
corner: str,
|
|
559
|
+
corner_dir: str,
|
|
560
|
+
) -> Dict[str, Any]:
|
|
561
|
+
info(f"Starting STA for the {corner} timing corner…")
|
|
562
|
+
current_env["_CURRENT_CORNER_NAME"] = corner
|
|
563
|
+
log_path = os.path.join(corner_dir, "sta.log")
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
subprocess_result = self.run_subprocess(
|
|
567
|
+
self.get_command(),
|
|
568
|
+
log_to=log_path,
|
|
569
|
+
env=current_env,
|
|
570
|
+
silent=True,
|
|
571
|
+
report_dir=corner_dir,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
generated_metrics = subprocess_result["generated_metrics"]
|
|
575
|
+
|
|
576
|
+
info(f"Finished STA for the {corner} timing corner.")
|
|
577
|
+
except subprocess.CalledProcessError as e:
|
|
578
|
+
self.err(f"Failed STA for the {corner} timing corner:")
|
|
579
|
+
raise e
|
|
580
|
+
|
|
581
|
+
return generated_metrics
|
|
582
|
+
|
|
583
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
584
|
+
kwargs, env = self.extract_env(kwargs)
|
|
585
|
+
env = self.prepare_env(env, state_in)
|
|
586
|
+
|
|
587
|
+
tpe = ThreadPoolExecutor(
|
|
588
|
+
max_workers=self.config["STA_THREADS"] or _get_process_limit()
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
futures: Dict[str, Future[MetricsUpdate]] = {}
|
|
592
|
+
files_so_far: Dict[OpenSTAStep.CornerFileList, str] = {}
|
|
593
|
+
corners_used: Set[str] = set()
|
|
594
|
+
for corner in self.config["STA_CORNERS"]:
|
|
595
|
+
_, file_list = self._get_corner_files(
|
|
596
|
+
corner, prioritize_nl=self.config["STA_MACRO_PRIORITIZE_NL"]
|
|
597
|
+
)
|
|
598
|
+
if previous := files_so_far.get(file_list):
|
|
599
|
+
info(
|
|
600
|
+
f"Skipping corner {corner} for STA (identical to {previous} at this stage)…"
|
|
601
|
+
)
|
|
602
|
+
continue
|
|
603
|
+
files_so_far[file_list] = corner
|
|
604
|
+
corners_used.add(corner)
|
|
605
|
+
|
|
606
|
+
current_env = env.copy()
|
|
607
|
+
file_list.set_env(current_env)
|
|
608
|
+
|
|
609
|
+
corner_dir = os.path.join(self.step_dir, corner)
|
|
610
|
+
mkdirp(corner_dir)
|
|
611
|
+
|
|
612
|
+
futures[corner] = tpe.submit(
|
|
613
|
+
self.run_corner,
|
|
614
|
+
state_in,
|
|
615
|
+
current_env,
|
|
616
|
+
corner,
|
|
617
|
+
corner_dir,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
metrics_updates: MetricsUpdate = {}
|
|
621
|
+
for corner, updates_future in futures.items():
|
|
622
|
+
metrics_updates.update(updates_future.result())
|
|
623
|
+
|
|
624
|
+
metric_updates_with_aggregates = aggregate_metrics(metrics_updates)
|
|
625
|
+
|
|
626
|
+
def format_count(count: Optional[Union[int, float, Decimal]]) -> str:
|
|
627
|
+
if count is None:
|
|
628
|
+
return "[gray]?"
|
|
629
|
+
count = int(count)
|
|
630
|
+
if count == 0:
|
|
631
|
+
return f"[green]{count}"
|
|
632
|
+
else:
|
|
633
|
+
return f"[red]{count}"
|
|
634
|
+
|
|
635
|
+
def format_slack(slack: Optional[Union[int, float, Decimal]]) -> str:
|
|
636
|
+
if slack is None:
|
|
637
|
+
return "[gray]?"
|
|
638
|
+
if slack == float(inf):
|
|
639
|
+
return "[gray]N/A"
|
|
640
|
+
slack = round(float(slack), 4)
|
|
641
|
+
formatted_slack = f"{slack:.4f}"
|
|
642
|
+
if slack < 0:
|
|
643
|
+
return f"[red]{formatted_slack}"
|
|
644
|
+
else:
|
|
645
|
+
return f"[green]{formatted_slack}"
|
|
646
|
+
|
|
647
|
+
table = rich.table.Table()
|
|
648
|
+
table.add_column("Corner/Group", width=20)
|
|
649
|
+
table.add_column("Hold Worst Slack")
|
|
650
|
+
table.add_column("Reg to Reg Paths")
|
|
651
|
+
table.add_column("Hold TNS")
|
|
652
|
+
table.add_column("Hold Vio Count")
|
|
653
|
+
table.add_column("of which reg to reg")
|
|
654
|
+
table.add_column("Setup Worst Slack")
|
|
655
|
+
table.add_column("Reg to Reg Paths")
|
|
656
|
+
table.add_column("Setup TNS")
|
|
657
|
+
table.add_column("Setup Vio Count")
|
|
658
|
+
table.add_column("of which reg to reg")
|
|
659
|
+
table.add_column("Max Cap Violations")
|
|
660
|
+
table.add_column("Max Slew Violations")
|
|
661
|
+
for corner in ["Overall"] + self.config["STA_CORNERS"]:
|
|
662
|
+
modifier = ""
|
|
663
|
+
if corner != "Overall":
|
|
664
|
+
if corner not in corners_used:
|
|
665
|
+
continue
|
|
666
|
+
modifier = f"__corner:{corner}"
|
|
667
|
+
row = [corner]
|
|
668
|
+
for metric in [
|
|
669
|
+
"timing__hold__ws",
|
|
670
|
+
"timing__hold_r2r__ws",
|
|
671
|
+
"timing__hold__tns",
|
|
672
|
+
"timing__hold_vio__count",
|
|
673
|
+
"timing__hold_r2r_vio__count",
|
|
674
|
+
"timing__setup__ws",
|
|
675
|
+
"timing__setup_r2r__ws",
|
|
676
|
+
"timing__setup__tns",
|
|
677
|
+
"timing__setup_vio__count",
|
|
678
|
+
"timing__setup_r2r_vio__count",
|
|
679
|
+
"design__max_cap_violation__count",
|
|
680
|
+
"design__max_slew_violation__count",
|
|
681
|
+
]:
|
|
682
|
+
formatter = format_count if metric.endswith("count") else format_slack
|
|
683
|
+
row.append(
|
|
684
|
+
formatter(metric_updates_with_aggregates.get(f"{metric}{modifier}"))
|
|
685
|
+
)
|
|
686
|
+
table.add_row(*row)
|
|
687
|
+
|
|
688
|
+
if not options.get_condensed_mode():
|
|
689
|
+
console.print(table)
|
|
690
|
+
file_console = rich.console.Console(
|
|
691
|
+
file=open(os.path.join(self.step_dir, "summary.rpt"), "w", encoding="utf8"),
|
|
692
|
+
width=160,
|
|
693
|
+
)
|
|
694
|
+
file_console.print(table)
|
|
695
|
+
|
|
696
|
+
return {}, metric_updates_with_aggregates
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@Step.factory.register()
|
|
700
|
+
class STAPrePNR(MultiCornerSTA):
|
|
701
|
+
"""
|
|
702
|
+
Performs hierarchical `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
|
|
703
|
+
using OpenSTA on the pre-PnR Verilog netlist, with all available timing information
|
|
704
|
+
for standard cells and macros for multiple corners.
|
|
705
|
+
|
|
706
|
+
If timing information is not available for a Macro, the macro in question
|
|
707
|
+
will be black-boxed.
|
|
708
|
+
|
|
709
|
+
During this step, the special variable `OPENLANE_SDC_IDEAL_CLOCKS` is
|
|
710
|
+
exposed to SDC files with a value of `1`. We encourage PNR SDC files to use
|
|
711
|
+
ideal clocks at this stage based on this variable's existence and value.
|
|
712
|
+
"""
|
|
713
|
+
|
|
714
|
+
id = "OpenROAD.STAPrePNR"
|
|
715
|
+
name = "STA (Pre-PnR)"
|
|
716
|
+
long_name = "Static Timing Analysis (Pre-PnR)"
|
|
717
|
+
|
|
718
|
+
def prepare_env(self, env: Dict, state: State) -> Dict:
|
|
719
|
+
env = super().prepare_env(env, state)
|
|
720
|
+
env["OPENLANE_SDC_IDEAL_CLOCKS"] = "1"
|
|
721
|
+
return env
|
|
722
|
+
|
|
723
|
+
def run_corner(
|
|
724
|
+
self, state_in: State, current_env: Dict[str, Any], corner: str, corner_dir: str
|
|
725
|
+
) -> Dict[str, Any]:
|
|
726
|
+
current_env["_SDF_SAVE_DIR"] = corner_dir
|
|
727
|
+
return super().run_corner(state_in, current_env, corner, corner_dir)
|
|
728
|
+
|
|
729
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
730
|
+
views_updates, metrics_updates = super().run(state_in, **kwargs)
|
|
731
|
+
|
|
732
|
+
sdf_dict = state_in[DesignFormat.SDF] or {}
|
|
733
|
+
if not isinstance(sdf_dict, dict):
|
|
734
|
+
raise StepException(
|
|
735
|
+
"Malformed input state: incoming value for SDF is not a dictionary."
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
sdf_dict = sdf_dict.copy()
|
|
739
|
+
|
|
740
|
+
for corner in self.config["STA_CORNERS"]:
|
|
741
|
+
sdf = os.path.join(
|
|
742
|
+
self.step_dir, corner, f"{self.config['DESIGN_NAME']}__{corner}.sdf"
|
|
743
|
+
)
|
|
744
|
+
if os.path.isfile(sdf):
|
|
745
|
+
sdf_dict[corner] = Path(sdf)
|
|
746
|
+
|
|
747
|
+
views_updates[DesignFormat.SDF] = sdf_dict
|
|
748
|
+
|
|
749
|
+
return views_updates, metrics_updates
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@Step.factory.register()
|
|
753
|
+
class STAPostPNR(STAPrePNR):
|
|
754
|
+
"""
|
|
755
|
+
Performs multi-corner `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
|
|
756
|
+
using OpenSTA on the post-PnR Verilog netlist, with extracted parasitics for
|
|
757
|
+
both the top-level module and any associated macros.
|
|
758
|
+
|
|
759
|
+
During this step, the special variable `OPENLANE_SDC_IDEAL_CLOCKS` is
|
|
760
|
+
exposed to SDC files with a value of `0`. We encourage PNR SDC files to use
|
|
761
|
+
propagated clocks at this stage based on this variable's existence and value.
|
|
762
|
+
"""
|
|
763
|
+
|
|
764
|
+
id = "OpenROAD.STAPostPNR"
|
|
765
|
+
name = "STA (Post-PnR)"
|
|
766
|
+
long_name = "Static Timing Analysis (Post-PnR)"
|
|
767
|
+
|
|
768
|
+
config_vars = STAPrePNR.config_vars + [
|
|
769
|
+
Variable(
|
|
770
|
+
"SIGNOFF_SDC_FILE",
|
|
771
|
+
Optional[Path],
|
|
772
|
+
"Specifies the SDC file for STA during signoff",
|
|
773
|
+
),
|
|
774
|
+
]
|
|
775
|
+
|
|
776
|
+
inputs = STAPrePNR.inputs + [DesignFormat.SPEF, DesignFormat.ODB]
|
|
777
|
+
outputs = STAPrePNR.outputs + [DesignFormat.LIB]
|
|
778
|
+
|
|
779
|
+
def prepare_env(self, env: dict, state: State) -> dict:
|
|
780
|
+
env = super().prepare_env(env, state)
|
|
781
|
+
if signoff_sdc_file := self.config["SIGNOFF_SDC_FILE"]:
|
|
782
|
+
env["_SDC_IN"] = signoff_sdc_file
|
|
783
|
+
env["OPENLANE_SDC_IDEAL_CLOCKS"] = "0"
|
|
784
|
+
return env
|
|
785
|
+
|
|
786
|
+
def filter_unannotated_report(
|
|
787
|
+
self,
|
|
788
|
+
corner: str,
|
|
789
|
+
corner_dir: str,
|
|
790
|
+
env: Dict,
|
|
791
|
+
checks_report: str,
|
|
792
|
+
odb_design: str,
|
|
793
|
+
):
|
|
794
|
+
tech_lefs = self.toolbox.filter_views(self.config, self.config["TECH_LEFS"])
|
|
795
|
+
if len(tech_lefs) != 1:
|
|
796
|
+
raise StepException(
|
|
797
|
+
"Misconfigured SCL: 'TECH_LEFS' must return exactly one Tech LEF for its default timing corner."
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
lefs = ["--input-lef", tech_lefs[0]]
|
|
801
|
+
for lef in self.config["CELL_LEFS"]:
|
|
802
|
+
lefs.append("--input-lef")
|
|
803
|
+
lefs.append(lef)
|
|
804
|
+
if extra_lefs := self.config["EXTRA_LEFS"]:
|
|
805
|
+
for lef in extra_lefs:
|
|
806
|
+
lefs.append("--input-lef")
|
|
807
|
+
lefs.append(lef)
|
|
808
|
+
metrics_path = os.path.join(corner_dir, "filter_unannotated_metrics.json")
|
|
809
|
+
filter_unannotated_cmd = [
|
|
810
|
+
"openroad",
|
|
811
|
+
"-exit",
|
|
812
|
+
"-no_splash",
|
|
813
|
+
"-metrics",
|
|
814
|
+
metrics_path,
|
|
815
|
+
"-python",
|
|
816
|
+
os.path.join(get_script_dir(), "odbpy", "filter_unannotated.py"),
|
|
817
|
+
"--corner",
|
|
818
|
+
corner,
|
|
819
|
+
"--checks-report",
|
|
820
|
+
checks_report,
|
|
821
|
+
odb_design,
|
|
822
|
+
] + lefs
|
|
823
|
+
|
|
824
|
+
subprocess_result = self.run_subprocess(
|
|
825
|
+
filter_unannotated_cmd,
|
|
826
|
+
log_to=os.path.join(corner_dir, "filter_unannotated.log"),
|
|
827
|
+
env=env,
|
|
828
|
+
silent=True,
|
|
829
|
+
report_dir=corner_dir,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
generated_metrics = subprocess_result["generated_metrics"]
|
|
833
|
+
|
|
834
|
+
if os.path.exists(metrics_path):
|
|
835
|
+
or_metrics_out = json.loads(open(metrics_path).read())
|
|
836
|
+
generated_metrics.update(or_metrics_out)
|
|
837
|
+
|
|
838
|
+
return generated_metrics
|
|
839
|
+
|
|
840
|
+
def run_corner(
|
|
841
|
+
self,
|
|
842
|
+
state_in: State,
|
|
843
|
+
current_env: Dict[str, Any],
|
|
844
|
+
corner: str,
|
|
845
|
+
corner_dir: str,
|
|
846
|
+
) -> MetricsUpdate:
|
|
847
|
+
current_env["_LIB_SAVE_DIR"] = corner_dir
|
|
848
|
+
metrics_updates = super().run_corner(state_in, current_env, corner, corner_dir)
|
|
849
|
+
try:
|
|
850
|
+
filter_unannotated_metrics = self.filter_unannotated_report(
|
|
851
|
+
corner=corner,
|
|
852
|
+
checks_report=os.path.join(corner_dir, "checks.rpt"),
|
|
853
|
+
corner_dir=corner_dir,
|
|
854
|
+
env=current_env,
|
|
855
|
+
odb_design=str(state_in[DesignFormat.ODB]),
|
|
856
|
+
)
|
|
857
|
+
except subprocess.CalledProcessError as e:
|
|
858
|
+
self.err(
|
|
859
|
+
f"Failed filtering unannotated nets for the {corner} timing corner."
|
|
860
|
+
)
|
|
861
|
+
raise e
|
|
862
|
+
return {**metrics_updates, **filter_unannotated_metrics}
|
|
863
|
+
|
|
864
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
865
|
+
views_updates, metrics_updates = super().run(state_in, **kwargs)
|
|
866
|
+
lib_dict = state_in[DesignFormat.LIB] or {}
|
|
867
|
+
if not isinstance(lib_dict, dict):
|
|
868
|
+
raise StepException(
|
|
869
|
+
"Malformed input state: value for LIB is not a dictionary."
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
lib_dict.copy()
|
|
873
|
+
|
|
874
|
+
for corner in self.config["STA_CORNERS"]:
|
|
875
|
+
lib = os.path.join(
|
|
876
|
+
self.step_dir, corner, f"{self.config['DESIGN_NAME']}__{corner}.lib"
|
|
877
|
+
)
|
|
878
|
+
lib_dict[corner] = Path(lib)
|
|
879
|
+
|
|
880
|
+
views_updates[DesignFormat.LIB] = lib_dict
|
|
881
|
+
return views_updates, metrics_updates
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@Step.factory.register()
|
|
885
|
+
class Floorplan(OpenROADStep):
|
|
886
|
+
"""
|
|
887
|
+
Creates DEF and ODB files with the initial floorplan based on the Yosys netlist.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
id = "OpenROAD.Floorplan"
|
|
891
|
+
name = "Floorplan Init"
|
|
892
|
+
long_name = "Floorplan Initialization"
|
|
893
|
+
|
|
894
|
+
inputs = [DesignFormat.NETLIST]
|
|
895
|
+
|
|
896
|
+
config_vars = OpenROADStep.config_vars + [
|
|
897
|
+
Variable(
|
|
898
|
+
"FP_SIZING",
|
|
899
|
+
Literal["absolute", "relative"],
|
|
900
|
+
"Sizing mode for floorplanning",
|
|
901
|
+
default="relative",
|
|
902
|
+
),
|
|
903
|
+
Variable(
|
|
904
|
+
"FP_ASPECT_RATIO",
|
|
905
|
+
Decimal,
|
|
906
|
+
"The core's aspect ratio (height / width).",
|
|
907
|
+
default=1,
|
|
908
|
+
),
|
|
909
|
+
Variable(
|
|
910
|
+
"FP_CORE_UTIL",
|
|
911
|
+
Decimal,
|
|
912
|
+
"The core utilization percentage.",
|
|
913
|
+
default=50,
|
|
914
|
+
units="%",
|
|
915
|
+
),
|
|
916
|
+
Variable(
|
|
917
|
+
"FP_OBSTRUCTIONS",
|
|
918
|
+
Optional[List[Tuple[Decimal, Decimal, Decimal, Decimal]]],
|
|
919
|
+
"Obstructions applied at floorplanning stage. Placement sites are never generated at these locations, which guarantees that it will remain empty throughout the entire flow.",
|
|
920
|
+
units="µm",
|
|
921
|
+
),
|
|
922
|
+
Variable(
|
|
923
|
+
"PL_SOFT_OBSTRUCTIONS",
|
|
924
|
+
Optional[List[Tuple[Decimal, Decimal, Decimal, Decimal]]],
|
|
925
|
+
"Soft placement blockages applied at the floorplanning stage. Areas that are soft-blocked will not be used by the initial placer, however, later phases such as buffer insertion or clock tree synthesis are still allowed to place cells in this area.",
|
|
926
|
+
units="µm",
|
|
927
|
+
),
|
|
928
|
+
Variable(
|
|
929
|
+
"CORE_AREA",
|
|
930
|
+
Optional[Tuple[Decimal, Decimal, Decimal, Decimal]],
|
|
931
|
+
"Specifies a core area (i.e. die area minus margins) to be used in floorplanning."
|
|
932
|
+
+ " It must be paired with `DIE_AREA`.",
|
|
933
|
+
units="µm",
|
|
934
|
+
),
|
|
935
|
+
Variable(
|
|
936
|
+
"BOTTOM_MARGIN_MULT",
|
|
937
|
+
Decimal,
|
|
938
|
+
"The core margin, in multiples of site heights, from the bottom boundary."
|
|
939
|
+
+ " If `DIEA_AREA` and `CORE_AREA` are set, this variable has no effect.",
|
|
940
|
+
default=4,
|
|
941
|
+
),
|
|
942
|
+
Variable(
|
|
943
|
+
"TOP_MARGIN_MULT",
|
|
944
|
+
Decimal,
|
|
945
|
+
"The core margin, in multiples of site heights, from the top boundary."
|
|
946
|
+
+ " If `DIE_AREA` and `CORE_AREA` are set, this variable has no effect.",
|
|
947
|
+
default=4,
|
|
948
|
+
),
|
|
949
|
+
Variable(
|
|
950
|
+
"LEFT_MARGIN_MULT",
|
|
951
|
+
Decimal,
|
|
952
|
+
"The core margin, in multiples of site widths, from the left boundary."
|
|
953
|
+
+ " If `DIE_AREA` are `CORE_AREA` are set, this variable has no effect.",
|
|
954
|
+
default=12,
|
|
955
|
+
),
|
|
956
|
+
Variable(
|
|
957
|
+
"RIGHT_MARGIN_MULT",
|
|
958
|
+
Decimal,
|
|
959
|
+
"The core margin, in multiples of site widths, from the right boundary."
|
|
960
|
+
+ " If `DIE_AREA` are `CORE_AREA` are set, this variable has no effect.",
|
|
961
|
+
default=12,
|
|
962
|
+
),
|
|
963
|
+
Variable(
|
|
964
|
+
"EXTRA_SITES",
|
|
965
|
+
Optional[List[str]],
|
|
966
|
+
"Explicitly specify sites other than `PLACE_SITE` to create rows for. If the alternate-site standard cells properly declare the `SITE` property, you do not need to provide this explicitly.",
|
|
967
|
+
pdk=True,
|
|
968
|
+
),
|
|
969
|
+
]
|
|
970
|
+
|
|
971
|
+
class Mode(str, Enum):
|
|
972
|
+
TEMPLATE = "template"
|
|
973
|
+
ABSOULTE = "absolute"
|
|
974
|
+
RELATIVE = "relative"
|
|
975
|
+
|
|
976
|
+
def get_script_path(self):
|
|
977
|
+
return os.path.join(get_script_dir(), "openroad", "floorplan.tcl")
|
|
978
|
+
|
|
979
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
980
|
+
path = self.config["FP_TRACKS_INFO"]
|
|
981
|
+
tracks_info_str = open(path).read()
|
|
982
|
+
tracks_commands = old_to_new_tracks(tracks_info_str)
|
|
983
|
+
new_tracks_info = os.path.join(self.step_dir, "config.tracks")
|
|
984
|
+
with open(new_tracks_info, "w") as f:
|
|
985
|
+
f.write(tracks_commands)
|
|
986
|
+
|
|
987
|
+
kwargs, env = self.extract_env(kwargs)
|
|
988
|
+
env["TRACKS_INFO_FILE_PROCESSED"] = new_tracks_info
|
|
989
|
+
return super().run(state_in, env=env, **kwargs)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _migrate_ppl_mode(migrated):
|
|
993
|
+
as_int = None
|
|
994
|
+
try:
|
|
995
|
+
as_int = int(migrated)
|
|
996
|
+
except ValueError:
|
|
997
|
+
pass
|
|
998
|
+
if as_int is not None:
|
|
999
|
+
if as_int < 0 or as_int > 2:
|
|
1000
|
+
raise ValueError(
|
|
1001
|
+
f"Legacy variable FP_IO_MODE can only either be 0 for matching or 1 for random_equidistant-- '{as_int}' is invalid.\nPlease see the documentation for the usage of the replacement variable, 'FP_PIN_MODE'."
|
|
1002
|
+
)
|
|
1003
|
+
return ["matching", "random_equidistant"][as_int]
|
|
1004
|
+
return migrated
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
@Step.factory.register()
|
|
1008
|
+
class IOPlacement(OpenROADStep):
|
|
1009
|
+
"""
|
|
1010
|
+
Places I/O pins on a floor-planned ODB file using OpenROAD's built-in placer.
|
|
1011
|
+
|
|
1012
|
+
If ``FP_PIN_ORDER_CFG`` is not ``None``, this step is skipped (for
|
|
1013
|
+
compatibility with LibreLane 1.)
|
|
1014
|
+
"""
|
|
1015
|
+
|
|
1016
|
+
id = "OpenROAD.IOPlacement"
|
|
1017
|
+
name = "I/O Placement"
|
|
1018
|
+
|
|
1019
|
+
config_vars = (
|
|
1020
|
+
OpenROADStep.config_vars
|
|
1021
|
+
+ io_layer_variables
|
|
1022
|
+
+ [
|
|
1023
|
+
Variable(
|
|
1024
|
+
"FP_PPL_MODE",
|
|
1025
|
+
Literal["matching", "random_equidistant", "annealing"],
|
|
1026
|
+
"Decides the mode of the random IO placement option.",
|
|
1027
|
+
default="matching",
|
|
1028
|
+
deprecated_names=[("FP_IO_MODE", _migrate_ppl_mode)],
|
|
1029
|
+
),
|
|
1030
|
+
Variable(
|
|
1031
|
+
"FP_IO_MIN_DISTANCE",
|
|
1032
|
+
Optional[Decimal],
|
|
1033
|
+
"The minimum distance between two pins. If unspecified by a PDK, OpenROAD will use the length of two routing tracks.",
|
|
1034
|
+
units="µm",
|
|
1035
|
+
pdk=True,
|
|
1036
|
+
),
|
|
1037
|
+
Variable(
|
|
1038
|
+
"FP_PIN_ORDER_CFG",
|
|
1039
|
+
Optional[Path],
|
|
1040
|
+
"Path to a custom pin configuration file.",
|
|
1041
|
+
),
|
|
1042
|
+
Variable(
|
|
1043
|
+
"FP_DEF_TEMPLATE",
|
|
1044
|
+
Optional[Path],
|
|
1045
|
+
"Points to the DEF file to be used as a template.",
|
|
1046
|
+
),
|
|
1047
|
+
Variable(
|
|
1048
|
+
"FP_IO_VLENGTH",
|
|
1049
|
+
Optional[Decimal],
|
|
1050
|
+
"""
|
|
1051
|
+
The length of the pins with a north or south orientation. If unspecified by a PDK, OpenROAD will use whichever is higher of the following two values:
|
|
1052
|
+
* The pin width
|
|
1053
|
+
* The minimum value satisfying the minimum area constraint given the pin width
|
|
1054
|
+
""",
|
|
1055
|
+
units="µm",
|
|
1056
|
+
pdk=True,
|
|
1057
|
+
),
|
|
1058
|
+
Variable(
|
|
1059
|
+
"FP_IO_HLENGTH",
|
|
1060
|
+
Optional[Decimal],
|
|
1061
|
+
"""
|
|
1062
|
+
The length of the pins with an east or west orientation. If unspecified by a PDK, OpenROAD will use whichever is higher of the following two values:
|
|
1063
|
+
* The pin width
|
|
1064
|
+
* The minimum value satisfying the minimum area constraint given the pin width
|
|
1065
|
+
""",
|
|
1066
|
+
units="µm",
|
|
1067
|
+
pdk=True,
|
|
1068
|
+
),
|
|
1069
|
+
]
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
def get_script_path(self):
|
|
1073
|
+
return os.path.join(get_script_dir(), "openroad", "ioplacer.tcl")
|
|
1074
|
+
|
|
1075
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1076
|
+
if self.config["FP_PIN_ORDER_CFG"] is not None:
|
|
1077
|
+
info(f"FP_PIN_ORDER_CFG is set. Skipping '{self.id}'…")
|
|
1078
|
+
return {}, {}
|
|
1079
|
+
if self.config["FP_DEF_TEMPLATE"] is not None:
|
|
1080
|
+
info(
|
|
1081
|
+
f"I/O pins were loaded from {self.config['FP_DEF_TEMPLATE']}. Skipping {self.id}…"
|
|
1082
|
+
)
|
|
1083
|
+
return {}, {}
|
|
1084
|
+
|
|
1085
|
+
return super().run(state_in, **kwargs)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
@Step.factory.register()
|
|
1089
|
+
class TapEndcapInsertion(OpenROADStep):
|
|
1090
|
+
"""
|
|
1091
|
+
Places well TAP cells across a floorplan, as well as end-cap cells at the
|
|
1092
|
+
edges of the floorplan.
|
|
1093
|
+
"""
|
|
1094
|
+
|
|
1095
|
+
id = "OpenROAD.TapEndcapInsertion"
|
|
1096
|
+
name = "Tap/Decap Insertion"
|
|
1097
|
+
|
|
1098
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1099
|
+
Variable(
|
|
1100
|
+
"FP_MACRO_HORIZONTAL_HALO",
|
|
1101
|
+
Decimal,
|
|
1102
|
+
"Specify the horizontal halo size around macros while cutting rows.",
|
|
1103
|
+
default=10,
|
|
1104
|
+
units="µm",
|
|
1105
|
+
deprecated_names=["FP_TAP_HORIZONTAL_HALO"],
|
|
1106
|
+
),
|
|
1107
|
+
Variable(
|
|
1108
|
+
"FP_MACRO_VERTICAL_HALO",
|
|
1109
|
+
Decimal,
|
|
1110
|
+
"Specify the vertical halo size around macros while cutting rows.",
|
|
1111
|
+
default=10,
|
|
1112
|
+
units="µm",
|
|
1113
|
+
deprecated_names=["FP_TAP_VERTICAL_HALO"],
|
|
1114
|
+
),
|
|
1115
|
+
]
|
|
1116
|
+
|
|
1117
|
+
def get_script_path(self):
|
|
1118
|
+
return os.path.join(get_script_dir(), "openroad", "tapcell.tcl")
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def get_psm_error_count(rpt: io.TextIOWrapper) -> int:
|
|
1122
|
+
sio = io.StringIO()
|
|
1123
|
+
|
|
1124
|
+
# Turn almost-YAML into YAML
|
|
1125
|
+
VIO_TYPE_PFX = "violation type: "
|
|
1126
|
+
for line in rpt:
|
|
1127
|
+
if line.startswith(VIO_TYPE_PFX):
|
|
1128
|
+
vio_type = line[len(VIO_TYPE_PFX) :].strip()
|
|
1129
|
+
sio.write(f"- type: {vio_type}\n")
|
|
1130
|
+
elif "bbox = " in line:
|
|
1131
|
+
sio.write(line.replace("bbox = ", "- bbox ="))
|
|
1132
|
+
else:
|
|
1133
|
+
sio.write(line)
|
|
1134
|
+
|
|
1135
|
+
sio.seek(0)
|
|
1136
|
+
violations = yaml.load(sio, Loader=yaml.SafeLoader) or []
|
|
1137
|
+
return functools.reduce(
|
|
1138
|
+
lambda acc, current: acc + len(current["srcs"]), violations, 0
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
@Step.factory.register()
|
|
1143
|
+
class GeneratePDN(OpenROADStep):
|
|
1144
|
+
"""
|
|
1145
|
+
Creates a power distribution network on a floorplanned ODB file.
|
|
1146
|
+
"""
|
|
1147
|
+
|
|
1148
|
+
id = "OpenROAD.GeneratePDN"
|
|
1149
|
+
name = "Generate PDN"
|
|
1150
|
+
long_name = "Power Distribution Network Generation"
|
|
1151
|
+
|
|
1152
|
+
config_vars = (
|
|
1153
|
+
OpenROADStep.config_vars
|
|
1154
|
+
+ pdn_variables
|
|
1155
|
+
+ [
|
|
1156
|
+
Variable(
|
|
1157
|
+
"FP_PDN_CFG",
|
|
1158
|
+
Optional[Path],
|
|
1159
|
+
"A custom PDN configuration file. If not provided, the default PDN config will be used.",
|
|
1160
|
+
deprecated_names=["PDN_CFG"],
|
|
1161
|
+
)
|
|
1162
|
+
]
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
def get_script_path(self):
|
|
1166
|
+
return os.path.join(get_script_dir(), "openroad", "pdn.tcl")
|
|
1167
|
+
|
|
1168
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1169
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1170
|
+
if self.config["FP_PDN_CFG"] is None:
|
|
1171
|
+
env["FP_PDN_CFG"] = os.path.join(
|
|
1172
|
+
get_script_dir(), "openroad", "common", "pdn_cfg.tcl"
|
|
1173
|
+
)
|
|
1174
|
+
info(f"'FP_PDN_CFG' not explicitly set, setting it to {env['FP_PDN_CFG']}…")
|
|
1175
|
+
views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
|
|
1176
|
+
|
|
1177
|
+
error_reports = glob(os.path.join(self.step_dir, "*-grid-errors.rpt"))
|
|
1178
|
+
for report in error_reports:
|
|
1179
|
+
net = os.path.basename(report).split("-", maxsplit=1)[0]
|
|
1180
|
+
count = get_psm_error_count(open(report, encoding="utf8"))
|
|
1181
|
+
metrics_updates[f"design__power_grid_violation__count__net:{net}"] = count
|
|
1182
|
+
|
|
1183
|
+
metric_updates_with_aggregates = aggregate_metrics(
|
|
1184
|
+
metrics_updates,
|
|
1185
|
+
{"design__power_grid_violation__count": (0, lambda x: sum(x))},
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
return views_updates, metric_updates_with_aggregates
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
class _GlobalPlacement(OpenROADStep):
|
|
1192
|
+
config_vars = (
|
|
1193
|
+
OpenROADStep.config_vars
|
|
1194
|
+
+ routing_layer_variables
|
|
1195
|
+
+ [
|
|
1196
|
+
Variable(
|
|
1197
|
+
"PL_TARGET_DENSITY_PCT",
|
|
1198
|
+
Optional[Decimal],
|
|
1199
|
+
"The desired placement density of cells. If not specified, the value will be equal to (`FP_CORE_UTIL` + 5 * `GPL_CELL_PADDING` + 10).",
|
|
1200
|
+
units="%",
|
|
1201
|
+
deprecated_names=[
|
|
1202
|
+
("PL_TARGET_DENSITY", lambda d: Decimal(d) * Decimal(100.0))
|
|
1203
|
+
],
|
|
1204
|
+
),
|
|
1205
|
+
Variable(
|
|
1206
|
+
"PL_SKIP_INITIAL_PLACEMENT",
|
|
1207
|
+
bool,
|
|
1208
|
+
"Specifies whether the placer should run initial placement or not.",
|
|
1209
|
+
default=False,
|
|
1210
|
+
),
|
|
1211
|
+
Variable(
|
|
1212
|
+
"PL_WIRE_LENGTH_COEF",
|
|
1213
|
+
Decimal,
|
|
1214
|
+
"Global placement initial wirelength coefficient."
|
|
1215
|
+
+ " Decreasing the variable will modify the initial placement of the standard cells to reduce the wirelengths",
|
|
1216
|
+
default=0.25,
|
|
1217
|
+
deprecated_names=["PL_WIRELENGTH_COEF"],
|
|
1218
|
+
),
|
|
1219
|
+
Variable(
|
|
1220
|
+
"PL_MIN_PHI_COEFFICIENT",
|
|
1221
|
+
Optional[Decimal],
|
|
1222
|
+
"Sets a lower bound on the µ_k variable in the GPL algorithm. Useful if global placement diverges. See https://openroad.readthedocs.io/en/latest/main/src/gpl/README.html",
|
|
1223
|
+
),
|
|
1224
|
+
Variable(
|
|
1225
|
+
"PL_MAX_PHI_COEFFICIENT",
|
|
1226
|
+
Optional[Decimal],
|
|
1227
|
+
"Sets a upper bound on the µ_k variable in the GPL algorithm. Useful if global placement diverges.See https://openroad.readthedocs.io/en/latest/main/src/gpl/README.html",
|
|
1228
|
+
),
|
|
1229
|
+
Variable(
|
|
1230
|
+
"FP_CORE_UTIL",
|
|
1231
|
+
Decimal,
|
|
1232
|
+
"The core utilization percentage.",
|
|
1233
|
+
default=50,
|
|
1234
|
+
units="%",
|
|
1235
|
+
),
|
|
1236
|
+
Variable(
|
|
1237
|
+
"GPL_CELL_PADDING",
|
|
1238
|
+
Decimal,
|
|
1239
|
+
"Cell padding value (in sites) for global placement. The number will be integer divided by 2 and placed on both sides.",
|
|
1240
|
+
units="sites",
|
|
1241
|
+
pdk=True,
|
|
1242
|
+
),
|
|
1243
|
+
]
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
def get_script_path(self):
|
|
1247
|
+
return os.path.join(get_script_dir(), "openroad", "gpl.tcl")
|
|
1248
|
+
|
|
1249
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1250
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1251
|
+
if self.config["PL_TARGET_DENSITY_PCT"] is None:
|
|
1252
|
+
util = self.config["FP_CORE_UTIL"]
|
|
1253
|
+
metrics_util = state_in.metrics.get("design__instance__utilization")
|
|
1254
|
+
if metrics_util is not None:
|
|
1255
|
+
util = metrics_util * 100
|
|
1256
|
+
|
|
1257
|
+
expr = util + (5 * self.config["GPL_CELL_PADDING"]) + 10
|
|
1258
|
+
expr = min(expr, 100)
|
|
1259
|
+
env["PL_TARGET_DENSITY_PCT"] = f"{expr}"
|
|
1260
|
+
info(
|
|
1261
|
+
f"'PL_TARGET_DENSITY_PCT' not explicitly set, using dynamically calculated target density: {expr}…"
|
|
1262
|
+
)
|
|
1263
|
+
return super().run(state_in, env=env, **kwargs)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
@Step.factory.register()
|
|
1267
|
+
class GlobalPlacement(_GlobalPlacement):
|
|
1268
|
+
"""
|
|
1269
|
+
Performs a somewhat nebulous initial placement for standard cells in a
|
|
1270
|
+
floorplan. While the placement is not concrete, it is enough to start
|
|
1271
|
+
accounting for issues such as fanout, transition time, et cetera.
|
|
1272
|
+
"""
|
|
1273
|
+
|
|
1274
|
+
id = "OpenROAD.GlobalPlacement"
|
|
1275
|
+
name = "Global Placement"
|
|
1276
|
+
|
|
1277
|
+
config_vars = _GlobalPlacement.config_vars + [
|
|
1278
|
+
Variable(
|
|
1279
|
+
"PL_TIME_DRIVEN",
|
|
1280
|
+
bool,
|
|
1281
|
+
"Specifies whether the placer should use time driven placement.",
|
|
1282
|
+
default=True,
|
|
1283
|
+
),
|
|
1284
|
+
Variable(
|
|
1285
|
+
"PL_ROUTABILITY_DRIVEN",
|
|
1286
|
+
bool,
|
|
1287
|
+
"Specifies whether the placer should use routability driven placement.",
|
|
1288
|
+
default=True,
|
|
1289
|
+
),
|
|
1290
|
+
Variable(
|
|
1291
|
+
"PL_ROUTABILITY_OVERFLOW_THRESHOLD",
|
|
1292
|
+
Optional[Decimal],
|
|
1293
|
+
"Sets overflow threshold for routability mode.",
|
|
1294
|
+
),
|
|
1295
|
+
]
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
@Step.factory.register()
|
|
1299
|
+
class GlobalPlacementSkipIO(_GlobalPlacement):
|
|
1300
|
+
"""
|
|
1301
|
+
Performs global placement without taking I/O into consideration.
|
|
1302
|
+
|
|
1303
|
+
This is useful for flows where the:
|
|
1304
|
+
* Cells are placed
|
|
1305
|
+
* I/Os are placed to match the cells
|
|
1306
|
+
* Cells are then re-placed for an optimal placement
|
|
1307
|
+
"""
|
|
1308
|
+
|
|
1309
|
+
id = "OpenROAD.GlobalPlacementSkipIO"
|
|
1310
|
+
name = "Global Placement Skip IO"
|
|
1311
|
+
|
|
1312
|
+
config_vars = _GlobalPlacement.config_vars + [
|
|
1313
|
+
Variable(
|
|
1314
|
+
"FP_PPL_MODE",
|
|
1315
|
+
Literal["matching", "random_equidistant", "annealing"],
|
|
1316
|
+
"Decides the mode of the random IO placement option.",
|
|
1317
|
+
default="matching",
|
|
1318
|
+
deprecated_names=[("FP_IO_MODE", _migrate_ppl_mode)],
|
|
1319
|
+
),
|
|
1320
|
+
Variable(
|
|
1321
|
+
"FP_DEF_TEMPLATE",
|
|
1322
|
+
Optional[Path],
|
|
1323
|
+
"Points to the DEF file to be used as a template.",
|
|
1324
|
+
),
|
|
1325
|
+
]
|
|
1326
|
+
|
|
1327
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1328
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1329
|
+
if self.config["FP_DEF_TEMPLATE"] is not None:
|
|
1330
|
+
info(
|
|
1331
|
+
f"I/O pins were loaded from {self.config['FP_DEF_TEMPLATE']}. Skipping the first global placement iteration…"
|
|
1332
|
+
)
|
|
1333
|
+
return {}, {}
|
|
1334
|
+
env["__PL_SKIP_IO"] = "1"
|
|
1335
|
+
return super().run(state_in, env=env, **kwargs)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
@Step.factory.register()
|
|
1339
|
+
class BasicMacroPlacement(OpenROADStep):
|
|
1340
|
+
id = "OpenROAD.BasicMacroPlacement"
|
|
1341
|
+
name = "Basic Macro Placement"
|
|
1342
|
+
|
|
1343
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1344
|
+
Variable(
|
|
1345
|
+
"PL_MACRO_HALO",
|
|
1346
|
+
str,
|
|
1347
|
+
"Macro placement halo. Format: `{Horizontal} {Vertical}`.",
|
|
1348
|
+
default="0 0",
|
|
1349
|
+
units="µm",
|
|
1350
|
+
),
|
|
1351
|
+
Variable(
|
|
1352
|
+
"PL_MACRO_CHANNEL",
|
|
1353
|
+
str,
|
|
1354
|
+
"Channel widths between macros. Format: `{Horizontal} {Vertical}`.",
|
|
1355
|
+
default="0 0",
|
|
1356
|
+
units="µm",
|
|
1357
|
+
),
|
|
1358
|
+
]
|
|
1359
|
+
|
|
1360
|
+
def get_script_path(self):
|
|
1361
|
+
raise NotImplementedError()
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
@Step.factory.register()
|
|
1365
|
+
class DetailedPlacement(OpenROADStep):
|
|
1366
|
+
"""
|
|
1367
|
+
Performs "detailed placement" on an ODB file with global placement. This results
|
|
1368
|
+
in a concrete and legal placement of all cells.
|
|
1369
|
+
"""
|
|
1370
|
+
|
|
1371
|
+
id = "OpenROAD.DetailedPlacement"
|
|
1372
|
+
name = "Detailed Placement"
|
|
1373
|
+
|
|
1374
|
+
config_vars = OpenROADStep.config_vars + dpl_variables
|
|
1375
|
+
|
|
1376
|
+
def get_script_path(self):
|
|
1377
|
+
return os.path.join(get_script_dir(), "openroad", "dpl.tcl")
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
@Step.factory.register()
|
|
1381
|
+
class CheckAntennas(OpenROADStep):
|
|
1382
|
+
"""
|
|
1383
|
+
Runs OpenROAD to check if one or more long nets may constitute an
|
|
1384
|
+
`antenna risk <https://en.wikipedia.org/wiki/Antenna_effect>`_.
|
|
1385
|
+
|
|
1386
|
+
The metric ``route__antenna_violation__count`` will be updated with the number of violating nets.
|
|
1387
|
+
"""
|
|
1388
|
+
|
|
1389
|
+
id = "OpenROAD.CheckAntennas"
|
|
1390
|
+
name = "Check Antennas"
|
|
1391
|
+
|
|
1392
|
+
# default inputs
|
|
1393
|
+
outputs = []
|
|
1394
|
+
|
|
1395
|
+
def get_script_path(self):
|
|
1396
|
+
return os.path.join(get_script_dir(), "openroad", "antenna_check.tcl")
|
|
1397
|
+
|
|
1398
|
+
def __summarize_antenna_report(self, report_file: str, output_file: str):
|
|
1399
|
+
"""
|
|
1400
|
+
Extracts the list of violating nets from an ARC report file"
|
|
1401
|
+
"""
|
|
1402
|
+
|
|
1403
|
+
class AntennaViolation:
|
|
1404
|
+
def __init__(self, net, pin, required_ratio, partial_ratio, layer):
|
|
1405
|
+
self.net = net
|
|
1406
|
+
self.pin = pin
|
|
1407
|
+
self.required_ratio = float(required_ratio)
|
|
1408
|
+
self.partial_ratio = float(partial_ratio)
|
|
1409
|
+
self.layer = layer
|
|
1410
|
+
self.partial_to_required = self.partial_ratio / self.required_ratio
|
|
1411
|
+
|
|
1412
|
+
def __lt__(self, other):
|
|
1413
|
+
return self.partial_to_required < other.partial_to_required
|
|
1414
|
+
|
|
1415
|
+
net_pattern = re.compile(r"\s*Net:\s*(\S+)")
|
|
1416
|
+
required_ratio_pattern = re.compile(r"\s*Required ratio:\s+([\d.]+)")
|
|
1417
|
+
partial_ratio_pattern = re.compile(r"\s*Partial area ratio:\s+([\d.]+)")
|
|
1418
|
+
layer_pattern = re.compile(r"\s*Layer:\s+(\S+)")
|
|
1419
|
+
pin_pattern = re.compile(r"\s*Pin:\s+(\S+)")
|
|
1420
|
+
|
|
1421
|
+
required_ratio = None
|
|
1422
|
+
layer = None
|
|
1423
|
+
partial_ratio = None
|
|
1424
|
+
required_ratio = None
|
|
1425
|
+
pin = None
|
|
1426
|
+
net = None
|
|
1427
|
+
violations: List[AntennaViolation] = []
|
|
1428
|
+
|
|
1429
|
+
net_pattern = re.compile(r"\s*Net:\s*(\S+)")
|
|
1430
|
+
required_ratio_pattern = re.compile(r"\s*Required ratio:\s+([\d.]+)")
|
|
1431
|
+
partial_ratio_pattern = re.compile(r"\s*Partial area ratio:\s+([\d.]+)")
|
|
1432
|
+
layer_pattern = re.compile(r"\s*Layer:\s+(\S+)")
|
|
1433
|
+
pin_pattern = re.compile(r"\s*Pin:\s+(\S+)")
|
|
1434
|
+
|
|
1435
|
+
with open(report_file, "r") as f:
|
|
1436
|
+
for line in f:
|
|
1437
|
+
pin_new = pin_pattern.match(line)
|
|
1438
|
+
required_ratio_new = required_ratio_pattern.match(line)
|
|
1439
|
+
partial_ratio_new = partial_ratio_pattern.match(line)
|
|
1440
|
+
layer_new = layer_pattern.match(line)
|
|
1441
|
+
net_new = net_pattern.match(line)
|
|
1442
|
+
required_ratio = (
|
|
1443
|
+
required_ratio_new.group(1)
|
|
1444
|
+
if required_ratio_new is not None
|
|
1445
|
+
else required_ratio
|
|
1446
|
+
)
|
|
1447
|
+
partial_ratio = (
|
|
1448
|
+
partial_ratio_new.group(1)
|
|
1449
|
+
if partial_ratio_new is not None
|
|
1450
|
+
else partial_ratio
|
|
1451
|
+
)
|
|
1452
|
+
layer = layer_new.group(1) if layer_new is not None else layer
|
|
1453
|
+
pin = pin_new.group(1) if pin_new is not None else pin
|
|
1454
|
+
net = net_new.group(1) if net_new is not None else net
|
|
1455
|
+
|
|
1456
|
+
if "VIOLATED" in line:
|
|
1457
|
+
violations.append(
|
|
1458
|
+
AntennaViolation(
|
|
1459
|
+
net=net,
|
|
1460
|
+
pin=pin,
|
|
1461
|
+
partial_ratio=partial_ratio,
|
|
1462
|
+
layer=layer,
|
|
1463
|
+
required_ratio=required_ratio,
|
|
1464
|
+
)
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
violations.sort(reverse=True)
|
|
1468
|
+
|
|
1469
|
+
# Partial/Required: 2.36, Required: 3091.96, Partial: 7298.29,
|
|
1470
|
+
# Net: net384, Pin: _22354_/A, Layer: met5
|
|
1471
|
+
table = rich.table.Table()
|
|
1472
|
+
decimal_places = 2
|
|
1473
|
+
row = []
|
|
1474
|
+
table.add_column("P / R")
|
|
1475
|
+
table.add_column("Partial")
|
|
1476
|
+
table.add_column("Required")
|
|
1477
|
+
table.add_column("Net")
|
|
1478
|
+
table.add_column("Pin")
|
|
1479
|
+
table.add_column("Layer")
|
|
1480
|
+
for violation in violations:
|
|
1481
|
+
row = [
|
|
1482
|
+
f"{violation.partial_to_required:.{decimal_places}f}",
|
|
1483
|
+
f"{violation.partial_ratio:.{decimal_places}f}",
|
|
1484
|
+
f"{violation.required_ratio:.{decimal_places}f}",
|
|
1485
|
+
f"{violation.net}",
|
|
1486
|
+
f"{violation.pin}",
|
|
1487
|
+
f"{violation.layer}",
|
|
1488
|
+
]
|
|
1489
|
+
table.add_row(*row)
|
|
1490
|
+
|
|
1491
|
+
if not options.get_condensed_mode() and len(violations):
|
|
1492
|
+
console.print(table)
|
|
1493
|
+
file_console = rich.console.Console(
|
|
1494
|
+
file=open(output_file, "w", encoding="utf8"), width=160
|
|
1495
|
+
)
|
|
1496
|
+
file_console.print(table)
|
|
1497
|
+
|
|
1498
|
+
def __get_antenna_nets(self, report: io.TextIOWrapper) -> int:
|
|
1499
|
+
pattern = re.compile(r"Net:\s*(\w+)")
|
|
1500
|
+
count = 0
|
|
1501
|
+
|
|
1502
|
+
for line in report:
|
|
1503
|
+
line = line.strip()
|
|
1504
|
+
m = pattern.match(line)
|
|
1505
|
+
if m is None:
|
|
1506
|
+
continue
|
|
1507
|
+
count += 1
|
|
1508
|
+
|
|
1509
|
+
return count
|
|
1510
|
+
|
|
1511
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1512
|
+
report_dir = os.path.join(self.step_dir, "reports")
|
|
1513
|
+
report_path = os.path.join(report_dir, "antenna.rpt")
|
|
1514
|
+
report_summary_path = os.path.join(report_dir, "antenna_summary.rpt")
|
|
1515
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1516
|
+
env["_ANTENNA_REPORT"] = report_path
|
|
1517
|
+
|
|
1518
|
+
mkdirp(os.path.join(self.step_dir, "reports"))
|
|
1519
|
+
|
|
1520
|
+
views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
|
|
1521
|
+
metrics_updates["route__antenna_violation__count"] = self.__get_antenna_nets(
|
|
1522
|
+
open(report_path)
|
|
1523
|
+
)
|
|
1524
|
+
self.__summarize_antenna_report(report_path, report_summary_path)
|
|
1525
|
+
|
|
1526
|
+
return views_updates, metrics_updates
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
@Step.factory.register()
|
|
1530
|
+
class GlobalRouting(OpenROADStep):
|
|
1531
|
+
"""
|
|
1532
|
+
The initial phase of routing. Given a detailed-placed ODB file, this
|
|
1533
|
+
phase starts assigning coarse-grained routing "regions" for each net so they
|
|
1534
|
+
may be later connected to wires.
|
|
1535
|
+
|
|
1536
|
+
Estimated capacitance and resistance values are much more accurate for
|
|
1537
|
+
global routing.
|
|
1538
|
+
"""
|
|
1539
|
+
|
|
1540
|
+
id = "OpenROAD.GlobalRouting"
|
|
1541
|
+
name = "Global Routing"
|
|
1542
|
+
|
|
1543
|
+
outputs = [DesignFormat.ODB, DesignFormat.DEF]
|
|
1544
|
+
|
|
1545
|
+
config_vars = OpenROADStep.config_vars + grt_variables + dpl_variables
|
|
1546
|
+
|
|
1547
|
+
def get_script_path(self):
|
|
1548
|
+
return os.path.join(get_script_dir(), "openroad", "grt.tcl")
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
class _DiodeInsertion(GlobalRouting):
|
|
1552
|
+
id = "DiodeInsertion"
|
|
1553
|
+
|
|
1554
|
+
def get_script_path(self):
|
|
1555
|
+
return os.path.join(get_script_dir(), "openroad", "antenna_repair.tcl")
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
@Step.factory.register()
|
|
1559
|
+
class RepairAntennas(CompositeStep):
|
|
1560
|
+
"""
|
|
1561
|
+
Applies `antenna effect <https://en.wikipedia.org/wiki/Antenna_effect>`_
|
|
1562
|
+
mitigations using global-routing information, then re-runs detailed placement
|
|
1563
|
+
and global routing to legalize any inserted diodes.
|
|
1564
|
+
|
|
1565
|
+
An antenna check is once again performed, updating the
|
|
1566
|
+
``route__antenna_violation__count`` metric.
|
|
1567
|
+
"""
|
|
1568
|
+
|
|
1569
|
+
id = "OpenROAD.RepairAntennas"
|
|
1570
|
+
name = "Antenna Repair"
|
|
1571
|
+
|
|
1572
|
+
Steps = [_DiodeInsertion, CheckAntennas]
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
@Step.factory.register()
|
|
1576
|
+
class DetailedRouting(OpenROADStep):
|
|
1577
|
+
"""
|
|
1578
|
+
The latter phase of routing. This transforms the abstract nets from global
|
|
1579
|
+
routing into wires on the metal layers that respect all design rules, avoids
|
|
1580
|
+
creating accidental shorts, and ensures all wires are connected.
|
|
1581
|
+
|
|
1582
|
+
This is by far the longest part of a typical flow, taking hours, days or weeks
|
|
1583
|
+
on larger designs.
|
|
1584
|
+
|
|
1585
|
+
After this point, all cells connected to a net can no longer be moved or
|
|
1586
|
+
removed without a custom-written step of some kind that will also rip up
|
|
1587
|
+
wires.
|
|
1588
|
+
"""
|
|
1589
|
+
|
|
1590
|
+
id = "OpenROAD.DetailedRouting"
|
|
1591
|
+
name = "Detailed Routing"
|
|
1592
|
+
|
|
1593
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1594
|
+
Variable(
|
|
1595
|
+
"DRT_THREADS",
|
|
1596
|
+
Optional[int],
|
|
1597
|
+
"Specifies the number of threads to be used in OpenROAD Detailed Routing. If unset, this will be equal to your machine's thread count.",
|
|
1598
|
+
deprecated_names=["ROUTING_CORES"],
|
|
1599
|
+
),
|
|
1600
|
+
Variable(
|
|
1601
|
+
"DRT_MIN_LAYER",
|
|
1602
|
+
Optional[str],
|
|
1603
|
+
"An optional override to the lowest layer used in detailed routing. For example, in sky130, you may want global routing to avoid li1, but let detailed routing use li1 if it has to.",
|
|
1604
|
+
),
|
|
1605
|
+
Variable(
|
|
1606
|
+
"DRT_MAX_LAYER",
|
|
1607
|
+
Optional[str],
|
|
1608
|
+
"An optional override to the highest layer used in detailed routing.",
|
|
1609
|
+
),
|
|
1610
|
+
Variable(
|
|
1611
|
+
"DRT_OPT_ITERS",
|
|
1612
|
+
int,
|
|
1613
|
+
"Specifies the maximum number of optimization iterations during Detailed Routing in TritonRoute.",
|
|
1614
|
+
default=64,
|
|
1615
|
+
),
|
|
1616
|
+
]
|
|
1617
|
+
|
|
1618
|
+
def get_script_path(self):
|
|
1619
|
+
return os.path.join(get_script_dir(), "openroad", "drt.tcl")
|
|
1620
|
+
|
|
1621
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1622
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1623
|
+
env["DRT_THREADS"] = env.get("DRT_THREADS", str(_get_process_limit()))
|
|
1624
|
+
info(f"Running TritonRoute with {env['DRT_THREADS']} threads…")
|
|
1625
|
+
return super().run(state_in, env=env, **kwargs)
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
@Step.factory.register()
|
|
1629
|
+
class LayoutSTA(OpenROADStep):
|
|
1630
|
+
"""
|
|
1631
|
+
Performs `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
|
|
1632
|
+
using OpenROAD on the ODB layout in its current state.
|
|
1633
|
+
"""
|
|
1634
|
+
|
|
1635
|
+
id = "OpenROAD.LayoutSTA"
|
|
1636
|
+
name = "Layout STA"
|
|
1637
|
+
long_name = "Layout Static Timing Analysis"
|
|
1638
|
+
|
|
1639
|
+
inputs = [DesignFormat.ODB]
|
|
1640
|
+
outputs = []
|
|
1641
|
+
|
|
1642
|
+
def get_script_path(self):
|
|
1643
|
+
return os.path.join(get_script_dir(), "openroad", "sta.tcl")
|
|
1644
|
+
|
|
1645
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1646
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1647
|
+
env["RUN_STANDALONE"] = "1"
|
|
1648
|
+
return super().run(state_in, env=env, **kwargs)
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
@Step.factory.register()
|
|
1652
|
+
class FillInsertion(OpenROADStep):
|
|
1653
|
+
"""
|
|
1654
|
+
Fills gaps in the floorplan with filler and decap cells.
|
|
1655
|
+
|
|
1656
|
+
This is run after detailed placement. After this point, the design is basically
|
|
1657
|
+
completely hardened.
|
|
1658
|
+
"""
|
|
1659
|
+
|
|
1660
|
+
id = "OpenROAD.FillInsertion"
|
|
1661
|
+
name = "Fill Insertion"
|
|
1662
|
+
|
|
1663
|
+
def get_script_path(self):
|
|
1664
|
+
return os.path.join(get_script_dir(), "openroad", "fill.tcl")
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
@Step.factory.register()
|
|
1668
|
+
class RCX(OpenROADStep):
|
|
1669
|
+
"""
|
|
1670
|
+
This extracts `parasitic <https://en.wikipedia.org/wiki/Parasitic_element_(electrical_networks)>`_
|
|
1671
|
+
electrical values from a detailed-placed circuit. These can be used to create
|
|
1672
|
+
basically the highest accurate STA possible for a given design.
|
|
1673
|
+
"""
|
|
1674
|
+
|
|
1675
|
+
id = "OpenROAD.RCX"
|
|
1676
|
+
name = "Parasitics (RC) Extraction"
|
|
1677
|
+
long_name = "Parasitic Resistance/Capacitance Extraction"
|
|
1678
|
+
|
|
1679
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1680
|
+
Variable(
|
|
1681
|
+
"RCX_MERGE_VIA_WIRE_RES",
|
|
1682
|
+
bool,
|
|
1683
|
+
"If enabled, the via and wire resistances will be merged.",
|
|
1684
|
+
default=True,
|
|
1685
|
+
),
|
|
1686
|
+
Variable(
|
|
1687
|
+
"RCX_SDC_FILE",
|
|
1688
|
+
Optional[Path],
|
|
1689
|
+
"Specifies SDC file to be used for RCX-based STA, which can be different from the one used for implementation.",
|
|
1690
|
+
),
|
|
1691
|
+
Variable(
|
|
1692
|
+
"RCX_RULESETS",
|
|
1693
|
+
Dict[str, Path],
|
|
1694
|
+
"Map of corner patterns to OpenRCX extraction rules.",
|
|
1695
|
+
pdk=True,
|
|
1696
|
+
),
|
|
1697
|
+
Variable(
|
|
1698
|
+
"STA_THREADS",
|
|
1699
|
+
Optional[int],
|
|
1700
|
+
"The maximum number of STA corners to run in parallel. If unset, this will be equal to your machine's thread count.",
|
|
1701
|
+
),
|
|
1702
|
+
]
|
|
1703
|
+
|
|
1704
|
+
inputs = [DesignFormat.DEF]
|
|
1705
|
+
outputs = [DesignFormat.SPEF]
|
|
1706
|
+
|
|
1707
|
+
def get_script_path(self):
|
|
1708
|
+
return os.path.join(get_script_dir(), "openroad", "rcx.tcl")
|
|
1709
|
+
|
|
1710
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1711
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1712
|
+
env = self.prepare_env(env, state_in)
|
|
1713
|
+
|
|
1714
|
+
def run_corner(corner: str):
|
|
1715
|
+
nonlocal env
|
|
1716
|
+
current_env = env.copy()
|
|
1717
|
+
|
|
1718
|
+
rcx_ruleset = self.config["RCX_RULESETS"].get(corner)
|
|
1719
|
+
if rcx_ruleset is None:
|
|
1720
|
+
self.warn(
|
|
1721
|
+
f"RCX ruleset for corner {corner} not found. The corner may be ill-defined."
|
|
1722
|
+
)
|
|
1723
|
+
return None
|
|
1724
|
+
|
|
1725
|
+
corner_sanitized = corner.strip("*_")
|
|
1726
|
+
corner_dir = os.path.join(self.step_dir, corner_sanitized)
|
|
1727
|
+
mkdirp(corner_dir)
|
|
1728
|
+
|
|
1729
|
+
tech_lefs = self.toolbox.filter_views(
|
|
1730
|
+
self.config, self.config["TECH_LEFS"], corner
|
|
1731
|
+
)
|
|
1732
|
+
if len(tech_lefs) < 1:
|
|
1733
|
+
self.warn(f"No tech lef for timing corner {corner} found.")
|
|
1734
|
+
return None
|
|
1735
|
+
elif len(tech_lefs) > 1:
|
|
1736
|
+
self.warn(
|
|
1737
|
+
f"Multiple tech lefs found for timing corner {corner}. Only the first one matched will be used."
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
current_env["RCX_LEF"] = tech_lefs[0]
|
|
1741
|
+
current_env["RCX_RULESET"] = rcx_ruleset
|
|
1742
|
+
|
|
1743
|
+
out = os.path.join(
|
|
1744
|
+
corner_dir, f"{self.config['DESIGN_NAME']}.{corner_sanitized}.spef"
|
|
1745
|
+
)
|
|
1746
|
+
current_env["SAVE_SPEF"] = out
|
|
1747
|
+
|
|
1748
|
+
corner_qualifier = f"the {corner} corner"
|
|
1749
|
+
if "*" in corner:
|
|
1750
|
+
corner_qualifier = f"corners matching {corner}"
|
|
1751
|
+
|
|
1752
|
+
log_path = os.path.join(corner_dir, "rcx.log")
|
|
1753
|
+
info(f"Running RCX for {corner_qualifier} ({log_path})…")
|
|
1754
|
+
|
|
1755
|
+
try:
|
|
1756
|
+
self.run_subprocess(
|
|
1757
|
+
self.get_command(),
|
|
1758
|
+
log_to=log_path,
|
|
1759
|
+
env=current_env,
|
|
1760
|
+
silent=True,
|
|
1761
|
+
)
|
|
1762
|
+
info(f"Finished RCX for {corner_qualifier}.")
|
|
1763
|
+
except subprocess.CalledProcessError as e:
|
|
1764
|
+
self.err(f"Failed RCX for the {corner_qualifier}:")
|
|
1765
|
+
raise e
|
|
1766
|
+
|
|
1767
|
+
return out
|
|
1768
|
+
|
|
1769
|
+
tpe = ThreadPoolExecutor(
|
|
1770
|
+
max_workers=self.config["STA_THREADS"] or _get_process_limit()
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
futures: Dict[str, Future[str]] = {}
|
|
1774
|
+
for corner in self.config["RCX_RULESETS"]:
|
|
1775
|
+
futures[corner] = tpe.submit(
|
|
1776
|
+
run_corner,
|
|
1777
|
+
corner,
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
views_updates: ViewsUpdate = {}
|
|
1781
|
+
metrics_updates: MetricsUpdate = {}
|
|
1782
|
+
|
|
1783
|
+
spef_dict = state_in[DesignFormat.SPEF] or {}
|
|
1784
|
+
if not isinstance(spef_dict, dict):
|
|
1785
|
+
raise StepException(
|
|
1786
|
+
"Malformed input state: value for SPEF is not a dictionary."
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
for corner, future in futures.items():
|
|
1790
|
+
if result := future.result():
|
|
1791
|
+
spef_dict[corner] = Path(result)
|
|
1792
|
+
|
|
1793
|
+
views_updates[DesignFormat.SPEF] = spef_dict
|
|
1794
|
+
|
|
1795
|
+
return views_updates, metrics_updates
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
@Step.factory.register()
|
|
1799
|
+
class IRDropReport(OpenROADStep):
|
|
1800
|
+
"""
|
|
1801
|
+
Performs static IR-drop analysis on the power distribution network. For power
|
|
1802
|
+
nets, this constitutes a decrease in voltage, and for ground nets, it constitutes
|
|
1803
|
+
an increase in voltage.
|
|
1804
|
+
"""
|
|
1805
|
+
|
|
1806
|
+
id = "OpenROAD.IRDropReport"
|
|
1807
|
+
name = "IR Drop Report"
|
|
1808
|
+
long_name = "Generate IR Drop Report"
|
|
1809
|
+
|
|
1810
|
+
inputs = [DesignFormat.ODB, DesignFormat.SPEF]
|
|
1811
|
+
outputs = []
|
|
1812
|
+
|
|
1813
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1814
|
+
Variable(
|
|
1815
|
+
"VSRC_LOC_FILES",
|
|
1816
|
+
Optional[Dict[str, Path]],
|
|
1817
|
+
"Map of power and ground nets to OpenROAD PSM location files. See [this](https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/src/psm#commands) for more info.",
|
|
1818
|
+
)
|
|
1819
|
+
]
|
|
1820
|
+
|
|
1821
|
+
def get_script_path(self):
|
|
1822
|
+
return os.path.join(get_script_dir(), "openroad", "irdrop.tcl")
|
|
1823
|
+
|
|
1824
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1825
|
+
from decimal import Decimal
|
|
1826
|
+
|
|
1827
|
+
assert state_in[DesignFormat.SPEF] is not None
|
|
1828
|
+
if not isinstance(state_in[DesignFormat.SPEF], dict):
|
|
1829
|
+
raise StepException(
|
|
1830
|
+
"Malformed input state: value for SPEF is not a dictionary."
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1834
|
+
|
|
1835
|
+
input_spef_dict = state_in[DesignFormat.SPEF]
|
|
1836
|
+
assert input_spef_dict is not None # Checked by start
|
|
1837
|
+
if not isinstance(input_spef_dict, dict):
|
|
1838
|
+
raise StepException(
|
|
1839
|
+
"Malformed input state: value for 'spef' is not a dictionary"
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
spefs_in = self.toolbox.filter_views(self.config, input_spef_dict)
|
|
1843
|
+
if len(spefs_in) > 1:
|
|
1844
|
+
raise StepException(
|
|
1845
|
+
"Found more than one input SPEF file for the default corner."
|
|
1846
|
+
)
|
|
1847
|
+
elif len(spefs_in) < 1:
|
|
1848
|
+
raise StepException("No SPEF file found for the default corner.")
|
|
1849
|
+
|
|
1850
|
+
libs_in = self.toolbox.filter_views(self.config, self.config["LIB"])
|
|
1851
|
+
|
|
1852
|
+
if self.config["VSRC_LOC_FILES"] is None:
|
|
1853
|
+
self.warn(
|
|
1854
|
+
"'VSRC_LOC_FILES' was not given a value, which may make the results of IR drop analysis inaccurate. If you are not integrating a top-level chip for manufacture, you may ignore this warning, otherwise, see the documentation for 'VSRC_LOC_FILES'."
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
if voltage := self.toolbox.get_lib_voltage(str(libs_in[0])):
|
|
1858
|
+
env["LIB_VOLTAGE"] = str(voltage)
|
|
1859
|
+
|
|
1860
|
+
env["CURRENT_SPEF_DEFAULT_CORNER"] = str(spefs_in[0])
|
|
1861
|
+
views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
|
|
1862
|
+
|
|
1863
|
+
report = open(os.path.join(self.step_dir, "irdrop.rpt")).read()
|
|
1864
|
+
|
|
1865
|
+
verbose(report)
|
|
1866
|
+
|
|
1867
|
+
voltage_rx = re.compile(r"Worstcase voltage\s*:\s*([\d\.\+\-e]+)\s*V")
|
|
1868
|
+
avg_drop_rx = re.compile(r"Average IR drop\s*:\s*([\d\.\+\-e]+)\s*V")
|
|
1869
|
+
worst_drop_rx = re.compile(r"Worstcase IR drop\s*:\s*([\d\.\+\-e]+)\s*V")
|
|
1870
|
+
|
|
1871
|
+
if m := voltage_rx.search(report):
|
|
1872
|
+
value_float = float(m[1])
|
|
1873
|
+
value_dec = Decimal(value_float)
|
|
1874
|
+
metrics_updates["ir__voltage__worst"] = value_dec
|
|
1875
|
+
else:
|
|
1876
|
+
raise Exception(
|
|
1877
|
+
"OpenROAD IR Drop Log format has changed- please file an issue."
|
|
1878
|
+
)
|
|
1879
|
+
|
|
1880
|
+
if m := avg_drop_rx.search(report):
|
|
1881
|
+
value_float = float(m[1])
|
|
1882
|
+
value_dec = Decimal(value_float)
|
|
1883
|
+
metrics_updates["ir__drop__avg"] = value_dec
|
|
1884
|
+
else:
|
|
1885
|
+
raise Exception(
|
|
1886
|
+
"OpenROAD IR Drop Log format has changed- please file an issue."
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
if m := worst_drop_rx.search(report):
|
|
1890
|
+
value_float = float(m[1])
|
|
1891
|
+
value_dec = Decimal(value_float)
|
|
1892
|
+
metrics_updates["ir__drop__worst"] = value_dec
|
|
1893
|
+
else:
|
|
1894
|
+
raise Exception(
|
|
1895
|
+
"OpenROAD IR Drop Log format has changed- please file an issue."
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
return views_updates, metrics_updates
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
@Step.factory.register()
|
|
1902
|
+
class CutRows(OpenROADStep):
|
|
1903
|
+
"""
|
|
1904
|
+
Cut floorplan rows with respect to placed macros.
|
|
1905
|
+
"""
|
|
1906
|
+
|
|
1907
|
+
id = "OpenROAD.CutRows"
|
|
1908
|
+
name = "Cut Rows"
|
|
1909
|
+
|
|
1910
|
+
inputs = [DesignFormat.ODB]
|
|
1911
|
+
outputs = [
|
|
1912
|
+
DesignFormat.ODB,
|
|
1913
|
+
DesignFormat.DEF,
|
|
1914
|
+
]
|
|
1915
|
+
|
|
1916
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1917
|
+
Variable(
|
|
1918
|
+
"FP_MACRO_HORIZONTAL_HALO",
|
|
1919
|
+
Decimal,
|
|
1920
|
+
"Specify the horizontal halo size around macros while cutting rows.",
|
|
1921
|
+
default=10,
|
|
1922
|
+
units="µm",
|
|
1923
|
+
deprecated_names=["FP_TAP_HORIZONTAL_HALO"],
|
|
1924
|
+
),
|
|
1925
|
+
Variable(
|
|
1926
|
+
"FP_MACRO_VERTICAL_HALO",
|
|
1927
|
+
Decimal,
|
|
1928
|
+
"Specify the vertical halo size around macros while cutting rows.",
|
|
1929
|
+
default=10,
|
|
1930
|
+
units="µm",
|
|
1931
|
+
deprecated_names=["FP_TAP_VERTICAL_HALO"],
|
|
1932
|
+
),
|
|
1933
|
+
]
|
|
1934
|
+
|
|
1935
|
+
def get_script_path(self):
|
|
1936
|
+
return os.path.join(get_script_dir(), "openroad", "cut_rows.tcl")
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
@Step.factory.register()
|
|
1940
|
+
class WriteViews(OpenROADStep):
|
|
1941
|
+
"""
|
|
1942
|
+
Write various layout views of an ODB design
|
|
1943
|
+
"""
|
|
1944
|
+
|
|
1945
|
+
id = "OpenROAD.WriteViews"
|
|
1946
|
+
name = "OpenROAD Write Views"
|
|
1947
|
+
outputs = OpenROADStep.outputs + [
|
|
1948
|
+
DesignFormat.POWERED_NETLIST_SDF_FRIENDLY,
|
|
1949
|
+
DesignFormat.POWERED_NETLIST_NO_PHYSICAL_CELLS,
|
|
1950
|
+
DesignFormat.OPENROAD_LEF,
|
|
1951
|
+
]
|
|
1952
|
+
|
|
1953
|
+
config_vars = OpenROADStep.config_vars + [
|
|
1954
|
+
Variable(
|
|
1955
|
+
"OPENROAD_LEF_BLOAT_OCCUPIED_LAYERS",
|
|
1956
|
+
bool,
|
|
1957
|
+
description="Generates cover obstructions (obstructions over the entire layer) for each layer where shapes are present",
|
|
1958
|
+
default=True,
|
|
1959
|
+
)
|
|
1960
|
+
]
|
|
1961
|
+
|
|
1962
|
+
def get_script_path(self):
|
|
1963
|
+
return os.path.join(get_script_dir(), "openroad", "write_views.tcl")
|
|
1964
|
+
|
|
1965
|
+
|
|
1966
|
+
# Resizer Steps
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
## ABC
|
|
1970
|
+
class ResizerStep(OpenROADStep):
|
|
1971
|
+
config_vars = OpenROADStep.config_vars + grt_variables + rsz_variables
|
|
1972
|
+
|
|
1973
|
+
def run(
|
|
1974
|
+
self,
|
|
1975
|
+
state_in,
|
|
1976
|
+
**kwargs,
|
|
1977
|
+
) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
1978
|
+
kwargs, env = self.extract_env(kwargs)
|
|
1979
|
+
|
|
1980
|
+
corners_key: str = "RSZ_CORNERS"
|
|
1981
|
+
|
|
1982
|
+
if "corners_key" in kwargs:
|
|
1983
|
+
corners_key = kwargs.pop("corners_key")
|
|
1984
|
+
|
|
1985
|
+
corners = self.config[corners_key] or self.config["STA_CORNERS"]
|
|
1986
|
+
lib_set_set = set()
|
|
1987
|
+
count = 0
|
|
1988
|
+
for corner in corners:
|
|
1989
|
+
_, libs, _, _ = self.toolbox.get_timing_files_categorized(
|
|
1990
|
+
self.config, corner
|
|
1991
|
+
)
|
|
1992
|
+
lib_set = frozenset(libs)
|
|
1993
|
+
if lib_set in lib_set_set:
|
|
1994
|
+
debug(f"Liberty files for '{corner}' already accounted for- skipped")
|
|
1995
|
+
continue
|
|
1996
|
+
lib_set_set.add(lib_set)
|
|
1997
|
+
env[f"RSZ_CORNER_{count}"] = TclStep.value_to_tcl([corner] + libs)
|
|
1998
|
+
debug(f"Liberty files for '{corner}' added: {libs}")
|
|
1999
|
+
count += 1
|
|
2000
|
+
|
|
2001
|
+
return super().run(state_in, env=env, **kwargs)
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
@Step.factory.register()
|
|
2005
|
+
class CTS(ResizerStep):
|
|
2006
|
+
"""
|
|
2007
|
+
Creates a `Clock tree <https://en.wikipedia.org/wiki/Clock_signal#Distribution>`_
|
|
2008
|
+
for an ODB file with detailed-placed cells, using reasonably accurate resistance
|
|
2009
|
+
and capacitance estimations. Detailed Placement is then re-performed to
|
|
2010
|
+
accommodate the new cells.
|
|
2011
|
+
"""
|
|
2012
|
+
|
|
2013
|
+
id = "OpenROAD.CTS"
|
|
2014
|
+
name = "Clock Tree Synthesis"
|
|
2015
|
+
|
|
2016
|
+
config_vars = (
|
|
2017
|
+
OpenROADStep.config_vars
|
|
2018
|
+
+ dpl_variables
|
|
2019
|
+
+ [
|
|
2020
|
+
Variable(
|
|
2021
|
+
"CTS_SINK_CLUSTERING_SIZE",
|
|
2022
|
+
int,
|
|
2023
|
+
"Specifies the maximum number of sinks per cluster.",
|
|
2024
|
+
default=25,
|
|
2025
|
+
),
|
|
2026
|
+
Variable(
|
|
2027
|
+
"CTS_SINK_CLUSTERING_MAX_DIAMETER",
|
|
2028
|
+
Decimal,
|
|
2029
|
+
"Specifies maximum diameter of the sink cluster.",
|
|
2030
|
+
default=50,
|
|
2031
|
+
units="µm",
|
|
2032
|
+
),
|
|
2033
|
+
Variable(
|
|
2034
|
+
"CTS_CLK_MAX_WIRE_LENGTH",
|
|
2035
|
+
Decimal,
|
|
2036
|
+
"Specifies the maximum wire length on the clock net.",
|
|
2037
|
+
default=0,
|
|
2038
|
+
units="µm",
|
|
2039
|
+
),
|
|
2040
|
+
Variable(
|
|
2041
|
+
"CTS_DISABLE_POST_PROCESSING",
|
|
2042
|
+
bool,
|
|
2043
|
+
"Specifies whether or not to disable post cts processing for outlier sinks.",
|
|
2044
|
+
default=False,
|
|
2045
|
+
),
|
|
2046
|
+
Variable(
|
|
2047
|
+
"CTS_DISTANCE_BETWEEN_BUFFERS",
|
|
2048
|
+
Decimal,
|
|
2049
|
+
"Specifies the distance between buffers when creating the clock tree.",
|
|
2050
|
+
default=0,
|
|
2051
|
+
units="µm",
|
|
2052
|
+
),
|
|
2053
|
+
Variable(
|
|
2054
|
+
"CTS_CORNERS",
|
|
2055
|
+
Optional[List[str]],
|
|
2056
|
+
"A list of fully-qualified IPVT corners to use during clock tree synthesis. If unspecified, the value for `STA_CORNERS` from the PDK will be used.",
|
|
2057
|
+
),
|
|
2058
|
+
Variable(
|
|
2059
|
+
"CTS_ROOT_BUFFER",
|
|
2060
|
+
str,
|
|
2061
|
+
"Defines the cell inserted at the root of the clock tree. Used in CTS.",
|
|
2062
|
+
pdk=True,
|
|
2063
|
+
),
|
|
2064
|
+
Variable(
|
|
2065
|
+
"CTS_CLK_BUFFERS",
|
|
2066
|
+
List[str],
|
|
2067
|
+
"Defines the list of clock buffers to be used in CTS.",
|
|
2068
|
+
deprecated_names=["CTS_CLK_BUFFER_LIST"],
|
|
2069
|
+
pdk=True,
|
|
2070
|
+
),
|
|
2071
|
+
Variable(
|
|
2072
|
+
"CTS_MAX_CAP",
|
|
2073
|
+
Optional[Decimal],
|
|
2074
|
+
"Overrides the maximum capacitance CTS characterization will test. If omitted, the capacitance is extracted from the lib information of the buffers in CTS_CLK_BUFFERS.",
|
|
2075
|
+
units="pF",
|
|
2076
|
+
),
|
|
2077
|
+
Variable(
|
|
2078
|
+
"CTS_MAX_SLEW",
|
|
2079
|
+
Optional[Decimal],
|
|
2080
|
+
"Overrides the maximum transition time CTS characterization will test. If omitted, the slew is extracted from the lib information of the buffers in CTS_CLK_BUFFERS.",
|
|
2081
|
+
units="ns",
|
|
2082
|
+
),
|
|
2083
|
+
]
|
|
2084
|
+
)
|
|
2085
|
+
|
|
2086
|
+
def get_script_path(self):
|
|
2087
|
+
return os.path.join(get_script_dir(), "openroad", "cts.tcl")
|
|
2088
|
+
|
|
2089
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
2090
|
+
kwargs, env = self.extract_env(kwargs)
|
|
2091
|
+
if self.config.get("CLOCK_NET") is None:
|
|
2092
|
+
if clock_port := self.config["CLOCK_PORT"]:
|
|
2093
|
+
if isinstance(clock_port, list):
|
|
2094
|
+
env["CLOCK_NET"] = TclUtils.join(clock_port)
|
|
2095
|
+
else:
|
|
2096
|
+
env["CLOCK_NET"] = clock_port
|
|
2097
|
+
else:
|
|
2098
|
+
self.warn(
|
|
2099
|
+
"No CLOCK_NET (or CLOCK_PORT) specified. CTS cannot be performed. Returning state unaltered…"
|
|
2100
|
+
)
|
|
2101
|
+
return {}, {}
|
|
2102
|
+
|
|
2103
|
+
views_updates, metrics_updates = super().run(
|
|
2104
|
+
state_in, corners_key="CTS_CORNERS", env=env, **kwargs
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
return views_updates, metrics_updates
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
@Step.factory.register()
|
|
2111
|
+
class RepairDesignPostGPL(ResizerStep):
|
|
2112
|
+
"""
|
|
2113
|
+
Runs a number of design "repairs" on a global-placed ODB file.
|
|
2114
|
+
"""
|
|
2115
|
+
|
|
2116
|
+
id = "OpenROAD.RepairDesignPostGPL"
|
|
2117
|
+
name = "Repair Design (Post-Global Placement)"
|
|
2118
|
+
|
|
2119
|
+
config_vars = ResizerStep.config_vars + [
|
|
2120
|
+
Variable(
|
|
2121
|
+
"DESIGN_REPAIR_BUFFER_INPUT_PORTS",
|
|
2122
|
+
bool,
|
|
2123
|
+
"Specifies whether or not to insert buffers on input ports when design repairs are run.",
|
|
2124
|
+
default=True,
|
|
2125
|
+
deprecated_names=["PL_RESIZER_BUFFER_INPUT_PORTS"],
|
|
2126
|
+
),
|
|
2127
|
+
Variable(
|
|
2128
|
+
"DESIGN_REPAIR_BUFFER_OUTPUT_PORTS",
|
|
2129
|
+
bool,
|
|
2130
|
+
"Specifies whether or not to insert buffers on input ports when design repairs are run.",
|
|
2131
|
+
default=True,
|
|
2132
|
+
deprecated_names=["PL_RESIZER_BUFFER_OUTPUT_PORTS"],
|
|
2133
|
+
),
|
|
2134
|
+
Variable(
|
|
2135
|
+
"DESIGN_REPAIR_TIE_FANOUT",
|
|
2136
|
+
bool,
|
|
2137
|
+
"Specifies whether or not to repair tie cells fanout when design repairs are run.",
|
|
2138
|
+
default=True,
|
|
2139
|
+
deprecated_names=["PL_RESIZER_REPAIR_TIE_FANOUT"],
|
|
2140
|
+
),
|
|
2141
|
+
Variable(
|
|
2142
|
+
"DESIGN_REPAIR_TIE_SEPARATION",
|
|
2143
|
+
bool,
|
|
2144
|
+
"Allows tie separation when performing design repairs.",
|
|
2145
|
+
default=False,
|
|
2146
|
+
deprecated_names=["PL_RESIZER_TIE_SEPERATION"],
|
|
2147
|
+
),
|
|
2148
|
+
Variable(
|
|
2149
|
+
"DESIGN_REPAIR_MAX_WIRE_LENGTH",
|
|
2150
|
+
Decimal,
|
|
2151
|
+
"Specifies the maximum wire length cap used by resizer to insert buffers during design repair. If set to 0, no buffers will be inserted.",
|
|
2152
|
+
default=0,
|
|
2153
|
+
units="µm",
|
|
2154
|
+
deprecated_names=["PL_RESIZER_MAX_WIRE_LENGTH"],
|
|
2155
|
+
),
|
|
2156
|
+
Variable(
|
|
2157
|
+
"DESIGN_REPAIR_MAX_SLEW_PCT",
|
|
2158
|
+
Decimal,
|
|
2159
|
+
"Specifies a margin for the slews during design repair.",
|
|
2160
|
+
default=20,
|
|
2161
|
+
units="%",
|
|
2162
|
+
deprecated_names=["PL_RESIZER_MAX_SLEW_MARGIN"],
|
|
2163
|
+
),
|
|
2164
|
+
Variable(
|
|
2165
|
+
"DESIGN_REPAIR_MAX_CAP_PCT",
|
|
2166
|
+
Decimal,
|
|
2167
|
+
"Specifies a margin for the capacitances during design repair.",
|
|
2168
|
+
default=20,
|
|
2169
|
+
units="%",
|
|
2170
|
+
deprecated_names=["PL_RESIZER_MAX_CAP_MARGIN"],
|
|
2171
|
+
),
|
|
2172
|
+
Variable(
|
|
2173
|
+
"DESIGN_REPAIR_REMOVE_BUFFERS",
|
|
2174
|
+
bool,
|
|
2175
|
+
"Invokes OpenROAD's remove_buffers command to remove buffers from synthesis, which gives OpenROAD more flexibility when buffering nets.",
|
|
2176
|
+
default=False,
|
|
2177
|
+
),
|
|
2178
|
+
]
|
|
2179
|
+
|
|
2180
|
+
def get_script_path(self):
|
|
2181
|
+
return os.path.join(get_script_dir(), "openroad", "repair_design.tcl")
|
|
2182
|
+
|
|
2183
|
+
|
|
2184
|
+
@Step.factory.register()
|
|
2185
|
+
class RepairDesign(RepairDesignPostGPL):
|
|
2186
|
+
"""
|
|
2187
|
+
This is identical to OpenROAD.RepairDesignPostGPL. It is retained for backwards compatibility.
|
|
2188
|
+
"""
|
|
2189
|
+
|
|
2190
|
+
id = "OpenROAD.RepairDesign"
|
|
2191
|
+
name = "Repair Design (Post-Global Placement)"
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
@Step.factory.register()
|
|
2195
|
+
class RepairDesignPostGRT(ResizerStep):
|
|
2196
|
+
"""
|
|
2197
|
+
Runs a number of design "repairs" on a global-routed ODB file.
|
|
2198
|
+
"""
|
|
2199
|
+
|
|
2200
|
+
id = "OpenROAD.RepairDesignPostGRT"
|
|
2201
|
+
name = "Repair Design (Post-Global Routing)"
|
|
2202
|
+
|
|
2203
|
+
config_vars = ResizerStep.config_vars + [
|
|
2204
|
+
Variable(
|
|
2205
|
+
"GRT_DESIGN_REPAIR_RUN_GRT",
|
|
2206
|
+
bool,
|
|
2207
|
+
"Enables running GRT before and after running resizer",
|
|
2208
|
+
default=True,
|
|
2209
|
+
),
|
|
2210
|
+
Variable(
|
|
2211
|
+
"GRT_DESIGN_REPAIR_MAX_WIRE_LENGTH",
|
|
2212
|
+
Decimal,
|
|
2213
|
+
"Specifies the maximum wire length cap used by resizer to insert buffers during post-grt design repair. If set to 0, no buffers will be inserted.",
|
|
2214
|
+
default=0,
|
|
2215
|
+
units="µm",
|
|
2216
|
+
deprecated_names=["GLB_RESIZER_MAX_WIRE_LENGTH"],
|
|
2217
|
+
),
|
|
2218
|
+
Variable(
|
|
2219
|
+
"GRT_DESIGN_REPAIR_MAX_SLEW_PCT",
|
|
2220
|
+
Decimal,
|
|
2221
|
+
"Specifies a margin for the slews during post-grt design repair.",
|
|
2222
|
+
default=10,
|
|
2223
|
+
units="%",
|
|
2224
|
+
deprecated_names=["GLB_RESIZER_MAX_SLEW_MARGIN"],
|
|
2225
|
+
),
|
|
2226
|
+
Variable(
|
|
2227
|
+
"GRT_DESIGN_REPAIR_MAX_CAP_PCT",
|
|
2228
|
+
Decimal,
|
|
2229
|
+
"Specifies a margin for the capacitances during design post-grt repair.",
|
|
2230
|
+
default=10,
|
|
2231
|
+
units="%",
|
|
2232
|
+
deprecated_names=["GLB_RESIZER_MAX_CAP_MARGIN"],
|
|
2233
|
+
),
|
|
2234
|
+
]
|
|
2235
|
+
|
|
2236
|
+
def get_script_path(self):
|
|
2237
|
+
return os.path.join(get_script_dir(), "openroad", "repair_design_postgrt.tcl")
|
|
2238
|
+
|
|
2239
|
+
|
|
2240
|
+
@Step.factory.register()
|
|
2241
|
+
class ResizerTimingPostCTS(ResizerStep):
|
|
2242
|
+
"""
|
|
2243
|
+
First attempt to meet timing requirements for a cell based on basic timing
|
|
2244
|
+
information after clock tree synthesis.
|
|
2245
|
+
|
|
2246
|
+
Standard cells may be resized, and buffer cells may be inserted to ensure
|
|
2247
|
+
that no hold violations exist and no setup violations exist at the current
|
|
2248
|
+
clock.
|
|
2249
|
+
"""
|
|
2250
|
+
|
|
2251
|
+
id = "OpenROAD.ResizerTimingPostCTS"
|
|
2252
|
+
name = "Resizer Timing Optimizations (Post-Clock Tree Synthesis)"
|
|
2253
|
+
|
|
2254
|
+
config_vars = ResizerStep.config_vars + [
|
|
2255
|
+
Variable(
|
|
2256
|
+
"PL_RESIZER_HOLD_SLACK_MARGIN",
|
|
2257
|
+
Decimal,
|
|
2258
|
+
"Specifies a time margin for the slack when fixing hold violations. Normally the resizer will stop when it reaches zero slack. This option allows you to overfix.",
|
|
2259
|
+
default=0.1,
|
|
2260
|
+
units="ns",
|
|
2261
|
+
),
|
|
2262
|
+
Variable(
|
|
2263
|
+
"PL_RESIZER_SETUP_SLACK_MARGIN",
|
|
2264
|
+
Decimal,
|
|
2265
|
+
"Specifies a time margin for the slack when fixing setup violations.",
|
|
2266
|
+
default=0.05,
|
|
2267
|
+
units="ns",
|
|
2268
|
+
),
|
|
2269
|
+
Variable(
|
|
2270
|
+
"PL_RESIZER_HOLD_MAX_BUFFER_PCT",
|
|
2271
|
+
Decimal,
|
|
2272
|
+
"Specifies a max number of buffers to insert to fix hold violations. This number is calculated as a percentage of the number of instances in the design.",
|
|
2273
|
+
default=50,
|
|
2274
|
+
deprecated_names=["PL_RESIZER_HOLD_MAX_BUFFER_PERCENT"],
|
|
2275
|
+
),
|
|
2276
|
+
Variable(
|
|
2277
|
+
"PL_RESIZER_SETUP_MAX_BUFFER_PCT",
|
|
2278
|
+
Decimal,
|
|
2279
|
+
"Specifies a max number of buffers to insert to fix setup violations. This number is calculated as a percentage of the number of instances in the design.",
|
|
2280
|
+
default=50,
|
|
2281
|
+
units="%",
|
|
2282
|
+
deprecated_names=["PL_RESIZER_SETUP_MAX_BUFFER_PERCENT"],
|
|
2283
|
+
),
|
|
2284
|
+
Variable(
|
|
2285
|
+
"PL_RESIZER_ALLOW_SETUP_VIOS",
|
|
2286
|
+
bool,
|
|
2287
|
+
"Allows the creation of setup violations when fixing hold violations. Setup violations are less dangerous as they simply mean a chip may not run at its rated speed, however, chips with hold violations are essentially dead-on-arrival.",
|
|
2288
|
+
default=False,
|
|
2289
|
+
),
|
|
2290
|
+
Variable(
|
|
2291
|
+
"PL_RESIZER_GATE_CLONING",
|
|
2292
|
+
bool,
|
|
2293
|
+
"Enables gate cloning when attempting to fix setup violations",
|
|
2294
|
+
default=True,
|
|
2295
|
+
),
|
|
2296
|
+
Variable(
|
|
2297
|
+
"PL_RESIZER_FIX_HOLD_FIRST",
|
|
2298
|
+
bool,
|
|
2299
|
+
"Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.",
|
|
2300
|
+
default=False,
|
|
2301
|
+
),
|
|
2302
|
+
]
|
|
2303
|
+
|
|
2304
|
+
def get_script_path(self):
|
|
2305
|
+
return os.path.join(get_script_dir(), "openroad", "rsz_timing_postcts.tcl")
|
|
2306
|
+
|
|
2307
|
+
|
|
2308
|
+
@Step.factory.register()
|
|
2309
|
+
class ResizerTimingPostGRT(ResizerStep):
|
|
2310
|
+
"""
|
|
2311
|
+
Second attempt to meet timing requirements for a cell based on timing
|
|
2312
|
+
information after estimating resistance and capacitance values based on
|
|
2313
|
+
global routing.
|
|
2314
|
+
|
|
2315
|
+
Standard cells may be resized, and buffer cells may be inserted to ensure
|
|
2316
|
+
that no hold violations exist and no setup violations exist at the current
|
|
2317
|
+
clock.
|
|
2318
|
+
"""
|
|
2319
|
+
|
|
2320
|
+
id = "OpenROAD.ResizerTimingPostGRT"
|
|
2321
|
+
name = "Resizer Timing Optimizations (Post-Global Routing)"
|
|
2322
|
+
|
|
2323
|
+
config_vars = ResizerStep.config_vars + [
|
|
2324
|
+
Variable(
|
|
2325
|
+
"GRT_RESIZER_HOLD_SLACK_MARGIN",
|
|
2326
|
+
Decimal,
|
|
2327
|
+
"Specifies a time margin for the slack when fixing hold violations. Normally the resizer will stop when it reaches zero slack. This option allows you to overfix.",
|
|
2328
|
+
default=0.05,
|
|
2329
|
+
units="ns",
|
|
2330
|
+
deprecated_names=["GLB_RESIZER_HOLD_SLACK_MARGIN"],
|
|
2331
|
+
),
|
|
2332
|
+
Variable(
|
|
2333
|
+
"GRT_RESIZER_SETUP_SLACK_MARGIN",
|
|
2334
|
+
Decimal,
|
|
2335
|
+
"Specifies a time margin for the slack when fixing setup violations.",
|
|
2336
|
+
default=0.025,
|
|
2337
|
+
units="ns",
|
|
2338
|
+
deprecated_names=["GLB_RESIZER_SETUP_SLACK_MARGIN"],
|
|
2339
|
+
),
|
|
2340
|
+
Variable(
|
|
2341
|
+
"GRT_RESIZER_HOLD_MAX_BUFFER_PCT",
|
|
2342
|
+
Decimal,
|
|
2343
|
+
"Specifies a max number of buffers to insert to fix hold violations. This number is calculated as a percentage of the number of instances in the design.",
|
|
2344
|
+
default=50,
|
|
2345
|
+
units="%",
|
|
2346
|
+
deprecated_names=["GLB_RESIZER_HOLD_MAX_BUFFER_PERCENT"],
|
|
2347
|
+
),
|
|
2348
|
+
Variable(
|
|
2349
|
+
"GRT_RESIZER_SETUP_MAX_BUFFER_PCT",
|
|
2350
|
+
Decimal,
|
|
2351
|
+
"Specifies a max number of buffers to insert to fix setup violations. This number is calculated as a percentage of the number of instances in the design.",
|
|
2352
|
+
default=50,
|
|
2353
|
+
units="%",
|
|
2354
|
+
deprecated_names=["GLB_RESIZER_SETUP_MAX_BUFFER_PERCENT"],
|
|
2355
|
+
),
|
|
2356
|
+
Variable(
|
|
2357
|
+
"GRT_RESIZER_ALLOW_SETUP_VIOS",
|
|
2358
|
+
bool,
|
|
2359
|
+
"Allows setup violations when fixing hold.",
|
|
2360
|
+
default=False,
|
|
2361
|
+
deprecated_names=["GLB_RESIZER_ALLOW_SETUP_VIOS"],
|
|
2362
|
+
),
|
|
2363
|
+
Variable(
|
|
2364
|
+
"GRT_RESIZER_GATE_CLONING",
|
|
2365
|
+
bool,
|
|
2366
|
+
"Enables gate cloning when attempting to fix setup violations",
|
|
2367
|
+
default=True,
|
|
2368
|
+
),
|
|
2369
|
+
Variable(
|
|
2370
|
+
"GRT_RESIZER_RUN_GRT",
|
|
2371
|
+
bool,
|
|
2372
|
+
"Gates running global routing after resizer steps. May be useful to disable for designs where global routing takes non-trivial time.",
|
|
2373
|
+
default=True,
|
|
2374
|
+
),
|
|
2375
|
+
Variable(
|
|
2376
|
+
"GRT_RESIZER_FIX_HOLD_FIRST",
|
|
2377
|
+
bool,
|
|
2378
|
+
"Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.",
|
|
2379
|
+
default=False,
|
|
2380
|
+
),
|
|
2381
|
+
]
|
|
2382
|
+
|
|
2383
|
+
def get_script_path(self):
|
|
2384
|
+
return os.path.join(get_script_dir(), "openroad", "rsz_timing_postgrt.tcl")
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
@Step.factory.register()
|
|
2388
|
+
class DEFtoODB(OpenROADStep):
|
|
2389
|
+
"""
|
|
2390
|
+
Converts a DEF view to an ODB view.
|
|
2391
|
+
|
|
2392
|
+
Useful if you have a custom step that manipulates the layout outside of
|
|
2393
|
+
OpenROAD, but you would like to update the OpenROAD database.
|
|
2394
|
+
"""
|
|
2395
|
+
|
|
2396
|
+
id = "OpenROAD.DEFtoODB"
|
|
2397
|
+
name = "DEF to OpenDB"
|
|
2398
|
+
|
|
2399
|
+
inputs = [DesignFormat.DEF]
|
|
2400
|
+
outputs = [DesignFormat.ODB]
|
|
2401
|
+
|
|
2402
|
+
def get_script_path(self) -> str:
|
|
2403
|
+
return os.path.join(get_script_dir(), "openroad", "write_views.tcl")
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
@Step.factory.register()
|
|
2407
|
+
class OpenGUI(Step):
|
|
2408
|
+
"""
|
|
2409
|
+
Opens the ODB view in the OpenROAD GUI. Useful to inspect some parameters,
|
|
2410
|
+
such as routing density and whatnot.
|
|
2411
|
+
"""
|
|
2412
|
+
|
|
2413
|
+
id = "OpenROAD.OpenGUI"
|
|
2414
|
+
name = "Open In GUI"
|
|
2415
|
+
|
|
2416
|
+
inputs = [DesignFormat.ODB]
|
|
2417
|
+
outputs = []
|
|
2418
|
+
|
|
2419
|
+
def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
|
|
2420
|
+
with tempfile.NamedTemporaryFile("a+", suffix=".tcl") as f:
|
|
2421
|
+
f.write(f"read_db \"{state_in['odb']}\"")
|
|
2422
|
+
f.flush()
|
|
2423
|
+
|
|
2424
|
+
subprocess.check_call(
|
|
2425
|
+
[
|
|
2426
|
+
"openroad",
|
|
2427
|
+
"-no_splash",
|
|
2428
|
+
"-gui",
|
|
2429
|
+
f.name,
|
|
2430
|
+
]
|
|
2431
|
+
)
|
|
2432
|
+
|
|
2433
|
+
return {}, {}
|