Skip to content

Commit eadfbf0

Browse files
committed
dft: sync ORFS clean branch with OpenROAD-clean-DFT
1 parent db0b0d0 commit eadfbf0

15 files changed

+1502
-282
lines changed

bullet-point-generator/make_testcases.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def _write_group_constraints(
124124
lines: List[str] = []
125125
lines.append("# Auto-generated scan constraints (groups + before).")
126126
lines.append("# Note: chain begin/end + chain_count are supplied by the runner.")
127+
lines.append(
128+
"# GROUP2 is intentionally chunked (no single GROUP2 supergroup) so K>1 runs can"
129+
"# still satisfy tight max_imbalance constraints by distributing chunks across chains."
130+
)
127131

128132
g1_chunks = list(_chunk(group1, chunk_size))
129133
for idx, chunk in enumerate(g1_chunks):
@@ -135,11 +139,8 @@ def _write_group_constraints(
135139
g2_chunks = list(_chunk(group2, chunk_size))
136140
for idx, chunk in enumerate(g2_chunks):
137141
lines.append("group G2_%d %s" % (idx, " ".join(chunk)))
138-
lines.append(
139-
"group GROUP2 %s" % (" ".join([f"G2_{i}" for i in range(len(g2_chunks))]))
140-
)
141-
142-
lines.append("before GROUP1 GROUP2")
142+
for idx in range(len(g2_chunks)):
143+
lines.append(f"before GROUP1 G2_{idx}")
143144
out_path.parent.mkdir(parents=True, exist_ok=True)
144145
out_path.write_text("\n".join(lines) + "\n")
145146

@@ -187,6 +188,7 @@ def main(argv: Optional[List[str]] = None) -> int:
187188
real1_dir.mkdir(parents=True, exist_ok=True)
188189
base_scan_odb = real1_dir / "jpeg_real1_scan.odb"
189190
scan_list_tsv = real1_dir / "scanffs.tsv"
191+
scan_pins_tsv = real1_dir / "scanff_pins.tsv"
190192
sdc_min = real1_dir / "dft.sdc"
191193
_write_min_clock_sdc(out_sdc=sdc_min, base_sdc=base.base_sdc, extra_clock_ports=[])
192194

@@ -218,14 +220,60 @@ def main(argv: Optional[List[str]] = None) -> int:
218220
# Emit scanff placements list for downstream selection.
219221
f"set fp [open {_tcl_quote(scan_list_tsv)} w]",
220222
"puts $fp \"name\\tx\\ty\\tmaster\"",
223+
f"set fp2 [open {_tcl_quote(scan_pins_tsv)} w]",
224+
"puts $fp2 \"name\\tin_x\\tin_y\\tout_x\\tout_y\\tin_pin\\tout_pin\\tmaster\"",
221225
"set block [ord::get_db_block]",
222226
"foreach inst [$block getInsts] {",
223227
" set master [$inst getMaster]",
224228
" if {[$master findMTerm SCD] == \"NULL\" && [$master findMTerm SI] == \"NULL\"} { continue }",
225229
" lassign [$inst getLocation] x y",
226230
" puts $fp \"[$inst getName]\\t$x\\t$y\\t[$master getName]\"",
231+
" # Pin-based coords for asymmetric (directed) costs: scan_out -> scan_in.",
232+
" set it_in [$inst findITerm SCD]",
233+
" set in_pin \"SCD\"",
234+
" if { $it_in == \"NULL\" } {",
235+
" set it_in [$inst findITerm SI]",
236+
" set in_pin \"SI\"",
237+
" }",
238+
" if { $it_in == \"NULL\" } { continue }",
239+
"",
240+
" # Prefer a connected Q, then a connected Q_N, then fall back to any present.",
241+
" set it_q [$inst findITerm Q]",
242+
" set it_qn [$inst findITerm QN]",
243+
" set it_qn2 [$inst findITerm Q_N]",
244+
" set it_out \"NULL\"",
245+
" set out_pin \"\"",
246+
" if { $it_q != \"NULL\" && [$it_q getNet] != \"NULL\" } {",
247+
" set it_out $it_q",
248+
" set out_pin \"Q\"",
249+
" } elseif { $it_qn != \"NULL\" && [$it_qn getNet] != \"NULL\" } {",
250+
" set it_out $it_qn",
251+
" set out_pin \"QN\"",
252+
" } elseif { $it_qn2 != \"NULL\" && [$it_qn2 getNet] != \"NULL\" } {",
253+
" set it_out $it_qn2",
254+
" set out_pin \"Q_N\"",
255+
" } elseif { $it_q != \"NULL\" } {",
256+
" set it_out $it_q",
257+
" set out_pin \"Q\"",
258+
" } elseif { $it_qn != \"NULL\" } {",
259+
" set it_out $it_qn",
260+
" set out_pin \"QN\"",
261+
" } elseif { $it_qn2 != \"NULL\" } {",
262+
" set it_out $it_qn2",
263+
" set out_pin \"Q_N\"",
264+
" }",
265+
" if { $it_out == \"NULL\" } { continue }",
266+
"",
267+
" set bb_in [$it_in getBBox]",
268+
" set in_x [$bb_in xMin]",
269+
" set in_y [$bb_in yMin]",
270+
" set bb_out [$it_out getBBox]",
271+
" set out_x [$bb_out xMin]",
272+
" set out_y [$bb_out yMin]",
273+
" puts $fp2 \"[$inst getName]\\t$in_x\\t$in_y\\t$out_x\\t$out_y\\t$in_pin\\t$out_pin\\t[$master getName]\"",
227274
"}",
228275
"close $fp",
276+
"close $fp2",
229277
f"write_db {_tcl_quote(base_scan_odb)}",
230278
"exit",
231279
]

bullet-point-generator/run_openroad_case.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def main(argv: Optional[List[str]] = None) -> int:
124124

125125
ap.add_argument("--max-imbalance", type=float, default=2.0)
126126
ap.add_argument("--clock-mixing", default="no_mix")
127-
ap.add_argument("--polarity-mode", default="mid", choices=["mid", "strict"])
127+
ap.add_argument("--polarity-mode", default="strict", choices=["mid", "strict"])
128128

129129
ap.add_argument("--solver", choices=["HEURISTIC", "SCANOPT"], default="SCANOPT")
130130
ap.add_argument("--scanopt-time-limit", type=float, default=15.0)
@@ -267,4 +267,3 @@ def main(argv: Optional[List[str]] = None) -> int:
267267

268268
if __name__ == "__main__":
269269
raise SystemExit(main())
270-

bullet-point-generator/run_ortools_case.py

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,34 @@ def _load_scanffs_tsv(tsv_path: Path) -> Tuple[List[str], List[int], List[int]]:
6565
return names, xs, ys
6666

6767

68+
def _load_scanff_pins_tsv(
69+
tsv_path: Path,
70+
) -> Tuple[List[str], List[int], List[int], List[int], List[int]]:
71+
names: List[str] = []
72+
in_xs: List[int] = []
73+
in_ys: List[int] = []
74+
out_xs: List[int] = []
75+
out_ys: List[int] = []
76+
lines = tsv_path.read_text(encoding="utf-8", errors="ignore").splitlines()
77+
if not lines or not lines[0].startswith("name\t"):
78+
raise ValueError(f"Unexpected TSV header: {tsv_path}")
79+
for line in lines[1:]:
80+
if not line.strip():
81+
continue
82+
fields = line.split("\t")
83+
if len(fields) < 5:
84+
continue
85+
name, in_x_s, in_y_s, out_x_s, out_y_s = fields[0], fields[1], fields[2], fields[3], fields[4]
86+
names.append(name)
87+
in_xs.append(int(in_x_s))
88+
in_ys.append(int(in_y_s))
89+
out_xs.append(int(out_x_s))
90+
out_ys.append(int(out_y_s))
91+
if not names:
92+
raise ValueError(f"No scanffs parsed from {tsv_path}")
93+
return names, in_xs, in_ys, out_xs, out_ys
94+
95+
6896
def _parse_corner(
6997
*,
7098
name: str,
@@ -86,10 +114,10 @@ def _parse_corner(
86114
raise ValueError(f"Unsupported corner '{name}'. Use LL/UL/UR or x,y.")
87115

88116

89-
def _plot_order_png(
117+
def _plot_edges_png(
90118
*,
91119
out_png: Path,
92-
order_xy: List[Tuple[int, int]],
120+
edges: List[Tuple[Tuple[int, int], Tuple[int, int]]],
93121
diearea_dbu: Tuple[int, int, int, int],
94122
highlight_top_k: int = 50,
95123
) -> None:
@@ -102,12 +130,10 @@ def _plot_order_png(
102130

103131
x0, y0, x1, y1 = diearea_dbu
104132

105-
pts = order_xy
106-
edges = [(pts[i], pts[i + 1]) for i in range(len(pts) - 1)]
107-
dists = [
108-
abs(pts[i + 1][0] - pts[i][0]) + abs(pts[i + 1][1] - pts[i][1])
109-
for i in range(len(pts) - 1)
110-
]
133+
if not edges:
134+
return
135+
136+
dists = [abs(b[0] - a[0]) + abs(b[1] - a[1]) for (a, b) in edges]
111137
idx_sorted = sorted(range(len(dists)), key=lambda i: dists[i], reverse=True)
112138
hi = set(idx_sorted[: max(0, highlight_top_k)])
113139

@@ -134,8 +160,8 @@ def _plot_order_png(
134160
)
135161

136162
# Start/end points.
137-
ax.scatter([pts[0][0]], [pts[0][1]], s=18, c="black", marker="o", zorder=10)
138-
ax.scatter([pts[-1][0]], [pts[-1][1]], s=18, c="black", marker="s", zorder=10)
163+
ax.scatter([edges[0][0][0]], [edges[0][0][1]], s=18, c="black", marker="o", zorder=10)
164+
ax.scatter([edges[-1][1][0]], [edges[-1][1][1]], s=18, c="black", marker="s", zorder=10)
139165

140166
ax.set_xlim(x0, x1)
141167
ax.set_ylim(y0, y1)
@@ -154,6 +180,7 @@ class OrtoolsResult:
154180
time_limit_s: float
155181
wall_s: float
156182
nodes_scanff: int
183+
cost_model: str
157184
total_cost_dbu: int
158185
total_cost_um: float
159186
begin_dbu: Tuple[int, int]
@@ -162,7 +189,9 @@ class OrtoolsResult:
162189

163190

164191
def main(argv: Optional[List[str]] = None) -> int:
165-
ap = argparse.ArgumentParser(description="Run OR-Tools TSP path for bullet-point-3 (K=1).")
192+
ap = argparse.ArgumentParser(
193+
description="Run OR-Tools directed TSP path (ATSP) for bullet-point-3 (K=1)."
194+
)
166195
ap.add_argument("--case-id", required=True, help="E.g. 3c, 3d.")
167196
ap.add_argument("--testcase", required=True, help="Typically JPEG-REAL1.")
168197
ap.add_argument("--testcases-dir", type=Path, default=Path("bullet-point-generator/testcases"))
@@ -186,26 +215,42 @@ def main(argv: Optional[List[str]] = None) -> int:
186215
raise FileNotFoundError(final_def)
187216

188217
scanffs_tsv = testcases_dir / args.testcase / "scanffs.tsv"
189-
if not scanffs_tsv.exists():
190-
raise FileNotFoundError(scanffs_tsv)
218+
scanff_pins_tsv = testcases_dir / args.testcase / "scanff_pins.tsv"
219+
if not scanff_pins_tsv.exists() and not scanffs_tsv.exists():
220+
raise FileNotFoundError(scanff_pins_tsv)
191221

192222
die = manifest.get("diearea_dbu")
193223
if die is None:
194224
raise ValueError(f"Missing diearea_dbu in {manifest_path}")
195225
begin = _parse_corner(name=args.begin, die_ll=die["ll"], die_ur=die["ur"])
196226
end = _parse_corner(name=args.end, die_ll=die["ll"], die_ur=die["ur"])
197227

198-
scan_names, scan_xs, scan_ys = _load_scanffs_tsv(scanffs_tsv)
228+
cost_model = "pin_atsp"
229+
if scanff_pins_tsv.exists():
230+
scan_names, in_xs, in_ys, out_xs, out_ys = _load_scanff_pins_tsv(scanff_pins_tsv)
231+
else:
232+
# Fallback: symmetric (point-based) costs only. Regenerate testcases to
233+
# get pin-level scan_in/scan_out coordinates for ATSP.
234+
print(
235+
f"[WARN] Missing {scanff_pins_tsv}; falling back to symmetric point costs from {scanffs_tsv}."
236+
)
237+
scan_names, scan_xs, scan_ys = _load_scanffs_tsv(scanffs_tsv)
238+
in_xs, in_ys = scan_xs, scan_ys
239+
out_xs, out_ys = scan_xs, scan_ys
240+
cost_model = "point_stsp_fallback"
199241
units_per_micron, diearea_dbu = _read_units_and_diearea(final_def)
200242

201243
# Node mapping:
202244
# 0 => BEGIN
203245
# 1..N => scanffs (in TSV order)
204246
# N+1 => END
205-
xs = [begin[0], *scan_xs, end[0]]
206-
ys = [begin[1], *scan_ys, end[1]]
207-
n = len(xs)
247+
in_x_all = [begin[0], *in_xs, end[0]]
248+
in_y_all = [begin[1], *in_ys, end[1]]
249+
out_x_all = [begin[0], *out_xs, end[0]]
250+
out_y_all = [begin[1], *out_ys, end[1]]
251+
n = len(in_x_all)
208252
assert n == len(scan_names) + 2
253+
assert n == len(in_y_all) == len(out_x_all) == len(out_y_all)
209254

210255
from ortools.constraint_solver import pywrapcp, routing_enums_pb2 # type: ignore[import-not-found]
211256

@@ -215,7 +260,7 @@ def main(argv: Optional[List[str]] = None) -> int:
215260
def dist_cb(from_index: int, to_index: int) -> int:
216261
a = manager.IndexToNode(from_index)
217262
b = manager.IndexToNode(to_index)
218-
return abs(xs[a] - xs[b]) + abs(ys[a] - ys[b])
263+
return abs(out_x_all[a] - in_x_all[b]) + abs(out_y_all[a] - in_y_all[b])
219264

220265
transit_cb = routing.RegisterTransitCallback(dist_cb)
221266
routing.SetArcCostEvaluatorOfAllVehicles(transit_cb)
@@ -242,6 +287,7 @@ def dist_cb(from_index: int, to_index: int) -> int:
242287
time_limit_s=args.time_limit_s,
243288
wall_s=wall_s,
244289
nodes_scanff=len(scan_names),
290+
cost_model=cost_model,
245291
total_cost_dbu=0,
246292
total_cost_um=0.0,
247293
begin_dbu=begin,
@@ -269,6 +315,7 @@ def dist_cb(from_index: int, to_index: int) -> int:
269315
time_limit_s=args.time_limit_s,
270316
wall_s=wall_s,
271317
nodes_scanff=len(scan_names),
318+
cost_model=cost_model,
272319
total_cost_dbu=cost_dbu,
273320
total_cost_um=cost_um,
274321
begin_dbu=begin,
@@ -278,13 +325,19 @@ def dist_cb(from_index: int, to_index: int) -> int:
278325
(out_dir / "metrics.json").write_text(json.dumps(asdict(res), indent=2) + "\n")
279326

280327
if not args.no_plot:
281-
order_xy = [(xs[node], ys[node]) for node in route_nodes]
282-
_plot_order_png(out_png=out_dir / "plot.png", order_xy=order_xy, diearea_dbu=diearea_dbu)
328+
plot_edges: List[Tuple[Tuple[int, int], Tuple[int, int]]] = []
329+
idx = routing.Start(0)
330+
while not routing.IsEnd(idx):
331+
a = manager.IndexToNode(idx)
332+
next_idx = sol.Value(routing.NextVar(idx))
333+
b = manager.IndexToNode(next_idx)
334+
plot_edges.append(((out_x_all[a], out_y_all[a]), (in_x_all[b], in_y_all[b])))
335+
idx = next_idx
336+
_plot_edges_png(out_png=out_dir / "plot.png", edges=plot_edges, diearea_dbu=diearea_dbu)
283337

284338
print(json.dumps(asdict(res), indent=2))
285339
return 0
286340

287341

288342
if __name__ == "__main__":
289343
raise SystemExit(main())
290-

dft-spec.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
3+
The command execute_dft_plan should create one or more stitched (i.e., ordered) scan chains, satisfying user-specified constraints.
4+
Each scan chain is a “directed Hamiltonian path” over ScanFF instances. The chain will connect from a legal starting scan-in port of a ScanFF (the first ScanFF in the chain), to a legal ending scan-out port of another ScanFF (the last ScanFF in the chain).
5+
Generally, the one or more scan chains produced by execute_dft_plan attempt to minimize total estimated wirelength (subject to other constraints: hold timing, setup timing criticality, congestion and blockage avoidance, specified ScanFF grouping and ordering, etc.). The estimated wirelength of a given ScanFF1-to-ScanFF2 connection in a scan chain is the Manhattan distance between the scan-out port of ScanFF1 and the scan-in port of ScanFF2.
6+
Scan chain naming
7+
The user must be able to specify particular scan chain names.
8+
Scan chain grouping, assignment and ordering
9+
The user must be able to assign particular groups of FFs (thus, their respective mapped ScanFF instances) to specific scan chains, or to other groups. (The latter implies that the grouping can be hierarchical.) // if ordering constraints are given without naming the (one) scan chain, will the tool accept this?
10+
The assignment of ScanFFs to a group may include ordering constraints.
11+
Strict order: the scan chain must, for the ScanFFs in a given group, follow the order in which the ScanFFs appear in the assignment to the group. No other ScanFFs may be interpolated within this “sub-path” of the scan chain that is produced. Note that strict order defines a sub-path of the containing “Hamiltonian path”, and note that such a sub-path may make the induced “asymmetric TSP” highly asymmetric: connecting to the scan-in (from the scan-out) port of the sub-path can have very different cost than if the order of ScanFFs in the group were to be reversed.
12+
Partial order: a “before” semantics must be made available to the user, to force a given FF_1 or FF_group_1 to occur in the scan chain solution before another given FF_2 or FF_group_2.
13+
As mentioned in (a) above: a group may be assigned to another group.
14+
In the execute_dft_plan solution, the ScanFFs assigned to any given group must remain together in a single (i.e., exactly one) scan chain.
15+
Scan chain begin-end port constraints
16+
Each scan chain must have user-specified BeginPort and EndPort locations. These are (x,y) locations (not FFs) – or, pins of placed instances / pads – in the place-and-route region.
17+
The scan chain ordering optimization must include in its calculation of chain cost the estimated wirelength from BeginPort to the first ScanFF’s scan-in port, and the estimated wirelength from the last ScanFF’s scan-out port to EndPort.
18+
Overall scan chain structural constraints
19+
The cost of performing scan-based testing is comprehended by the product architecture. In particular, use of additional on-chip resources, and/or more expensive ATE equipment, can permit the use of multiple scan chains in the solution produced by execute_dft_plan.
20+
The user can specify a maximum length (number of ScanFFs in the chain). This is a constraint on all scan chains in the solution.
21+
The user can specify the number of scan chains in the solution. This is also a constraint on the overall solution.
22+
The scan chain optimization should comprehend both feasibility and balance.
23+
For example, if max_length * max_num_chains < num_ScanFFs, then no solution will satisfy the constraints and an error should be thrown.
24+
As another example, if the number of ScanFFs in a specified scan group is larger than max_length, then no solution is possible.
25+
Length balancing over all scan chains produced reflects the goal of reducing time spent on the (ATE) tester. A max_imbalance parameter with default of 30 (percent) should be made available to the user. This adds a simple constraint: the ratio of the lengths of any two scan chains should never exceed (1 + max_imbalance / 100), e.g., a ratio of 1.3 with the default value of 30.
26+
Clock mixing
27+
Clock mixing refers to stitching ScanFFs from multiple clock domains into a single chain.
28+
See clock_mixing in set_dft_config.
29+
If enabled, then a lockup latch must be instantiated between ScanFFs that are adjacent in a scan chain but belong to different clock domains.
30+
If not enabled, then execute_dft_plan must not output a scan chain stitching solution that mixes ScanFFs from multiple clock domains.
31+
Polarity
32+
The polarity constraint in its simplest form is that rising edge-triggered and falling edge-triggered ScanFFs cannot coexist in the same scan chain.
33+
A “mid” way to handle ScanFF polarity is to ensure that all falling edge-triggered ScanFFs exist before all rising edge-triggered ScanFFs in any given scan chain. Note that such a structural constraint may be inconsistent with other constraints induced by grouping and ordering; such an inconsistency should be flagged by the tool.
34+
Polarity is a hard constraint. The tool must comprehend the polarity of all ScanFFs that it stitches together.
35+
KGF inputs 1/29: [KGF] Some other suggestions to consider
36+
You need to be able to exclude certain FFs from scan chain (i.e. reset synchronizer)
37+
In some cases using Qbar output (or changing the FF to one with a Qbar output) can help with setup timing
38+
Shift register recognition, shift registers do not need an additional scan chain through them
39+
Special cell recognition, clock gate cells need to be active during scan and internal tristate drivers should probably be disabled during scan or should not be disturbed by scan.
40+
Stitching to existing scan chains (i.e. in SRAM macros)
41+
42+
The above are mandatory for a v1.0 version. Future extensions would include the following.
43+
44+
Support of multi-bit MBFFs and multi-bit ScanFFs
45+
Internal scan MBFFs, derived cells, etc. (Google document)
46+
Support of multiple power domains
47+
The tool should “freely” stitch ScanFFs from multiple power domains only if the power domains have the same supply voltage and are always-on.
48+
If ScanFFs from different voltage domains (at different voltage levels) are adjacent in a scan chain, then a level shifter must be inserted.
49+
If either domain is switched, then an isolation cell is needed.
50+
Initially, the tool should throw warnings (but not attempt to implement a correct solution) if (b) or (c) hold.
51+
Support for importing and exporting scan chains to support external tools
52+
This can help future scan insertion tools like Difetto
53+
The tool should be able to take in a list of scan cells
54+
Sometimes, a scan cell is composed of multiple cells e.g, Fault generates an FF and mux pair if the PDK does not contain scan cells
55+
These cells should be connected together in anywhere from 1 to N initial chains, with a primary input/output per chain.
56+
Cells may also be connected hierarchically in groups (see 3.)
57+
58+

0 commit comments

Comments
 (0)