librelane 2.4.0__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.

Files changed (170) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +479 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +63 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +246 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +456 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +116 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +155 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +743 -0
  27. librelane/container.py +285 -0
  28. librelane/env_info.py +320 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +327 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +1049 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/help/__main__.py +39 -0
  52. librelane/logging/__init__.py +40 -0
  53. librelane/logging/logger.py +323 -0
  54. librelane/open_pdks_rev +1 -0
  55. librelane/plugins.py +21 -0
  56. librelane/py.typed +0 -0
  57. librelane/scripts/base.sdc +80 -0
  58. librelane/scripts/klayout/Readme.md +2 -0
  59. librelane/scripts/klayout/open_design.py +63 -0
  60. librelane/scripts/klayout/render.py +121 -0
  61. librelane/scripts/klayout/stream_out.py +176 -0
  62. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  63. librelane/scripts/klayout/xor.drc +120 -0
  64. librelane/scripts/magic/Readme.md +1 -0
  65. librelane/scripts/magic/common/read.tcl +114 -0
  66. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  67. librelane/scripts/magic/def/mag.tcl +19 -0
  68. librelane/scripts/magic/def/mag_gds.tcl +79 -0
  69. librelane/scripts/magic/drc.tcl +78 -0
  70. librelane/scripts/magic/extract_spice.tcl +98 -0
  71. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  72. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  73. librelane/scripts/magic/gds/extras_mag.tcl +45 -0
  74. librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
  75. librelane/scripts/magic/get_bbox.tcl +11 -0
  76. librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
  77. librelane/scripts/magic/lef/maglef.tcl +26 -0
  78. librelane/scripts/magic/lef.tcl +57 -0
  79. librelane/scripts/magic/open.tcl +28 -0
  80. librelane/scripts/magic/wrapper.tcl +21 -0
  81. librelane/scripts/netgen/setup.tcl +28 -0
  82. librelane/scripts/odbpy/apply_def_template.py +49 -0
  83. librelane/scripts/odbpy/cell_frequency.py +107 -0
  84. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  85. librelane/scripts/odbpy/contextualize.py +109 -0
  86. librelane/scripts/odbpy/defutil.py +573 -0
  87. librelane/scripts/odbpy/diodes.py +373 -0
  88. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  89. librelane/scripts/odbpy/eco_buffer.py +181 -0
  90. librelane/scripts/odbpy/eco_diode.py +139 -0
  91. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  92. librelane/scripts/odbpy/io_place.py +482 -0
  93. librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
  94. librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
  95. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  96. librelane/scripts/odbpy/lefutil.py +97 -0
  97. librelane/scripts/odbpy/placers.py +162 -0
  98. librelane/scripts/odbpy/power_utils.py +397 -0
  99. librelane/scripts/odbpy/random_place.py +57 -0
  100. librelane/scripts/odbpy/reader.py +250 -0
  101. librelane/scripts/odbpy/remove_buffers.py +173 -0
  102. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  103. librelane/scripts/odbpy/wire_lengths.py +93 -0
  104. librelane/scripts/openroad/antenna_check.tcl +20 -0
  105. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  106. librelane/scripts/openroad/basic_mp.tcl +24 -0
  107. librelane/scripts/openroad/buffer_list.tcl +10 -0
  108. librelane/scripts/openroad/common/dpl.tcl +24 -0
  109. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  110. librelane/scripts/openroad/common/grt.tcl +32 -0
  111. librelane/scripts/openroad/common/io.tcl +540 -0
  112. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  113. librelane/scripts/openroad/common/resizer.tcl +103 -0
  114. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  115. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  116. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  117. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  118. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  119. librelane/scripts/openroad/cts.tcl +80 -0
  120. librelane/scripts/openroad/cut_rows.tcl +24 -0
  121. librelane/scripts/openroad/dpl.tcl +24 -0
  122. librelane/scripts/openroad/drt.tcl +37 -0
  123. librelane/scripts/openroad/fill.tcl +30 -0
  124. librelane/scripts/openroad/floorplan.tcl +145 -0
  125. librelane/scripts/openroad/gpl.tcl +88 -0
  126. librelane/scripts/openroad/grt.tcl +30 -0
  127. librelane/scripts/openroad/gui.tcl +37 -0
  128. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  129. librelane/scripts/openroad/ioplacer.tcl +67 -0
  130. librelane/scripts/openroad/irdrop.tcl +51 -0
  131. librelane/scripts/openroad/pdn.tcl +52 -0
  132. librelane/scripts/openroad/rcx.tcl +32 -0
  133. librelane/scripts/openroad/repair_design.tcl +70 -0
  134. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  135. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  136. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  137. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  138. librelane/scripts/openroad/sta/corner.tcl +393 -0
  139. librelane/scripts/openroad/tapcell.tcl +25 -0
  140. librelane/scripts/openroad/write_views.tcl +27 -0
  141. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  142. librelane/scripts/pyosys/json_header.py +84 -0
  143. librelane/scripts/pyosys/synthesize.py +493 -0
  144. librelane/scripts/pyosys/ys_common.py +153 -0
  145. librelane/scripts/tclsh/hello.tcl +1 -0
  146. librelane/state/__init__.py +24 -0
  147. librelane/state/__main__.py +61 -0
  148. librelane/state/design_format.py +195 -0
  149. librelane/state/state.py +359 -0
  150. librelane/steps/__init__.py +61 -0
  151. librelane/steps/__main__.py +510 -0
  152. librelane/steps/checker.py +637 -0
  153. librelane/steps/common_variables.py +340 -0
  154. librelane/steps/cvc_rv.py +169 -0
  155. librelane/steps/klayout.py +509 -0
  156. librelane/steps/magic.py +576 -0
  157. librelane/steps/misc.py +160 -0
  158. librelane/steps/netgen.py +253 -0
  159. librelane/steps/odb.py +1088 -0
  160. librelane/steps/openroad.py +2460 -0
  161. librelane/steps/openroad_alerts.py +102 -0
  162. librelane/steps/pyosys.py +640 -0
  163. librelane/steps/step.py +1571 -0
  164. librelane/steps/tclstep.py +288 -0
  165. librelane/steps/verilator.py +222 -0
  166. librelane/steps/yosys.py +371 -0
  167. librelane-2.4.0.dist-info/METADATA +169 -0
  168. librelane-2.4.0.dist-info/RECORD +170 -0
  169. librelane-2.4.0.dist-info/WHEEL +4 -0
  170. librelane-2.4.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2022 Efabless Corporation
3
+ # place_diodes Copyright (C) 2020 Sylvain Munaut <tnt@246tNt.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import odb
18
+
19
+ import sys
20
+ import click
21
+ import random
22
+ from decimal import Decimal
23
+ from typing import Optional, List
24
+ from reader import click_odb, OdbReader
25
+
26
+
27
+ @click.group()
28
+ def cli():
29
+ pass
30
+
31
+
32
+ class DiodeInserter:
33
+ def __init__(
34
+ self,
35
+ reader: OdbReader,
36
+ diode_cell: str,
37
+ diode_pin: str,
38
+ threshold_microns: Decimal,
39
+ side_strategy: str = "source",
40
+ port_protect_polarities: Optional[List[str]] = None,
41
+ verbose=False,
42
+ ):
43
+ print(f"Using threshold {threshold_microns}µm…")
44
+
45
+ self.reader = reader
46
+ self.block = reader.block
47
+ self.verbose = verbose
48
+
49
+ self.diode_cell = diode_cell
50
+ self.diode_pin = diode_pin
51
+ self.side_strategy = side_strategy
52
+ self.threshold_microns = threshold_microns
53
+ self.port_protect = port_protect_polarities or []
54
+
55
+ self.diode_master = self.block.getDataBase().findMaster(diode_cell)
56
+ self.diode_site = self.diode_master.getSite().getConstName()
57
+
58
+ self.inserted = {}
59
+ self.insts_by_name = {i.getName(): i for i in self.block.getInsts()}
60
+
61
+ def debug(self, msg):
62
+ if self.verbose:
63
+ print(msg, file=sys.stderr)
64
+
65
+ def error(self, msg):
66
+ print(msg, file=sys.stderr)
67
+
68
+ def net_source(self, net):
69
+ # See if it's an input pad
70
+ for bt in net.getBTerms():
71
+ if bt.getIoType() != "INPUT":
72
+ continue
73
+ good, x, y = bt.getFirstPinLocation()
74
+ if good:
75
+ return (x, y)
76
+
77
+ # Or maybe output of a cell
78
+ # x = odb.new_int(0)
79
+ # y = odb.new_int(0)
80
+
81
+ for it in net.getITerms():
82
+ if not it.isOutputSignal():
83
+ continue
84
+ found, x, y = it.getAvgXY()
85
+ if found:
86
+ return x, y
87
+
88
+ # Nothing found
89
+ return None
90
+
91
+ def should_protect_io_net(self, net, io_types=None):
92
+ for bt in net.getBTerms():
93
+ if (io_types is None) or (bt.getIoType() in io_types):
94
+ return True
95
+ return False
96
+
97
+ def net_has_diode(self, net, *, silly_verbose=False):
98
+ for it in net.getITerms():
99
+ cell_type = it.getInst().getMaster().getConstName()
100
+ cell_pin = it.getMTerm().getConstName()
101
+ if silly_verbose:
102
+ print(
103
+ f"Net {net.getName()} is connected to {cell_type}/{cell_pin} via {it.getInst().getName()}"
104
+ )
105
+ if (cell_type == self.diode_cell) and (cell_pin == self.diode_pin):
106
+ if silly_verbose:
107
+ print("Found diode!")
108
+ return True
109
+ return False
110
+
111
+ def net_manhattan_distance(self, net):
112
+ xs = []
113
+ ys = []
114
+
115
+ for bt in net.getBTerms():
116
+ good, x, y = bt.getFirstPinLocation()
117
+ if good:
118
+ xs.append(x)
119
+ ys.append(y)
120
+
121
+ for it in net.getITerms():
122
+ x, y = self.pin_position(it)
123
+ xs.append(x)
124
+ ys.append(y)
125
+
126
+ if len(xs) == 0:
127
+ return 0
128
+
129
+ return (max(ys) - min(ys)) + (max(xs) - min(xs))
130
+
131
+ def pin_position(self, it):
132
+ # px = odb.new_int(0)
133
+ # py = odb.new_int(0)
134
+
135
+ found, px, py = it.getAvgXY()
136
+ if found:
137
+ # Got it
138
+ return px, py
139
+ else:
140
+ # Failed, use the center coordinate of the instance as fall back
141
+ return it.getInst().getLocation()
142
+
143
+ def place_diode_stdcell(self, it, px, py, src_pos=None):
144
+ # Get information about the instance
145
+ inst_name = it.getInst().getConstName()
146
+ inst_width = it.getInst().getMaster().getWidth()
147
+ inst_pos = it.getInst().getLocation()
148
+ inst_ori = it.getInst().getOrient()
149
+
150
+ # Is the pin left-ish, center-ish or right-ish ?
151
+ pos = None
152
+
153
+ if self.side_strategy == "source":
154
+ # Always be on the side of the source
155
+ if src_pos is not None:
156
+ pos = "l" if (src_pos[0] < inst_pos[0]) else "r"
157
+
158
+ elif self.side_strategy == "pin":
159
+ # Always be on the side of the pin
160
+ pos = "l" if (px < (inst_pos[0] + inst_width // 2)) else "r"
161
+
162
+ elif self.side_strategy == "balanced":
163
+ # If pin is really on the side, use that, else use source side
164
+ th_left = int(inst_pos[0] + inst_width * 0.25)
165
+ th_right = int(inst_pos[0] + inst_width * 0.75)
166
+
167
+ if px < th_left:
168
+ pos = "l"
169
+ elif px > th_right:
170
+ pos = "r"
171
+ elif src_pos is not None:
172
+ # Sort of middle, so put it on the side where signal is coming from
173
+ pos = "l" if (src_pos[0] < inst_pos[0]) else "r"
174
+
175
+ if pos is None:
176
+ # Coin toss ...
177
+ pos = "l" if (random.random() > 0.5) else "r"
178
+
179
+ # X position
180
+ dw = self.diode_master.getWidth()
181
+
182
+ if pos == "l":
183
+ dx = inst_pos[0] - dw * (1 + self.inserted.get((inst_name, "l"), 0))
184
+ else:
185
+ dx = inst_pos[0] + inst_width + dw * self.inserted.get((inst_name, "r"), 0)
186
+
187
+ # Record insertion
188
+ self.inserted[(inst_name, pos)] = self.inserted.get((inst_name, pos), 0) + 1
189
+
190
+ # Done
191
+ return dx, inst_pos[1], inst_ori
192
+
193
+ def place_diode_macro(self, it, px, py, src_pos=None):
194
+ # Scan all rows to see how close we can get to the point
195
+ best = None
196
+
197
+ for row in self.block.getRows():
198
+ rbb = row.getBBox()
199
+
200
+ dx = max(min(rbb.xMax(), px), rbb.xMin())
201
+ dy = rbb.yMin()
202
+ do = row.getOrient()
203
+
204
+ d = abs(px - dx) + abs(py - dy)
205
+
206
+ if (best is None) or (best[0] > d):
207
+ best = (d, dx, dy, do)
208
+
209
+ return best[1:]
210
+
211
+ def insert_diode(self, net, iterm, src_pos):
212
+ # Get information about the instance
213
+ inst = iterm.getInst()
214
+ inst_name = inst.getConstName()
215
+ inst_site = (
216
+ inst.getMaster().getSite().getConstName()
217
+ if (inst.getMaster().getSite() is not None)
218
+ else None
219
+ )
220
+
221
+ # Find where the pin is
222
+ px, py = self.pin_position(iterm)
223
+
224
+ # Apply standard cell or macro placement ?
225
+ if inst_site == self.diode_site:
226
+ dx, dy, do = self.place_diode_stdcell(iterm, px, py, src_pos)
227
+ else:
228
+ dx, dy, do = self.place_diode_macro(iterm, px, py, src_pos)
229
+
230
+ # Insert instance and wire it up
231
+ base_diode_inst_name = f"ANTENNA_{inst_name}_{iterm.getMTerm().getConstName()}"
232
+ diode_inst_name = base_diode_inst_name
233
+ counter = 0
234
+ while self.insts_by_name.get(diode_inst_name) is not None:
235
+ self.debug(
236
+ f"[d] Net {net.getName()}: diode {diode_inst_name} appears to already exist."
237
+ )
238
+ counter += 1
239
+ diode_inst_name = f"{base_diode_inst_name}_{counter}"
240
+
241
+ diode_inst = odb.dbInst_create(self.block, self.diode_master, diode_inst_name)
242
+
243
+ diode_inst.setOrient(do)
244
+ diode_inst.setLocation(dx, dy)
245
+ diode_inst.setPlacementStatus("PLACED")
246
+
247
+ ait = diode_inst.findITerm(self.diode_pin)
248
+ ait.connect(iterm.getNet())
249
+
250
+ def execute(self):
251
+ # Scan all nets
252
+ for net in self.block.getNets():
253
+ # Skip special nets
254
+ if net.isSpecial():
255
+ self.debug(f"[d] Skipping special net {net.getConstName():s}")
256
+ continue
257
+
258
+ # Check if we already have diode on the net
259
+ # if yes, then we assume that the user took care of that some
260
+ # other way
261
+ if self.net_has_diode(net, silly_verbose=False):
262
+ self.debug(f"[d] Skipping already-protected net {net.getConstName():s}")
263
+ continue
264
+
265
+ # Find signal source (first one found ...)
266
+ src_pos = self.net_source(net)
267
+
268
+ # Is this an IO we need to protect
269
+ io_protect = None
270
+ if self.should_protect_io_net(net, io_types=["INPUT", "OUTPUT"]):
271
+ io_protect = self.should_protect_io_net(net, io_types=self.port_protect)
272
+ if io_protect:
273
+ self.debug(
274
+ f"[d] Forcing protection diode on I/O net {net.getConstName():s}"
275
+ )
276
+ else:
277
+ self.debug(f"[d] Skipping I/O net {net.getConstName():s}")
278
+ continue
279
+
280
+ # Determine the span of the signal and skip small internal nets
281
+ span = self.net_manhattan_distance(net) / self.block.getDbUnitsPerMicron()
282
+ if (span < self.threshold_microns) and not io_protect:
283
+ if self.threshold_microns != Decimal("Infinity"):
284
+ self.debug(
285
+ f"[d] Skipping small net {net.getConstName():s} ({span:f})"
286
+ )
287
+ continue
288
+
289
+ self.debug(
290
+ f"[d] Inserting diode(s) for net {net.getConstName():s} ({span:f})"
291
+ )
292
+
293
+ # Scan all internal terminals
294
+ if len(net.getITerms()) == 0:
295
+ self.debug(
296
+ f"[d] Skipping net {net.getConstName():s}: not connected to any instances"
297
+ )
298
+ else:
299
+ for iterm in net.getITerms():
300
+ self.insert_diode(net, iterm, src_pos)
301
+
302
+
303
+ @click.command()
304
+ @click.option(
305
+ "-v", "--verbose", default=False, is_flag=True, help="Verbose debug output"
306
+ )
307
+ @click.option(
308
+ "-c",
309
+ "--diode-cell",
310
+ default="sky130_fd_sc_hd__diode_2",
311
+ help="Name of the cell to use as diode",
312
+ )
313
+ @click.option(
314
+ "-p", "--diode-pin", default="DIODE", help="Name of the pin to use on diode cells"
315
+ )
316
+ @click.option(
317
+ "--side-strategy",
318
+ type=click.Choice(["source", "pin", "balanced", "random"]),
319
+ default="source",
320
+ help="Strategy to select if placing diode left/right of the cell",
321
+ )
322
+ @click.option(
323
+ "--port-protect",
324
+ type=click.Choice(["none", "in", "out", "both"]),
325
+ default="in",
326
+ help="Always place a true diode on nets connected to selected ports",
327
+ )
328
+ @click.option(
329
+ "-t",
330
+ "--threshold",
331
+ "threshold_microns",
332
+ type=Decimal,
333
+ default=None,
334
+ help="Minimum Manhattan distance of a net to be considered an antenna risk requiring a diode. By default, the value used is 200 * the minimum site width.",
335
+ )
336
+ @click_odb
337
+ def place(
338
+ reader,
339
+ verbose,
340
+ diode_cell,
341
+ diode_pin,
342
+ side_strategy,
343
+ port_protect,
344
+ threshold_microns,
345
+ ):
346
+ print(f"Design name: {reader.name}")
347
+
348
+ pp_val = {
349
+ "none": [],
350
+ "in": ["INPUT"],
351
+ "out": ["OUTPUT"],
352
+ "both": ["INPUT", "OUTPUT"],
353
+ }
354
+
355
+ di = DiodeInserter(
356
+ reader,
357
+ diode_cell=diode_cell,
358
+ diode_pin=diode_pin,
359
+ side_strategy=side_strategy,
360
+ threshold_microns=threshold_microns,
361
+ port_protect_polarities=pp_val[port_protect],
362
+ verbose=verbose,
363
+ )
364
+ di.execute()
365
+
366
+ print("Inserted", len(di.inserted), "diodes.")
367
+
368
+
369
+ cli.add_command(place)
370
+
371
+
372
+ if __name__ == "__main__":
373
+ cli()
@@ -0,0 +1,305 @@
1
+ # Copyright 2022 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 sys
15
+ from dataclasses import dataclass
16
+ from typing import Dict, Literal, Optional, Sequence, Union
17
+
18
+ import odb
19
+ import utl
20
+
21
+ from reader import click, click_odb, OdbReader
22
+ from reader import rich
23
+ from reader import Table
24
+ from rich.console import Console
25
+
26
+
27
+ def is_connected(term: Union[odb.dbITerm, odb.dbBTerm]) -> bool:
28
+ if isinstance(term, odb.dbITerm):
29
+ return term.getNet() is not None
30
+ # all bterms have a net, we need to check if it has another
31
+ net = term.getNet()
32
+ iterms = net.getITerms()
33
+ return len(iterms) != 0
34
+
35
+
36
+ @dataclass
37
+ class Port:
38
+ polarity: Literal["INPUT", "OUTPUT", "INOUT"]
39
+ signal_type: Optional[Literal["POWER", "GROUND", "SIGNAL"]]
40
+ connected: bool = False
41
+
42
+
43
+ class Module(object):
44
+ @dataclass
45
+ class PortStats:
46
+ inputs = 0
47
+ inputs_connected = 0
48
+ outputs = 0
49
+ outputs_connected = 0
50
+ power_inouts = 0
51
+ power_inouts_connected = 0
52
+ ground_inouts = 0
53
+ ground_inouts_connected = 0
54
+ other_inouts = 0
55
+ other_inouts_connected = 0
56
+
57
+ @classmethod
58
+ def from_object(
59
+ Self,
60
+ module: "Module",
61
+ ):
62
+ result = Self()
63
+ for name, terminal in module.ports.items():
64
+ if terminal.polarity == "INPUT":
65
+ result.inputs += 1
66
+ if terminal.connected:
67
+ result.inputs_connected += 1
68
+ elif terminal.polarity == "OUTPUT":
69
+ result.outputs += 1
70
+ if terminal.connected:
71
+ result.outputs_connected += 1
72
+ elif terminal.polarity == "INOUT":
73
+ if terminal.signal_type == "POWER":
74
+ result.power_inouts += 1
75
+ if terminal.connected:
76
+ result.power_inouts_connected += 1
77
+ elif terminal.signal_type == "GROUND":
78
+ result.ground_inouts += 1
79
+ if terminal.connected:
80
+ result.ground_inouts_connected += 1
81
+ else:
82
+ result.other_inouts += 1
83
+ if terminal.connected:
84
+ result.other_inouts_connected += 1
85
+ return result
86
+
87
+ @property
88
+ def disconnected_pin_count(self) -> int:
89
+ return (
90
+ (self.inputs - self.inputs_connected)
91
+ + (self.outputs - self.outputs_connected)
92
+ + (self.power_inouts - self.power_inouts_connected)
93
+ + (self.ground_inouts - self.ground_inouts_connected)
94
+ + (self.other_inouts - self.other_inouts_connected)
95
+ )
96
+
97
+ @property
98
+ def top_module_critical_disconnected_pin_count(self) -> int:
99
+ # At least one of each kind needs to be connected, otherwise there
100
+ # are critical disconnects.
101
+ critical_disconnected_pins = 0
102
+ if self.inputs_connected == 0 and self.inputs != 0:
103
+ critical_disconnected_pins += self.inputs
104
+ if self.outputs_connected == 0 and self.outputs != 0:
105
+ critical_disconnected_pins += self.outputs
106
+ if self.power_inouts_connected == 0:
107
+ critical_disconnected_pins += self.power_inouts
108
+ if self.ground_inouts_connected == 0:
109
+ critical_disconnected_pins += self.ground_inouts
110
+
111
+ return critical_disconnected_pins
112
+
113
+ @property
114
+ def instance_critical_disconnected_pin_count(self):
115
+ critical_disconnected_pins = 0
116
+ if self.inputs_connected != self.inputs:
117
+ critical_disconnected_pins += self.inputs - self.inputs_connected
118
+ elif self.outputs_connected == 0:
119
+ critical_disconnected_pins += self.outputs
120
+ elif self.power_inouts != self.power_inouts_connected:
121
+ critical_disconnected_pins += (
122
+ self.power_inouts - self.power_inouts_connected
123
+ )
124
+ elif self.ground_inouts != self.ground_inouts_connected:
125
+ critical_disconnected_pins += (
126
+ self.ground_inouts - self.ground_inouts_connected
127
+ )
128
+ return critical_disconnected_pins
129
+
130
+ def __init__(self, object: Union[odb.dbBlock, odb.dbInst]) -> None:
131
+ self.name = object.getName()
132
+ self.ports: Dict[str, Port] = {}
133
+ terminals = (
134
+ object.getBTerms()
135
+ if isinstance(object, odb.dbBlock)
136
+ else object.getITerms()
137
+ )
138
+ power_found = False
139
+ ground_found = True
140
+ for terminal in terminals:
141
+ signal_type = terminal.getSigType()
142
+ if signal_type == "POWER":
143
+ power_found = True
144
+ elif signal_type == "GROUND":
145
+ ground_found = True
146
+ self.ports[terminal.getName()] = Port(
147
+ terminal.getIoType(),
148
+ signal_type=terminal.getSigType(),
149
+ connected=is_connected(terminal),
150
+ )
151
+ if not power_found:
152
+ print(
153
+ f"[ERROR] Macro/instance {object.getName()} has no power pins- add it to IGNORE_DISCONNECTED_MODULES if this is intentional",
154
+ file=sys.stderr,
155
+ )
156
+ self.ports["<ANY POWER PIN>"] = Port("INOUT", "POWER", False)
157
+ if not ground_found:
158
+ print(
159
+ f"[ERROR] Macro/instance {object.getName()} has no ground pins- add it to IGNORE_DISCONNECTED_MODULES if this is intentional",
160
+ file=sys.stderr,
161
+ )
162
+ self.ports["<ANY GROUND PIN>"] = Port("INOUT", "GROUND", False)
163
+ self._port_stats = Module.PortStats.from_object(self)
164
+ if self._port_stats.outputs != 0 and self._port_stats.outputs_connected == 0:
165
+ print(
166
+ f"[ERROR] No outputs of macro/instance '{object.getName()}' are connected- add it to IGNORE_DISCONNECTED_MODULES if this is intentional",
167
+ file=sys.stderr,
168
+ )
169
+ if (
170
+ self._port_stats.inputs_connected != self._port_stats.inputs
171
+ and not isinstance(object, odb.dbBlock)
172
+ ):
173
+ print(
174
+ f"[ERROR] Some inputs of instance '{object.getName()}' are not connected- add it to IGNORE_DISCONNECTED_MODULES if this is intentional",
175
+ file=sys.stderr,
176
+ )
177
+ self.disconnected_pin_count = self._port_stats.disconnected_pin_count
178
+ self.critical_disconnected_pin_count = (
179
+ self._port_stats.top_module_critical_disconnected_pin_count
180
+ if isinstance(object, odb.dbBlock)
181
+ else self._port_stats.instance_critical_disconnected_pin_count
182
+ )
183
+
184
+ def write_disconnected_pins(self, full_table: Table, critical_table: Table):
185
+ if self.disconnected_pin_count == 0:
186
+ return
187
+ row = (
188
+ self.name,
189
+ "\n".join(
190
+ [
191
+ k
192
+ for k, v in self.ports.items()
193
+ if v.signal_type in ["POWER", "GROUND"] and v.connected
194
+ ]
195
+ ),
196
+ "\n".join(
197
+ [
198
+ k
199
+ for k, v in self.ports.items()
200
+ if v.signal_type in ["POWER", "GROUND"] and not v.connected
201
+ ]
202
+ ),
203
+ "\n".join(
204
+ [
205
+ k
206
+ for k, v in self.ports.items()
207
+ if v.signal_type == "SIGNAL" and v.connected
208
+ ]
209
+ ),
210
+ "\n".join(
211
+ [
212
+ k
213
+ for k, v in self.ports.items()
214
+ if v.signal_type == "SIGNAL" and not v.connected
215
+ ]
216
+ ),
217
+ )
218
+ full_table.add_row(*row)
219
+ if self.critical_disconnected_pin_count == 0:
220
+ return
221
+ critical_table.add_row(*row)
222
+
223
+
224
+ @click.command()
225
+ @click.option(
226
+ "--write-full-table-to",
227
+ type=click.Path(file_okay=True, dir_okay=False, writable=True),
228
+ default=None,
229
+ help="Write a table with all disconnected pins to this file",
230
+ )
231
+ @click.option(
232
+ "--ignore-module",
233
+ "ignore_modules",
234
+ default=(),
235
+ multiple=True,
236
+ type=str,
237
+ help="Modules to ignore",
238
+ )
239
+ @click_odb
240
+ def main(
241
+ reader: OdbReader,
242
+ ignore_modules: Sequence[str],
243
+ write_full_table_to: Optional[str],
244
+ ):
245
+ db = reader.db
246
+ block = db.getChip().getBlock()
247
+ instances = block.getInsts()
248
+ full_table = Table(
249
+ "Macro/Instance",
250
+ "Power Pins",
251
+ "Disconnected",
252
+ "Signal Pins",
253
+ "Disconnected",
254
+ title="",
255
+ show_lines=True,
256
+ )
257
+ critical_table = Table(
258
+ "Macro/Instance",
259
+ "Power Pins",
260
+ "Disconnected",
261
+ "Signal Pins",
262
+ "Disconnected",
263
+ title="",
264
+ )
265
+
266
+ disconnected_pin_count, critical_disconnected_pin_count = (0, 0)
267
+ if block.getName() not in ignore_modules:
268
+ block_module = Module(block)
269
+ disconnected_pin_count += block_module.disconnected_pin_count
270
+ critical_disconnected_pin_count += block_module.critical_disconnected_pin_count
271
+ block_module.write_disconnected_pins(full_table, critical_table)
272
+
273
+ for instance in instances:
274
+ if instance.getMaster().getName() in ignore_modules:
275
+ continue
276
+ if instance.getName().startswith("clkload"): # TritonCTS dummy clock loads
277
+ continue
278
+ instance_module = Module(instance)
279
+ disconnected_pin_count += instance_module.disconnected_pin_count
280
+ critical_disconnected_pin_count += (
281
+ instance_module.critical_disconnected_pin_count
282
+ )
283
+ instance_module.write_disconnected_pins(full_table, critical_table)
284
+
285
+ print(
286
+ f"Found {disconnected_pin_count} disconnected pin(s), of which {critical_disconnected_pin_count} are critical."
287
+ )
288
+
289
+ if critical_table.row_count > 0:
290
+ rich.print(critical_table)
291
+ if full_table.row_count > 0:
292
+ if full_table_path := write_full_table_to:
293
+ console = Console(
294
+ file=open(full_table_path, "w", encoding="utf8"), width=160
295
+ )
296
+ console.print(full_table)
297
+
298
+ utl.metric_integer("design__disconnected_pin__count", disconnected_pin_count)
299
+ utl.metric_integer(
300
+ "design__critical_disconnected_pin__count", critical_disconnected_pin_count
301
+ )
302
+
303
+
304
+ if __name__ == "__main__":
305
+ main()