Merge pull request #127 from SymbiFlow/12k

Add '12k' device
diff --git a/diamond.sh b/diamond.sh
index 4a69df2..b5ab3e5 100755
--- a/diamond.sh
+++ b/diamond.sh
@@ -11,27 +11,53 @@
 #  - lfe5u-85
 #  - lfe5u-45
 #  - lfe5u-25
+#  - LCMXO2-1200HC
 
-# Currently this script supports Linux only.
+# Currently this script supports Linux and Windows using a MINGW64 bash shell.
 
 # You need to set the DIAMONDDIR environment variable to the path where you have
 # installed Lattice Diamond, unless it matches this default.
 
+if [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
+	WINDOWS=true
+else
+	WINDOWS=false
+fi
+
 if [ -z "$DIAMONDVER" ]; then
 	diamondver="3.10"
 else
 	diamondver="$DIAMONDVER"
 fi
 
-diamonddir="${DIAMONDDIR:-/usr/local/diamond/${diamondver}_x64}"
+if $WINDOWS; then
+	diamonddir="${DIAMONDDIR:-/c/lscc/diamond/${diamondver}_x64}"
+else
+	diamonddir="${DIAMONDDIR:-/usr/local/diamond/${diamondver}_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
@@ -98,6 +124,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"
@@ -140,7 +176,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"
@@ -148,6 +189,7 @@
 touch synth_impl.prf
 fi
 
+
 else
 cat > impl_lse.prj << EOT
 #device
@@ -197,6 +239,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
 
@@ -204,6 +249,11 @@
 "$fpgabindir"/bitgen -d par_impl.ncd -jedec output.jed synth_impl.prf
 fi
 
+if [ -n "$COMPRESSED_BITSTREAM" ]; then
+	sed 's/COMPRESS_CONFIG=OFF/COMPRESS_CONFIG=ON/' synth_impl.prf > synth_impl_comp.prf
+	"$fpgabindir"/bitgen -d par_impl.ncd $BITARGS output-comp.bit synth_impl_comp.prf
+fi
+
 # dump bitmap
 "$fpgabindir"/bstool -d output.bit > output.dump
 
@@ -212,9 +262,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
@@ -239,6 +294,9 @@
 if [ -n "$JEDEC_BITSTREAM" ]; then
 cp "$2.tmp"/output.jed "$2.jed"
 fi
+if [ -n "$COMPRESSED_BITSTREAM" ]; then
+cp "$2.tmp"/output-comp.bit "$2-comp.bit"
+fi
 if [ -n "$BACKANNO" ]; then
 cp "$2.tmp"/par_impl.sdf "$2.sdf"
 fi
diff --git a/diamond_tcl.sh b/diamond_tcl.sh
index e7172b6..0bbb4f6 100755
--- a/diamond_tcl.sh
+++ b/diamond_tcl.sh
@@ -1,14 +1,50 @@
 #!/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 [ -z "$DIAMONDVER" ]; then
+	diamondver="3.10"
+else
+	diamondver="$DIAMONDVER"
+fi
+
+if $WINDOWS; then
+	diamonddir="${DIAMONDDIR:-/c/lscc/diamond/${diamondver}_x64}"
+else
+	diamonddir="${DIAMONDDIR:-/usr/local/diamond/${diamondver}_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/interconnect_poc/fuzz_single_mux.py b/experiments/ECP5/interconnect_poc/fuzz_single_mux.py
similarity index 100%
rename from experiments/interconnect_poc/fuzz_single_mux.py
rename to experiments/ECP5/interconnect_poc/fuzz_single_mux.py
diff --git a/experiments/interconnect_poc/mux_template.ncl b/experiments/ECP5/interconnect_poc/mux_template.ncl
similarity index 100%
rename from experiments/interconnect_poc/mux_template.ncl
rename to experiments/ECP5/interconnect_poc/mux_template.ncl
diff --git a/experiments/lut_init/fuzz_lut_init.py b/experiments/ECP5/lut_init/fuzz_lut_init.py
similarity index 100%
rename from experiments/lut_init/fuzz_lut_init.py
rename to experiments/ECP5/lut_init/fuzz_lut_init.py
diff --git a/experiments/lut_init/lut_init_template.ncl b/experiments/ECP5/lut_init/lut_init_template.ncl
similarity index 100%
rename from experiments/lut_init/lut_init_template.ncl
rename to experiments/ECP5/lut_init/lut_init_template.ncl
diff --git a/libtrellis/.gitignore b/libtrellis/.gitignore
index cab4ee8..a2b28a2 100644
--- a/libtrellis/.gitignore
+++ b/libtrellis/.gitignore
@@ -19,8 +19,22 @@
 ecpbram
 ecppack
 ecpunpack
+ecpmulti
 ecppll
 libtrellis.dylib
 *~
 ecpmulti
 generated/
+
+# Windows
+ecpbram.exe
+ecppack.exe
+ecpunpack.exe
+ecpmulti.exe
+ecppll.exe
+pytrellis.pyd
+
+# Ninja
+.ninja_*
+build.ninja
+rules.ninja
diff --git a/libtrellis/src/Bitstream.cpp b/libtrellis/src/Bitstream.cpp
index c00799c..fb0aa38 100644
--- a/libtrellis/src/Bitstream.cpp
+++ b/libtrellis/src/Bitstream.cpp
@@ -32,6 +32,45 @@
 static const uint32_t multiboot_flag = 1 << 20;
 static const uint32_t background_flag = 0x2E000000;
 
+// Bitstream generation can be tweaked in various ways; this class encapsulates
+// these options and provides defaults that mimic Diamond-generated output.
+class BitstreamOptions {
+public:
+    BitstreamOptions(const Chip &chip) {
+      if (chip.info.family == "MachXO2") {
+          // Write frames out in order 0 => max or reverse (max => 0).
+          // This apparently does NOT apply to compressed bitstreams, which
+          // uses reversed_frames = true unconditionally.
+          reversed_frames = false;
+          dummy_bytes_after_preamble = 2;
+          crc_meta = 0xE0; // CRC check (0x80), once at end (0x40), dummy bits
+                           // in bitstream, no dummy bytes after each frame.
+          // FIXME: Diamond seems to set include_dummy_bits for MachXO2
+          // sometimes (0x20). Add this functionality later, as I'm not sure
+          // any MachXO2 members have dummy bits at this time.
+          crc_after_each_frame = false;
+          dummy_bytes_after_frame = 0;
+          security_sed_space = 8;
+      } else if (chip.info.family == "ECP5") {
+          reversed_frames = true;
+          dummy_bytes_after_preamble = 4;
+          crc_meta = 0x91; // CRC check (0x80), per frame (bit 6 cleared),
+                           // and there 1 dummy bytes after each frame (0x10).
+          crc_after_each_frame = true;
+          dummy_bytes_after_frame = 1;
+          security_sed_space = 12;
+      } else
+          throw runtime_error("Unknown chip family: " + chip.info.family);
+    };
+
+    bool reversed_frames;
+    size_t dummy_bytes_after_preamble;
+    uint8_t crc_meta;
+    bool crc_after_each_frame;
+    size_t dummy_bytes_after_frame;
+    size_t security_sed_space;
+};
+
 // The BitstreamReadWriter class stores state (including CRC16) whilst reading
 // the bitstream
 class BitstreamReadWriter {
@@ -199,7 +238,7 @@
         }
     }
 
-    void write_compressed_frames(const std::vector<std::vector<uint8_t>> &frames_in) {
+    void write_compressed_frames(const std::vector<std::vector<uint8_t>> &frames_in, BitstreamOptions &ops) {
         // Build a histogram of bytes to aid creating the dictionary
         int histogram[256];
         for (int i = 0; i < 256; i++)
@@ -223,7 +262,7 @@
             write_byte(dict_entries[i]);
         // Write data
         write_byte(uint8_t(BitstreamCommand::LSC_PROG_INCR_CMP));
-        write_byte(0x91); //CRC check, 1 dummy byte
+        write_byte(ops.crc_meta); //CRC check, 1 dummy byte
         uint16_t frames = uint16_t(frames_in.size());
         write_byte(uint8_t((frames >> 8) & 0xFF));
         write_byte(uint8_t(frames & 0xFF));
@@ -286,8 +325,13 @@
             // This ensures compressed frame is 8-bit aligned
             flush_bits();
             // Post-frame CRC and 0xFF byte
-            insert_crc16();
-            write_byte(0xFF);
+            if(ops.crc_after_each_frame) {
+                insert_crc16();
+            }
+
+            for(size_t j = 0; j < ops.dummy_bytes_after_frame; j++) {
+                write_byte(0xFF);
+            }
         }
     }
 
@@ -530,13 +574,8 @@
                 // This is the main bitstream payload
                 if (!chip)
                     throw BitstreamParseError("start of bitstream data before chip was identified", rd.get_offset());
-                bool reversed_frames;
-                if (chip->info.family == "MachXO2")
-                    reversed_frames = false;
-                else if (chip->info.family == "ECP5")
-                    reversed_frames = true;
-                else
-                    throw BitstreamParseError("Unknown chip family: " + chip->info.family);
+
+                BitstreamOptions ops(chip.get()); // Only reversed_frames is meaningful here.
 
                 uint8_t params[3];
                 rd.get_bytes(params, 3);
@@ -561,7 +600,9 @@
                     bytes_per_frame += (7 - ((bytes_per_frame - 1) % 8));
                 unique_ptr<uint8_t[]> frame_bytes = make_unique<uint8_t[]>(bytes_per_frame);
                 for (size_t i = 0; i < frame_count; i++) {
-                    size_t idx = reversed_frames? (chip->info.num_frames - 1) - i : i;
+                    // Apparently when a bitstream is compressed, even on
+                    // MachXO2, frames are written in reverse order!
+                    const size_t idx = (chip->info.num_frames - 1) - i;
                     if (cmd == BitstreamCommand::LSC_PROG_INCR_CMP)
                         rd.get_compressed_bytes(frame_bytes.get(), bytes_per_frame, compression_dict.get());
                     else
@@ -687,10 +728,13 @@
 
 Bitstream Bitstream::serialise_chip(const Chip &chip, const map<string, string> options) {
     BitstreamReadWriter wr;
+
+    BitstreamOptions ops(chip);
+
     // Preamble
     wr.write_bytes(preamble.begin(), preamble.size());
     // Padding
-    wr.insert_dummy(4);
+    wr.insert_dummy(ops.dummy_bytes_after_preamble);
 
     if (options.count("spimode")) {
         auto spimode = find_if(spi_modes.begin(), spi_modes.end(), [&](const pair<string, uint8_t> &fp){
@@ -737,9 +781,16 @@
             ctrl0 &= ~background_flag;
     }
     wr.write_uint32(ctrl0);
+
+    // Seems pretty consistent...
+    if (chip.info.family == "MachXO2") {
+        wr.insert_dummy(4);
+    }
+
     // Init address
     wr.write_byte(uint8_t(BitstreamCommand::LSC_INIT_ADDRESS));
     wr.insert_zeros(3);
+
     if (options.count("compress") && options.at("compress") == "yes") {
         // First create an uncompressed array of frames
         std::vector<std::vector<uint8_t>> frames_data;
@@ -758,11 +809,11 @@
             }
         }
         // Then compress and write
-        wr.write_compressed_frames(frames_data);
+        wr.write_compressed_frames(frames_data, ops);
     } else {
         // Bitstream data
         wr.write_byte(uint8_t(BitstreamCommand::LSC_PROG_INCR_RTI));
-        wr.write_byte(0x91); //CRC check, 1 dummy byte
+        wr.write_byte(ops.crc_meta);
         uint16_t frames = uint16_t(chip.info.num_frames);
         wr.write_byte(uint8_t((frames >> 8) & 0xFF));
         wr.write_byte(uint8_t(frames & 0xFF));
@@ -770,21 +821,32 @@
                                   chip.info.pad_bits_before_frame) / 8U;
         unique_ptr<uint8_t[]> frame_bytes = make_unique<uint8_t[]>(bytes_per_frame);
         for (size_t i = 0; i < frames; i++) {
+            size_t idx = ops.reversed_frames? (chip.info.num_frames - 1) - i : i;
             fill(frame_bytes.get(), frame_bytes.get() + bytes_per_frame, 0x00);
             for (int j = 0; j < chip.info.bits_per_frame; j++) {
                 size_t ofs = j + chip.info.pad_bits_after_frame;
                 assert(((bytes_per_frame - 1) - (ofs / 8)) < bytes_per_frame);
                 frame_bytes[(bytes_per_frame - 1) - (ofs / 8)] |=
-                        (chip.cram.bit((chip.info.num_frames - 1) - i, j) & 0x01) << (ofs % 8);
+                        (chip.cram.bit(idx, j) & 0x01) << (ofs % 8);
             }
             wr.write_bytes(frame_bytes.get(), bytes_per_frame);
-            wr.insert_crc16();
-            wr.write_byte(0xFF);
+            if(ops.crc_after_each_frame) {
+                wr.insert_crc16();
+            }
+
+            for(size_t j = 0; j < ops.dummy_bytes_after_frame; j++) {
+                wr.write_byte(0xFF);
+            }
         }
     }
 
+    if(!ops.crc_after_each_frame) {
+        wr.insert_crc16();
+    }
+
     // Post-bitstream space for SECURITY and SED (not used here)
-    wr.insert_dummy(12);
+    wr.insert_dummy(ops.security_sed_space);
+
     // Program Usercode
     wr.write_byte(uint8_t(BitstreamCommand::ISC_PROGRAM_USERCODE));
     wr.write_byte(0x80);
@@ -821,6 +883,14 @@
         }
         wr.insert_crc16();
     }
+
+    // MachXO2 indeed writes this info twice for some reason...
+    if (chip.info.family == "MachXO2") {
+        wr.write_byte(uint8_t(BitstreamCommand::LSC_PROG_CNTRL0));
+        wr.insert_zeros(3);
+        wr.write_uint32(ctrl0);
+    }
+
     // Program DONE
     wr.write_byte(uint8_t(BitstreamCommand::ISC_PROGRAM_DONE));
     wr.insert_zeros(3);
@@ -844,7 +914,7 @@
 Bitstream Bitstream::serialise_chip_partial(const Chip &chip, const vector<uint32_t> &frames, const map<string, string> options)
 {
     BitstreamReadWriter wr;
-    
+
     // Address encoding for partial frame writes
     static const map<uint32_t, uint32_t> msb_weights_45k = {
         {{0x0000}, { 0*106}},
diff --git a/libtrellis/src/Tile.cpp b/libtrellis/src/Tile.cpp
index 2b3345a..05b2cc0 100644
--- a/libtrellis/src/Tile.cpp
+++ b/libtrellis/src/Tile.cpp
@@ -6,17 +6,51 @@
 #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 = {
+    // 1200HC
+    {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/minitests/dsp/mult.v b/minitests/ECP5/dsp/mult.v
similarity index 100%
rename from minitests/dsp/mult.v
rename to minitests/ECP5/dsp/mult.v
diff --git a/minitests/dsp/mult1.v b/minitests/ECP5/dsp/mult1.v
similarity index 100%
rename from minitests/dsp/mult1.v
rename to minitests/ECP5/dsp/mult1.v
diff --git a/minitests/dsp/mult1n.ncl b/minitests/ECP5/dsp/mult1n.ncl
similarity index 100%
rename from minitests/dsp/mult1n.ncl
rename to minitests/ECP5/dsp/mult1n.ncl
diff --git a/minitests/dsp/preadd.v b/minitests/ECP5/dsp/preadd.v
similarity index 100%
rename from minitests/dsp/preadd.v
rename to minitests/ECP5/dsp/preadd.v
diff --git a/minitests/iologic/iddr.v b/minitests/ECP5/iologic/iddr.v
similarity index 100%
rename from minitests/iologic/iddr.v
rename to minitests/ECP5/iologic/iddr.v
diff --git a/minitests/iologic/iddr2.v b/minitests/ECP5/iologic/iddr2.v
similarity index 100%
rename from minitests/iologic/iddr2.v
rename to minitests/ECP5/iologic/iddr2.v
diff --git a/minitests/iologic/iddr7.v b/minitests/ECP5/iologic/iddr7.v
similarity index 100%
rename from minitests/iologic/iddr7.v
rename to minitests/ECP5/iologic/iddr7.v
diff --git a/minitests/iologic/iddr_LSR.v b/minitests/ECP5/iologic/iddr_LSR.v
similarity index 100%
rename from minitests/iologic/iddr_LSR.v
rename to minitests/ECP5/iologic/iddr_LSR.v
diff --git a/minitests/iologic/iddr_inv.v b/minitests/ECP5/iologic/iddr_inv.v
similarity index 100%
rename from minitests/iologic/iddr_inv.v
rename to minitests/ECP5/iologic/iddr_inv.v
diff --git a/minitests/iologic/idelay.v b/minitests/ECP5/iologic/idelay.v
similarity index 100%
rename from minitests/iologic/idelay.v
rename to minitests/ECP5/iologic/idelay.v
diff --git a/minitests/iologic/ireg.v b/minitests/ECP5/iologic/ireg.v
similarity index 100%
rename from minitests/iologic/ireg.v
rename to minitests/ECP5/iologic/ireg.v
diff --git a/minitests/iologic/ireg2.v b/minitests/ECP5/iologic/ireg2.v
similarity index 100%
rename from minitests/iologic/ireg2.v
rename to minitests/ECP5/iologic/ireg2.v
diff --git a/minitests/iologic/oddr.v b/minitests/ECP5/iologic/oddr.v
similarity index 100%
rename from minitests/iologic/oddr.v
rename to minitests/ECP5/iologic/oddr.v
diff --git a/minitests/iologic/oddr7.v b/minitests/ECP5/iologic/oddr7.v
similarity index 100%
rename from minitests/iologic/oddr7.v
rename to minitests/ECP5/iologic/oddr7.v
diff --git a/minitests/iologic/oreg.v b/minitests/ECP5/iologic/oreg.v
similarity index 100%
rename from minitests/iologic/oreg.v
rename to minitests/ECP5/iologic/oreg.v
diff --git a/minitests/iologic/oshx2a.v b/minitests/ECP5/iologic/oshx2a.v
similarity index 100%
rename from minitests/iologic/oshx2a.v
rename to minitests/ECP5/iologic/oshx2a.v
diff --git a/minitests/iologic/toreg.v b/minitests/ECP5/iologic/toreg.v
similarity index 100%
rename from minitests/iologic/toreg.v
rename to minitests/ECP5/iologic/toreg.v
diff --git a/minitests/ncl/lut.ncl b/minitests/ECP5/ncl/lut.ncl
similarity index 100%
rename from minitests/ncl/lut.ncl
rename to minitests/ECP5/ncl/lut.ncl
diff --git a/minitests/ncl/lut_0.ncl b/minitests/ECP5/ncl/lut_0.ncl
similarity index 100%
rename from minitests/ncl/lut_0.ncl
rename to minitests/ECP5/ncl/lut_0.ncl
diff --git a/minitests/ncl/lut_or.ncl b/minitests/ECP5/ncl/lut_or.ncl
similarity index 100%
rename from minitests/ncl/lut_or.ncl
rename to minitests/ECP5/ncl/lut_or.ncl
diff --git a/minitests/potpourri/dtr.v b/minitests/ECP5/potpourri/dtr.v
similarity index 100%
rename from minitests/potpourri/dtr.v
rename to minitests/ECP5/potpourri/dtr.v
diff --git a/minitests/potpourri/jtagg.v b/minitests/ECP5/potpourri/jtagg.v
similarity index 100%
rename from minitests/potpourri/jtagg.v
rename to minitests/ECP5/potpourri/jtagg.v
diff --git a/minitests/potpourri/osc.v b/minitests/ECP5/potpourri/osc.v
similarity index 100%
rename from minitests/potpourri/osc.v
rename to minitests/ECP5/potpourri/osc.v
diff --git a/minitests/potpourri/osc_div.ncl b/minitests/ECP5/potpourri/osc_div.ncl
similarity index 100%
rename from minitests/potpourri/osc_div.ncl
rename to minitests/ECP5/potpourri/osc_div.ncl
diff --git a/timing/fuzzers/010-basic-cells/fuzzer.py b/timing/fuzzers/ECP5/010-basic-cells/fuzzer.py
similarity index 100%
rename from timing/fuzzers/010-basic-cells/fuzzer.py
rename to timing/fuzzers/ECP5/010-basic-cells/fuzzer.py
diff --git a/timing/fuzzers/012-io/fuzzer.py b/timing/fuzzers/ECP5/012-io/fuzzer.py
similarity index 100%
rename from timing/fuzzers/012-io/fuzzer.py
rename to timing/fuzzers/ECP5/012-io/fuzzer.py
diff --git a/timing/fuzzers/012-io/pio.v b/timing/fuzzers/ECP5/012-io/pio.v
similarity index 100%
rename from timing/fuzzers/012-io/pio.v
rename to timing/fuzzers/ECP5/012-io/pio.v
diff --git a/timing/fuzzers/013-iol/fuzzer.py b/timing/fuzzers/ECP5/013-iol/fuzzer.py
similarity index 100%
rename from timing/fuzzers/013-iol/fuzzer.py
rename to timing/fuzzers/ECP5/013-iol/fuzzer.py
diff --git a/timing/fuzzers/013-iol/pio.v b/timing/fuzzers/ECP5/013-iol/pio.v
similarity index 100%
rename from timing/fuzzers/013-iol/pio.v
rename to timing/fuzzers/ECP5/013-iol/pio.v
diff --git a/timing/fuzzers/014-ebr/ebr_regmode.v b/timing/fuzzers/ECP5/014-ebr/ebr_regmode.v
similarity index 100%
rename from timing/fuzzers/014-ebr/ebr_regmode.v
rename to timing/fuzzers/ECP5/014-ebr/ebr_regmode.v
diff --git a/timing/fuzzers/014-ebr/ebr_writemode.v b/timing/fuzzers/ECP5/014-ebr/ebr_writemode.v
similarity index 100%
rename from timing/fuzzers/014-ebr/ebr_writemode.v
rename to timing/fuzzers/ECP5/014-ebr/ebr_writemode.v
diff --git a/timing/fuzzers/014-ebr/fuzzer.py b/timing/fuzzers/ECP5/014-ebr/fuzzer.py
similarity index 100%
rename from timing/fuzzers/014-ebr/fuzzer.py
rename to timing/fuzzers/ECP5/014-ebr/fuzzer.py
diff --git a/timing/fuzzers/015-mult/fuzzer.py b/timing/fuzzers/ECP5/015-mult/fuzzer.py
similarity index 100%
rename from timing/fuzzers/015-mult/fuzzer.py
rename to timing/fuzzers/ECP5/015-mult/fuzzer.py
diff --git a/timing/fuzzers/015-mult/mult_pipemode.v b/timing/fuzzers/ECP5/015-mult/mult_pipemode.v
similarity index 100%
rename from timing/fuzzers/015-mult/mult_pipemode.v
rename to timing/fuzzers/ECP5/015-mult/mult_pipemode.v
diff --git a/timing/fuzzers/020-basic_routing/fuzzer.py b/timing/fuzzers/ECP5/020-basic_routing/fuzzer.py
similarity index 100%
rename from timing/fuzzers/020-basic_routing/fuzzer.py
rename to timing/fuzzers/ECP5/020-basic_routing/fuzzer.py
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..2bb8aa8 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,13 +106,21 @@
     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
 
     desfiles: a tuple (ncdfile, prffile)
     wires: list of canonical names of the wire
     drivers_only: only include arcs driving the wire in the output
+    dir_override: Dictionary that specificies whether a net queried by ispTcl
+    is a "sink" or "driver" when ispTcl returns "---" (since ISPTcl always puts
+    the queried net on the RHS of an an arc). dir_override is only consulted if
+    ispTcl returns "---" for the direction of a given net, and will
+    additionally override drivers_only=False for any nets specified as
+    "driver". Two additional strings are allowed: "ignore" to ignore "---"
+    connections to/from the queried net, and "mark" to return the connection as
+    an AmbiguousArc for later processing.
 
     Returns a map between wire name and a list of arc tuples (source, sink)
     """
@@ -122,8 +152,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 e7885eb..b05cdff 100644
--- a/util/common/nets.py
+++ b/util/common/nets.py
@@ -312,6 +312,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/dbfixup.py b/util/fuzz/dbfixup.py
index dee2db0..734522f 100644
--- a/util/fuzz/dbfixup.py
+++ b/util/fuzz/dbfixup.py
@@ -3,7 +3,9 @@
 """
 Database fix utility
 
-Run at the end of fuzzing to "finalise" the database and remove problems that may occur during fuzzing
+Run at the end of fuzzing to "finalise" the database and remove problems that may occur during fuzzing.
+
+The remaining functions can be called in other fuzzers as necessary.
 """
 
 
@@ -26,3 +28,47 @@
         if deleteFc:
             db.remove_fixed_sink(mux)
     db.save()
+
+
+def remove_enum_bits(family, device, tiletype, lowerright, upperleft=(0, 0)):
+    """
+    Remove bits from enumerations in a given tile that actually belong
+    to routing bits. This can happen when e.g. routing is required for Diamond
+    to set certain bits in the output, as is the case for fuzzing I/O enums
+    in PIC_L0 and PIC_R0.
+
+    Bounds are (0,0)-based. Upperleft is inclusive, lowerright is exclusive.
+    """
+    def in_bounding_box(bit):
+        (x, y) = (bit.frame, bit.bit)
+
+        if upperleft[0] > x or upperleft[1] > y:
+            return False
+
+        if lowerright[0] <= x or lowerright[1] <= y:
+            return False
+
+        return True
+
+    db = pytrellis.get_tile_bitdata(
+        pytrellis.TileLocator(family, device, tiletype))
+
+    for enum in db.get_settings_enums():
+        fixed_enum = pytrellis.EnumSettingBits()
+
+        for option in db.get_data_for_enum(enum).options:
+            key = option.key()
+            fixed_bg = pytrellis.BitGroup()
+
+            for bit in option.data().bits:
+                if in_bounding_box(bit):
+                    fixed_bg.bits.add(bit)
+
+            fixed_enum.options[key] = fixed_bg
+
+        fixed_enum.name = db.get_data_for_enum(enum).name
+        fixed_enum.defval = db.get_data_for_enum(enum).defval
+
+        db.remove_setting_enum(enum)
+        db.add_setting_enum(fixed_enum)
+    db.save()
diff --git a/util/fuzz/interconnect.py b/util/fuzz/interconnect.py
index e40045b..5774f78 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
@@ -48,6 +49,10 @@
     :param enable_span1_fix: if True, include span1 wires that are excluded due to a Tcl API bug
     :param func_cib: if True, we are fuzzing a special function to CIB interconnect, enable optimisations for this
     :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. This dictionary overrides
+    func_cib=False for the nets in question.
     :param nonlocal_prefix: add a prefix to non-global and non-neighbour wires for device-specific fuzzers
     :param bias: Apply offset correction for n-based column numbering, n > 0. Used used by Lattice
     on certain families.
@@ -70,7 +75,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 +89,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 +106,14 @@
     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. This dictionary overrides
+    bidir=False for the nets in question.
     :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()