Add a tool to convert ECP5 bitstreams to Verilog. Inspired by icebox_vlog.py from Project IceStorm. This script was cleaned up from a script that was used to solve the Pwn2Win 2021 CTF challenge "Ethernet from Above".
diff --git a/tools/ecp_vlog.py b/tools/ecp_vlog.py new file mode 100644 index 0000000..8c3c59d --- /dev/null +++ b/tools/ecp_vlog.py
@@ -0,0 +1,898 @@ +import os +import sys +from collections import defaultdict +from dataclasses import dataclass, field +from enum import IntEnum +from functools import lru_cache +from typing import Callable, ClassVar, Dict, List, Optional, Sequence, Set, Tuple, Type + +try: + # optional import to get natural sorting of integers (i.e. 1, 5, 9, 10 instead of 1, 10, 5, 9) + from natsort import natsorted +except ImportError: + natsorted = sorted + +import pytrellis +import database + + +# Conversions between tiles and locations +@dataclass +class TileData: + tile: pytrellis.Tile + cfg: pytrellis.TileConfig + + +Location = Tuple[int, int] # pytrellis.Location cannot be used as a dictionary key +TilesByLoc = Dict[Location, List[TileData]] + + +def make_tiles_by_loc(chip: pytrellis.Chip) -> TilesByLoc: + tiles_by_loc: TilesByLoc = defaultdict(list) + + for tilename, tile in chip.tiles.items(): + locator = pytrellis.TileLocator(chip.info.family, chip.info.name, tile.info.type) + tilebitdb = pytrellis.get_tile_bitdata(locator) + tilecfg = tilebitdb.tile_cram_to_config(tile.cram) + + rc = tile.info.get_row_col() + row, col = rc.first, rc.second + tileloc = pytrellis.Location(col, row) + + tiles_by_loc[tileloc.x, tileloc.y].append(TileData(tile, tilecfg)) + + return tiles_by_loc + + +# Utility classes representing a graph of configured connections +@dataclass(eq=True, order=True, frozen=True) +class Ident: + """ An identifier in the routing graph """ + + # place label first so we sort by identifier + label: str = field(compare=False) + # Idents are unique by ID so we only need to compare IDs + id: int = field(repr=False) + # Having a cache for Ident objects reduces memory pressure, + # speeds up Ident creation slightly, and significantly reduces + # the size of pickled graphs. + _cache: ClassVar[Dict[int, "Ident"]] = {} + + @classmethod + def from_id(cls, rgraph: pytrellis.RoutingGraph, id: int) -> "Ident": + if id in cls._cache: + return cls._cache[id] + inst = Ident(rgraph.to_str(id), id) + cls._cache[id] = inst + return inst + + @classmethod + def from_label(cls, rgraph: pytrellis.RoutingGraph, label: str) -> "Ident": + return cls.from_id(rgraph, rgraph.ident(label)) + + def __str__(self) -> str: + return self.label + + +@dataclass(eq=True, order=True, frozen=True) +class Node: + """ A node in the routing graph - either a wire or a BEL pin """ + + # put y first so we sort by row, then column + y: int + x: int + id: Ident + pin: Optional[Ident] = None + + @property + def loc(self) -> pytrellis.Location: + return pytrellis.Location(self.x, self.y) + + @property + def mod_name(self) -> str: + return f"R{self.y}C{self.x}_{self.name}" + + @property + def name(self) -> str: + return self.id.label + + @property + def pin_name(self) -> str: + if self.pin is None: + return "" + return self.pin.label + + def __str__(self) -> str: + res = self.mod_name + if self.pin is not None: + res += "$" + self.pin_name + return res + + +EdgeMap = Dict[Node, Set[Node]] + + +@dataclass +class Component: + graph: "ConnectionGraph" + nodes: Set[Node] = field(default_factory=set) + + def get_roots(self) -> Set[Node]: + roots = set() + seen: Dict[Node, int] = {} + + def visit(node: Node) -> None: + if node in seen: + if seen[node] == 0: + print(f"Warning: node {node} is part of a cycle!") + return + seen[node] = 0 + if not self.graph.edges_rev[node]: + roots.add(node) + else: + for x in self.graph.edges_rev[node]: + visit(x) + seen[node] = 1 + + for x in self.nodes: + visit(x) + + return roots + + def get_leaves(self) -> Set[Node]: + leaves = set() + seen: Dict[Node, int] = {} + + def visit(node: Node) -> None: + if node in seen: + if seen[node] == 0: + print(f"Warning: node {node} is part of a cycle!") + return + seen[node] = 0 + if not self.graph.edges_fwd[node]: + leaves.add(node) + else: + for x in self.graph.edges_fwd[node]: + visit(x) + seen[node] = 1 + + for x in self.nodes: + visit(x) + + return leaves + + +@dataclass +class ConnectionGraph: + """ A directed graph of Nodes. """ + + edges_fwd: EdgeMap = field(default_factory=lambda: defaultdict(set)) + edges_rev: EdgeMap = field(default_factory=lambda: defaultdict(set)) + + def add_edge(self, source: Node, sink: Node) -> None: + self.edges_fwd[source].add(sink) + self.edges_rev[sink].add(source) + + def get_components(self) -> List[Component]: + seen: Set[Node] = set() + + def visit(node: Node, component: Component) -> None: + if node in seen: + return + seen.add(node) + + component.nodes.add(node) + if node in self.edges_fwd: + for x in self.edges_fwd[node]: + visit(x, component) + if node in self.edges_rev: + for x in self.edges_rev[node]: + visit(x, component) + + components: List[Component] = [] + for edges in (self.edges_rev, self.edges_fwd): + for node in edges: + if node in seen: + continue + component = Component(self) + visit(node, component) + components.append(component) + + return components + + +# Connection graph generation +def gen_config_graph(chip: pytrellis.Chip, rgraph: pytrellis.RoutingGraph, tiles_by_loc: TilesByLoc) -> ConnectionGraph: + @lru_cache(None) + def get_zero_bit_arcs(chip: pytrellis.Chip, tiletype: str) -> Dict[str, List[str]]: + """Get configurable zero-bit arcs from the given tile. + + tile_cram_to_config ignores zero-bit arcs when generating the TileConfig, + which means that if all bits are unset for a given mux, no connection is + generated at all.""" + locator = pytrellis.TileLocator(chip.info.family, chip.info.name, tiletype) + tilebitdb = pytrellis.get_tile_bitdata(locator) + arcs: Dict[str, List[str]] = defaultdict(list) + for sink in tilebitdb.get_sinks(): + mux_data = tilebitdb.get_mux_data_for_sink(sink) + for arc_name, arc_data in mux_data.arcs.items(): + if len(arc_data.bits.bits) == 0: + arcs[sink].append(arc_name) + return arcs + + def bel_to_node(pos: Tuple[pytrellis.RoutingId, int]) -> Node: + rid, bel_pin = pos + id = Ident.from_id(rgraph, rid.id) + pin = Ident.from_id(rgraph, bel_pin) + return Node(x=rid.loc.x, y=rid.loc.y, id=id, pin=pin) + + def wire_to_node(rid: pytrellis.RoutingId) -> Node: + id = Ident.from_id(rgraph, rid.id) + return Node(x=rid.loc.x, y=rid.loc.y, id=id) + + config_graph = ConnectionGraph() + + for loc in tiles_by_loc: + rtile = rgraph.tiles[pytrellis.Location(loc[0], loc[1])] + for tiledata in tiles_by_loc[loc]: + tile = tiledata.tile + for arc in tiledata.cfg.carcs: + rarc = rtile.arcs[rgraph.ident(f"{arc.source}->{arc.sink}")] + sourcenode = wire_to_node(rarc.source) + sinknode = wire_to_node(rarc.sink) + config_graph.add_edge(sourcenode, sinknode) + + # Expand configuration arcs to include BEL connections and zero-bit arcs + arc_graph = ConnectionGraph() + nodes_seen: Set[Node] = set() + + def visit_node(node: Node, bel_func: Callable[[Node], None]) -> None: + """ Add unconfigurable or implicit arcs to the given node """ + if node in nodes_seen: + return + nodes_seen.add(node) + + try: + rtile = rgraph.tiles[node.loc] + rwire = rtile.wires[node.id.id] + except KeyError: + # there's a handful of troublesome cases which are outside of my control. + # Example: R0C31_G_ULDDRDEL does not exist; it's actually supposed to be the "fixed" + # connection G_ULDDRDEL=>DDRDEL but G_ULDDRDEL is not in the same tile. + print(f"Error: failed to find node {str(node)}", file=sys.stderr) + return + + if node not in config_graph.edges_rev: + # Not configured - possible zero-bit configuration + for tiledata in tiles_by_loc[node.x, node.y]: + arcs = get_zero_bit_arcs(chip, tiledata.tile.info.type) + sources = arcs.get(node.id.label, []) + if not sources: + continue + for source in sources: + sourceid = Ident.from_label(rgraph, source) + sourcenode = Node(x=node.x, y=node.y, id=sourceid) + arc_graph.add_edge(sourcenode, node) + visit_node(sourcenode, bel_func) + + # Add fixed connections + for bel in rwire.belsUphill: + arc_graph.add_edge(bel_to_node(bel), node) + bel_func(wire_to_node(bel[0])) + for bel in rwire.belsDownhill: + arc_graph.add_edge(node, bel_to_node(bel)) + bel_func(wire_to_node(bel[0])) + for routes in [rwire.uphill, rwire.downhill]: + for rarcrid in routes: + rarcname = rgraph.to_str(rarcrid.id) + if "=>" in rarcname: + # => means a fixed (unconfigurable) connection + rarc = rgraph.tiles[rarcrid.loc].arcs[rarcrid.id] + sourcenode = wire_to_node(rarc.source) + sinknode = wire_to_node(rarc.sink) + arc_graph.add_edge(sourcenode, sinknode) + visit_node(sourcenode, bel_func) + visit_node(sinknode, bel_func) + + # Add global (clock) connections - Project Trellis omits a lot of these :( + if node.name.startswith("G_HPBX"): + # TAP_DRIVE -> PLB tile + tap = chip.global_data.get_tap_driver(node.y, node.x) + if tap.dir == pytrellis.TapDir.LEFT: + tap_name = node.name.replace("G_HPBX", "L_HPBX") + else: + tap_name = node.name.replace("G_HPBX", "R_HPBX") + tap_id = Ident.from_label(rgraph, tap_name) + tap_node = Node(x=tap.col, y=node.y, id=tap_id) + arc_graph.add_edge(tap_node, node) + visit_node(tap_node, bel_func) + + elif node.name.startswith("G_VPTX"): + # Spine tile -> TAP_DRIVE + tap = chip.global_data.get_tap_driver(node.y, node.x) + if tap.col == node.x: + # Spine output + quadrant = chip.global_data.get_quadrant(node.y, node.x) + spine = chip.global_data.get_spine_driver(quadrant, node.x) + spine_node = Node(x=spine.second, y=spine.first, id=node.id) + arc_graph.add_edge(spine_node, node) + visit_node(spine_node, bel_func) + + elif node.name.startswith("G_HPRX"): + # Center mux -> spine tile (qqPCLKn -> G_HPRXnn00) + quadrant = chip.global_data.get_quadrant(node.y, node.x) + assert node.name.endswith("00") + clkid = int(node.name[6:-2]) + global_id = Ident.from_label(rgraph, f"G_{quadrant}PCLK{clkid}") + global_node = Node(x=0, y=0, id=global_id) + arc_graph.add_edge(global_node, node) + visit_node(global_node, bel_func) + + # Visit every configured arc and record all BELs seen + bels_todo: Set[Node] = set() + for sourcenode, nodes in config_graph.edges_fwd.items(): + for sinknode in nodes: + arc_graph.add_edge(sourcenode, sinknode) + visit_node(sourcenode, bels_todo.add) + visit_node(sinknode, bels_todo.add) + + # Adding *every* fixed connection is too expensive. + # As a compromise, add any fixed connection connected + # to used BELs. Ignore BELs that don't have any configured + # arcs. + for node in bels_todo: + rtile = rgraph.tiles[node.loc] + for _, rwire in rtile.wires.items(): + wireident = Ident.from_id(rgraph, rwire.id) + wirenode = Node(x=node.x, y=node.y, id=wireident) + for bel in rwire.belsUphill: + if bel[0].id == node.id.id: + arc_graph.add_edge(bel_to_node(bel), wirenode) + visit_node(wirenode, lambda node: None) + for bel in rwire.belsDownhill: + if bel[0].id == node.id.id: + arc_graph.add_edge(wirenode, bel_to_node(bel)) + visit_node(wirenode, lambda node: None) + + return arc_graph + + +# Verilog generation + + +def filter_node(node: Node) -> bool: + if node.pin is None: + # This is a bit extreme, but we assume that all *useful* wires + # go between BELs. + return False + if node.pin_name.startswith("IOLDO") or node.pin_name.startswith("IOLTO"): + # IOLDO/IOLTO are for internal use: + # https://freenode.irclog.whitequark.org/~h~openfpga/2018-12-25#23748701; + # 07:55 <daveshah> kbeckmann: IOLDO and IOLTO are for internal use only + # 07:55 <daveshah> They are for the dedicated interconnect between IOLOGIC and PIO + return False + if node.pin_name == "INDD": + # I don't know what this pin is, but it often appears to be connected to $DI. + # Disabling it because sometimes it ends up in a multi-root configuration with PIO$O, + # which makes it (probably) redundant? + return False + return True + + +@dataclass +class Module: + """ A class to encapsulate a synthesized BEL supported by simulation """ + + module_name: str + tiledata: TileData + pin_map: Dict[str, Node] + + input_pins: ClassVar[List[str]] = [] + output_pins: ClassVar[List[str]] = [] + + @classmethod + def create_from_node(cls, node: Node, tiles_by_loc: TilesByLoc) -> Optional["Module"]: + modcls: Type[Module] + if node.name.startswith("SLICE"): + modcls = SliceModule + tiletype = "PLC2" + elif node.name.startswith("EBR"): + modcls = EBRModule + tiletype = "MIB_EBR" + else: + return None + + for tiledata in tiles_by_loc[node.x, node.y]: + if tiledata.tile.info.type.startswith(tiletype): + break + else: + raise Exception(f"Tile type {tiletype} not found for node {node}") + + return modcls(node.name, tiledata, {}) + + @classmethod + def print_definition(cls) -> None: + """ Print the Verilog code for the module definition """ + raise NotImplementedError() + + def _print_parameters(self, param_renames: Dict[str, str]) -> None: + """ Print the BEL's enums and words as an instance parameter list """ + strs: List[str] = [] + + # Dump enumerations in Verilog-compatible format + for e in self.tiledata.cfg.cenums: + bel, ename = e.name.split(".", 1) + ename = ename.replace(".", "_") + ename = param_renames.get(ename, ename) + if bel == self.module_name: + strs.append(f' .{ename}("{e.value}")') + # Dump initialization words in Verilog format + for w in self.tiledata.cfg.cwords: + bel, ename = w.name.split(".", 1) + ename = ename.replace(".", "_") + ename = param_renames.get(ename, ename) + if bel == self.module_name: + value = [str(int(c)) for c in w.value] + valuestr = "".join(value[::-1]) + strs.append(f" .{ename}({len(value)}'b{valuestr})") + + if strs: + print(",\n".join(strs)) + + def _print_pins(self) -> None: + """ Print the BEL's enums and words as an instance parameter list """ + strs: List[str] = [] + + # Dump input/output pins (already referenced to root pins), inputs first + output_pins = set(self.pin_map.keys()) - set(self.input_pins) + allpins = self.input_pins + natsorted(output_pins) + defpin = "1'b0" + for pin in allpins: + strs.append(f" .{pin}({self.pin_map.get(pin, defpin)})") + + if strs: + print(",\n".join(strs)) + + def print_instance(self, instname: str) -> None: + """ Print the Verilog code for this specific module instance """ + raise NotImplementedError() + + +@dataclass +class SliceModule(Module): + input_pins: ClassVar[List[str]] = [ + "A0", + "B0", + "C0", + "D0", + "A1", + "B1", + "C1", + "D1", + "M0", + "M1", + "FCI", + "FXA", + "FXB", + "CLK", + "LSR", + "CE", + "DI0", + "DI1", + "WD0", + "WD1", + "WAD0", + "WAD1", + "WAD2", + "WAD3", + "WRE", + "WCK", + ] + + output_pins: ClassVar[List[str]] = [ + "F0", + "Q0", + "F1", + "Q1", + "FCO", + "OFX0", + "OFX1", + "WDO0", + "WDO1", + "WDO2", + "WDO3", + "WADO0", + "WADO1", + "WADO2", + "WADO3", + ] + + @classmethod + def print_definition(cls) -> None: + """ Print the Verilog code for the module definition """ + params = [ + "MODE", + "GSR", + "SRMODE", + "CEMUX", + "CLKMUX", + "LSRMUX", + "LUT0_INITVAL", + "LUT1_INITVAL", + "REG0_SD", + "REG1_SD", + "REG0_REGSET", + "REG1_REGSET", + "REG0_LSRMODE", + "REG1_LSRMODE", + "CCU2_INJECT1_0", + "CCU2_INJECT1_1", + "WREMUX", + "WCKMUX", + "A0MUX", + "A1MUX", + "B0MUX", + "B1MUX", + "C0MUX", + "C1MUX", + "D0MUX", + "D1MUX", + ] + + print(f""" +/* Use the cells_sim library from yosys/techlibs/ecp5 */ +`include "../inc/cells_sim.v" +module ECP5_SLICE( + input {", ".join(cls.input_pins)}, + output {", ".join(cls.output_pins)} +); + + /* These defaults correspond to all-zero-bit enumeration values */ + parameter MODE = "LOGIC"; + parameter GSR = "ENABLED"; + parameter SRMODE = "LSR_OVER_CE"; + parameter [127:0] CEMUX = "CE"; + parameter CLKMUX = "CLK"; + parameter LSRMUX = "LSR"; + parameter LUT0_INITVAL = 16'hFFFF; + parameter LUT1_INITVAL = 16'hFFFF; + parameter REG0_SD = "1"; + parameter REG1_SD = "1"; + parameter REG0_REGSET = "SET"; + parameter REG1_REGSET = "SET"; + parameter REG0_LSRMODE = "LSR"; + parameter REG1_LSRMODE = "LSR"; + parameter [127:0] CCU2_INJECT1_0 = "YES"; + parameter [127:0] CCU2_INJECT1_1 = "YES"; + parameter WREMUX = "WRE"; + parameter WCKMUX = "WCK"; + + parameter A0MUX = "A0"; + parameter A1MUX = "A1"; + parameter B0MUX = "B0"; + parameter B1MUX = "B1"; + parameter C0MUX = "C0"; + parameter C1MUX = "C1"; + parameter D0MUX = "D0"; + parameter D1MUX = "D1"; + + TRELLIS_SLICE #( + {", ".join(f".{param}({param})" for param in params)} + ) impl ( + {", ".join(f".{pin}({pin})" for pin in cls.input_pins)}, + {", ".join(f".{pin}({pin})" for pin in cls.output_pins)} + ); +endmodule +""".strip()) + + def print_instance(self, instname: str) -> None: + print("ECP5_SLICE #(") + self._print_parameters( + { + "K0_INIT": "LUT0_INITVAL", + "K1_INIT": "LUT1_INITVAL", + } + ) + print(f") {instname} (") + self._print_pins() + print(");") + print() + + +class EBRModule(Module): + input_pins: ClassVar[List[str]] = [ + # Byte Enable wires + "ADA0", + "ADA1", + "ADA2", + "ADA3", + # ADW + "ADA5", + "ADA6", + "ADA7", + "ADA8", + "ADA9", + "ADA10", + "ADA11", + "ADA12", + "ADA13", + # ADR + "ADB5", + "ADB6", + "ADB7", + "ADB8", + "ADB9", + "ADB10", + "ADB11", + "ADB12", + "ADB13", + "CEB", # CER + "CLKA", # CLKW + "CLKB", # CLKR + # DI + "DIA0", + "DIA1", + "DIA2", + "DIA3", + "DIA4", + "DIA5", + "DIA6", + "DIA7", + "DIA8", + "DIA9", + "DIA10", + "DIA11", + "DIA12", + "DIA13", + "DIA14", + "DIA15", + "DIA16", + "DIA17", + "DIB0", + "DIB1", + "DIB2", + "DIB3", + "DIB4", + "DIB5", + "DIB6", + "DIB7", + "DIB8", + "DIB9", + "DIB10", + "DIB11", + "DIB12", + "DIB13", + "DIB14", + "DIB15", + "DIB16", + "DIB17", + ] + + output_pins: ClassVar[List[str]] = [ + # DO + "DOA0", + "DOA1", + "DOA2", + "DOA3", + "DOA4", + "DOA5", + "DOA6", + "DOA7", + "DOA8", + "DOA9", + "DOA10", + "DOA11", + "DOA12", + "DOA13", + "DOA14", + "DOA15", + "DOA16", + "DOA17", + "DOB0", + "DOB1", + "DOB2", + "DOB3", + "DOB4", + "DOB5", + "DOB6", + "DOB7", + "DOB8", + "DOB9", + "DOB10", + "DOB11", + "DOB12", + "DOB13", + "DOB14", + "DOB15", + "DOB16", + "DOB17", + ] + + @classmethod + def print_definition(cls) -> None: + """ Print the Verilog code for the module definition """ + print(f""" +module ECP5_EBR( + input {", ".join(cls.input_pins)}, + output {", ".join(cls.output_pins)} +); + + /* These defaults correspond to all-zero-bit enumeration values */ + parameter CSDECODE_A = 3'b111; + parameter CSDECODE_B = 3'b111; + parameter ADA0MUX = "ADA0"; + parameter ADA2MUX = "ADA2"; + parameter ADA3MUX = "ADA3"; + parameter ADB0MUX = "ADB0"; + parameter ADB1MUX = "ADB1"; + parameter CEAMUX = "CEA"; + parameter CEBMUX = "CEB"; + parameter CLKAMUX = "CLKA"; + parameter CLKBMUX = "CLKB"; + parameter DP16KD_DATA_WIDTH_A = "18"; + parameter DP16KD_DATA_WIDTH_B = "18"; + parameter DP16KD_WRITEMODE_A = "NORMAL"; + parameter DP16KD_WRITEMODE_B = "NORMAL"; + parameter MODE = "NONE"; + parameter OCEAMUX = "OCEA"; + parameter OCEBMUX = "OCEB"; + parameter PDPW16KD_DATA_WIDTH_R = "18"; + parameter PDPW16KD_RESETMODE = "SYNC"; + parameter WEAMUX = "WEA"; + parameter WEBMUX = "WEB"; + + /* TODO! */ + +endmodule +""".strip()) + + def print_instance(self, instname: str) -> None: + print("ECB5_EBR #(") + self._print_parameters({}) + print(f") {instname} (") + self._print_pins() + print(");") + print() + + +def print_verilog(graph: ConnectionGraph, tiles_by_loc: TilesByLoc) -> None: + # Extract connected components and their roots & leaves + sorted_components: List[Tuple[Component, List[Node], List[Node]]] = [] + for component in graph.get_components(): + roots = sorted([node for node in component.get_roots() if filter_node(node)]) + if not roots: + continue + leaves = sorted([node for node in component.get_leaves() if filter_node(node)]) + if not leaves: + continue + sorted_components.append((component, roots, leaves)) + sorted_components = sorted(sorted_components, key=lambda x: x[1][0]) + + # Verilog input, output, and external wires + mod_sources: Set[Node] = set() + mod_sinks: Dict[Node, Node] = {} + mod_globals: Set[Node] = set() + + def print_component(roots: Sequence[Node]) -> None: + def visit(node: Node, level: int) -> None: + print(" " * level, node, sep="") + for x in graph.edges_fwd[node]: + visit(x, level + 1) + + for root in roots: + visit(root, 0) + + modules: Dict[str, Module] = {} + + print("/* Automatically generated by ecp_vlog.py") + for component, roots, leaves in sorted_components: + if len(roots) > 1: + print() + print("Unhandled multi-root component:") + print(*roots, sep=", ") + print(" -> ", end="") + print(*leaves, sep=", ") + continue + + mod_sources.add(roots[0]) + for node in leaves: + mod_sinks[node] = roots[0] + for node in roots + leaves: + if node.mod_name in modules: + modules[node.mod_name].pin_map[node.pin_name] = roots[0] + continue + + mod_def = Module.create_from_node(node, tiles_by_loc) + if not mod_def: + mod_globals.add(node) + continue + mod_def.pin_map[node.pin_name] = roots[0] + modules[node.mod_name] = mod_def + + # filter out any globals that are just copies of inputs or other outputs + for node in mod_globals: + if node in mod_sinks and mod_sinks[node] in mod_globals: + print(f"filtered out useless output: {mod_sinks[node]} -> {node}") + del mod_sinks[node] + all_sources: Set[Node] = set() + for sink in mod_sinks: + all_sources.add(mod_sinks[sink]) + for node in mod_globals: + if node in mod_sources and node not in all_sources: + print(f"filtered out useless input: {node}") + mod_sources.discard(node) + print("*/") + + for mod_type in set(type(mod_def) for mod_def in modules.values()): + mod_type.print_definition() + + print("module top(") + mod_globals_vars = [" input wire " + str(node) for node in mod_sources & mod_globals] + mod_globals_vars += [" output wire " + str(node) for node in set(mod_sinks) & mod_globals] + print(",\n".join(natsorted(mod_globals_vars))) + print(");") + print() + + # sources are either connected to global inputs + # or are outputs from some other node + for node in natsorted(mod_sources - mod_globals, key=str): + print(f"wire {node};") + print() + + # sinks are either fed directly into a BEL, + # in which case they are directly substituted, + # or they are global outputs + for node in natsorted(set(mod_sinks) & mod_globals, key=str): + print(f"assign {node} = {mod_sinks[node]};") + print() + + for modname in natsorted(modules): + modules[modname].print_instance(modname) + + # debugging: print out any enums or words that we didn't handle in a Module + print("/* Unhandled enums/words:") + seen_enums: Set[Tuple[pytrellis.TileConfig, int]] = set() + seen_words: Set[Tuple[pytrellis.TileConfig, int]] = set() + for module in modules.values(): + for i, e in enumerate(module.tiledata.cfg.cenums): + bel, _ = e.name.split(".", 1) + if bel == module.module_name: + seen_enums.add((module.tiledata.cfg, i)) + for i, w in enumerate(module.tiledata.cfg.cwords): + bel, _ = w.name.split(".", 1) + if bel == module.module_name: + seen_words.add((module.tiledata.cfg, i)) + for loc in sorted(tiles_by_loc.keys(), key=lambda loc: (loc[1], loc[0])): + for tiledata in tiles_by_loc[loc]: + for i, e in enumerate(tiledata.cfg.cenums): + if (tiledata.cfg, i) not in seen_enums: + print(" ", tiledata.tile.info.name, "enum:", e.name, e.value) + for i, w in enumerate(tiledata.cfg.cwords): + if (tiledata.cfg, i) not in seen_words: + valuestr = "".join([str(int(c)) for c in w.value][::-1]) + print(" ", tiledata.tile.info.name, "word:", w.name, valuestr) + print("*/") + print("endmodule") + + +def main(argv: List[str]) -> None: + import argparse + + parser = argparse.ArgumentParser("Convert a .bit file into a .v verilog file for simulation") + + parser.add_argument("bitfile", help="Input .bit file") + args = parser.parse_args(argv) + + pytrellis.load_database(database.get_db_root()) + + bitstream = pytrellis.Bitstream.read_bit(args.bitfile) + chip = bitstream.deserialise_chip() + rgraph = chip.get_routing_graph() + + tiles_by_loc = make_tiles_by_loc(chip) + graph = gen_config_graph(chip, rgraph, tiles_by_loc) + print_verilog(graph, tiles_by_loc) + + +if __name__ == "__main__": + main(sys.argv[1:])