ecp_vlog: Fix special edge cases for OFXn and RXDATA{4,5,6}.

Tested and working with the "Ethernet from above" challenge file.
diff --git a/tools/ecp_vlog.py b/tools/ecp_vlog.py
index af49233..3df23af 100644
--- a/tools/ecp_vlog.py
+++ b/tools/ecp_vlog.py
@@ -1,9 +1,10 @@
 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, Sequence, Set, Tuple, Type
+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)
@@ -82,7 +83,7 @@
     x: int
     id: Ident
     pin: Optional[Ident] = None
-    mod_name_map: ClassVar[Optional[Dict[str, str]]] = None
+    mod_name_map: ClassVar[Dict[str, str]] = {}
 
     @property
     def loc(self) -> pytrellis.Location:
@@ -91,9 +92,7 @@
     @property
     def mod_name(self) -> str:
         res = f"R{self.y}C{self.x}_{self.name}"
-        if self.mod_name_map:
-            return self.mod_name_map.get(res, res)
-        return res
+        return self.mod_name_map.get(res, res)
 
     @property
     def name(self) -> str:
@@ -127,7 +126,7 @@
         def visit(node: Node) -> None:
             if node in seen:
                 if seen[node] == 0:
-                    print(f"Warning: node {node} is part of a cycle!")
+                    print(f"Warning: node {node} is part of a cycle!", file=sys.stderr)
                 return
             seen[node] = 0
             if not self.graph.edges_rev[node]:
@@ -149,7 +148,7 @@
         def visit(node: Node) -> None:
             if node in seen:
                 if seen[node] == 0:
-                    print(f"Warning: node {node} is part of a cycle!")
+                    print(f"Warning: node {node} is part of a cycle!", file=sys.stderr)
                 return
             seen[node] = 0
             if not self.graph.edges_fwd[node]:
@@ -233,6 +232,83 @@
         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:
@@ -243,7 +319,7 @@
                 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)
+                add_edge(config_graph, sourcenode, sinknode)
 
     # Expand configuration arcs to include BEL connections and zero-bit arcs
     arc_graph = ConnectionGraph()
@@ -275,15 +351,15 @@
                 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)
+                    add_edge(arc_graph, sourcenode, node)
                     visit_node(sourcenode, bel_func)
 
         # Add fixed connections
         for bel in rwire.belsUphill:
-            arc_graph.add_edge(bel_to_node(bel), node)
+            add_edge(arc_graph, 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))
+            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:
@@ -293,7 +369,7 @@
                     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)
+                    add_edge(arc_graph, sourcenode, sinknode)
                     visit_node(sourcenode, bel_func)
                     visit_node(sinknode, bel_func)
 
@@ -307,7 +383,7 @@
                 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)
+            add_edge(arc_graph, tap_node, node)
             visit_node(tap_node, bel_func)
 
         elif node.name.startswith("G_VPTX"):
@@ -318,7 +394,7 @@
                 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)
+                add_edge(arc_graph, spine_node, node)
                 visit_node(spine_node, bel_func)
 
         elif node.name.startswith("G_HPRX"):
@@ -328,14 +404,14 @@
             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)
+            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:
-            arc_graph.add_edge(sourcenode, sinknode)
+            add_edge(arc_graph, sourcenode, sinknode)
             visit_node(sourcenode, bels_todo.add)
             visit_node(sinknode, bels_todo.add)
 
@@ -350,11 +426,11 @@
             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)
+                    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:
-                    arc_graph.add_edge(wirenode, bel_to_node(bel))
+                    add_edge(arc_graph, wirenode, bel_to_node(bel))
                     visit_node(wirenode, lambda node: None)
 
     return arc_graph
@@ -363,8 +439,11 @@
 # 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.
+        # 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:
@@ -543,7 +622,8 @@
         print(
             f"""
 /* Use the cells_sim library from yosys/techlibs/ecp5 */
-`include "../inc/cells_sim.v"
+`define NO_INCLUDES 1
+`include "cells_sim.v"
 module ECP5_SLICE(
     input {", ".join(cls.input_pins)},
     output {", ".join(cls.output_pins)}
@@ -752,7 +832,7 @@
         )
 
     def print_instance(self, instname: str) -> None:
-        print("ECB5_EBR #(")
+        print("ECP5_EBR #(")
         self._print_parameters({})
         print(f") {instname} (")
         self._print_pins()
@@ -778,15 +858,6 @@
     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")
@@ -817,14 +888,14 @@
     # 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}")
+            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 useless input: {node}")
+            print(f"filtered out unused input: {node}")
             mod_sources.discard(node)
     print("*/")
 
@@ -892,6 +963,7 @@
 
     pytrellis.load_database(database.get_db_root())
 
+    print("Loading bitstream...", file=sys.stderr)
     bitstream = pytrellis.Bitstream.read_bit(args.bitfile)
     chip = bitstream.deserialise_chip()
 
@@ -908,12 +980,18 @@
 
         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)
 
+    print("Done!", file=sys.stderr)
+
 
 if __name__ == "__main__":
     main(sys.argv[1:])