harness: Add initial version of ROI harness

Signed-off-by: Tomasz Michalak <tmichalak@antmicro.com>
diff --git a/minitests/roi_harness/Makefile b/minitests/roi_harness/Makefile
new file mode 100644
index 0000000..573f416
--- /dev/null
+++ b/minitests/roi_harness/Makefile
@@ -0,0 +1,31 @@
+URAY_PINCFG ?= ZYNQUSP-SWBUT
+export URAY_PINCFG
+
+BUILD_DIR=build_$(URAY_PINCFG)
+HARNESS_DIR ?= harness_$(URAY_PINCFG)
+export BUILD_DIR
+
+HARNESS_FILES=$(BUILD_DIR)/design.bit $(BUILD_DIR)/design.txt $(BUILD_DIR)/design.json $(BUILD_DIR)/design.dcp
+
+# official demo configuration
+all: $(BUILD_DIR)/run.ok
+
+$(BUILD_DIR)/run.ok: runme.sh runme.tcl
+	./runme.sh
+	touch $(BUILD_DIR)/run.ok
+
+$(BUILD_DIR)/copy.ok: $(BUILD_DIR)/run.ok
+	mkdir -p $(HARNESS_DIR)
+	cp $(HARNESS_FILES) $(HARNESS_DIR)
+	touch $(BUILD_DIR)/copy.ok
+
+copy: $(BUILD_DIR)/copy.ok
+
+clean:
+	rm -rf build_* harness_*
+
+run:
+	+$(MAKE) clean
+	+$(MAKE) copy
+
+.PHONY: all clean copy run
diff --git a/minitests/roi_harness/Makefile_mini b/minitests/roi_harness/Makefile_mini
new file mode 100644
index 0000000..79cddbe
--- /dev/null
+++ b/minitests/roi_harness/Makefile_mini
@@ -0,0 +1,34 @@
+PART = xczu7ev-ffvc1156-2-i
+BIT2FASM_ARGS = --verbose
+SOURCES = top_mini.v
+
+all: top.fasm top.bits segprint.log
+
+clean:
+	@rm -f *.bit
+	@rm -f *.bin
+	@rm -f *.bits
+	@rm -f *.fasm
+	@rm -f *.log
+	@rm -rf build
+
+.PHONY: all clean
+
+top.bit: $(VIVADO) $(SOURCES) runme.py
+	mkdir -p build
+	source ../zynqusp-swbut.sh; \
+	cd build && python ../runme.py
+	cp build/*.bit ./
+
+top.fasm: top.bit
+	PYTHONPATH="$(URAY_DIR):$(URAY_DIR)/utils:$(URAY_DIR)/third_party/fasm" \
+    PATH="$(URAY_DIR)/build/tools:$(PATH)" \
+    $(URAY_BIT2FASM) $(BIT2FASM_ARGS) \
+        top.bit >top.fasm \
+        || (rm -f top.fasm && exit 1)
+
+top.bits: top.bit
+	$(URAY_BITREAD) -part_file $(URAY_DIR)/database/zynqusp/$(PART)/part.yaml -o top.bits -z -y top.bit
+
+segprint.log: top.bits
+	$(URAY_SEGPRINT) -z -D -b top.bits > segprint.log
diff --git a/minitests/roi_harness/create_design_json.py b/minitests/roi_harness/create_design_json.py
new file mode 100644
index 0000000..1c30fbd
--- /dev/null
+++ b/minitests/roi_harness/create_design_json.py
@@ -0,0 +1,120 @@
+import xjson
+import csv
+import argparse
+import sys
+import fasm
+from prjxray.db import Database
+from prjxray.roi import Roi
+from prjxray.util import get_db_root, get_part
+from prjxray.xjson import extract_numbers
+
+
+def set_port_wires(ports, name, pin, wires_outside_roi):
+    for port in ports:
+        if name == port['name']:
+            port['wires_outside_roi'] = sorted(
+                wires_outside_roi, key=extract_numbers)
+            assert port['pin'] == pin
+            return
+
+    assert False, name
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=
+        "Creates design.json from output of ROI generation tcl script.")
+    parser.add_argument('--design_txt', required=True)
+    parser.add_argument('--design_info_txt', required=True)
+    parser.add_argument('--pad_wires', required=True)
+    parser.add_argument('--design_fasm', required=True)
+
+    args = parser.parse_args()
+
+    design_json = {}
+    design_json['ports'] = []
+    design_json['info'] = {}
+    with open(args.design_txt) as f:
+        for d in csv.DictReader(f, delimiter=' '):
+            design_json['ports'].append(d)
+
+    with open(args.design_info_txt) as f:
+        for l in f:
+            name, value = l.strip().split(' = ')
+
+            design_json['info'][name] = int(value)
+
+    db = Database(get_db_root(), get_part())
+    grid = db.grid()
+
+    roi = Roi(
+        db=db,
+        x1=design_json['info']['GRID_X_MIN'],
+        y1=design_json['info']['GRID_Y_MIN'],
+        x2=design_json['info']['GRID_X_MAX'],
+        y2=design_json['info']['GRID_Y_MAX'],
+    )
+
+    with open(args.pad_wires) as f:
+        for l in f:
+            parts = l.strip().split(' ')
+            name = parts[0]
+            pin = parts[1]
+            wires = parts[2:]
+
+            wires_outside_roi = []
+
+            for wire in wires:
+                tile = wire.split('/')[0]
+
+                loc = grid.loc_of_tilename(tile)
+
+                if not roi.tile_in_roi(loc):
+                    wires_outside_roi.append(wire)
+
+            set_port_wires(design_json['ports'], name, pin, wires_outside_roi)
+
+    frames_in_use = set()
+    for tile in roi.gen_tiles():
+        gridinfo = grid.gridinfo_at_tilename(tile)
+
+        for bit in gridinfo.bits.values():
+            frames_in_use.add(bit.base_address)
+
+    required_features = []
+    for fasm_line in fasm.parse_fasm_filename(args.design_fasm):
+        if fasm_line.annotations:
+            for annotation in fasm_line.annotations:
+                if annotation.name != 'unknown_segment':
+                    continue
+
+                unknown_base_address = int(annotation.value, 0)
+
+                assert False, "Found unknown bit in base address 0x{:08x}".format(
+                    unknown_base_address)
+
+        if not fasm_line.set_feature:
+            continue
+
+        tile = fasm_line.set_feature.feature.split('.')[0]
+
+        loc = grid.loc_of_tilename(tile)
+        gridinfo = grid.gridinfo_at_tilename(tile)
+
+        not_in_roi = not roi.tile_in_roi(loc)
+
+        if not_in_roi:
+            required_features.append(fasm_line)
+
+    design_json['required_features'] = sorted(
+        fasm.fasm_tuple_to_string(required_features,
+                                  canonical=True).split('\n'),
+        key=extract_numbers)
+
+    design_json['ports'].sort(key=lambda x: extract_numbers(x['name']))
+
+    xjson.pprint(sys.stdout, design_json)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/minitests/roi_harness/fasm2bit.sh b/minitests/roi_harness/fasm2bit.sh
new file mode 100755
index 0000000..ee1d389
--- /dev/null
+++ b/minitests/roi_harness/fasm2bit.sh
@@ -0,0 +1,40 @@
+# Example pre-req
+# ./runme.sh
+# XRAY_ROIV=roi_inv.v XRAY_FIXED_XDC=out_xc7a35tcpg236-1_BASYS3-SWBUT_roi_basev/fixed_noclk.xdc ./runme.sh
+
+set -ex
+
+fasm_in=$1
+if [ -z "$fasm_in" ] ; then
+    echo "need .fasm arg"
+    exit
+fi
+bit_in=$2
+if [ -z "$bit_in" ] ; then
+    echo "need .bit arg"
+    exit
+fi
+bit_out=$3
+if [ -z "$bit_out" ] ; then
+    bit_out=$(echo $fasm_in |sed s/.fasm/.bit/)
+    if [ "$bit_out" = "$fasm_in" ] ; then
+        echo "Expected fasm file"
+        exit 1
+    fi
+fi
+
+echo "Design .fasm: $fasm_in"
+echo "Harness .bit: $bit_in"
+echo "Out .bit: $bit_out"
+
+${XRAY_FASM2FRAMES} --sparse $fasm_in roi_partial.frm
+
+${XRAY_TOOLS_DIR}/xc7patch \
+	--part_name ${XRAY_PART} \
+	--part_file ${XRAY_PART_YAML} \
+	--bitstream_file $bit_in \
+	--frm_file roi_partial.frm \
+	--output_file $bit_out
+
+#openocd -f $XRAY_DIR/utils/openocd/board-digilent-basys3.cfg -c "init; pld load 0 $bit_out; exit"
+
diff --git a/minitests/roi_harness/roi_base.v b/minitests/roi_harness/roi_base.v
new file mode 100644
index 0000000..a77662c
--- /dev/null
+++ b/minitests/roi_harness/roi_base.v
@@ -0,0 +1,49 @@
+//See README and tcl for more info
+
+`include "defines.v"
+
+module roi(input clk,
+        input [DIN_N-1:0] din, output [DOUT_N-1:0] dout);
+    parameter DIN_N = `DIN_N;
+    parameter DOUT_N = `DOUT_N;
+
+    genvar i;
+    generate
+        //CLK
+        (* KEEP, DONT_TOUCH *)
+        reg clk_reg;
+        always @(posedge clk) begin
+            clk_reg <= clk_reg;
+        end
+
+        //DIN
+        for (i = 0; i < DIN_N; i = i+1) begin:ins
+            (* KEEP, DONT_TOUCH *)
+            LUT6 #(
+                .INIT(64'b01)
+            ) lut (
+                .I0(din[i]),
+                .I1(1'b0),
+                .I2(1'b0),
+                .I3(1'b0),
+                .I4(1'b0),
+                .I5(1'b0),
+                .O());
+        end
+
+        //DOUT
+        for (i = 0; i < DOUT_N; i = i+1) begin:outs
+            (* KEEP, DONT_TOUCH *)
+            LUT6 #(
+                .INIT(64'b01)
+            ) lut (
+                .I0(1'b0),
+                .I1(1'b0),
+                .I2(1'b0),
+                .I3(1'b0),
+                .I4(1'b0),
+                .I5(1'b0),
+                .O(dout[i]));
+        end
+    endgenerate
+endmodule
diff --git a/minitests/roi_harness/runme.py b/minitests/roi_harness/runme.py
new file mode 100644
index 0000000..a889124
--- /dev/null
+++ b/minitests/roi_harness/runme.py
@@ -0,0 +1,57 @@
+import edalize
+import os
+
+work_root = 'build'
+
+pins = {
+    'di_0': os.environ['URAY_PIN_02'],
+    'di_1': os.environ['URAY_PIN_03'],
+    'do':  os.environ['URAY_PIN_04'],
+}
+
+xdc_file = os.path.realpath(os.path.join(work_root, 'top.xdc'))
+pre_imp_file = os.path.realpath(os.path.join(work_root, 'pre.tcl'))
+post_imp_file = os.path.realpath(os.path.join(work_root, 'post.tcl'))
+
+os.makedirs(work_root, exist_ok=True)
+
+synth_tool = 'yosys' if 'USE_YOSYS' in os.environ else 'vivado'
+
+with open(xdc_file, 'w') as f:
+    f.write('set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets *]\n')
+    for key, val in pins.items():
+        f.write('set_property PACKAGE_PIN {} [get_ports {}]\n'.format(val, key))
+        f.write('set_property IOSTANDARD LVCMOS33 [get_ports {}]\n'.format(key))
+
+with open(pre_imp_file, 'w') as f:
+    f.write('create_pblock roi\n')
+    f.write('add_cells_to_pblock [get_pblocks roi] [get_cells roi]\n')
+    f.write('resize_pblock [get_pblocks roi] -add "{}"\n'.format(os.environ['URAY_ROI']))
+    f.write('set_property BITSTREAM.GENERAL.PERFRAMECRC YES [current_design]\n')
+
+with open(post_imp_file, 'w') as f:
+    f.write('write_checkpoint -force design.dcp')
+
+files = [
+    {'name': os.path.realpath('top_mini.v'), 'file_type': 'verilogSource'},
+    {'name': xdc_file, 'file_type': 'xdc'},
+]
+
+tool = 'vivado'
+
+edam = {
+  'files' : files,
+  'name'  : 'design',
+  'toplevel': 'top',
+  'tool_options' : {'vivado' : {
+    'part' : os.environ['URAY_PART'],
+    'pre_imp' : pre_imp_file,
+    'post_imp' : post_imp_file,
+    'synth' : synth_tool
+    }}
+}
+
+backend = edalize.get_edatool(tool)(edam=edam, work_root=work_root)
+
+backend.configure("")
+backend.build()
diff --git a/minitests/roi_harness/runme.sh b/minitests/roi_harness/runme.sh
new file mode 100755
index 0000000..d14e409
--- /dev/null
+++ b/minitests/roi_harness/runme.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+# This script requires an XC7A50T family part
+
+set -ex
+
+source ${URAY_DIR}/utils/environment.sh
+
+export URAY_PINCFG=${URAY_PINCFG:-ZYNQUSP-SWBUT}
+export BUILD_DIR=${BUILD_DIR:-build}
+
+# not part of the normal DB
+# to generate:
+#cat >/dev/null <<EOF
+#pushd ${URAY_DIR}
+#source minitests/roi_harness/basys3.sh
+#pushd fuzzers/001-part-yaml
+#make clean
+#make
+#make pushdb
+#popd
+#popd
+#EOF
+stat ${URAY_PART_YAML} >/dev/null
+
+# 6x by 18y CLBs (108)
+if [ "$SMALL" = Y ] ; then
+    echo "Design: small"
+    export PITCH=${URAY_PITCH:-1}
+    export DIN_N=${URAY_DIN_N_SMALL:-8}
+    export DOUT_N=${URAY_DOUT_N_SMALL:-8}
+    export URAY_ROI=${URAY_ROI_SMALL:-SLICE_X12Y100:SLICE_X17Y117}
+# All of CMT X0Y2
+else
+    echo "Design: large"
+    export PITCH=${URAY_PITCH:-2}
+    export DIN_N=${URAY_DIN_N_LARGE:-8}
+    export DOUT_N=${URAY_DOUT_N_LARGE:-8}
+    export URAY_ROI=${URAY_ROI_LARGE:-SLICE_X0Y100:SLICE_X35Y149}
+fi
+
+echo ${DIN_N}
+echo ${DOUT_N}
+echo ${URAY_ROI}
+
+mkdir -p $BUILD_DIR
+pushd $BUILD_DIR
+
+cat >defines.v <<EOF
+\`ifndef DIN_N
+\`define DIN_N $DIN_N
+\`endif
+
+\`ifndef DOUT_N
+\`define DOUT_N $DOUT_N
+\`endif
+EOF
+
+${URAY_VIVADO} -mode batch -source ../runme.tcl
+test -z "$(fgrep CRITICAL vivado.log)"
+
+${URAY_BITREAD} -F $URAY_ROI_FRAMES -o design.bits -z -y design.bit
+python3 ${URAY_DIR}/utils/bit2fasm.py --verbose design.bit > design.fasm
+python3 ${URAY_DIR}/utils/fasm2frames.py design.fasm design.frm
+PYTHONPATH=$PYTHONPATH:$URAY_DIR/utils python3 ../create_design_json.py \
+    --design_info_txt design_info.txt \
+    --design_txt design.txt \
+    --pad_wires design_pad_wires.txt \
+    --design_fasm design.fasm > design.json
+
+# Hack to get around weird clock error related to clk net not found
+# Remove following lines:
+#set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk_IBUF]
+#set_property FIXED_ROUTE { { CLK_BUFG_BUFGCTRL0_O CLK_BUFG_CK_GCLK0 ... CLK_L1 CLBLM_M_CLK }  } [get_nets clk_net]
+if [ -f fixed.xdc ] ; then
+    cat fixed.xdc |fgrep -v 'CLOCK_DEDICATED_ROUTE FALSE' |fgrep -v 'set_property FIXED_ROUTE { { CLK_BUFG_BUFGCTRL0_O' >fixed_noclk.xdc
+fi
+popd
diff --git a/minitests/roi_harness/runme.tcl b/minitests/roi_harness/runme.tcl
new file mode 100644
index 0000000..bfa7c99
--- /dev/null
+++ b/minitests/roi_harness/runme.tcl
@@ -0,0 +1,432 @@
+# WARNING: this is somewhat paramaterized, but is only tested on A50T/A35T with the traditional ROI
+# Your ROI should at least have a SLICEL on the left
+
+# Number of package inputs going to ROI
+set DIN_N 8
+if { [info exists ::env(DIN_N) ] } {
+    set DIN_N "$::env(DIN_N)"
+}
+# Number of ROI outputs going to package
+set DOUT_N 8
+if { [info exists ::env(DOUT_N) ] } {
+    set DOUT_N "$::env(DOUT_N)"
+}
+# How many rows between pins
+# Reduces routing pressure
+set PITCH 1
+if { [info exists ::env(PITCH) ] } {
+    set PITCH "$::env(PITCH)"
+}
+
+if { [info exists ::env(URAY_ROI_HCLK)] } {
+    set URAY_ROI_HCLK "$::env(URAY_ROI_HCLK)"
+} else {
+    puts "WARNING: No HCLK has been set"
+}
+
+# Setting all the PIPs for DIN and DOUT
+if { [info exists ::env(URAY_ROI_DIN_LPIP)] } {
+    set DIN_LPIP "$::env(URAY_ROI_DIN_LPIP)"
+} else { puts "Warning: No left pip for DIN has been set"  }
+
+if { [info exists ::env(URAY_ROI_DIN_RPIP)] } {
+    set DIN_RPIP "$::env(URAY_ROI_DIN_RPIP)"
+} else { puts "Warning: No right pip for DIN has been set"  }
+
+if { [info exists ::env(URAY_ROI_DOUT_LPIP)] } {
+    set DOUT_LPIP "$::env(URAY_ROI_DOUT_LPIP)"
+} else { puts "Warning: No left pip for DOUT has been set"  }
+
+if { [info exists ::env(URAY_ROI_DOUT_RPIP)] } {
+    set DOUT_RPIP "$::env(URAY_ROI_DOUT_RPIP)"
+} else { puts "Warning: No right pip for DOUT has been set"  }
+
+# Setting all INT_L/R tiles for DIN and DOUT X values
+if { [info exists ::env(URAY_ROI_DIN_INT_L_X)] } {
+    set DIN_INT_L_X "$::env(URAY_ROI_DIN_INT_L_X)"
+} else { puts "Warning: No INT_L for DIN has been set"  }
+
+if { [info exists ::env(URAY_ROI_DIN_INT_R_X)] } {
+    set DIN_INT_R_X "$::env(URAY_ROI_DIN_INT_R_X)"
+} else { puts "Warning: No INT_R for DIN has been set"  }
+
+if { [info exists ::env(URAY_ROI_DOUT_INT_L_X)] } {
+    set DOUT_INT_L_X "$::env(URAY_ROI_DOUT_INT_L_X)"
+} else { puts "Warning: No INT_L for DOUT has been set"  }
+
+if { [info exists ::env(URAY_ROI_DOUT_INT_R_X)] } {
+    set DOUT_INT_R_X "$::env(URAY_ROI_DOUT_INT_R_X)"
+} else { puts "Warning: No INT_R for DOUT has been set"  }
+
+# X12 in the ROI, X10 just to the left
+# Start at bottom left of ROI and work up
+# (IOs are to left)
+# SLICE_X12Y100:SLICE_X27Y149
+# set X_BASE 12
+set URAY_ROI_X0 [lindex [split [lindex [split "$::env(URAY_ROI)" Y] 0] X] 1]
+set URAY_ROI_X1 [lindex [split [lindex [split "$::env(URAY_ROI)" X] 2] Y] 0]
+set URAY_ROI_Y0 [lindex [split [lindex [split "$::env(URAY_ROI)" Y] 1] :] 0]
+set URAY_ROI_Y1 [lindex [split "$::env(URAY_ROI)" Y] 2]
+
+set X_BASE $URAY_ROI_X0
+set Y_BASE $URAY_ROI_Y0
+
+set Y_CLK_BASE $Y_BASE
+# Clock lut in middle
+set Y_DIN_BASE [expr "$Y_CLK_BASE + $PITCH"]
+# Sequential
+# set Y_DOUT_BASE [expr "$Y_DIN_BASE + $DIN_N"]
+# At top. This relieves routing pressure by spreading things out
+# Note: can actually go up one more if we want
+set Y_DOUT_BASE [expr "$URAY_ROI_Y1 - $DOUT_N * $PITCH"]
+
+# Y_OFFSET: offset amount to shift the components on the y column to avoid hard blocks
+set Y_OFFSET 24
+
+set part "$::env(URAY_PART)"
+set pincfg ""
+if { [info exists ::env(URAY_PINCFG) ] } {
+    set pincfg "$::env(URAY_PINCFG)"
+}
+set roiv "../roi_base.v"
+if { [info exists ::env(URAY_ROIV) ] } {
+    set roiv "$::env(URAY_ROIV)"
+}
+set roiv_trim [string map {.v v} $roiv]
+
+puts "Environment"
+puts "  URAY_ROI: $::env(URAY_ROI)"
+puts "  X_BASE: $X_BASE"
+puts "  Y_DIN_BASE: $Y_DIN_BASE"
+puts "  Y_CLK_BASE: $Y_CLK_BASE"
+puts "  Y_DOUT_BASE: $Y_DOUT_BASE"
+
+source ../../../utils/utils.tcl
+
+create_project -force -part $::env(URAY_PART) design design
+read_verilog ../top.v
+read_verilog $roiv
+set fixed_xdc ""
+if { [info exists ::env(URAY_FIXED_XDC) ] } {
+    set fixed_xdc "$::env(URAY_FIXED_XDC)"
+}
+
+# added flatten_hierarchy
+# dout_shr was getting folded into the pblock
+# synth_design -top top -flatten_hierarchy none -no_lc -keep_equivalent_registers -resource_sharing off
+synth_design -top top -flatten_hierarchy none -verilog_define DIN_N=$DIN_N -verilog_define DOUT_N=$DOUT_N
+
+if {$fixed_xdc ne ""} {
+    read_xdc $fixed_xdc
+}
+
+# Map of top level net names to IOB pin names
+array set net2pin [list]
+
+if {$part eq "xczu7ev-ffvc1156-2-i"} {
+    if {$pincfg eq "ZYNQUSP-SWBUT"} {
+        set sw_but "E4 D4" ;# DIP_SW_0 DIP_SW_1
+        set leds "D5 D6" ;# LED0 LED1
+
+        # 125 MHz CLK onboard
+        set clkp "F23"
+        set clkn "E23"
+        set net2pin(clkp) $clkp
+        set net2pin(clk) $clkn
+
+        # DIN
+        for {set i 0} {$i < $DIN_N} {incr i} {
+            set pin [lindex $sw_but $i]
+            set net2pin(din[$i]) $pin
+        }
+
+        # DOUT
+        for {set i 0} {$i < $DOUT_N} {incr i} {
+            set pin [lindex $leds $i]
+            set net2pin(dout[$i]) $pin
+        }
+
+    } else {
+        error "Unsupported config $pincfg"
+    }
+} else {
+    error "Pins: unsupported part $part"
+}
+
+# Now actually apply the pin definitions
+puts "Applying pin definitions"
+foreach {net pin} [array get net2pin] {
+    puts "  Net $net to pin $pin"
+    set_property -dict "PACKAGE_PIN $pin IOSTANDARD LVCMOS33" [get_ports $net]
+}
+
+if {$fixed_xdc eq ""} {
+    create_pblock roi
+    set_property EXCLUDE_PLACEMENT 1 [get_pblocks roi]
+    set_property CONTAIN_ROUTING true [get_pblocks roi]
+    set_property DONT_TOUCH true [get_cells roi]
+    add_cells_to_pblock [get_pblocks roi] [get_cells roi]
+    resize_pblock [get_pblocks roi] -add "$::env(URAY_ROI)"
+
+    #set_property CFGBVS VCCO [current_design]
+    set_property CONFIG_VOLTAGE 3.3 [current_design]
+
+    set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk_IBUF]
+
+    #write_checkpoint -force synth.dcp
+}
+
+
+proc loc_roi_clk_left {ff_x ff_y} {
+    # Place an ROI clk on the left edge of the ROI
+    # It doesn't actually matter where we place this, just do it to keep things neat looking
+    # ff_x: ROI SLICE X position
+    # ff_yy: row primitives will be placed at
+
+    set slice_ff "SLICE_X${ff_x}Y${ff_y}"
+
+    # Fix FFs to guide route in
+    set cell [get_cells "roi/clk_reg_reg"]
+    set_property LOC $slice_ff $cell
+    set_property BEL AFF $cell
+}
+
+proc loc_lut_in {index lut_x lut_y} {
+    # Place a lut at specified coordinates in BEL A
+    # index: input bus index
+    # lut_x: SLICE X position
+    # lut_y: SLICE Y position
+
+    set slice_lut "SLICE_X${lut_x}Y${lut_y}"
+
+    # Fix LUTs near the edge
+    set cell [get_cells "roi/ins[$index].lut"]
+    set_property LOC $slice_lut $cell
+    set_property BEL A6LUT $cell
+}
+
+proc loc_lut_out {index lut_x lut_y} {
+    # Place a lut at specified coordinates in BEL A
+    # index: input bus index
+    # lut_x: SLICE X position
+    # lut_y: SLICE Y position
+
+    set slice_lut "SLICE_X${lut_x}Y${lut_y}"
+
+    # Fix LUTs near the edge
+    set cell [get_cells "roi/outs[$index].lut"]
+    set_property LOC $slice_lut $cell
+    set_property BEL A6LUT $cell
+}
+
+proc net_bank_left {net} {
+    # return 1 if net goes to a leftmost die IO bank
+    # return 0 if net goes to a rightmost die IO bank
+
+    set bank [get_property IOBANK [get_ports $net]]
+    set left_banks "14 15 16"
+    set right_banks "34 35"
+
+    # left
+    if {[lsearch -exact $left_banks $bank] >= 0} {
+        return 1
+        # right
+    } elseif {[lsearch -exact $right_banks $bank] >= 0} {
+        return 0
+    } else {
+        error "Bad bank $bank"
+    }
+}
+
+# Manual placement
+if {$fixed_xdc eq ""} {
+    set x $X_BASE
+
+    # Place ROI clock right after inputs
+    puts "Placing ROI clock"
+    loc_roi_clk_left $x $Y_CLK_BASE
+
+    # Place ROI inputs
+    puts "Placing ROI inputs"
+    set y_left $Y_DIN_BASE
+    # Shift y_right up to avoid PCIe block that makes routing hard.
+    set y_right [expr {$Y_DIN_BASE + $Y_OFFSET}]
+    for {set i 0} {$i < $DIN_N} {incr i} {
+        if {[net_bank_left "din[$i]"]} {
+            loc_lut_in $i $URAY_ROI_X0 $y_left
+            set y_left [expr {$y_left + $PITCH}]
+        } else {
+            loc_lut_in $i $URAY_ROI_X1 $y_right
+            set y_right [expr {$y_right + $PITCH}]
+        }
+    }
+
+    # Place ROI outputs
+    set y_left $Y_DOUT_BASE
+    set y_right $Y_DOUT_BASE
+    puts "Placing ROI outputs"
+    for {set i 0} {$i < $DOUT_N} {incr i} {
+        if {[net_bank_left "dout[$i]"]} {
+            loc_lut_out $i $URAY_ROI_X0 $y_left
+            set y_left [expr {$y_left + $PITCH}]
+        } else {
+            loc_lut_out $i $URAY_ROI_X1 $y_right
+            set y_right [expr {$y_right + $PITCH}]
+        }
+    }
+}
+
+place_design
+#write_checkpoint -force placed.dcp
+
+# Version with more error checking for missing end node
+# Will do best effort in this case
+proc route_via2 {net nodes} {
+    # net: net as string
+    # nodes: string list of one or more intermediate routing nodes to visit
+
+    set net [get_nets $net]
+    # Start at the net source
+    set fixed_route [get_nodes -of_objects [get_site_pins -filter {DIRECTION == OUT} -of_objects $net]]
+    # End at the net destination
+    # For sone reason this doesn't always show up
+    set site_pins [get_site_pins -filter {DIRECTION == IN} -of_objects $net]
+    if {$site_pins eq ""} {
+        puts "WARNING: could not find end node"
+        #error "Could not find end node"
+    } else {
+        set end_node [get_nodes -of_objects]
+        lappend nodes [$end_node]
+    }
+
+    puts ""
+    puts "Routing net $net:"
+
+    foreach to_node $nodes {
+        if {$to_node eq ""} {
+            error "Empty node"
+        }
+
+        # Node string to object
+        set to_node [get_nodes -of_objects [get_wires $to_node]]
+        # Start at last routed position
+        set from_node [lindex $fixed_route end]
+        # Let vivado do heavy liftin in between
+        set route [find_routing_path -quiet -from $from_node -to $to_node]
+        if {$route == ""} {
+            # Some errors print a huge route
+            puts [concat [string range "  $from_node -> $to_node" 0 1000] ": no route found - assuming direct PIP"]
+            lappend fixed_route $to_node
+        } {
+            puts [concat [string range "  $from_node -> $to_node: $route" 0 1000] "routed"]
+            set fixed_route [concat $fixed_route [lrange $route 1 end]]
+        }
+        set_property -quiet FIXED_ROUTE $fixed_route $net
+    }
+
+    set_property -quiet FIXED_ROUTE $fixed_route $net
+    puts ""
+}
+
+# Return the wire on the ROI boundary
+proc node2wire {node} {
+    set wires [get_wires -of_objects [get_nodes $node]]
+    set wire [lsearch -inline $wires *VBRK*]
+    return $wire
+}
+
+proc write_grid_roi {fp} {
+    puts $fp "GRID_X_MIN = $::env(URAY_ROI_GRID_X1)"
+    puts $fp "GRID_X_MAX = $::env(URAY_ROI_GRID_X2)"
+    puts $fp "GRID_Y_MIN = $::env(URAY_ROI_GRID_Y1)"
+    puts $fp "GRID_Y_MAX = $::env(URAY_ROI_GRID_Y2)"
+}
+
+set fp [open "design_info.txt" w]
+write_grid_roi $fp
+close $fp
+
+set fp [open "design.txt" w]
+set fp_wires [open "design_pad_wires.txt" w]
+puts $fp "name node pin wire"
+# Manual routing
+if {$fixed_xdc eq ""} {
+    set x $X_BASE
+
+    # No routing strictly needed for clk
+    # It will go to high level interconnect that goes everywhere
+    # But we still need to record something, so lets force a route
+    # FIXME: very ROI specific
+    set node "$URAY_ROI_HCLK"
+    set wire [node2wire $node]
+    route_via2 "clk_IBUF_BUFG" "$node"
+    set net "clk"
+    set pin "$net2pin($net)"
+    puts $fp "$net $node $pin $wire"
+
+    puts "Routing ROI inputs"
+    # Arbitrary offset as observed
+    set y_left $Y_DIN_BASE
+    set y_right [expr {$Y_DIN_BASE + $Y_OFFSET}]
+    for {set i 0} {$i < $DIN_N} {incr i} {
+        # needed to force routes away to avoid looping into ROI
+        if {[net_bank_left "din[$i]"]} {
+            set node "INT_L_X${DIN_INT_L_X}Y${y_left}/${DIN_LPIP}"
+            route_via2 "din_IBUF[$i]" "$node"
+            set y_left [expr {$y_left + $PITCH}]
+        } else {
+            set node "INT_R_X${DIN_INT_R_X}Y${y_right}/${DIN_RPIP}"
+            route_via2 "din_IBUF[$i]" "$node"
+            set y_right [expr {$y_right + $PITCH}]
+        }
+        set net "din[$i]"
+        set pin "$net2pin($net)"
+        set wire [node2wire $node]
+        puts $fp "$net $node $pin $wire"
+
+        set wires [get_wires -of_objects [get_nets "din_IBUF[$i]"]]
+        puts $fp_wires "$net $pin $wires"
+    }
+
+    puts "Routing ROI outputs"
+    # Arbitrary offset as observed
+    set y_left [expr {$Y_DOUT_BASE + 0}]
+    set y_right [expr {$Y_DOUT_BASE + 0}]
+    for {set i 0} {$i < $DOUT_N} {incr i} {
+        if {[net_bank_left "dout[$i]"]} {
+            set node "INT_L_X${DOUT_INT_L_X}Y${y_left}/${DOUT_LPIP}"
+            route_via2 "roi/dout[$i]" "$node"
+            set y_left [expr {$y_left + $PITCH}]
+            # XXX: only care about right ports on Arty
+        } else {
+            set node "INT_R_X${DOUT_INT_R_X}Y${y_right}/${DOUT_RPIP}"
+            route_via2 "roi/dout[$i]" "$node"
+            set y_right [expr {$y_right + $PITCH}]
+        }
+        set net "dout[$i]"
+        set pin "$net2pin($net)"
+        set wire [node2wire $node]
+        puts $fp "$net $node $pin $wire"
+
+        set wires [get_wires -of_objects [get_nets "roi/dout[$i]"]]
+        puts $fp_wires "$net $pin $wires"
+    }
+}
+close $fp
+close $fp_wires
+
+puts "routing design"
+route_design
+
+# Don't set for user designs
+# Makes things easier to debug
+if {$fixed_xdc eq ""} {
+    set_property IS_ROUTE_FIXED 1 [get_nets -hierarchical]
+    #set_property IS_LOC_FIXED 1 [get_cells -hierarchical]
+    #set_property IS_BEL_FIXED 1 [get_cells -hierarchical]
+    write_xdc -force fixed.xdc
+}
+
+write_checkpoint -force design.dcp
+#set_property BITSTREAM.GENERAL.DEBUGBITSTREAM YES [current_design]
+write_bitstream -force design.bit
diff --git a/minitests/roi_harness/top.v b/minitests/roi_harness/top.v
new file mode 100644
index 0000000..c91257f
--- /dev/null
+++ b/minitests/roi_harness/top.v
@@ -0,0 +1,20 @@
+//See README and tcl for more info
+
+`include "defines.v"
+
+module top(input wire clkp, input wire clkn,
+        inout wire [DIN_N-1:0] din, output wire [DOUT_N-1:0] dout);
+    parameter DIN_N = `DIN_N;
+    parameter DOUT_N = `DOUT_N;
+    wire clk;
+    IBUFDS clk_input_buffer (
+        .O(clk),
+        .I(clkp),
+        .IB(clkn)
+    );
+    roi #(.DIN_N(DIN_N), .DOUT_N(DOUT_N)) roi (
+        .clk(clk),
+        .din(din), .dout(dout)
+    );
+endmodule
+
diff --git a/minitests/roi_harness/top_mini.v b/minitests/roi_harness/top_mini.v
new file mode 100644
index 0000000..a4de1c4
--- /dev/null
+++ b/minitests/roi_harness/top_mini.v
@@ -0,0 +1,22 @@
+//See README and tcl for more info
+
+module top(
+    input wire di_0,
+    input wire di_1,
+    output wire do
+);
+
+	(* LOC="SLICE_X44Y240", BEL="A6LUT", KEEP, DONT_TOUCH *)
+	LUT6 #(
+		.INIT(64'h0000_0000_0000_0001)
+	) lut (
+		.I0(di_0),
+		.I1(di_1),
+		.I2(1'b0),
+		.I3(1'b0),
+		.I4(1'b0),
+		.I5(1'b0),
+		.O(do));
+
+endmodule
+
diff --git a/minitests/roi_harness/zynqusp-common.sh b/minitests/roi_harness/zynqusp-common.sh
new file mode 100644
index 0000000..7de9868
--- /dev/null
+++ b/minitests/roi_harness/zynqusp-common.sh
@@ -0,0 +1,48 @@
+export URAY_PART="xczu7ev-ffvc1156-2-i"
+export URAY_EQUIV_PART="xczu7ev-ffvc1156-2-i"
+
+if [ -z "$URAY_PINCFG" ]; then
+	echo "URAY_PINCFG not set"
+	return 1
+fi
+if [ -z "$URAY_DIN_N_LARGE" ]; then
+	echo "URAY_DIN_N_LARGE not set"
+	return 1
+fi
+if [ -z "$URAY_DOUT_N_LARGE" ]; then
+	echo "URAY_DOUT_N_LARGE not set"
+	return 1
+fi
+
+# For generating DB
+export URAY_PIN_00="B4" #PB_SW_0
+export URAY_PIN_01="C4" #PB_SW_1
+export URAY_PIN_02="E4" #DIP_SW_0
+export URAY_PIN_03="D4" #DIP_SW_1
+export URAY_PIN_04="D5" #LED0
+export URAY_PIN_05="D6" #LED1
+export URAY_PIN_06="E23" #CLK_125_N
+export URAY_PIN_07="F23" #CLK_125_P
+
+# ROI is in top right
+export URAY_ROI_LARGE=""
+
+# HCS (Horizontal Clock Spine) Tile
+export URAY_ROI_HCLK=""
+
+# PITCH
+export URAY_PITCH=3
+
+# INT_L/R for DOUT and DIN
+export URAY_ROI_DIN_INT_L_X=
+export URAY_ROI_DIN_INT_R_X=
+export URAY_ROI_DOUT_INT_L_X=
+export URAY_ROI_DOUT_INT_R_X=
+
+# PIPS for DOUT and DIN
+export URAY_ROI_DIN_LPIP=
+export URAY_ROI_DIN_RPIP=
+export URAY_ROI_DOUT_LPIP=
+export URAY_ROI_DOUT_RPIP=
+
+source $URAY_DIR/utils/environment.sh
diff --git a/minitests/roi_harness/zynqusp-swbut.sh b/minitests/roi_harness/zynqusp-swbut.sh
new file mode 100644
index 0000000..e1bb901
--- /dev/null
+++ b/minitests/roi_harness/zynqusp-swbut.sh
@@ -0,0 +1,17 @@
+export URAY_PINCFG=ZYNQUSP-SWBUT
+export URAY_DIN_N_SMALL=2
+export URAY_DOUT_N_SMALL=1
+export HARNESS_DIR=$URAY_DIR/database/zynqusp/harness/swbut/
+export SMALL="Y"
+
+export URAY_PIN_00="B4" #PB_SW_0
+export URAY_PIN_01="C4" #PB_SW_1
+export URAY_PIN_02="E4" #DIP_SW_0
+export URAY_PIN_03="D4" #DIP_SW_1
+export URAY_PIN_04="D5" #LED0
+export URAY_PIN_05="D6" #LED1
+export URAY_PIN_06="E23" #CLK_125_N
+export URAY_PIN_07="F23" #CLK_125_P
+
+source $URAY_DIR/minitests/roi_harness/zynqusp-common.sh
+
diff --git a/utils/environment.sh b/utils/environment.sh
index 863b05e..a04fa0a 100644
--- a/utils/environment.sh
+++ b/utils/environment.sh
@@ -28,7 +28,7 @@
 export URAY_MASKMERGE="bash ${URAY_UTILS_DIR}/maskmerge.sh"
 export URAY_SEGMATCH="${URAY_TOOLS_DIR}/segmatch"
 export URAY_SEGPRINT="python3 ${URAY_UTILS_DIR}/segprint.py"
-export URAY_BIT2FASM="python3 ${URAY_UTILS_DIR}/bit2fasm.py"
+export URAY_BIT2FASM="python3 ${URAY_UTILS_DIR}/bit2fasm.py --architecture ${URAY_ARCH}"
 export URAY_FASM2FRAMES="python3 ${URAY_UTILS_DIR}/fasm2frames.py"
 export URAY_BITTOOL="${URAY_TOOLS_DIR}/bittool"
 export URAY_BLOCKWIDTH="python3 ${URAY_UTILS_DIR}/blockwidth.py"