Merge remote-tracking branch 'origin' into facade
diff --git a/.gitmodules b/.gitmodules
index e837083..d01ee12 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "database"]
 	path = database
-	url = https://github.com/SymbiFlow/prjtrellis-db
+	url = https://github.com/cr1901/prjtrellis-db.git 
+	branch = facade
diff --git a/database b/database
index d0b219a..2d3b111 160000
--- a/database
+++ b/database
@@ -1 +1 @@
-Subproject commit d0b219af41ae3da6150645fbc5cc5613b530603f
+Subproject commit 2d3b111146f68cc788f0806fca6888f58beabd54
diff --git a/devices.json b/devices.json
index a2095be..4d79d6e 100644
--- a/devices.json
+++ b/devices.json
@@ -12,7 +12,7 @@
                 "max_row" : 50,
                 "max_col" : 72,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5U-45F": {
                 "packages": ["csfBGA285", "caBGA256", "caBGA381", "caBGA554", "caBGA756"],
@@ -24,7 +24,7 @@
                 "max_row" : 71,
                 "max_col" : 90,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5U-85F": {
                 "packages": ["csfBGA285", "caBGA381", "caBGA554", "caBGA756"],
@@ -36,7 +36,7 @@
                 "max_row" : 95,
                 "max_col" : 126,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM-25F": {
                 "packages": ["csfBGA285", "caBGA256", "caBGA381", "caBGA554", "caBGA756"],
@@ -48,7 +48,7 @@
                 "max_row" : 50,
                 "max_col" : 72,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM-45F": {
                 "packages": ["csfBGA285", "caBGA256", "caBGA381", "caBGA554", "caBGA756"],
@@ -60,7 +60,7 @@
                 "max_row" : 71,
                 "max_col" : 90,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM-85F": {
                 "packages": ["csfBGA285", "caBGA381", "caBGA554", "caBGA756"],
@@ -72,7 +72,7 @@
                 "max_row" : 95,
                 "max_col" : 126,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM5G-25F": {
                 "packages": ["csfBGA285", "caBGA256", "caBGA381", "caBGA554", "caBGA756"],
@@ -84,7 +84,7 @@
                 "max_row" : 50,
                 "max_col" : 72,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM5G-45F": {
                 "packages": ["csfBGA285", "caBGA256", "caBGA381", "caBGA554", "caBGA756"],
@@ -96,7 +96,7 @@
                 "max_row" : 71,
                 "max_col" : 90,
                 "col_bias" : 0,
-                "fuzz": 1
+                "fuzz": 0
             },
             "LFE5UM5G-85F": {
                 "packages": ["csfBGA285", "caBGA381", "caBGA554", "caBGA756"],
@@ -108,6 +108,35 @@
                 "max_row" : 95,
                 "max_col" : 126,
                 "col_bias" : 0,
+                "fuzz": 0
+            }
+        }
+    },
+
+    "MachXO2" : {
+        "devices" : {
+            "LCMXO2-256HC": {
+                "packages": ["QFN32"],
+                "idcode": "0x012b8043",
+                "frames": 186,
+                "bits_per_frame": 504,
+                "pad_bits_after_frame": 0,
+                "pad_bits_before_frame": 0,
+                "max_row" : 7,
+                "max_col" : 9,
+                "col_bias" : 1,
+                "fuzz": 1
+            },
+            "LCMXO2-1200HC": {
+                "packages": ["QFN32"],
+                "idcode": "0x012ba043",
+                "frames": 333,
+                "bits_per_frame": 1080,
+                "pad_bits_after_frame": 0,
+                "pad_bits_before_frame": 0,
+                "max_row" : 12,
+                "max_col" : 21,
+                "col_bias" : 1,
                 "fuzz": 1
             }
         }
diff --git a/diamond.sh b/diamond.sh
index b23aff5..97928e4 100755
--- a/diamond.sh
+++ b/diamond.sh
@@ -17,15 +17,40 @@
 # You need to set the DIAMONDDIR environment variable to the path where you have
 # installed Lattice Diamond, unless it matches this default.
 
-diamonddir="${DIAMONDDIR:-/usr/local/diamond/3.10_x64}"
+if [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
+	WINDOWS=true
+else
+	WINDOWS=false
+fi
+
+if $WINDOWS; then
+	diamonddir="${DIAMONDDIR:-/c/lscc/diamond/3.10_x64}"
+else
+	diamonddir="${DIAMONDDIR:-/usr/local/diamond/3.10_x64}"
+fi
 export FOUNDRY="${diamonddir}/ispfpga"
-bindir="${diamonddir}/bin/lin64"
+
+if $WINDOWS; then
+	bindir="${diamonddir}/bin/nt64"
+else
+	bindir="${diamonddir}/bin/lin64"
+fi
 LSC_DIAMOND=true
 export LSC_DIAMOND
 export NEOCAD_MAXLINEWIDTH=32767
 export TCL_LIBRARY="${diamonddir}/tcltk/lib/tcl8.5"
-export fpgabindir=${FOUNDRY}/bin/lin64
-export LD_LIBRARY_PATH="${bindir}:${fpgabindir}"
+
+if $WINDOWS; then
+	export fpgabindir=${FOUNDRY}/bin/nt64
+else
+	export fpgabindir=${FOUNDRY}/bin/lin64
+fi
+
+if $WINDOWS; then
+	export PATH="${bindir}:${fpgabindir}:$PATH"
+else
+	export LD_LIBRARY_PATH="${bindir}:${fpgabindir}"
+fi
 export LM_LICENSE_FILE="${diamonddir}/license/license.dat"
 
 set -ex
@@ -92,6 +117,16 @@
 		LSE_ARCH="ECP5UM5G"
 		;;
 
+	LCMXO2-256HC)
+		PACKAGE="${DEV_PACKAGE:-QFN32}"
+		DEVICE="LCMXO2-256HC"
+		LSE_ARCH="MachXO2"
+		;;
+	LCMXO2-1200HC)
+		PACKAGE="${DEV_PACKAGE:-QFN32}"
+		DEVICE="LCMXO2-1200HC"
+		LSE_ARCH="MachXO2"
+		;;
 	LCMXO2-2000HC)
 		PACKAGE="${DEV_PACKAGE:-TQFP100}"
 		DEVICE="LCMXO2-2000HC"
@@ -134,7 +169,12 @@
 touch input.lpf
 
 if [ -n "$USE_NCL" ]; then
-"$FOUNDRY"/userware/unix/bin/lin64/ncl2ncd input.ncl -drc -o par_impl.ncd
+
+if $WINDOWS; then
+	"$FOUNDRY"/userware/NT/bin/nt64/ncl2ncd input.ncl -drc -o par_impl.ncd
+else
+	"$FOUNDRY"/userware/unix/bin/lin64/ncl2ncd input.ncl -drc -o par_impl.ncd
+fi
 
 if test -f "input.prf"; then
 cp "input.prf" "synth_impl.prf"
@@ -142,6 +182,7 @@
 touch synth_impl.prf
 fi
 
+
 else
 cat > impl_lse.prj << EOT
 #device
@@ -191,6 +232,9 @@
 
 fi
 
+# Forcefully disable compression
+echo "SYSCONFIG COMPRESS_CONFIG=OFF ;" >> synth_impl.prf
+
 # make bitmap
 "$fpgabindir"/bitgen -d par_impl.ncd $BITARGS output.bit synth_impl.prf
 
@@ -202,9 +246,14 @@
 "$fpgabindir"/bstool -t output.bit > output.test
 
 # convert ngd to ncl
-"$FOUNDRY"/userware/unix/bin/lin64/ncd2ncl par_impl.ncd output.ncl
+if $WINDOWS; then
+	"$FOUNDRY"/userware/NT/bin/nt64/ncd2ncl par_impl.ncd output.ncl
+else
+	"$FOUNDRY"/userware/unix/bin/lin64/ncd2ncl par_impl.ncd output.ncl
+fi
 
 fi
+
 if [ -z "$NO_TRCE" ]; then
 # run trce
 "$fpgabindir"/trce -v -u -c  par_impl.ncd
@@ -228,4 +277,4 @@
 fi
 if [ -n "$BACKANNO" ]; then
 cp "$2.tmp"/par_impl.sdf "$2.sdf"
-fi
\ No newline at end of file
+fi
diff --git a/diamond_tcl.sh b/diamond_tcl.sh
index e7172b6..5dac379 100755
--- a/diamond_tcl.sh
+++ b/diamond_tcl.sh
@@ -1,14 +1,44 @@
 #!/bin/bash
 
-# Script to start a Diamond ispTcl consoke
-diamonddir="${DIAMONDDIR:-/usr/local/diamond/3.10_x64}"
+# Script to start a Diamond ispTcl console
+if [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
+	WINDOWS=true
+else
+	WINDOWS=false
+fi
+
+if $WINDOWS; then
+	diamonddir="${DIAMONDDIR:-/c/lscc/diamond/3.10_x64}"
+else
+	diamonddir="${DIAMONDDIR:-/usr/local/diamond/3.10_x64}"
+fi
 export FOUNDRY="${diamonddir}/ispfpga"
-bindir="${diamonddir}/bin/lin64"
+
+if $WINDOWS; then
+	bindir="${diamonddir}/bin/nt64"
+else
+	bindir="${diamonddir}/bin/lin64"
+fi
 LSC_DIAMOND=true
 export LSC_DIAMOND
 export NEOCAD_MAXLINEWIDTH=32767
 export TCL_LIBRARY="${diamonddir}/tcltk/lib/tcl8.5"
-export fpgabindir=${FOUNDRY}/bin/lin64
-export LD_LIBRARY_PATH="${bindir}:${fpgabindir}"
+
+if $WINDOWS; then
+	export fpgabindir=${FOUNDRY}/bin/nt64
+else
+	export fpgabindir=${FOUNDRY}/bin/lin64
+fi
+
+if $WINDOWS; then
+	export PATH="${bindir}:${fpgabindir}:$PATH"
+else
+	export LD_LIBRARY_PATH="${bindir}:${fpgabindir}"
+fi
 export LM_LICENSE_FILE="${diamonddir}/license/license.dat"
-$FOUNDRY/userware/unix/bin/lin64/ispTcl $1
+
+if $WINDOWS; then
+    $FOUNDRY/userware/NT/bin/nt64/ispTcl $1
+else
+    $FOUNDRY/userware/unix/bin/lin64/ispTcl $1
+fi
diff --git a/experiments/machxo2/findnets/findnets.py b/experiments/machxo2/findnets/findnets.py
new file mode 100644
index 0000000..603ec37
--- /dev/null
+++ b/experiments/machxo2/findnets/findnets.py
@@ -0,0 +1,35 @@
+import sys
+from collections import defaultdict
+
+from fuzzconfig import FuzzConfig
+import pytrellis
+import isptcl
+
+
+def main(row, col):
+    pytrellis.load_database("../../../database")
+
+    cfg = FuzzConfig(job="FINDNETS_R{}C{}".format(row, col), family="MachXO2", device="LCMXO2-1200HC", ncl="plc2route.ncl", tiles=[])
+    cfg.setup()
+
+    netdata = isptcl.get_wires_at_position(cfg.ncd_prf, (row, col))
+    netnames = [x[0] for x in netdata]
+    arcs = isptcl.get_arcs_on_wires(cfg.ncd_prf, netnames, False, defaultdict(lambda : str("mark")))
+
+    ambiguous_arcs = list()
+    for (k, v) in arcs.items():
+        for c in v:
+            if isinstance(c, isptcl.AmbiguousArc):
+                # ISPTcl always puts queried net on RHS
+                ambiguous_arcs.append(c)
+
+    with open("r{}c{}.txt".format(row, col), "w") as fp:
+        for a in ambiguous_arcs:
+            fp.write(str(a) + "\n")
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        print("Usage: {} [row] [col]".format(sys.argv[0]))
+    else:
+        main(sys.argv[1], sys.argv[2])
diff --git a/experiments/machxo2/findnets/plc2route.ncl b/experiments/machxo2/findnets/plc2route.ncl
new file mode 100644
index 0000000..7e7370b
--- /dev/null
+++ b/experiments/machxo2/findnets/plc2route.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, A0);
+      ${route}
+   }
+}
diff --git a/experiments/machxo2/interconnect_poc/fuzz_single_mux.py b/experiments/machxo2/interconnect_poc/fuzz_single_mux.py
new file mode 100644
index 0000000..af81853
--- /dev/null
+++ b/experiments/machxo2/interconnect_poc/fuzz_single_mux.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+import os
+from os import path
+import shutil
+
+import diamond
+from string import Template
+import pytrellis
+
+device = "LCMXO2-1200HC"
+sink = "R2C2_A0"
+# Drivers found using ispTcl
+drivers = [
+    "R1C2_V02S0501",
+    # "R2C2_H02W0501",
+    # "R2C2_H01E0001",
+    # "R2C3_H02W0701",
+    # "R2C3_H02W0501",
+    # "R2C2_H02W0701",
+    # "R2C2_H02E0501",
+    # "R3C2_V02N0501",
+    # "R1C2_V02S0701",
+    # "R2C2_F5",
+    # "R2C2_H00L0000",
+    # "R2C2_F7",
+    # "R2C2_H02E0701",
+    # "R2C2_V02N0701",
+    # "R2C1_H02E0701",
+    # "R2C3_H01E0001",
+    # "R2C2_V02S0501",
+    # "R3C2_V02N0701",
+    # "R2C2_V02S0701",
+    # "R2C2_V01N0101",
+    # "R2C2_V02N0501",
+    # "R2C1_H02E0501",
+    # "R2C2_H00L0100",
+    # "R2C2_H00R0000"
+]
+
+
+def run_get_bits(mux_driver):
+    route = ""
+    if mux_driver != "":
+        route = "route\n\t\t\t" + mux_driver + "." + sink + ";"
+    with open("mux_template.ncl", "r") as inf:
+        with open("work/mux.ncl", "w") as ouf:
+            ouf.write(Template(inf.read()).substitute(route=route))
+    diamond.run(device, "work/mux.ncl")
+    bs = pytrellis.Bitstream.read_bit("work/mux.bit")
+    chip = bs.deserialise_chip()
+    tile = chip.tiles["R2C2:PLC"]
+    return tile.cram
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    shutil.rmtree("work", ignore_errors=True)
+    os.mkdir("work")
+    baseline = run_get_bits("")
+    with open("a0_mux_out.txt", "w") as f:
+        for d in drivers:
+            bits = run_get_bits(d)
+            diff = bits - baseline
+            diff_str = ["{}F{}B{}".format("!" if b.delta < 0 else "", b.frame, b.bit) for b in diff]
+            print("{0: <18}{1}".format(d, " ".join(diff_str)), file=f)
+            f.flush()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/experiments/machxo2/interconnect_poc/mux_template.ncl b/experiments/machxo2/interconnect_poc/mux_template.ncl
new file mode 100644
index 0000000..7e7370b
--- /dev/null
+++ b/experiments/machxo2/interconnect_poc/mux_template.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, A0);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/001-plc2_routing/fuzzer.py b/fuzzers/machxo2/001-plc2_routing/fuzzer.py
new file mode 100644
index 0000000..b3f86cd
--- /dev/null
+++ b/fuzzers/machxo2/001-plc2_routing/fuzzer.py
@@ -0,0 +1,28 @@
+from fuzzconfig import FuzzConfig
+import interconnect
+import nets
+import pytrellis
+import re
+
+cfg = FuzzConfig(job="PLC2ROUTE", family="MachXO2", device="LCMXO2-1200HC", ncl="plc2route.ncl", tiles=["R5C10:PLC"])
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+
+    span1_re = re.compile(r'R\d+C\d+_[VH]01[NESWTLBR]\d{4}')
+
+    def nn_filter(net, netnames):
+        """ Match nets that are: in the tile according to Tcl, global nets, or span-1 nets that are accidentally
+        left out by Tcl"""
+        return net in netnames or nets.is_global(net) or span1_re.match(net)
+
+    interconnect.fuzz_interconnect(config=cfg, location=(5, 10),
+                                   netname_predicate=nn_filter,
+                                   netname_filter_union=True,
+                                   enable_span1_fix=True,
+                                   bias=1)
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/001-plc2_routing/plc2route.ncl b/fuzzers/machxo2/001-plc2_routing/plc2route.ncl
new file mode 100644
index 0000000..7e7370b
--- /dev/null
+++ b/fuzzers/machxo2/001-plc2_routing/plc2route.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, A0);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/003-lut_init/empty.ncl b/fuzzers/machxo2/003-lut_init/empty.ncl
new file mode 100644
index 0000000..11b72ab
--- /dev/null
+++ b/fuzzers/machxo2/003-lut_init/empty.ncl
@@ -0,0 +1,12 @@
+::FROM-WRITER;
+design top
+{
+   device
+   {
+       architecture xo2c00;
+       device LCMXO2-1200HC;
+       package QFN32;
+       performance "6";
+   }
+
+}
diff --git a/fuzzers/machxo2/003-lut_init/fuzzer.py b/fuzzers/machxo2/003-lut_init/fuzzer.py
new file mode 100644
index 0000000..96d9983
--- /dev/null
+++ b/fuzzers/machxo2/003-lut_init/fuzzer.py
@@ -0,0 +1,46 @@
+from fuzzconfig import FuzzConfig
+import nonrouting
+import fuzzloops
+import nets
+import pytrellis
+import re
+
+cfg = FuzzConfig(job="PLC2INIT", family="MachXO2", device="LCMXO2-1200HC", ncl="empty.ncl", tiles=["R10C11:PLC"])
+
+
+def get_lut_function(init_bits):
+    sop_terms = []
+    lut_inputs = ["A", "B", "C", "D"]
+    for i in range(16):
+        if init_bits[i]:
+            p_terms = []
+            for j in range(4):
+                if i & (1 << j) != 0:
+                    p_terms.append(lut_inputs[j])
+                else:
+                    p_terms.append("~" + lut_inputs[j])
+            sop_terms.append("({})".format("*".join(p_terms)))
+    if len(sop_terms) == 0:
+        lut_func = "0"
+    else:
+        lut_func = "+".join(sop_terms)
+    return lut_func
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+    empty_bitfile = cfg.build_design(cfg.ncl, {})
+    cfg.ncl = "lut.ncl"
+
+    def per_slice(slicen):
+        for k in range(2):
+            def get_substs(bits):
+                return dict(slice=slicen, k=str(k), lut_func=get_lut_function(bits))
+            nonrouting.fuzz_word_setting(cfg, "SLICE{}.K{}.INIT".format(slicen, k), 16, get_substs, empty_bitfile)
+
+    fuzzloops.parallel_foreach(["A", "B", "C", "D"], per_slice)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/003-lut_init/lut.ncl b/fuzzers/machxo2/003-lut_init/lut.ncl
new file mode 100644
index 0000000..3f9fa20
--- /dev/null
+++ b/fuzzers/machxo2/003-lut_init/lut.ncl
@@ -0,0 +1,25 @@
+::FROM-WRITER;
+design top
+{
+   device
+   {
+       architecture xo2c00;
+       device LCMXO2-1200HC;
+       package QFN32;
+       performance "6";
+   }
+
+   comp SLICE_0
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K${k}::H${k}=${lut_func} "
+                 "F${k}:F ";
+         primitive K${k} i3_4_lut;
+      }
+      site R10C11${slice};
+   }
+
+}
diff --git a/fuzzers/machxo2/005-reg_config/empty.ncl b/fuzzers/machxo2/005-reg_config/empty.ncl
new file mode 100644
index 0000000..11b72ab
--- /dev/null
+++ b/fuzzers/machxo2/005-reg_config/empty.ncl
@@ -0,0 +1,12 @@
+::FROM-WRITER;
+design top
+{
+   device
+   {
+       architecture xo2c00;
+       device LCMXO2-1200HC;
+       package QFN32;
+       performance "6";
+   }
+
+}
diff --git a/fuzzers/machxo2/005-reg_config/fuzzer.py b/fuzzers/machxo2/005-reg_config/fuzzer.py
new file mode 100644
index 0000000..c332b6f
--- /dev/null
+++ b/fuzzers/machxo2/005-reg_config/fuzzer.py
@@ -0,0 +1,38 @@
+from fuzzconfig import FuzzConfig
+import nonrouting
+import fuzzloops
+import nets
+import pytrellis
+import re
+
+cfg = FuzzConfig(job="PLC2REG", family="MachXO2", device="LCMXO2-1200HC", ncl="empty.ncl", tiles=["R10C11:PLC"])
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+    empty_bitfile = cfg.build_design(cfg.ncl, {})
+    cfg.ncl = "reg.ncl"
+
+    def per_slice(slicen):
+        r = 0
+
+        def get_substs(regset="RESET", sd="0", gsr="DISABLED"):
+            return dict(slice=slicen, r=str(r), regset=regset, sd=sd, gsr=gsr)
+
+        for r in range(2):
+            nonrouting.fuzz_enum_setting(cfg, "SLICE{}.REG{}.REGSET".format(slicen, r), ["RESET", "SET"],
+                                         lambda x: get_substs(regset=x),
+                                         empty_bitfile)
+            nonrouting.fuzz_enum_setting(cfg, "SLICE{}.REG{}.SD".format(slicen, r), ["0", "1"],
+                                         lambda x: get_substs(sd=x),
+                                         empty_bitfile)
+        nonrouting.fuzz_enum_setting(cfg, "SLICE{}.GSR".format(slicen), ["DISABLED", "ENABLED"],
+                                     lambda x: get_substs(gsr=x),
+                                     empty_bitfile)
+
+    fuzzloops.parallel_foreach(["A", "B", "C", "D"], per_slice)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/005-reg_config/reg.ncl b/fuzzers/machxo2/005-reg_config/reg.ncl
new file mode 100644
index 0000000..a59a8b9
--- /dev/null
+++ b/fuzzers/machxo2/005-reg_config/reg.ncl
@@ -0,0 +1,31 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "REG${r}:::REGSET=${regset}:SD=${sd} "
+                 "Q${r}:Q "
+                 "GSR:${gsr} "
+                 "CLKMUX:CLK "
+                 "CEMUX:1 "
+                 "LSRMUX:LSR "
+                 "SRMODE:LSR_OVER_CE "
+                 "M0MUX:M0 ";
+         primitive REG${r} q_6;
+      }
+      site R10C11${slice};
+   }
+
+}
diff --git a/fuzzers/machxo2/030-cib_cfg0/cibroute.ncl b/fuzzers/machxo2/030-cib_cfg0/cibroute.ncl
new file mode 100644
index 0000000..7e7370b
--- /dev/null
+++ b/fuzzers/machxo2/030-cib_cfg0/cibroute.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, A0);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/030-cib_cfg0/fuzzer.py b/fuzzers/machxo2/030-cib_cfg0/fuzzer.py
new file mode 100644
index 0000000..0175fa0
--- /dev/null
+++ b/fuzzers/machxo2/030-cib_cfg0/fuzzer.py
@@ -0,0 +1,38 @@
+from collections import defaultdict
+
+from fuzzconfig import FuzzConfig
+import interconnect
+import nets
+import pytrellis
+import re
+
+cfg = FuzzConfig(job="CIBCFG0ROUTE", family="MachXO2", device="LCMXO2-1200HC", ncl="cibroute.ncl", tiles=["CIB_R1C4:CIB_CFG0"])
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+
+    span1_re = re.compile(r'R\d+C\d+_[VH]01[NESWTLBR]\d{4}')
+
+    def nn_filter(net, netnames):
+        """ Match nets that are: in the tile according to Tcl, global nets, or span-1 nets that are accidentally
+        left out by Tcl"""
+        return ((net in netnames or span1_re.match(net)) and nets.is_cib(net)) or nets.is_global(net)
+
+    def fc_filter(arc, netnames):
+        """ Ignore connections between two general routing nets. These are edge buffers which vary based on location
+        and must be excluded from the CIB database.
+        """
+        return not (nets.general_routing_re.match(arc[0]) and nets.general_routing_re.match(arc[1]))
+    interconnect.fuzz_interconnect(config=cfg, location=(1, 4),
+                                   netname_predicate=nn_filter,
+                                   fc_predicate=fc_filter,
+                                   netname_filter_union=True,
+                                   enable_span1_fix=True,
+                                   netdir_override=defaultdict(lambda : str("ignore")),
+                                   bias=1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/031-cib_cfg1/cibroute.ncl b/fuzzers/machxo2/031-cib_cfg1/cibroute.ncl
new file mode 100644
index 0000000..7e7370b
--- /dev/null
+++ b/fuzzers/machxo2/031-cib_cfg1/cibroute.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, A0);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/031-cib_cfg1/fuzzer.py b/fuzzers/machxo2/031-cib_cfg1/fuzzer.py
new file mode 100644
index 0000000..5e70b6e
--- /dev/null
+++ b/fuzzers/machxo2/031-cib_cfg1/fuzzer.py
@@ -0,0 +1,38 @@
+from collections import defaultdict
+
+from fuzzconfig import FuzzConfig
+import interconnect
+import nets
+import pytrellis
+import re
+
+cfg = FuzzConfig(job="CIBCFG1ROUTE", family="MachXO2", device="LCMXO2-1200HC", ncl="cibroute.ncl", tiles=["CIB_R1C5:CIB_CFG1"])
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+
+    span1_re = re.compile(r'R\d+C\d+_[VH]01[NESWTLBR]\d{4}')
+
+    def nn_filter(net, netnames):
+        """ Match nets that are: in the tile according to Tcl, global nets, or span-1 nets that are accidentally
+        left out by Tcl"""
+        return ((net in netnames or span1_re.match(net)) and nets.is_cib(net)) or nets.is_global(net)
+
+    def fc_filter(arc, netnames):
+        """ Ignore connections between two general routing nets. These are edge buffers which vary based on location
+        and must be excluded from the CIB database.
+        """
+        return not (nets.general_routing_re.match(arc[0]) and nets.general_routing_re.match(arc[1]))
+    interconnect.fuzz_interconnect(config=cfg, location=(1, 5),
+                                   netname_predicate=nn_filter,
+                                   fc_predicate=fc_filter,
+                                   netname_filter_union=True,
+                                   enable_span1_fix=True,
+                                   netdir_override=defaultdict(lambda : str("ignore")),
+                                   bias=1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/032-copy-cib_cfg0/fuzzer.py b/fuzzers/machxo2/032-copy-cib_cfg0/fuzzer.py
new file mode 100644
index 0000000..1c1cdeb
--- /dev/null
+++ b/fuzzers/machxo2/032-copy-cib_cfg0/fuzzer.py
@@ -0,0 +1,12 @@
+import dbcopy
+import pytrellis
+
+
+def main():
+    pytrellis.load_database("../../../database")
+    # dbcopy.dbcopy("MachXO2", "LCMXO2-1200HC", "CIB_CFG0", "CIB_PIC_T0")
+    dbcopy.dbcopy("MachXO2", "LCMXO2-1200HC", "CIB_CFG0", "CIB_PIC_T_DUMMY")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/040-center-ebr-cib/center-ebr-cib_1200.ncl b/fuzzers/machxo2/040-center-ebr-cib/center-ebr-cib_1200.ncl
new file mode 100644
index 0000000..3f9d75c
--- /dev/null
+++ b/fuzzers/machxo2/040-center-ebr-cib/center-ebr-cib_1200.ncl
@@ -0,0 +1,35 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp SLICE_0
+      [,,,,A0,B0,D0,C0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,]
+   {
+      logical
+      {
+         cellmodel-name SLICE;
+         program "MODE:LOGIC "
+                 "K0::H0=0 "
+                 "F0:F ";
+         primitive K0 i3_4_lut;
+      }
+      site R2C2A;
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (SLICE_0, F0),
+         // loads
+         (SLICE_0, CLK);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/040-center-ebr-cib/fuzzer.py b/fuzzers/machxo2/040-center-ebr-cib/fuzzer.py
new file mode 100644
index 0000000..4f60e93
--- /dev/null
+++ b/fuzzers/machxo2/040-center-ebr-cib/fuzzer.py
@@ -0,0 +1,125 @@
+from collections import defaultdict
+
+from fuzzconfig import FuzzConfig
+import interconnect
+import nets
+import pytrellis
+import re
+
+
+def mk_numbered_nets(net_list, range_iter):
+    return [n.format(i) for i in range_iter for n in net_list]
+
+# Some inputs to the center MUX are two nets concatenated together; the "first"
+# net is a sink from locations throughout the FPGA, the "second" is the actual
+# input to the center MUX. They should all become fixed connections, but
+# fuzz _just_ in case.
+def get_first_concat_nets_input():
+    nets_01 = mk_numbered_nets(["R6C13_JPCLKCIBLLQ{0}",
+                                "R6C13_JPCLKCIBLRQ{0}",
+                                "R6C13_JPCLKCIBVIQB{0}",
+                                "R6C13_JECLKCIBB{0}",
+                                "R6C13_JECLKCIBT{0}",
+                                "R6C13_JPCLKCIBVIQT{0}"], range(2))
+    nets_23 = mk_numbered_nets(["R6C13_JPCLKCIBMID{0}"], range(2,4))
+    return nets_01 + nets_23
+
+# Required to indicate fixed connections in the database.
+def get_input_nets():
+    # The "second" net of the concatenated inputs.
+    def get_second_concat_nets_input():
+        nets_01 = mk_numbered_nets(["R6C13_PCLKCIBLLQ{0}",
+                                    "R6C13_PCLKCIBLRQ{0}",
+                                    "R6C13_PCLKCIBVIQB{0}",
+                                    "R6C13_ECLKCIBB{0}",
+                                    "R6C13_ECLKCIBT{0}",
+                                    "R6C13_PCLKCIBVIQT{0}"], range(2))
+        nets_23 = mk_numbered_nets(["R6C13_PCLKCIBMID{0}"], range(2,4))
+        return nets_01 + nets_23
+
+    # All other inputs
+    nets_07 = mk_numbered_nets(["R6C13_JSNETCIBMID{0}"], range(8))
+    nets_01 = mk_numbered_nets(["R6C13_JSNETCIBL{0}",
+                                "R6C13_JPCLKT2{0}",
+                                "R6C13_JBCDIVX{0}",
+                                "R6C13_JBCDIV1{0}",
+                                "R6C13_JTCDIVX{0}",
+                                "R6C13_JTCDIV1{0}",
+                                "R6C13_JPCLKT0{0}",
+                                "R6C13_JSNETCIBT{0}",
+                                "R6C13_JSNETCIBR{0}",
+                                "R6C13_JSNETCIBB{0}"], range(2))
+    nets_03 = mk_numbered_nets(["R6C13_JLPLLCLK{0}"], range(4))
+    nets_02 = mk_numbered_nets(["R6C13_JPCLKT3{0}"], range(3))
+    nets_0 = ["R6C13_JPCLKT10"]
+
+    return get_second_concat_nets_input() + nets_07 + nets_01 + nets_03 + nets_02 + nets_0
+
+# Unfortunately, same deal w/ concatenated nets also applies to the output.
+# "second" is kinda a misnomer here- clock nets go through tristates, although
+# TCL claims a fixed connection. There are multiple layers of fixed connections
+# where the net names change on the outputs, so we just take care of all
+# suspected fixed connections in one fell swoop.
+def get_fixed_nets_output():
+    nets_01 = mk_numbered_nets(["R6C13_CLK{0}_6_DCM",
+                                "R6C13_CLK{0}_7_DCM",
+                                "R6C13_CLK{0}_1_ECLKBRIDGECS",
+                                "R6C13_CLK{0}_0_ECLKBRIDGECS",
+                                "R6C13_JSEL{0}_ECLKBRIDGECS"], range(2))
+    nets_07 = mk_numbered_nets(["R6C13_CLKI{0}_DCC",
+                                # "R6C13_CLKO{0}_DCC", # XXX: I suspect these
+                                # are gated by a tristate or a mux.
+                                "R6C13_JCE{0}_DCC"], range(8))
+    nets_67 = mk_numbered_nets(["R6C13_JSEL{0}_DCM"], range(6,8))
+    return nets_01 + nets_07 + nets_67
+
+
+cfg = FuzzConfig(job="CIB_1200", family="MachXO2", device="LCMXO2-1200HC", ncl="center-ebr-cib_1200.ncl",
+                  tiles=["CENTER6:CENTER_EBR_CIB", "CENTER8:CENTER7", "CENTER9:CENTER8"])
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+
+    # TODO: Add fc_prefix=job["prefix"] argument.
+
+    interconnect.fuzz_interconnect_with_netnames(config=cfg, netnames=get_first_concat_nets_input(),
+                                                 netname_filter_union=False,
+                                                 netdir_override=defaultdict(lambda : str("sink")),
+                                                 full_mux_style=True,
+                                                 bias=1)
+
+    interconnect.fuzz_interconnect_with_netnames(config=cfg, netnames=get_input_nets(),
+                                                 netname_filter_union=False,
+                                                 netdir_override=defaultdict(lambda : str("sink")),
+                                                 full_mux_style=True,
+                                                 bias=1)
+
+    interconnect.fuzz_interconnect_with_netnames(config=cfg, netnames=get_fixed_nets_output(),
+                                                 netname_filter_union=False,
+                                                 netdir_override=defaultdict(lambda : str("sink")),
+                                                 full_mux_style=True,
+                                                 bias=1)
+
+    # Test fuzzers follow below.
+    # interconnect.fuzz_interconnect_with_netnames(config=cfg, netnames=["R6C13_PCLKCIBVIQT0", "R6C13_JPCLKCIBVIQT0", "R6C13_VPRXCLKI0", "R6C13_CLKI0_DCC"],
+    #                                              netname_filter_union=False,
+    #                                              netdir_override = {
+    #                                                 "R6C13_JPCLKCIBVIQT0" : "sink",
+    #                                              },
+    #                                              full_mux_style=True,
+    #                                              bias=1)
+
+    # TODO: R6C13_JA0 --> R6C13_JCE0_DCC. But TCL also claims
+    # R6C13_CLKI0_DCC --> R6C13_CLKO0_DCC (pseudo = 1). Contradiction?
+    # interconnect.fuzz_interconnect_with_netnames(config=cfg, netnames=["R6C13_CLKI0_DCC", "R6C13_CLKO0_DCC", "R6C13_JCE0_DCC"],
+    #                                              netname_filter_union=False,
+    #                                              netdir_override = {
+    #                                                 "R6C13_JCE0_DCC" : "sink",
+    #                                              },
+    #                                              full_mux_style=True,
+    #                                              bias=1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/050-pio_routing/fuzzer.py b/fuzzers/machxo2/050-pio_routing/fuzzer.py
new file mode 100644
index 0000000..0d5f27a
--- /dev/null
+++ b/fuzzers/machxo2/050-pio_routing/fuzzer.py
@@ -0,0 +1,123 @@
+from collections import defaultdict
+from itertools import product
+
+from fuzzconfig import FuzzConfig
+import interconnect
+import nets
+import pytrellis
+import re
+
+import isptcl
+
+# Hint: F/Q are sinks for "driver"s, A-D are sources for "sinks".
+# Bottom Fuzzing
+b_overrides = dict()
+for (n, r, d) in [(["R12C9_JRXDA{}_BIOLOGIC"], range(0,8), "driver"),
+                  (["R12C9_JDI{}",
+                    "R12C9_JIN{}_IOLOGIC"], nets.char_range("A","E"), "driver"),
+                  (["R12C9_JPADDO{}",
+                    "R12C9_JPADDT{}"], nets.char_range("A","E"), "driver"),
+                  (["R12C9_JRXD{}A_BIOLOGIC",
+                    "R12C9_JRXD{}C_BSIOLOGIC"], range(0,4), "driver"),
+                  (["R12C9_JDEL{}A_BIOLOGIC",
+                    "R12C9_JDEL{}C_BSIOLOGIC"], range(0,5), "sink"),
+                  (["R12C9_JI{}A_BIOLOGIC",
+                    "R12C9_JI{}B_IOLOGIC",
+                    "R12C9_JI{}C_BSIOLOGIC",
+                    "R12C9_JI{}D_IOLOGIC"], ["N", "P"], "sink"),
+                  (["R12C9_JOPOS{}",
+                    "R12C9_JONEG{}",
+                    "R12C9_JTS{}",
+                    "R12C9_JCLK{}",
+                    "R12C9_JLSR{}",
+                    "R12C9_JCE{}"], ["A_BIOLOGIC",
+                                     "B_IOLOGIC",
+                                     "C_BSIOLOGIC",
+                                     "D_IOLOGIC"], "sink"),
+                    (["R12C9_JSLIP{}"], ["A_BIOLOGIC",
+                                         "C_BSIOLOGIC"], "sink")
+              ]:
+    for p in nets.net_product(n, r):
+        b_overrides[p] = d
+
+def nn_filterb(net, netnames):
+    return not nets.is_cib(net)
+
+
+# Left and Right are done from CIB's POV because
+# there are no tiles dedicated strictly to I/O connections.
+# Ignore loopback/CIBTEST nets.
+l_overrides = defaultdict(lambda : str("ignore"))
+
+# grep "R10C1_J.*" r10c1.txt | grep -v "CIBTEST" | sort | less
+for (n, r, d) in [(["R10C1_JA{}",
+                    "R10C1_JB{}",
+                    "R10C1_JC{}",
+                    "R10C1_JCLK{}",
+                    "R10C1_LSR{}",
+                    "R10C1_JCE{}"], range(0,4), "driver"),
+                  (["R10C1_JQ{}"], range(0,4), "sink"),
+                  (["R10C1_JF{}"], range(0,8), "sink")
+              ]:
+    for p in nets.net_product(n, r):
+        l_overrides[p] = d
+
+span1_re = re.compile(r'R\d+C\d+_[VH]01[NESWTLBR]\d{4}')
+def nn_filterl(net, netnames):
+    """ Match nets that are: in the tile according to Tcl, global nets, or span-1 nets that are accidentally
+    left out by Tcl"""
+    return net in netnames or nets.is_global(net) or span1_re.match(net)
+
+
+jobs = [
+        {
+           "pos" : [(12, 9)],
+           "cfg" : FuzzConfig(job="PIOROUTEB", family="MachXO2", device="LCMXO2-1200HC", ncl="pioroute.ncl",
+                                  tiles=["PB9:PIC_B0"]),
+           "nn_filter" : nn_filterb,
+           "netnames_override" : b_overrides,
+        },
+
+        {
+           "pos" : [(10, 1)],
+           "cfg" : FuzzConfig(job="PIOROUTEL", family="MachXO2", device="LCMXO2-1200HC", ncl="pioroute.ncl",
+                                  tiles=["PL10:PIC_L0"]),
+           "nn_filter" : nn_filterl,
+           "netnames_override" : l_overrides,
+        },
+
+        # Probably the same thing as PIC_L0 plus some additional fixed connections?
+        {
+           "pos" : [(11, 1)],
+           "cfg" : FuzzConfig(job="PIOROUTEL", family="MachXO2", device="LCMXO2-1200HC", ncl="pioroute.ncl",
+                                  tiles=["PL11:LLC0"]),
+           "nn_filter" : nn_filterl,
+           "netnames_override" : l_overrides,
+        },
+
+        {
+            "pos" : [(10, 22)],
+            "cfg" : FuzzConfig(job="PIOROUTER", family="MachXO2", device="LCMXO2-1200HC", ncl="pioroute.ncl",
+                                   tiles=["PR10:PIC_R0"]),
+            "nn_filter" : nn_filterl,
+            "netnames_override" : l_overrides,
+        },
+]
+
+def main():
+    pytrellis.load_database("../../../database")
+    for job in jobs:
+        cfg = job["cfg"]
+        cfg.setup()
+
+        for pos in job["pos"]:
+            interconnect.fuzz_interconnect(config=cfg, location=pos,
+                                           netname_predicate=job["nn_filter"],
+                                           netdir_override=job["netnames_override"],
+                                           netname_filter_union=False,
+                                           enable_span1_fix=True,
+                                           bias=1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/050-pio_routing/pioroute.ncl b/fuzzers/machxo2/050-pio_routing/pioroute.ncl
new file mode 100644
index 0000000..a7e908a
--- /dev/null
+++ b/fuzzers/machxo2/050-pio_routing/pioroute.ncl
@@ -0,0 +1,39 @@
+::FROM-WRITER;
+design top
+{
+    device
+    {
+        architecture xo2c00;
+        device LCMXO2-1200HC;
+        package QFN32;
+        performance "6";
+    }
+
+   comp PIO
+   {
+      logical
+      {
+         cellmodel-name PIO;
+         program "TRIMUX:PADDT:::PADDT=0 "
+                 "IOBUF:::PULLMODE=NONE,DRIVE=8, \"
+                    "SLEWRATE=SLOW,HYSTERESIS=NA "
+                 "DATAMUX:PADDO "
+                 "VREF:OFF "
+                 "ODMUX:TRIMUX "
+                 "LVDSMUX:DATAMUX ";
+         primitive IOBUF PIO_pad;
+         primitive PAD PIO;
+      }
+      site "11";
+   }
+
+    signal q_c
+   {
+      signal-pins
+         // drivers
+         (PIO, PADDI),
+         // loads
+         (PIO, PADDO);
+      ${route}
+   }
+}
diff --git a/fuzzers/machxo2/102-oscg/empty.ncl b/fuzzers/machxo2/102-oscg/empty.ncl
new file mode 100644
index 0000000..7779581
--- /dev/null
+++ b/fuzzers/machxo2/102-oscg/empty.ncl
@@ -0,0 +1,11 @@
+::FROM-WRITER;
+design top
+{
+   device
+   {
+       architecture xo2c00;
+       device LCMXO2-1200HC;
+       package QFN32;
+       performance "6";
+   }
+}
diff --git a/fuzzers/machxo2/102-oscg/fuzzer.py b/fuzzers/machxo2/102-oscg/fuzzer.py
new file mode 100644
index 0000000..98db71f
--- /dev/null
+++ b/fuzzers/machxo2/102-oscg/fuzzer.py
@@ -0,0 +1,78 @@
+import sys
+
+from fuzzconfig import FuzzConfig
+import nonrouting
+import pytrellis
+import fuzzloops
+import interconnect
+
+cfg = FuzzConfig(job="OSCH", family="MachXO2", device="LCMXO2-1200HC", ncl="empty.ncl",
+                                          tiles=["PT8:PIC_T_DUMMY_OSC",
+                                                "PT4:CFG0", "PT5:CFG1",
+                                                "PT6:CFG2", "PT7:CFG3",
+                                                "CIB_R1C4:CIB_CFG0",
+                                                "CIB_R1C5:CIB_CFG1"])
+
+
+def get_substs(mode="OSCH", nom_freq="2.08", stdby="0"):
+    if mode == "NONE":
+        comment = "//"
+    else:
+        comment = ""
+
+    if stdby == "1":
+        stdby = ""
+        stdby_0 = "//"
+    else:
+        stdby = "//"
+        stdby_0 = ""
+
+    if nom_freq == "2.08":
+        using_non_default_freq = ""
+        using_default_freq = "//"
+    else:
+        using_non_default_freq = "//"
+        using_default_freq = ""
+
+    return dict(comment=comment,
+                nom_freq=nom_freq,
+                stdby=stdby,
+                stdby_0=stdby_0,
+                using_non_default_freq=using_non_default_freq,
+                using_default_freq=using_default_freq)
+
+def main():
+    pytrellis.load_database("../../../database")
+    cfg.setup()
+    empty_bitfile = cfg.build_design(cfg.ncl, {})
+    cfg.ncl = "osc.ncl"
+
+    nonrouting.fuzz_enum_setting(cfg, "OSCH.STDBY", ["0", "1"],
+                                 lambda x: get_substs(stdby=x), empty_bitfile)
+    nonrouting.fuzz_enum_setting(cfg, "OSCH.MODE", ["NONE", "OSCH"],
+                                 lambda x: get_substs(mode=x), empty_bitfile)
+
+    # Takes a long time, so permit opt-out.
+    if "-s" not in sys.argv:
+        nonrouting.fuzz_enum_setting(cfg, "OSCH.NOM_FREQ",
+            ["{}".format(i) for i in [
+                2.08, 2.15, 2.22, 2.29, 2.38, 2.46, 2.56, 2.66, 2.77, 2.89,
+                3.02, 3.17, 3.33, 3.50, 3.69, 3.91, 4.16, 4.29, 4.43, 4.59,
+                4.75, 4.93, 5.12, 5.32, 5.54, 5.78, 6.05, 6.33, 6.65, 7.00,
+                7.39, 7.82, 8.31, 8.58, 8.87, 9.17, 9.50, 9.85, 10.23, 10.64,
+                11.08, 11.57, 12.09, 12.67, 13.30, 14.00, 14.78, 15.65, 15.65, 16.63,
+                17.73, 19.00, 20.46, 22.17, 24.18, 26.60, 29.56, 33.25, 38.00, 44.33,
+                53.20, 66.50, 88.67, 133.00
+            ]], lambda x: get_substs(nom_freq=x), empty_bitfile)
+
+    cfg.ncl = "osc_routing.ncl"
+    interconnect.fuzz_interconnect_with_netnames(
+        cfg,
+        ["R1C4_JOSC_OSC"],
+        bidir=True,
+        netdir_override={"R1C4_JOSC_OSC" : "driver"},
+        bias=1
+    )
+
+if __name__ == "__main__":
+    main()
diff --git a/fuzzers/machxo2/102-oscg/osc.ncl b/fuzzers/machxo2/102-oscg/osc.ncl
new file mode 100644
index 0000000..1396f62
--- /dev/null
+++ b/fuzzers/machxo2/102-oscg/osc.ncl
@@ -0,0 +1,23 @@
+::FROM-WRITER;
+design top
+{
+   device
+   {
+       architecture xo2c00;
+       device LCMXO2-1200HC;
+       package QFN32;
+       performance "6";
+   }
+
+   ${comment} comp OSC
+   ${comment} {
+   ${comment}    logical {
+   ${comment}       cellmodel-name OSC;
+   ${comment}       program "${program}"
+   ${using_non_default_freq} ${stdby} ${comment}               "OSCH:#ON ";
+   ${using_non_default_freq} ${stdby_0} ${comment}               "OSCH::::STDBY=0 ";
+   ${using_default_freq} ${comment}               "OSCH:::NOM_FREQ=${nom_freq}:STDBY=0 ";
+   ${comment}    }
+   ${comment}    site OSC;
+   ${comment} }
+}
diff --git a/libtrellis/.gitignore b/libtrellis/.gitignore
index 245392f..ad1dcd0 100644
--- a/libtrellis/.gitignore
+++ b/libtrellis/.gitignore
@@ -18,6 +18,7 @@
 install_manifest.txt
 ecppack
 ecpunpack
+ecpmulti
 ecppll
 libtrellis.dylib
 *~
diff --git a/libtrellis/include/Tile.hpp b/libtrellis/include/Tile.hpp
index 4c5059f..7d87bf0 100644
--- a/libtrellis/include/Tile.hpp
+++ b/libtrellis/include/Tile.hpp
@@ -1,6 +1,7 @@
 #ifndef LIBTRELLIS_TILE_HPP
 #define LIBTRELLIS_TILE_HPP
 
+#include <iostream>
 #include <string>
 #include <iostream>
 #include <cstdint>
diff --git a/libtrellis/src/Tile.cpp b/libtrellis/src/Tile.cpp
index 2b3345a..ba0ef69 100644
--- a/libtrellis/src/Tile.cpp
+++ b/libtrellis/src/Tile.cpp
@@ -6,17 +6,50 @@
 #include "Util.hpp"
 
 namespace Trellis {
-// Regex to extract row/column from a tile name
+// Regexes to extract row/column from a tile name.
 static const regex tile_rxcx_re(R"(R(\d+)C(\d+))");
 
+// MachXO2-specific, in order of precedence (otherwise, e.g.
+// CENTER_EBR matches r_regex)
+static const regex tile_center_re(R"(CENTER(\d+))");
+static const regex tile_centerb_re(R"(CENTER_B)");
+static const regex tile_centert_re(R"(CENTER_T)");
+static const regex tile_centerebr_re(R"(CENTER_EBR(\d+))");
+static const regex tile_t_re(R"([A-Za-z0-9_]*T(\d+))");
+static const regex tile_b_re(R"([A-Za-z0-9_]*B(\d+))");
+static const regex tile_l_re(R"([A-Za-z0-9_]*L(\d+))");
+static const regex tile_r_re(R"([A-Za-z0-9_]*R(\d+))");
+
+// Given the zero-indexed max chip_size, return the zero-indexed
+// center. Mainly for MachXO2.
+// TODO: Make const.
+map<pair<int, int>, pair<int, int>> center_map = {
+    {make_pair(12, 21), make_pair(6, 12)}
+};
+
 // Universal function to get a zero-indexed row/column pair.
 pair<int, int> get_row_col_pair_from_chipsize(string name, pair<int, int> chip_size, int bias) {
     smatch m;
-    bool match;
 
-    match = regex_search(name, m, tile_rxcx_re);
-    if(match) {
-        return make_pair(stoi(m.str(1)), stoi(m.str(2)));
+    if(regex_search(name, m, tile_rxcx_re)) {
+        return make_pair(stoi(m.str(1)), stoi(m.str(2)) - bias);
+    } else if(regex_search(name, m, tile_centert_re)) {
+        return make_pair(0, center_map[chip_size].second);
+    } else if(regex_search(name, m, tile_centerb_re)) {
+        return make_pair(chip_size.first, center_map[chip_size].second);
+    } else if(regex_search(name, m, tile_centerebr_re)) {
+        // TODO: This may not apply to devices larger than 1200.
+        return make_pair(center_map[chip_size].first, stoi(m.str(1)) - bias);
+    } else if(regex_search(name, m, tile_center_re)) {
+        return make_pair(stoi(m.str(1)), center_map[chip_size].second);
+    } else if(regex_search(name, m, tile_t_re)) {
+        return make_pair(0, stoi(m.str(1)) - bias);
+    } else if(regex_search(name, m, tile_b_re)) {
+        return make_pair(chip_size.first, stoi(m.str(1)) - bias);
+    } else if(regex_search(name, m, tile_l_re)) {
+        return make_pair(stoi(m.str(1)), 0);
+    } else if(regex_search(name, m, tile_r_re)) {
+        return make_pair(stoi(m.str(1)), chip_size.second);
     } else {
         throw runtime_error(fmt("Could not extract position from " << name));
     }
diff --git a/metadata/MachXO2/LCMXO2-1200HC/globals.json b/metadata/MachXO2/LCMXO2-1200HC/globals.json
new file mode 100644
index 0000000..a408e0b
--- /dev/null
+++ b/metadata/MachXO2/LCMXO2-1200HC/globals.json
@@ -0,0 +1,8 @@
+{
+  "quadrants": {
+  },
+  "taps": {
+  },
+  "spines": {
+  }
+}
diff --git a/metadata/MachXO2/LCMXO2-256HC/globals.json b/metadata/MachXO2/LCMXO2-256HC/globals.json
new file mode 100644
index 0000000..a408e0b
--- /dev/null
+++ b/metadata/MachXO2/LCMXO2-256HC/globals.json
@@ -0,0 +1,8 @@
+{
+  "quadrants": {
+  },
+  "taps": {
+  },
+  "spines": {
+  }
+}
diff --git a/minitests/hello/hello-machxo2.v b/minitests/hello/hello-machxo2.v
new file mode 100644
index 0000000..49ed054
--- /dev/null
+++ b/minitests/hello/hello-machxo2.v
@@ -0,0 +1,26 @@
+// Modified from:
+// https://github.com/tinyfpga/TinyFPGA-A-Series/tree/master/template_a2
+
+module TinyFPGA_A2 (
+  inout pin1
+);
+
+
+  wire clk;
+
+  OSCH #(
+    .NOM_FREQ("2.08")
+  ) internal_oscillator_inst (
+    .STDBY(1'b0),
+    .OSC(clk)
+  );
+
+  reg [23:0] led_timer;
+
+  always @(posedge clk) begin
+    led_timer <= led_timer + 1;
+  end
+
+  // left side of board
+  assign pin1 = led_timer[23];
+endmodule
diff --git a/tools/connectivity.py b/tools/connectivity.py
index 66fe964..04b53b7 100755
--- a/tools/connectivity.py
+++ b/tools/connectivity.py
@@ -1,8 +1,11 @@
 #!/usr/bin/env python3
+import sys
 import pytrellis
 import nets
 import tiles
 import database
+if sys.platform in ("win32"):
+    import pyreadline.rlmain
 import readline
 import re
 
@@ -20,56 +23,58 @@
     pytrellis.load_database(database.get_db_root())
     c = pytrellis.Chip("LFE5U-45F")
     chip_size = (c.get_max_row(), c.get_max_col())
+    bias = c.info.col_bias
+
     # Get fan-in to a net
     # Returns (source, configurable, loc)
     def get_fanin(net):
         drivers = []
-        npos = tiles.pos_from_name(net, chip_size, 0)
+        npos = tiles.pos_from_name(net, chip_size, bias)
         for tile in c.get_all_tiles():
             tinf = tile.info
             tname = tinf.name
-            pos = tiles.pos_from_name(tname, chip_size, 0)
+            pos = tiles.pos_from_name(tname, chip_size, bias)
             if abs(pos[0] - npos[0]) >= 10 or abs(pos[1] - npos[1]) >= 10:
                 continue
             if net.startswith("G_"):
                 tnet = net
             else:
-                tnet = nets.normalise_name(chip_size, tname, net, 0)
+                tnet = nets.normalise_name(chip_size, tname, net, bias)
             tdb = pytrellis.get_tile_bitdata(pytrellis.TileLocator(c.info.family, c.info.name, tinf.type))
             try:
                 mux = tdb.get_mux_data_for_sink(tnet)
                 for src in mux.get_sources():
-                    drivers.append((nets.canonicalise_name(chip_size, tname, src, 0), True, tname))
+                    drivers.append((nets.canonicalise_name(chip_size, tname, src, bias), True, tname))
             except IndexError:
                 pass
             for fc in tdb.get_fixed_conns():
                 if fc.sink == tnet:
-                    drivers.append((nets.canonicalise_name(chip_size, tname, fc.source, 0), False, tname))
+                    drivers.append((nets.canonicalise_name(chip_size, tname, fc.source, bias), False, tname))
         return drivers
 
     # Get fan-out of a net
     # Returns (dest, configurable, loc)
     def get_fanout(net):
         drivers = []
-        npos = tiles.pos_from_name(net, chip_size, 0)
+        npos = tiles.pos_from_name(net, chip_size, bias)
         for tile in c.get_all_tiles():
             tinf = tile.info
             tname = tinf.name
-            pos = tiles.pos_from_name(tname, chip_size, 0)
+            pos = tiles.pos_from_name(tname, chip_size, bias)
             if abs(pos[0] - npos[0]) >= 12 or abs(pos[1] - npos[1]) >= 12:
                 continue
             if net.startswith("G_"):
                 tnet = net
             else:
-                tnet = nets.normalise_name(chip_size, tname, net, 0)
+                tnet = nets.normalise_name(chip_size, tname, net, bias)
             tdb = pytrellis.get_tile_bitdata(pytrellis.TileLocator(c.info.family, c.info.name, tinf.type))
             for sink in tdb.get_sinks():
                 mux = tdb.get_mux_data_for_sink(sink)
                 if tnet in mux.arcs:
-                    drivers.append((nets.canonicalise_name(chip_size, tname, sink, 0), True, tname))
+                    drivers.append((nets.canonicalise_name(chip_size, tname, sink, bias), True, tname))
             for fc in tdb.get_fixed_conns():
                 if fc.source == tnet:
-                    drivers.append((nets.canonicalise_name(chip_size, tname, fc.sink, 0), False, tname))
+                    drivers.append((nets.canonicalise_name(chip_size, tname, fc.sink, bias), False, tname))
         return drivers
 
 
@@ -105,7 +110,7 @@
     def completer(str, idx):
         if not tile_net_re.match(str):
             return None
-        loc = tiles.pos_from_name(str, chip_size, 0)
+        loc = tiles.pos_from_name(str, chip_size, bias)
         nets = get_nets_at(loc)
         for n in nets:
             if n.startswith(str):
diff --git a/tools/demobuilder/design.py b/tools/demobuilder/design.py
index d79f53f..a57c781 100644
--- a/tools/demobuilder/design.py
+++ b/tools/demobuilder/design.py
@@ -6,6 +6,7 @@
 class Design:
     def __init__(self, family):
         self.chip = pytrellis.Chip("LFE5U-45F")
+        self.bias = self.chip.info.col_bias
         self.router = route.Autorouter(self.chip)
         self.config = {_.info.name: pytrellis.TileConfig() for _ in self.chip.get_all_tiles()}
         # TODO: load skeleton config
@@ -33,7 +34,7 @@
             tinf = tile.info
             tname = tinf.name
             chip_size = (self.chip.get_max_row(), self.chip.get_max_col())
-            pos = tiles.pos_from_name(tname, chip_size, 0)
+            pos = tiles.pos_from_name(tname, chip_size, self.bias)
             if tinf.type == "PLC2":
                 for loc in ("A", "B", "C", "D"):
                     bel = "R{}C{}{}".format(pos[0], pos[1], loc)
@@ -62,7 +63,7 @@
         beltype, belloc = self.bels[bel]
         tile, loc = belloc
         chip_size = (self.chip.get_max_row(), self.chip.get_max_col())
-        pos = tiles.pos_from_name(tile, chip_size, 0)
+        pos = tiles.pos_from_name(tile, chip_size, self.bias)
         net_prefix = "R{}C{}".format(pos[0], pos[1])
         slice_index = "ABCD".index(loc)
         lc0 = 2 * slice_index
diff --git a/tools/demobuilder/route.py b/tools/demobuilder/route.py
index 788ad17..36975f5 100755
--- a/tools/demobuilder/route.py
+++ b/tools/demobuilder/route.py
@@ -11,6 +11,7 @@
     def __init__(self, chip):
         self.chip = chip
         self.chip_size = (self.chip.get_max_row(), self.chip.get_max_col())
+        self.bias = self.chip.info.col_bias
         self.dh_arc_cache = {}
         self.net_to_wire = {}
         self.wire_to_net = {}
@@ -24,7 +25,7 @@
             drivers = []
             chip_size = (self.chip.get_max_row(), self.chip.get_max_col())
             try:
-                npos = tiles.pos_from_name(wire, chip_size, 0)
+                npos = tiles.pos_from_name(wire, chip_size, self.bias)
             except AssertionError:
                 return []
             wname = wire.split("_", 1)[1]
@@ -42,20 +43,20 @@
                     tname = tinf.name
                     if tname.startswith("TAP"):
                         continue
-                    pos = tiles.pos_from_name(tname, chip_size, 0)
+                    pos = tiles.pos_from_name(tname, chip_size, self.bias)
 
                     if abs(pos[0] - npos[0]) not in (vspan, 0) or abs(pos[1] - npos[1]) not in (hspan, 0):
                         continue
                     if wire.startswith("G_"):
                         twire = wire
                     else:
-                        twire = nets.normalise_name(self.chip_size, tname, wire, 0)
+                        twire = nets.normalise_name(self.chip_size, tname, wire, self.bias)
 
                     tdb = pytrellis.get_tile_bitdata(
                         pytrellis.TileLocator(self.chip.info.family, self.chip.info.name, tinf.type))
                     downhill = tdb.get_downhill_wires(twire)
                     for sink in downhill:
-                        nn = nets.canonicalise_name(self.chip_size, tname, sink.first, 0)
+                        nn = nets.canonicalise_name(self.chip_size, tname, sink.first, self.bias)
                         if nn is not None:
                             drivers.append((nn, sink.second, tname))
             self.dh_arc_cache[wire] = drivers
@@ -73,8 +74,8 @@
         else:
             self.net_to_wire[net] = {dest_wire}
         if configurable and not exists:
-            src_wirename = nets.normalise_name(self.chip_size, tile, uphill_wire, 0)
-            sink_wirename = nets.normalise_name(self.chip_size, tile, dest_wire, 0)
+            src_wirename = nets.normalise_name(self.chip_size, tile, uphill_wire, self.bias)
+            sink_wirename = nets.normalise_name(self.chip_size, tile, dest_wire, self.bias)
             config[tile].add_arc(sink_wirename, src_wirename)
 
     # Bind a net to a wire (used for port connections)
@@ -90,9 +91,9 @@
     def route_net_to_wire(self, net, wire, config):
         print("     Routing net '{}' to wire/pin '{}'...".format(net, wire))
         chip_size = (self.chip.get_max_row(), self.chip.get_max_col())
-        dest_pos = tiles.pos_from_name(wire, chip_size, 0)
+        dest_pos = tiles.pos_from_name(wire, chip_size, self.bias)
         def get_score(x_wire):
-            pos = tiles.pos_from_name(x_wire, chip_size, 0)
+            pos = tiles.pos_from_name(x_wire, chip_size, self.bias)
             score = abs(pos[0] - dest_pos[0]) + abs(pos[1] - dest_pos[1])
             x_wname = x_wire.split("_", 1)[1]
             if x_wname[1:3].isdigit() and score > 3:
diff --git a/tools/extract_tilegrid.py b/tools/extract_tilegrid.py
index 1411d66..08686b8 100755
--- a/tools/extract_tilegrid.py
+++ b/tools/extract_tilegrid.py
@@ -25,6 +25,8 @@
     r'^\s+([A-Z0-9_]+) \((-?\d+), (-?\d+)\)')
 
 parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument('-m', action="store_true",
+                    help="Use MachXO2 family layout")
 parser.add_argument('infile', type=argparse.FileType('r'),
                     help="input file from bstool")
 parser.add_argument('outfile', type=argparse.FileType('w'),
@@ -38,14 +40,24 @@
         tile_m = tile_re.match(line)
         if tile_m:
             name = tile_m.group(6)
-            current_tile = {
-                "type": tile_m.group(1),
-                "start_bit": int(tile_m.group(4)),
-                "start_frame": int(tile_m.group(5)),
-                "rows": int(tile_m.group(2)),
-                "cols": int(tile_m.group(3)),
-                "sites": []
-            }
+            if args.m:
+                current_tile = {
+                    "type": tile_m.group(1),
+                    "start_bit": int(tile_m.group(5)),
+                    "start_frame": int(tile_m.group(4)),
+                    "rows": int(tile_m.group(3)),
+                    "cols": int(tile_m.group(2)),
+                    "sites": []
+                }
+            else:
+                current_tile = {
+                    "type": tile_m.group(1),
+                    "start_bit": int(tile_m.group(4)),
+                    "start_frame": int(tile_m.group(5)),
+                    "rows": int(tile_m.group(2)),
+                    "cols": int(tile_m.group(3)),
+                    "sites": []
+                }
             identifier = name + ":" + tile_m.group(1)
             assert identifier not in tiles
             tiles[identifier] = current_tile
diff --git a/tools/get_tilegrid_all.py b/tools/get_tilegrid_all.py
index 5e687b9..53bc1f2 100755
--- a/tools/get_tilegrid_all.py
+++ b/tools/get_tilegrid_all.py
@@ -21,10 +21,14 @@
     devices = database.get_devices()
     for family in sorted(devices["families"].keys()):
         for device in sorted(devices["families"][family]["devices"].keys()):
-            diamond.run(device, "work_tilegrid/wire.v")
-            output_file = path.join(database.get_db_subdir(family, device), "tilegrid.json")
-            extract_tilegrid.main(["extract_tilegrid", "work_tilegrid/wire.tmp/output.test", output_file])
+            if devices["families"][family]["devices"][device]["fuzz"]:
+                diamond.run(device, "work_tilegrid/wire.v")
+                output_file = path.join(database.get_db_subdir(family, device), "tilegrid.json")
+                if family in ["MachXO2"]:
+                    extract_tilegrid.main(["extract_tilegrid", "-m", "work_tilegrid/wire.tmp/output.test", output_file])
+                else:
+                    extract_tilegrid.main(["extract_tilegrid", "work_tilegrid/wire.tmp/output.test", output_file])
 
 
 if __name__ == "__main__":
-    main()
\ No newline at end of file
+    main()
diff --git a/tools/html_all.py b/tools/html_all.py
index 8ada8eb..42edfcd 100755
--- a/tools/html_all.py
+++ b/tools/html_all.py
@@ -84,8 +84,6 @@
     docs_toc = ""
     pytrellis.load_database(database.get_db_root())
     for fam, fam_data in sorted(database.get_devices()["families"].items()):
-        if fam == "MachXO2":
-            continue
         fdir = path.join(args.fld, fam)
         if not path.exists(fdir):
             os.mkdir(fdir)
@@ -112,33 +110,35 @@
                     dev
                 )
 
-        docs_toc += "</ul>"
-        docs_toc += "<h4>Cell Timing Documentation</h4>"
-        docs_toc += "<ul>"
-        for spgrade in ["6", "7", "8", "8_5G"]:
-            tdir = path.join(fdir, "timing")
-            if not path.exists(tdir):
-                os.mkdir(tdir)
-            docs_toc += '<li><a href="{}">Speed Grade -{}</a></li>'.format(
-                '{}/timing/cell_timing_{}.html'.format(fam, spgrade),
-                spgrade
-            )
-            cell_html.make_cell_timing_html(timing_dbs.cells_db_path(fam, spgrade), fam, spgrade,
-                                            path.join(tdir, 'cell_timing_{}.html'.format(spgrade)))
-        docs_toc += "</ul>"
-        docs_toc += "<h4>Interconnect Timing Documentation</h4>"
-        docs_toc += "<ul>"
-        for spgrade in ["6", "7", "8", "8_5G"]:
-            tdir = path.join(fdir, "timing")
-            if not path.exists(tdir):
-                os.mkdir(tdir)
-            docs_toc += '<li><a href="{}">Speed Grade -{}</a></li>'.format(
-                '{}/timing/interconn_timing_{}.html'.format(fam, spgrade),
-                spgrade
-            )
-            interconnect_html.make_interconn_timing_html(timing_dbs.interconnect_db_path(fam, spgrade), fam, spgrade,
-                                            path.join(tdir, 'interconn_timing_{}.html'.format(spgrade)))
-        docs_toc += "</ul>"
+        # No timing stuff for MachXO2 yet.
+        if fam in ["ECP5"]:
+            docs_toc += "</ul>"
+            docs_toc += "<h4>Cell Timing Documentation</h4>"
+            docs_toc += "<ul>"
+            for spgrade in ["6", "7", "8", "8_5G"]:
+                tdir = path.join(fdir, "timing")
+                if not path.exists(tdir):
+                    os.mkdir(tdir)
+                docs_toc += '<li><a href="{}">Speed Grade -{}</a></li>'.format(
+                    '{}/timing/cell_timing_{}.html'.format(fam, spgrade),
+                    spgrade
+                )
+                cell_html.make_cell_timing_html(timing_dbs.cells_db_path(fam, spgrade), fam, spgrade,
+                                                path.join(tdir, 'cell_timing_{}.html'.format(spgrade)))
+            docs_toc += "</ul>"
+            docs_toc += "<h4>Interconnect Timing Documentation</h4>"
+            docs_toc += "<ul>"
+            for spgrade in ["6", "7", "8", "8_5G"]:
+                tdir = path.join(fdir, "timing")
+                if not path.exists(tdir):
+                    os.mkdir(tdir)
+                docs_toc += '<li><a href="{}">Speed Grade -{}</a></li>'.format(
+                    '{}/timing/interconn_timing_{}.html'.format(fam, spgrade),
+                    spgrade
+                )
+                interconnect_html.make_interconn_timing_html(timing_dbs.interconnect_db_path(fam, spgrade), fam, spgrade,
+                                                path.join(tdir, 'interconn_timing_{}.html'.format(spgrade)))
+            docs_toc += "</ul>"
 
     index_html = Template(trellis_docs_index).substitute(
         datetime=build_dt,
diff --git a/tools/html_tilegrid.py b/tools/html_tilegrid.py
index cc7e683..6b471ff 100755
--- a/tools/html_tilegrid.py
+++ b/tools/html_tilegrid.py
@@ -46,6 +46,7 @@
 
     max_row = device_info["max_row"]
     max_col = device_info["max_col"]
+    bias = device_info["col_bias"]
 
     tiles = []
     for i in range(max_row + 1):
@@ -56,7 +57,7 @@
 
     for identifier, data in sorted(tilegrid.items()):
         name = identifier.split(":")[0]
-        row, col = tilelib.pos_from_name(name, (max_row, max_col), 0)
+        row, col = tilelib.pos_from_name(name, (max_row, max_col), bias)
         colour = get_colour(data["type"])
         tiles[row][col].append((name, data["type"], colour))
 
diff --git a/util/common/isptcl.py b/util/common/isptcl.py
index 6b0742e..70512c7 100644
--- a/util/common/isptcl.py
+++ b/util/common/isptcl.py
@@ -2,12 +2,34 @@
 Interface between Python fuzzer scripts and Lattice Diamond ispTcl
 """
 
+from collections import defaultdict
+
 import database
 import subprocess
 import tempfile
 from os import path
 import re
 
+
+# Arc whose direction is ambiguous "---"
+class AmbiguousArc:
+    # I
+    def __init__(self, lhs, rhs):
+        self.lhs = lhs
+        self.rhs = rhs
+
+    def __getitem__(self, idx):
+        if idx == 0:
+            return self.lhs
+        elif idx == 1:
+            return self.rhs
+        else:
+            raise IndexError("AmbiguousArc only connects two nets")
+
+    def __repr__(self):
+        return "{} --- {}".format(self.lhs, self.rhs)
+
+
 def run(commands):
     """Run a list of Tcl commands, returning the output as a string"""
     dtcl_path = path.join(database.get_trellis_root(), "diamond_tcl.sh")
@@ -84,7 +106,7 @@
     return wires
 
 
-def get_arcs_on_wires(desfiles, wires, drivers_only=False):
+def get_arcs_on_wires(desfiles, wires, drivers_only=False, dir_override=dict()):
     """
     Use ispTcl to get a list of arcs sinking or sourcing a list of wires
 
@@ -122,8 +144,29 @@
                     if not drivers_only:
                         arcs.append((splitline[2].strip(), splitline[0].strip()))
                 elif splitline[1].strip() == "---":
-                    # Edge wires, currently ignored
-                    pass
+                    if isinstance(dir_override, defaultdict):
+                        # get() overrides defaultdict behavior, and a user may
+                        # have a valid reason to provide a default such as
+                        # ignore.
+                        override = dir_override[wires[wire_idx]]
+                    else:
+                        override = dir_override.get(wires[wire_idx], "")
+                    if override:
+                        if override == "sink":
+                            arcs.append((splitline[0].strip(), splitline[2].strip()))
+                        elif override == "driver":
+                            arcs.append((splitline[2].strip(), splitline[0].strip()))
+                        elif override == "mark":
+                            arcs.append(AmbiguousArc(splitline[0].strip(), splitline[2].strip()))
+                        elif override == "ignore":
+                            pass
+                        else:
+                            assert False, ("invalid override for wire {}".
+                                            format(wires[wire_idx]))
+                    else:
+                        assert False, ("'---' found in ispTcl output, and no netdir_override"
+                                       " was given for {wire}. Full line:\n{line}".
+                                       format(wire=wires[wire_idx], line=line))
                 else:
                     print (splitline)
                     assert False, "invalid output from Tcl command `dev_list_arcs`"
diff --git a/util/common/nets.py b/util/common/nets.py
index ea9ae18..3a69425 100644
--- a/util/common/nets.py
+++ b/util/common/nets.py
@@ -302,6 +302,16 @@
     return "R{}C{}_{}".format(wire_pos[0], wire_pos[1], wire)
 
 
+# Useful functions for constructing nets.
+def char_range(c1, c2):
+    """Generates the characters from `c1` to `c2`, exclusive."""
+    for c in range(ord(c1), ord(c2)):
+        yield chr(c)
+
+def net_product(net_list, range_iter):
+    return [n.format(i) for i in range_iter for n in net_list]
+
+
 def main():
     assert is_global("R2C7_HPBX0100")
     assert is_global("R24C12_VPTX0700")
diff --git a/util/fuzz/interconnect.py b/util/fuzz/interconnect.py
index e40045b..9d16da0 100644
--- a/util/fuzz/interconnect.py
+++ b/util/fuzz/interconnect.py
@@ -28,6 +28,7 @@
                       func_cib=False,
                       fc_prefix="",
                       nonlocal_prefix="",
+                      netdir_override=dict(),
                       bias=0):
     """
     The fully-automatic interconnect fuzzer function. This performs the fuzzing and updates the database with the
@@ -70,7 +71,7 @@
     if func_cib and not netname_filter_union:
         netnames = list(filter(lambda x: netname_predicate(x, netnames), netnames))
     fuzz_interconnect_with_netnames(config, netnames, netname_predicate, arc_predicate, fc_predicate, func_cib,
-                                    netname_filter_union, False, fc_prefix, nonlocal_prefix, bias)
+                                    netname_filter_union, False, fc_prefix, nonlocal_prefix, netdir_override, bias)
 
 
 def fuzz_interconnect_with_netnames(
@@ -84,6 +85,7 @@
         full_mux_style=False,
         fc_prefix="",
         nonlocal_prefix="",
+        netdir_override=dict(),
         bias=0):
     """
     Fuzz interconnect given a list of netnames to analyse. Arcs associated these netnames will be found using the Tcl
@@ -100,10 +102,13 @@
     nets much pass the predicate.
     :param full_mux_style: if True, is a full mux, and all 0s is considered a valid config bit possibility
     :param fc_prefix: add a prefix to non-global fixed connections for device-specific fuzzers
+    :param netdir_override: Manually specify whether the nets in the dictionary are driven by other nets (`"sink"`,
+    specified as "-->" in ispTcl), or drive other nets (`"driver"`, specified as "<--" in ispTcl). The dictionary is
+    only consulted if ispTcl returns "---" for the direction of a given net.
     :param bias: Apply offset correction for n-based column numbering, n > 0. Used used by Lattice
     on certain families.
     """
-    net_arcs = isptcl.get_arcs_on_wires(config.ncd_prf, netnames, not bidir)
+    net_arcs = isptcl.get_arcs_on_wires(config.ncd_prf, netnames, not bidir, netdir_override)
     baseline_bitf = config.build_design(config.ncl, {}, "base_")
     baseline_chip = pytrellis.Bitstream.read_bit(baseline_bitf).deserialise_chip()