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:])