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.
@@ -0,0 +1,3 @@
1
+ """Pictures and an interactive viewer of Vivado implemented designs."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -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