fpga-design-implview 0.1.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.
- fpga_design_implview/__init__.py +3 -0
- fpga_design_implview/__main__.py +3 -0
- fpga_design_implview/cache.py +26 -0
- fpga_design_implview/cli.py +256 -0
- fpga_design_implview/dcp.py +41 -0
- fpga_design_implview/design.py +73 -0
- fpga_design_implview/device.py +126 -0
- fpga_design_implview/hier.py +181 -0
- fpga_design_implview/highlight.py +245 -0
- fpga_design_implview/html.py +679 -0
- fpga_design_implview/layout.py +399 -0
- fpga_design_implview/placement.py +127 -0
- fpga_design_implview/render.py +531 -0
- fpga_design_implview/tcl/batch.tcl +28 -0
- fpga_design_implview/tcl/implview.tcl +177 -0
- fpga_design_implview/vivado.py +74 -0
- fpga_design_implview-0.1.0.dist-info/METADATA +146 -0
- fpga_design_implview-0.1.0.dist-info/RECORD +22 -0
- fpga_design_implview-0.1.0.dist-info/WHEEL +5 -0
- fpga_design_implview-0.1.0.dist-info/entry_points.txt +3 -0
- fpga_design_implview-0.1.0.dist-info/licenses/LICENSE +21 -0
- fpga_design_implview-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exports are cached: devices by part, designs by checkpoint and options."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def root():
|
|
8
|
+
base = os.environ.get("IMPLVIEW_CACHE") or os.path.join(
|
|
9
|
+
os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"), "implview")
|
|
10
|
+
return base
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def device_path(part):
|
|
14
|
+
d = os.path.join(root(), "devices")
|
|
15
|
+
os.makedirs(d, exist_ok=True)
|
|
16
|
+
return os.path.join(d, f"{part}.ivdev.gz")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def design_path(dcp, *opts):
|
|
20
|
+
st = os.stat(dcp)
|
|
21
|
+
key = f"{os.path.abspath(dcp)}|{st.st_mtime_ns}|{st.st_size}|{'|'.join(map(str, opts))}"
|
|
22
|
+
h = hashlib.sha1(key.encode()).hexdigest()[:16]
|
|
23
|
+
d = os.path.join(root(), "designs")
|
|
24
|
+
os.makedirs(d, exist_ok=True)
|
|
25
|
+
name = os.path.splitext(os.path.basename(dcp))[0]
|
|
26
|
+
return os.path.join(d, f"{name}-{h}.ivd.gz")
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""implview command line."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from . import __version__, cache, vivado
|
|
9
|
+
from .dcp import dcp_info, find_dcp
|
|
10
|
+
from .design import load_design
|
|
11
|
+
from .device import load_device
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _log(msg):
|
|
15
|
+
sys.stderr.write(f"implview: {msg}\n")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _atomic_run(outputs, args, a, version):
|
|
19
|
+
"""Run Vivado writing to temporary names, then move into place."""
|
|
20
|
+
tmp = {p: p + f".tmp{os.getpid()}" for p in outputs}
|
|
21
|
+
args = [tmp.get(x, x) for x in args]
|
|
22
|
+
try:
|
|
23
|
+
vivado.run(args, a.vivado, version)
|
|
24
|
+
for p, t in tmp.items():
|
|
25
|
+
os.replace(t, p)
|
|
26
|
+
finally:
|
|
27
|
+
for t in tmp.values():
|
|
28
|
+
if os.path.exists(t):
|
|
29
|
+
os.remove(t)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def export(a, src):
|
|
33
|
+
"""Design (and device on a cache miss) for a checkpoint, run directory or
|
|
34
|
+
exported .ivd.gz. Returns (device, design, path of the design export)."""
|
|
35
|
+
if src.endswith(".ivd.gz"):
|
|
36
|
+
des = load_design(src)
|
|
37
|
+
devp = os.path.join(os.path.dirname(src), f"{des.part}.ivdev.gz")
|
|
38
|
+
if not os.path.exists(devp):
|
|
39
|
+
devp = cache.device_path(des.part)
|
|
40
|
+
if not os.path.exists(devp):
|
|
41
|
+
raise SystemExit(f"implview: no device file for {des.part}; run `fpga-design-implview device {des.part}`")
|
|
42
|
+
des.source = src
|
|
43
|
+
_check_placed(des)
|
|
44
|
+
return load_device(devp), des, src
|
|
45
|
+
|
|
46
|
+
dcp = find_dcp(src)
|
|
47
|
+
info = dcp_info(dcp)
|
|
48
|
+
part = info.get("part", "")
|
|
49
|
+
routing = not getattr(a, "no_routing", False)
|
|
50
|
+
devp = cache.device_path(part)
|
|
51
|
+
out = getattr(a, "output_design", None)
|
|
52
|
+
desp = out or cache.design_path(dcp, routing)
|
|
53
|
+
if not out and not routing and not os.path.exists(desp) and os.path.exists(cache.design_path(dcp, True)):
|
|
54
|
+
desp = cache.design_path(dcp, True) # an export with routing has it all
|
|
55
|
+
refresh = getattr(a, "refresh", False)
|
|
56
|
+
if refresh or not os.path.exists(desp) or not os.path.exists(devp):
|
|
57
|
+
args = ["design", os.path.abspath(dcp), desp]
|
|
58
|
+
outputs = [desp]
|
|
59
|
+
if refresh or not os.path.exists(devp):
|
|
60
|
+
args += ["-device", devp]
|
|
61
|
+
outputs.append(devp)
|
|
62
|
+
if routing:
|
|
63
|
+
args.append("-routing")
|
|
64
|
+
_log(f"{dcp}: {part}, top {info.get('top', '?')}, Vivado {info.get('vivado', '?')}: exporting")
|
|
65
|
+
t0 = time.time()
|
|
66
|
+
_atomic_run(outputs, args, a, info.get("vivado", ""))
|
|
67
|
+
_log(f"export done in {time.time() - t0:.0f} s")
|
|
68
|
+
des = load_design(desp)
|
|
69
|
+
des.source = dcp
|
|
70
|
+
if not routing:
|
|
71
|
+
des.pips = {}
|
|
72
|
+
_check_placed(des)
|
|
73
|
+
return load_device(devp), des, desp
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_placed(des):
|
|
77
|
+
"""Warn when most cells have no site: a synthesized checkpoint (like the
|
|
78
|
+
unsuffixed <top>.dcp Vivado copies into impl_1) draws almost nothing."""
|
|
79
|
+
unplaced = des.unplaced()
|
|
80
|
+
total = len(des.cells) + (unplaced or 0)
|
|
81
|
+
if not unplaced or unplaced <= 0.01 * total:
|
|
82
|
+
return
|
|
83
|
+
if len(des.cells) >= total / 2:
|
|
84
|
+
_log(f"warning: {unplaced:,} of {total:,} cells are not placed")
|
|
85
|
+
return
|
|
86
|
+
_log(f"warning: only {len(des.cells):,} of {total:,} cells have a site: "
|
|
87
|
+
f"the checkpoint is not placed")
|
|
88
|
+
if des.source.endswith(".dcp"):
|
|
89
|
+
try:
|
|
90
|
+
best = find_dcp(os.path.dirname(os.path.abspath(des.source)))
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
return
|
|
93
|
+
if os.path.abspath(best) != os.path.abspath(des.source):
|
|
94
|
+
_log(f"the implemented checkpoint of this run is {best}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _render_options(a):
|
|
98
|
+
"""Highlight entries and render options: the command line wins over the
|
|
99
|
+
[options] of the highlight file."""
|
|
100
|
+
from .highlight import Entry, load_config
|
|
101
|
+
entries, opts = [], {}
|
|
102
|
+
if a.highlight:
|
|
103
|
+
try:
|
|
104
|
+
cfg = load_config(a.highlight)
|
|
105
|
+
except (OSError, ValueError) as e:
|
|
106
|
+
raise SystemExit(f"implview: {e}")
|
|
107
|
+
entries, opts = cfg.entries, cfg.options
|
|
108
|
+
entries += [Entry(g) for g in a.group or []]
|
|
109
|
+
pick = lambda k, d: getattr(a, k) if getattr(a, k) is not None else opts.get(k, d)
|
|
110
|
+
return entries, {"theme": pick("theme", "light"), "stats": pick("stats", False),
|
|
111
|
+
"labels": pick("labels", True), "panel": pick("panel", True),
|
|
112
|
+
"label_size": pick("label_size", 10.0),
|
|
113
|
+
"label_style": pick("label_style", "pill"),
|
|
114
|
+
"hide_other": opts.get("others") == "hide"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_render(a):
|
|
118
|
+
from .layout import build_layout
|
|
119
|
+
from .placement import build_placement, group_cells
|
|
120
|
+
entries, opt = _render_options(a)
|
|
121
|
+
dev, des, _ = export(a, a.input)
|
|
122
|
+
lay = build_layout(dev)
|
|
123
|
+
groups, cg, unmatched = group_cells(des, min(a.max_groups, 8), entries, a.depth)
|
|
124
|
+
for p in unmatched:
|
|
125
|
+
_log(f"warning: highlight path {p!r} matches nothing")
|
|
126
|
+
place = build_placement(dev, des, groups, cg, opt["hide_other"])
|
|
127
|
+
if place.missing:
|
|
128
|
+
_log(f"warning: {place.missing} cells are on sites the device file does not have")
|
|
129
|
+
if a.save_highlight:
|
|
130
|
+
from .highlight import write_config
|
|
131
|
+
from .render import group_colors
|
|
132
|
+
write_config(a.save_highlight, groups, group_colors("light", groups), des.top)
|
|
133
|
+
_log(f"highlight file written to {a.save_highlight}")
|
|
134
|
+
outs = a.output or [f"{des.top or 'design'}.png", f"{des.top or 'design'}.pdf"]
|
|
135
|
+
if a.outdir:
|
|
136
|
+
os.makedirs(a.outdir, exist_ok=True)
|
|
137
|
+
outs = [o if os.path.isabs(o) else os.path.join(a.outdir, o) for o in outs]
|
|
138
|
+
html = [o for o in outs if o.lower().endswith(".html")]
|
|
139
|
+
pics = [o for o in outs if o not in html]
|
|
140
|
+
written = []
|
|
141
|
+
if pics:
|
|
142
|
+
from .render import write_outputs
|
|
143
|
+
written += write_outputs(pics, dev, lay, des, place,
|
|
144
|
+
**{k: v for k, v in opt.items() if k != "hide_other"})
|
|
145
|
+
if html:
|
|
146
|
+
from .html import write_html
|
|
147
|
+
for o in html:
|
|
148
|
+
write_html(o, dev, lay, des, place)
|
|
149
|
+
written.append(o)
|
|
150
|
+
for w in written:
|
|
151
|
+
print(w)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_export(a):
|
|
155
|
+
a.output_design = a.output
|
|
156
|
+
dev, des, path = export(a, a.input)
|
|
157
|
+
print(path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_device(a):
|
|
161
|
+
path = a.output or cache.device_path(a.part)
|
|
162
|
+
if a.refresh or not os.path.exists(path):
|
|
163
|
+
_atomic_run([path], ["device", a.part, path], a, "")
|
|
164
|
+
print(path)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_info(a):
|
|
168
|
+
dcp = find_dcp(a.input)
|
|
169
|
+
info = dcp_info(dcp)
|
|
170
|
+
print(f"checkpoint {dcp}")
|
|
171
|
+
for k in ("part", "top", "product"):
|
|
172
|
+
print(f"{k:<11} {info.get(k, '')}")
|
|
173
|
+
devp = cache.device_path(info.get("part", ""))
|
|
174
|
+
print(f"device {devp} ({'cached' if os.path.exists(devp) else 'not cached'})")
|
|
175
|
+
try:
|
|
176
|
+
exe = vivado.command(a.vivado, info.get("vivado", ""))
|
|
177
|
+
print(f"vivado {' '.join(exe)}")
|
|
178
|
+
except vivado.VivadoError as e:
|
|
179
|
+
print(f"vivado {e}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_tcl(a):
|
|
183
|
+
print(os.path.join(vivado.TCL_DIR, "implview.tcl"))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv=None):
|
|
187
|
+
ap = argparse.ArgumentParser(
|
|
188
|
+
prog="fpga-design-implview",
|
|
189
|
+
description="Pictures of Vivado implemented designs: the device resources and "
|
|
190
|
+
"what the design uses of them.")
|
|
191
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
192
|
+
ap.add_argument("--vivado", help="command that runs Vivado (default: $IMPLVIEW_VIVADO, "
|
|
193
|
+
"vivado on the PATH, or an install matching the checkpoint)")
|
|
194
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
195
|
+
|
|
196
|
+
def source(p):
|
|
197
|
+
p.add_argument("input", help="routed checkpoint, run directory (impl_1), project "
|
|
198
|
+
"directory, or an exported .ivd.gz")
|
|
199
|
+
p.add_argument("--no-routing", action="store_true", help="skip the routing usage export")
|
|
200
|
+
p.add_argument("--refresh", action="store_true", help="export again, ignore the cache")
|
|
201
|
+
|
|
202
|
+
p = sub.add_parser("render", help="write PNG/PDF/SVG/HTML pictures of a design")
|
|
203
|
+
source(p)
|
|
204
|
+
p.add_argument("-o", "--output", action="append",
|
|
205
|
+
help="output file, by extension: .png .pdf .svg .html (repeatable; "
|
|
206
|
+
"default <top>.png and <top>.pdf)")
|
|
207
|
+
p.add_argument("-O", "--outdir", help="directory for the outputs")
|
|
208
|
+
p.add_argument("--theme", choices=("light", "dark"), help="default light")
|
|
209
|
+
p.add_argument("-H", "--highlight", metavar="FILE",
|
|
210
|
+
help="highlight file (TOML): which hierarchy to color and how")
|
|
211
|
+
p.add_argument("-g", "--group", action="append", metavar="PATH",
|
|
212
|
+
help="hierarchy path to color, palette colors, repeatable (* inside a "
|
|
213
|
+
"level, ** any depth)")
|
|
214
|
+
p.add_argument("--depth", type=int, help="group by hierarchy cut at this depth")
|
|
215
|
+
p.add_argument("--max-groups", type=int, default=8, help="automatic groups (max 8)")
|
|
216
|
+
p.add_argument("--stats", action=argparse.BooleanOptionalAction,
|
|
217
|
+
help="utilization table and cells per group in the panel (default off)")
|
|
218
|
+
p.add_argument("--labels", action=argparse.BooleanOptionalAction,
|
|
219
|
+
help="group and clock region names on the device (default on)")
|
|
220
|
+
p.add_argument("--label-size", type=float, metavar="PT",
|
|
221
|
+
help="font size of the group names on the device (default 10)")
|
|
222
|
+
p.add_argument("--label-style", choices=("pill", "text"),
|
|
223
|
+
help="group names as colored badges (default) or plain text")
|
|
224
|
+
p.add_argument("--panel", action=argparse.BooleanOptionalAction,
|
|
225
|
+
help="design, part and legend on the right (default on)")
|
|
226
|
+
p.add_argument("--save-highlight", metavar="FILE",
|
|
227
|
+
help="write the groups used as a highlight file, to edit")
|
|
228
|
+
p.set_defaults(func=cmd_render)
|
|
229
|
+
|
|
230
|
+
p = sub.add_parser("export", help="only export a design (and its device) from Vivado")
|
|
231
|
+
source(p)
|
|
232
|
+
p.add_argument("-o", "--output", help="design file (default: in the cache)")
|
|
233
|
+
p.set_defaults(func=cmd_export)
|
|
234
|
+
|
|
235
|
+
p = sub.add_parser("device", help="export the resource map of a part into the cache")
|
|
236
|
+
p.add_argument("part", help="e.g. xcvu37p-fsvh2892-2L-e")
|
|
237
|
+
p.add_argument("-o", "--output", help="device file (default: in the cache)")
|
|
238
|
+
p.add_argument("--refresh", action="store_true")
|
|
239
|
+
p.set_defaults(func=cmd_device)
|
|
240
|
+
|
|
241
|
+
p = sub.add_parser("info", help="part, top and cache status of a checkpoint")
|
|
242
|
+
p.add_argument("input")
|
|
243
|
+
p.set_defaults(func=cmd_info)
|
|
244
|
+
|
|
245
|
+
p = sub.add_parser("tcl", help="path of implview.tcl, to export from a Vivado session")
|
|
246
|
+
p.set_defaults(func=cmd_tcl)
|
|
247
|
+
|
|
248
|
+
a = ap.parse_args(argv)
|
|
249
|
+
try:
|
|
250
|
+
a.func(a)
|
|
251
|
+
except (vivado.VivadoError, FileNotFoundError) as e:
|
|
252
|
+
raise SystemExit(f"implview: {e}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""What a checkpoint is, without Vivado: dcp.xml inside the zip names the part."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
# Checkpoints of an implementation run, most complete first.
|
|
10
|
+
STAGES = ("_routed.dcp", "_postroute_physopt.dcp", "_physopt.dcp", "_placed.dcp", "_opt.dcp")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dcp_info(path):
|
|
14
|
+
with zipfile.ZipFile(path) as z:
|
|
15
|
+
root = ET.fromstring(z.read("dcp.xml"))
|
|
16
|
+
info = {}
|
|
17
|
+
for tag, key in (("Part", "part"), ("Top", "top"), ("PRODUCT", "product")):
|
|
18
|
+
e = root.find(tag)
|
|
19
|
+
if e is not None:
|
|
20
|
+
info[key] = e.get("Name", "")
|
|
21
|
+
m = re.search(r"v(\d{4}\.\d+)", info.get("product", ""))
|
|
22
|
+
info["vivado"] = m.group(1) if m else ""
|
|
23
|
+
return info
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_dcp(path):
|
|
27
|
+
"""The checkpoint to show for a path: a .dcp, a run directory (impl_1)
|
|
28
|
+
or a project directory (its impl_1 run)."""
|
|
29
|
+
if os.path.isfile(path):
|
|
30
|
+
return path
|
|
31
|
+
if not os.path.isdir(path):
|
|
32
|
+
raise FileNotFoundError(path)
|
|
33
|
+
runs = [path]
|
|
34
|
+
runs += sorted(glob.glob(os.path.join(path, "*.runs", "impl_*")))
|
|
35
|
+
runs += sorted(glob.glob(os.path.join(path, "impl_*")))
|
|
36
|
+
for run in runs:
|
|
37
|
+
for stage in STAGES:
|
|
38
|
+
hits = sorted(glob.glob(os.path.join(run, "*" + stage)), key=os.path.getmtime)
|
|
39
|
+
if hits:
|
|
40
|
+
return hits[-1]
|
|
41
|
+
raise FileNotFoundError(f"no implemented checkpoint (*_routed.dcp, ...) under {path}")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Implemented design: placed primitive cells, pblocks, routing usage.
|
|
2
|
+
|
|
3
|
+
Read back from the gzip'ed TSV written by `implview::export_design`
|
|
4
|
+
(tcl/implview.tcl).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import gzip
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Cell:
|
|
14
|
+
name: str
|
|
15
|
+
site: str
|
|
16
|
+
bel: str
|
|
17
|
+
ref: str # REF_NAME (LUT6, FDRE, RAMB36E2, ...)
|
|
18
|
+
group: str # PRIMITIVE_GROUP (LUT, FLOP_LATCH, BLOCKRAM, ...)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Design:
|
|
23
|
+
part: str = ""
|
|
24
|
+
top: str = ""
|
|
25
|
+
info: dict = field(default_factory=dict)
|
|
26
|
+
cells: list = field(default_factory=list)
|
|
27
|
+
pblocks: list = field(default_factory=list) # (name, grid ranges)
|
|
28
|
+
pads: list = field(default_factory=list) # (site, port, a cell on its net)
|
|
29
|
+
pips: dict = field(default_factory=dict) # tile name -> used pips
|
|
30
|
+
source: str = "" # checkpoint the export came from
|
|
31
|
+
|
|
32
|
+
def slack(self, key):
|
|
33
|
+
v = self.info.get(key, "")
|
|
34
|
+
try:
|
|
35
|
+
return float(v)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def group_counts(self):
|
|
40
|
+
return Counter(c.group for c in self.cells)
|
|
41
|
+
|
|
42
|
+
def unplaced(self):
|
|
43
|
+
"""Primitive cells without a site, None for exports that do not say."""
|
|
44
|
+
try:
|
|
45
|
+
return max(int(self.info["primitives"]) - len(self.cells), 0)
|
|
46
|
+
except (KeyError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_design(path):
|
|
51
|
+
d = Design()
|
|
52
|
+
with gzip.open(path, "rt") as f:
|
|
53
|
+
header = f.readline().rstrip("\n").split("\t")
|
|
54
|
+
if header[0] != "#implview-design":
|
|
55
|
+
raise ValueError(f"{path}: not an implview design file")
|
|
56
|
+
for line in f:
|
|
57
|
+
r = line.rstrip("\n").split("\t")
|
|
58
|
+
k = r[0]
|
|
59
|
+
if k == "U":
|
|
60
|
+
d.cells.append(Cell(r[5], r[1], r[2], r[3], r[4]))
|
|
61
|
+
elif k == "P":
|
|
62
|
+
d.pips[r[1]] = int(r[2])
|
|
63
|
+
elif k == "X":
|
|
64
|
+
d.pblocks.append((r[1], r[2] if len(r) > 2 else ""))
|
|
65
|
+
elif k == "O":
|
|
66
|
+
d.pads.append((r[1], r[2], r[3]))
|
|
67
|
+
elif k == "part":
|
|
68
|
+
d.part = r[1]
|
|
69
|
+
elif k == "top":
|
|
70
|
+
d.top = r[1]
|
|
71
|
+
else:
|
|
72
|
+
d.info[k] = r[1] if len(r) > 1 else ""
|
|
73
|
+
return d
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Device resource map: tiles on the Vivado tile grid, their sites and BELs.
|
|
2
|
+
|
|
3
|
+
Read back from the gzip'ed TSV written by `implview::export_device`
|
|
4
|
+
(tcl/implview.tcl). One file per part, cached by `implview.cache`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import gzip
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
SITE_NAME_RE = re.compile(r"^(.*)_X(\d+)Y(\d+)$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Site:
|
|
18
|
+
name: str
|
|
19
|
+
type: str
|
|
20
|
+
tile: int # index into Device.tile_*
|
|
21
|
+
clock_region: str
|
|
22
|
+
bonded: bool
|
|
23
|
+
prefix: str # name without the _X#Y# suffix (SLICE, RAMB18, ...)
|
|
24
|
+
x: int # site X/Y from the name, -1 if the name has none
|
|
25
|
+
y: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ClockRegion:
|
|
30
|
+
name: str
|
|
31
|
+
row: int
|
|
32
|
+
col: int
|
|
33
|
+
gx0: int # grid bounds, inclusive (top left .. bottom right)
|
|
34
|
+
gy0: int
|
|
35
|
+
gx1: int
|
|
36
|
+
gy1: int
|
|
37
|
+
slr: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Device:
|
|
42
|
+
part: str = ""
|
|
43
|
+
info: dict = field(default_factory=dict)
|
|
44
|
+
tile_name: list = field(default_factory=list)
|
|
45
|
+
tile_type: list = field(default_factory=list)
|
|
46
|
+
gx: np.ndarray = None
|
|
47
|
+
gy: np.ndarray = None
|
|
48
|
+
tile_nsites: np.ndarray = None
|
|
49
|
+
tile_slr: np.ndarray = None
|
|
50
|
+
sites: list = field(default_factory=list)
|
|
51
|
+
bels: dict = field(default_factory=dict) # site type -> [(bel, bel type)]
|
|
52
|
+
clock_regions: list = field(default_factory=list)
|
|
53
|
+
slrs: list = field(default_factory=list) # (name, index, is_master, config order)
|
|
54
|
+
pins: dict = field(default_factory=dict) # site name -> package pin
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def name(self):
|
|
58
|
+
return self.info.get("device", self.part)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def width(self):
|
|
62
|
+
return int(self.gx.max()) + 1
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def height(self):
|
|
66
|
+
return int(self.gy.max()) + 1
|
|
67
|
+
|
|
68
|
+
def site_index(self):
|
|
69
|
+
return {s.name: i for i, s in enumerate(self.sites)}
|
|
70
|
+
|
|
71
|
+
def tile_index(self):
|
|
72
|
+
return {n: i for i, n in enumerate(self.tile_name)}
|
|
73
|
+
|
|
74
|
+
def tile_sites(self):
|
|
75
|
+
"""Site indices of each tile (list of lists)."""
|
|
76
|
+
out = [[] for _ in self.tile_name]
|
|
77
|
+
for i, s in enumerate(self.sites):
|
|
78
|
+
out[s.tile].append(i)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_site_name(name):
|
|
83
|
+
m = SITE_NAME_RE.match(name)
|
|
84
|
+
if not m:
|
|
85
|
+
return name, -1, -1
|
|
86
|
+
return m.group(1), int(m.group(2)), int(m.group(3))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_device(path):
|
|
90
|
+
dev = Device()
|
|
91
|
+
gx, gy, ns, slr = [], [], [], []
|
|
92
|
+
with gzip.open(path, "rt") as f:
|
|
93
|
+
header = f.readline().rstrip("\n").split("\t")
|
|
94
|
+
if header[0] != "#implview-device":
|
|
95
|
+
raise ValueError(f"{path}: not an implview device file")
|
|
96
|
+
for line in f:
|
|
97
|
+
r = line.rstrip("\n").split("\t")
|
|
98
|
+
k = r[0]
|
|
99
|
+
if k == "T":
|
|
100
|
+
dev.tile_name.append(r[1])
|
|
101
|
+
dev.tile_type.append(r[2])
|
|
102
|
+
gx.append(int(r[3]))
|
|
103
|
+
gy.append(int(r[4]))
|
|
104
|
+
ns.append(int(r[5]))
|
|
105
|
+
slr.append(int(r[6]) if r[6] else 0)
|
|
106
|
+
elif k == "S":
|
|
107
|
+
prefix, x, y = parse_site_name(r[2])
|
|
108
|
+
dev.sites.append(Site(r[2], r[3], int(r[1]), r[4], r[5] == "1", prefix, x, y))
|
|
109
|
+
elif k == "B":
|
|
110
|
+
dev.bels[r[1]] = [tuple(b.split(":", 1)) for b in r[2:] if b]
|
|
111
|
+
elif k == "R":
|
|
112
|
+
dev.clock_regions.append(ClockRegion(
|
|
113
|
+
r[1], int(r[2]), int(r[3]), int(r[4]), int(r[5]), int(r[6]), int(r[7]), r[8]))
|
|
114
|
+
elif k == "L":
|
|
115
|
+
dev.slrs.append((r[1], int(r[2]), r[3] == "1", int(r[4]) if r[4] else 0))
|
|
116
|
+
elif k == "K":
|
|
117
|
+
dev.pins[r[2]] = r[1]
|
|
118
|
+
elif k == "part":
|
|
119
|
+
dev.part = r[1]
|
|
120
|
+
else:
|
|
121
|
+
dev.info[k] = r[1] if len(r) > 1 else ""
|
|
122
|
+
dev.gx = np.array(gx, dtype=np.int32)
|
|
123
|
+
dev.gy = np.array(gy, dtype=np.int32)
|
|
124
|
+
dev.tile_nsites = np.array(ns, dtype=np.int32)
|
|
125
|
+
dev.tile_slr = np.array(slr, dtype=np.int32)
|
|
126
|
+
return dev
|