diff --git a/minitests/ps7/xtra/Makefile b/minitests/ps7/xtra/Makefile
new file mode 100644
index 00000000..fc98bc23
--- /dev/null
+++ b/minitests/ps7/xtra/Makefile
@@ -0,0 +1,16 @@
+.PHONY: all clean
+
+all: ps7.v
+
+clean:
+ rm -rf ps7.v
+ rm -rf ps7.csv
+ rm -rf *.xml
+ rm -rf *.log
+ rm -rf .Xil
+
+ps7.csv: dump_ps7.tcl
+ $(XRAY_VIVADO) -mode batch -source dump_ps7.tcl -nojournal -log $(basename $@).log
+
+ps7.v: ps7.csv make_cell.py
+ python3 make_cell.py $<
diff --git a/minitests/ps7/xtra/README.md b/minitests/ps7/xtra/README.md
new file mode 100644
index 00000000..d44b22a1
--- /dev/null
+++ b/minitests/ps7/xtra/README.md
@@ -0,0 +1,3 @@
+# PS7 verilog cell definition extractor
+
+Extracts all pins of the PS7 bel from Vivado, groups them into buses, removes those that are not connected (TEST*, DEBUG*) and writes the PS7 verilog cell definition.
diff --git a/minitests/ps7/xtra/dump_ps7.tcl b/minitests/ps7/xtra/dump_ps7.tcl
new file mode 100644
index 00000000..ffb23c03
--- /dev/null
+++ b/minitests/ps7/xtra/dump_ps7.tcl
@@ -0,0 +1,18 @@
+create_project -force -in_memory -name design -part xc7z020clg400-1
+set_property design_mode PinPlanning [current_fileset]
+open_io_design -name io_1
+
+set fp [open ps7.csv w]
+puts $fp "name,is_input,is_output"
+
+set pins [get_bel_pins -of_objects [get_bels -of_objects [get_sites PS7* -of_objects [get_tiles PSS*]]]]
+foreach pin $pins {
+
+ set pin_name [lindex [split $pin "/"] 2]
+ set is_input [get_property IS_INPUT $pin]
+ set is_output [get_property IS_OUTPUT $pin]
+
+ puts $fp "$pin_name,$is_input,$is_output"
+}
+
+close $fp
diff --git a/minitests/ps7/xtra/make_cell.py b/minitests/ps7/xtra/make_cell.py
new file mode 100644
index 00000000..60d98b4b
--- /dev/null
+++ b/minitests/ps7/xtra/make_cell.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+import argparse
+import csv
+import re
+
+from collections import defaultdict
+
+# =============================================================================
+
+
+def main():
+
+ BUS_REGEX = re.compile("(.*[A-Z_])([0-9]+)$")
+
+ # Parse arguments
+ parser = argparse.ArgumentParser()
+ parser.add_argument("csv", type=str, help="PS7 pin dump file")
+ args = parser.parse_args()
+
+ # Load pin dump
+ with open(args.csv, "r") as fp:
+ pin_dump = list(csv.DictReader(fp))
+
+ # Group pins into buses
+ buses = defaultdict(lambda :{
+ "direction": None,
+ "min": None,
+ "max": None,
+ "width": 0
+ })
+
+ for pin in list(pin_dump):
+
+ # Get bus name and signal index
+ match = BUS_REGEX.match(pin["name"])
+ if match:
+ name = match.group(1)
+ idx = int(match.group(2))
+ else:
+ name = pin["name"]
+ idx = 0
+
+ # Get direction
+ if int(pin["is_input"]):
+ direction = "input"
+ if int(pin["is_output"]):
+ direction = "output"
+
+ # Add to bus
+ bus = buses[name]
+
+ if bus["direction"] is None:
+ bus["direction"] = direction
+ else:
+ assert bus["direction"] == direction
+
+ if bus["min"] is None:
+ bus["min"] = idx
+ else:
+ bus["min"] = min(bus["min"], idx)
+
+ if bus["max"] is None:
+ bus["max"] = idx
+ else:
+ bus["max"] = max(bus["max"], idx)
+
+ bus["width"] = bus["max"] - bus["min"] + 1
+
+ # Sort buses by their purpose
+ for name, bus in buses.items():
+
+ # A test pin (unconnected)
+ if name.startswith("TEST"):
+ cls = "test"
+
+ # A debug pin (unconnected)
+ elif name.startswith("DEBUG"):
+ cls = "debug"
+
+ # A MIO/DDR pin.
+ elif name.startswith("MIO") or name.startswith("DDR") and \
+ name != "DDRARB":
+ cls = "mio"
+
+ # "Normal" pin
+ else:
+ cls = "normal"
+
+ bus["class"] = cls
+
+ # .....................................................
+ # Generate XML model
+ model_xml = """
+
+"""
+
+ # Inputs
+ model_xml += """
+"""
+ for name in sorted(buses.keys()):
+ bus = buses[name]
+
+ # Skip not relevant pins
+ if bus["class"] not in ["normal", "mio"]:
+ continue
+
+ if bus["direction"] != "input":
+ continue
+
+ model_xml += " \n".format(name)
+
+ # Outputs
+ model_xml += """
+
+"""
+ for name in sorted(buses.keys()):
+ bus = buses[name]
+
+ # Skip not relevant pins
+ if bus["class"] not in ["normal", "mio"]:
+ continue
+
+ if bus["direction"] != "output":
+ continue
+
+ model_xml += " \n".format(name)
+
+ model_xml += """
+"""
+
+ model_xml += """
+"""
+
+ with open("ps7.model.xml", "w") as fp:
+ fp.write(model_xml)
+
+ # .....................................................
+ # Generate XML pb_type
+ pb_name = "PS7"
+ blif_model = "PS7"
+
+ pb_xml = """
+""".format(pb_name, blif_model)
+
+ for name in sorted(buses.keys()):
+ bus = buses[name]
+
+ # Skip not relevant pins
+ if bus["class"] not in ["normal", "mio"]:
+ continue
+
+ pb_xml += " <{} name=\"{}\" num_pins=\"{}\"/>\n".format(
+ bus["direction"].ljust(6), name, bus["width"])
+
+ pb_xml += """
+"""
+
+ with open("ps7.pb_type.xml", "w") as fp:
+ fp.write(pb_xml)
+
+ # .....................................................
+ # Prepare Verilog module definition for the PS7
+ pin_strs = []
+ for name in sorted(buses.keys()):
+ bus = buses[name]
+
+ # Skip not relevant pins
+ if bus["class"] not in ["normal", "mio"]:
+ continue
+
+ if bus["width"] > 1:
+ pin_str = " {} [{:>2d}:{:>2d}] {}".format(
+ bus["direction"].ljust(6), bus["max"], bus["min"], name)
+ else:
+ pin_str = " {} {}".format(bus["direction"].ljust(6), name)
+
+ pin_strs.append(pin_str)
+
+ verilog = """(* blackbox *)
+module PS7 (
+{}
+);
+
+endmodule
+""".format(",\n".join(pin_strs))
+
+ with open("ps7.v", "w") as fp:
+ fp.write(verilog)
+
+
+if __name__ == "__main__":
+ main()