Merge pull request #168 from nneonneo/master

Add a tool for automatic bitstream -> Verilog conversion
diff --git a/tools/ecp_vlog.py b/tools/ecp_vlog.py
new file mode 100644
index 0000000..f125770
--- /dev/null
+++ b/tools/ecp_vlog.py
@@ -0,0 +1,1046 @@
+import os
+import re
+import sys
+from collections import defaultdict
+from dataclasses import dataclass, field
+from functools import lru_cache
+from typing import Callable, ClassVar, Dict, List, Optional, 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
+    mod_name_map: ClassVar[Dict[str, str]] = {}
+
+    @property
+    def loc(self) -> pytrellis.Location:
+        return pytrellis.Location(self.x, self.y)
+
+    @property
+    def mod_name(self) -> str:
+        res = f"R{self.y}C{self.x}_{self.name}"
+        return res
+
+    @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:
+        mod_name = self.mod_name
+        pin_name = self.pin_name
+        res = self.mod_name_map.get(mod_name, mod_name)
+        if pin_name:
+            res += "$" + 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!", file=sys.stderr)
+                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!", file=sys.stderr)
+                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)
+
+    def _get_enum_value(cfg: pytrellis.TileConfig, enum_name: str) -> Optional[str]:
+        for cenum in cfg.cenums:
+            if cenum.name == enum_name:
+                return cenum.value
+        return None
+
+    def _filter_data_pin(node: Node) -> bool:
+        # IOLOGIC[AC].[RT]XDATA[456] are mutually exclusive with IOLOGIC[BD].[RT]XDATA[0123],
+        # depending on whether 7:1 gearing is used, becacuse 7:1 gearing occupies two adjacent
+        # IOLOGIC units (A+B or C+D). Because they're mutually exclusive, some of the pins are
+        # hardwired together (e.g. 4A and 0B). To avoid a multi-root situation and spurious
+        # inputs/outputs, we need to pick which set to include based on the IO configuration.
+
+        bel_id = node.mod_name[-1]
+        assert bel_id in "ABCD"
+        pin_id = node.pin_name[-1]
+        assert pin_id in "0123456"
+
+        if bel_id in "AC" and pin_id in "0123":
+            # These pins are unconflicted
+            return True
+
+        if bel_id in "AB":
+            tiles = tiles_by_loc[node.x, node.y]
+            main_mod = "IOLOGICA"
+        else:
+            # HACK: The IOLOGICC enums seem to be the PIC[LR]2 tiles,
+            # which appear to always be exactly two tiles down from
+            # the PIC[LR]0 tiles where the actual pins are.
+            # This seems very fragile.
+            tiles = tiles_by_loc[node.x, node.y + 2]
+            main_mod = "IOLOGICC"
+
+        # Make sure we get the right tile on the tile location
+        for tiledata in tiles:
+            if any(site.type == main_mod for site in tiledata.tile.info.sites):
+                break
+        else:
+            print("error: could not locate IOLOGIC enums", file=sys.stderr)
+            return True
+
+        if node.pin_name.startswith("RX"):
+            is_71_mode = _get_enum_value(tiledata.cfg, main_mod + "IDDRXN.MODE") == "IDDR71"
+        else:
+            is_71_mode = _get_enum_value(tiledata.cfg, main_mod + "ODDRXN.MODE") == "ODDR71"
+
+        # Note that [456][BD] do not exist.
+        if pin_id in "456" and is_71_mode:
+            return True
+        elif pin_id in "0123" and not is_71_mode:
+            return True
+        return False
+
+    def add_edge(graph: ConnectionGraph, sourcenode: Node, sinknode: Node) -> None:
+        """ Add an edge subject to special-case filtering """
+
+        if re.match(r"^F[5X][ABCD]_SLICE$", sourcenode.name) and re.match(r"^F\d$", sinknode.name):
+            # Some of the -> Fn muxes use the same bits as the CCU2.INJECT enums.
+            # In CCU2 mode, these muxes should be fixed to Fn_SLICE -> Fn, and should
+            # not be set to F[5X] -> Fn no matter what the value of the mux bits are
+            # (since they represent CCU2_INJECT instead)
+            enum_name = f"SLICE{sourcenode.name[2]}.MODE"
+            for tiledata in tiles_by_loc[sourcenode.x, sinknode.y]:
+                if tiledata.tile.info.type.startswith("PLC2") and _get_enum_value(tiledata.cfg, enum_name) == "CCU2":
+                    # CCU2: correct F[5X]n_SLICE connection to Fn_SLICE -> Fn
+                    newsource = Ident.from_label(rgraph, sinknode.name + "_SLICE")
+                    sourcenode = Node(x=sourcenode.x, y=sourcenode.y, id=newsource)
+                    break
+        elif sourcenode.pin_name.startswith("RXDATA") and not _filter_data_pin(sourcenode):
+            # See comment in _filter_data_pin
+            return
+        elif sinknode.pin_name.startswith("TXDATA") and not _filter_data_pin(sinknode):
+            # See comment in _filter_data_pin
+            return
+
+        graph.add_edge(sourcenode, sinknode)
+
+    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)
+                add_edge(config_graph, 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)
+                    add_edge(arc_graph, sourcenode, node)
+                    visit_node(sourcenode, bel_func)
+
+        # Add fixed connections
+        for bel in rwire.belsUphill:
+            add_edge(arc_graph, bel_to_node(bel), node)
+            bel_func(wire_to_node(bel[0]))
+        for bel in rwire.belsDownhill:
+            add_edge(arc_graph, 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)
+                    add_edge(arc_graph, 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)
+            add_edge(arc_graph, 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)
+                add_edge(arc_graph, 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)
+            add_edge(arc_graph, 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:
+            add_edge(arc_graph, 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:
+                    add_edge(arc_graph, bel_to_node(bel), wirenode)
+                    visit_node(wirenode, lambda node: None)
+            for bel in rwire.belsDownhill:
+                if bel[0].id == node.id.id:
+                    add_edge(arc_graph, 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:
+        # We assume that all *useful* wires go between BELs.
+        return False
+    if "_ECLKSYNC" in node.mod_name:
+        # ECLKSYNC BELs appear to basically coincide with ECLKBUF BELs, making them redundant
+        # for the purposes of Verilog generation.
+        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
+        # Since we don't currently implement I/O modules, these pins do not
+        # need to be exported to Verilog.
+        return False
+    if node.pin_name == "INDD":
+        # INDD is the input after the delay block. This is currently redundant because
+        # the input source (PIO$O) will be exposed as an independent input, so the module's
+        # caller can simply hard-code an appropriate delay to the module input.
+        # If the I/O modules are ever implemented, it will be necessary to disambiguate
+        # PIO$O from INDD for the IOLOGIC$DI input to avoid a multi-root situation.
+        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
+        pin_map_pins = set(self.pin_map.keys())
+        all_input_pins = set(self.input_pins)
+        output_pins = natsorted(pin_map_pins - all_input_pins)
+        input_pins = natsorted(pin_map_pins & all_input_pins)
+        for pin in input_pins + output_pins:
+            strs.append(f"  .{pin}( {self.pin_map[pin]} )")
+
+        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"""
+/* This module requires the cells_sim library from yosys/techlibs/ecp5/cells.sim.v
+   for the TRELLIS_SLICE definition. Include that cell library before including this
+   file. */
+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("ECP5_EBR #(")
+        self._print_parameters({})
+        print(f") {instname} (")
+        self._print_pins()
+        print(");")
+        print()
+
+
+def print_verilog(graph: ConnectionGraph, tiles_by_loc: TilesByLoc, top_name: str) -> 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()
+
+    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 passed-through 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 unused 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(f"module {top_name}(")
+    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 parse_lpf(filename: str) -> Dict[str, str]:
+    import shlex
+
+    lines = []
+    with open(filename, "r") as f:
+        for row in f:
+            row = row.split("#", 1)[0].split("//", 1)[0].strip()
+            if row:
+                lines.append(row)
+
+    sites: Dict[str, str] = {}
+
+    commands = " ".join(lines).split(";")
+    for cmd in commands:
+        cmd = cmd.strip()
+        if not cmd:
+            continue
+
+        words = shlex.split(cmd)
+        if words[0] == "LOCATE":
+            if len(words) != 5 or words[1] != "COMP" or words[3] != "SITE":
+                print("ignoring malformed LOCATE in LPF:", cmd, file=sys.stderr)
+            sites[words[4]] = words[2]
+
+    return sites
+
+
+def main(argv: List[str]) -> None:
+    import argparse
+    import json
+
+    parser = argparse.ArgumentParser("Convert a .bit file into a .v verilog file for simulation")
+
+    parser.add_argument("bitfile", help="Input .bit file")
+    parser.add_argument("--package", help="Physical package (e.g. CABGA256), for renaming I/O ports")
+    parser.add_argument("--lpf", help="Use LOCATE COMP commands from this LPF file to name I/O ports")
+    parser.add_argument("-n", "--module-name", help="Name for the top-level module (default: top)", default="top")
+    args = parser.parse_args(argv)
+
+    if args.lpf and not args.package:
+        parser.error("Cannot use a LPF file without specifying the chip package")
+
+    pytrellis.load_database(database.get_db_root())
+
+    print("Loading bitstream...", file=sys.stderr)
+    bitstream = pytrellis.Bitstream.read_bit(args.bitfile)
+    chip = bitstream.deserialise_chip()
+
+    if args.package:
+        dbfn = os.path.join(database.get_db_subdir(chip.info.family, chip.info.name), "iodb.json")
+        with open(dbfn, "r") as f:
+            iodb = json.load(f)
+
+        if args.lpf:
+            lpf_map = parse_lpf(args.lpf)
+        else:
+            lpf_map = {}
+
+        # Rename PIO and IOLOGIC BELs based on their connected pins, for readability
+        mod_renames = {}
+        for pin_name, pin_data in iodb["packages"][args.package].items():
+            if pin_name in lpf_map:
+                # escape LPF name in case it has funny characters
+                pin_name = "\\" + lpf_map[pin_name]
+            # PIO and IOLOGIC do not share pin names except for IOLDO/IOLTO
+            mod_renames["R{row}C{col}_PIO{pio}".format(**pin_data)] = f"{pin_name}"
+            mod_renames["R{row}C{col}_IOLOGIC{pio}".format(**pin_data)] = f"{pin_name}"
+
+        # Note: the mod_name_map only affects str(node), not node.mod_name
+        Node.mod_name_map = mod_renames
+
+    print("Computing routing graph...", file=sys.stderr)
+    rgraph = chip.get_routing_graph()
+
+    print("Computing connection graph...", file=sys.stderr)
+    tiles_by_loc = make_tiles_by_loc(chip)
+    graph = gen_config_graph(chip, rgraph, tiles_by_loc)
+
+    print("Generating Verilog...", file=sys.stderr)
+    print_verilog(graph, tiles_by_loc, args.module_name)
+
+    print("Done!", file=sys.stderr)
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])