Merge branch 'separate-xcu-tools' into 'master'

xcutools: separation of xc7 in xc7, xcu and xcup libraries

See merge request repositories/google-prjuray-tools!2
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 3edcf3c..bebc2d1 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -24,6 +24,22 @@
 	xilinx/xc7series/configuration_bus.cc
 	xilinx/xc7series/configuration_column.cc
 	xilinx/xc7series/ecc.cc
+	# UltraScale specific
+	xilinx/xcuseries/frame_address.cc
+	xilinx/xcuseries/part.cc
+	xilinx/xcuseries/configuration_row.cc
+	xilinx/xcuseries/block_type.cc
+	xilinx/xcuseries/configuration_bus.cc
+	xilinx/xcuseries/configuration_column.cc
+	xilinx/xcuseries/ecc.cc
+	# UltraScalePlus specific
+	xilinx/xcupseries/frame_address.cc
+	xilinx/xcupseries/part.cc
+	xilinx/xcupseries/configuration_row.cc
+	xilinx/xcupseries/block_type.cc
+	xilinx/xcupseries/configuration_bus.cc
+	xilinx/xcupseries/configuration_column.cc
+	xilinx/xcupseries/ecc.cc
 )
 target_include_directories(libprjxray PUBLIC "include")
 target_link_libraries(libprjxray absl::optional absl::variant absl::strings absl::span absl::time yaml-cpp)
diff --git a/lib/include/prjxray/xilinx/architectures.h b/lib/include/prjxray/xilinx/architectures.h
index 9459f7a..c0a8212 100644
--- a/lib/include/prjxray/xilinx/architectures.h
+++ b/lib/include/prjxray/xilinx/architectures.h
@@ -10,6 +10,10 @@
 #include <prjxray/xilinx/spartan6/part.h>
 #include <prjxray/xilinx/xc7series/frame_address.h>
 #include <prjxray/xilinx/xc7series/part.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+#include <prjxray/xilinx/xcuseries/part.h>
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+#include <prjxray/xilinx/xcupseries/part.h>
 
 namespace prjxray {
 namespace xilinx {
@@ -56,18 +60,22 @@
 	static constexpr int words_per_frame = 101;
 };
 
-class UltraScalePlus : public Series7 {
-       public:
-	UltraScalePlus() : Series7("UltraScalePlus") {}
-	static constexpr int words_per_frame = 93;
-};
-
 class UltraScale : public Series7 {
        public:
 	UltraScale() : Series7("UltraScale") {}
+	using Part = xcuseries::Part;
+	using FrameAddress = xcuseries::FrameAddress;
 	static constexpr int words_per_frame = 123;
 };
 
+class UltraScalePlus : public Series7 {
+       public:
+	UltraScalePlus() : Series7("UltraScalePlus") {}
+	using Part = xcupseries::Part;
+	using FrameAddress = xcupseries::FrameAddress;
+	static constexpr int words_per_frame = 93;
+};
+
 class ArchitectureFactory {
        public:
 	static Architecture::Container create_architecture(
diff --git a/lib/include/prjxray/xilinx/configuration_register.h b/lib/include/prjxray/xilinx/configuration_register.h
index 0699f6c..22eb81a 100644
--- a/lib/include/prjxray/xilinx/configuration_register.h
+++ b/lib/include/prjxray/xilinx/configuration_register.h
@@ -49,6 +49,8 @@
 
 // Series-7 configuration register addresses
 // according to UG470, pg. 109
+// UltraScale and UltraScalePlus configuration register addresses
+// according to UG570, pg. 162-163
 enum class Series7ConfigurationRegister : unsigned int {
 	CRC = 0x00,
 	FAR = 0x01,
diff --git a/lib/include/prjxray/xilinx/xcupseries/block_type.h b/lib/include/prjxray/xilinx/xcupseries/block_type.h
new file mode 100644
index 0000000..dadf49d
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/block_type.h
@@ -0,0 +1,34 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_BLOCK_TYPE_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_BLOCK_TYPE_H_
+
+#include <ostream>
+
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+enum class BlockType : unsigned int {
+	CLB_IO_CLK = 0x0,
+	BLOCK_RAM = 0x1,
+	CFG_CLB = 0x2,
+	/* reserved = 0x3, */
+};
+
+std::ostream& operator<<(std::ostream& o, BlockType value);
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::BlockType> {
+	static Node encode(const prjxray::xilinx::xcupseries::BlockType& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcupseries::BlockType& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_BLOCK_TYPE_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/command.h b/lib/include/prjxray/xilinx/xcupseries/command.h
new file mode 100644
index 0000000..016990d
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/command.h
@@ -0,0 +1,34 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_COMMAND_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_COMMAND_H_
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+enum class Command : uint32_t {
+	NOP = 0x0,
+	WCFG = 0x1,
+	MFW = 0x2,
+	LFRM = 0x3,
+	RCFG = 0x4,
+	START = 0x5,
+	RCAP = 0x6,
+	RCRC = 0x7,
+	AGHIGH = 0x8,
+	SWITCH = 0x9,
+	GRESTORE = 0xA,
+	SHUTDOWN = 0xB,
+	GCAPTURE = 0xC,
+	DESYNC = 0xD,
+	IPROG = 0xF,
+	CRCC = 0x10,
+	LTIMER = 0x11,
+	BSPI_READ = 0x12,
+	FALL_EDGE = 0x13,
+};
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_COMMAND_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/configuration_bus.h b/lib/include/prjxray/xilinx/xcupseries/configuration_bus.h
new file mode 100644
index 0000000..b1fafdc
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/configuration_bus.h
@@ -0,0 +1,91 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_BUS_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_BUS_H_
+
+#include <algorithm>
+#include <cassert>
+#include <map>
+#include <memory>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcupseries/configuration_column.h>
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+// ConfigurationBus represents a bus for sending frames to a specific BlockType
+// within a Row.  An instance of ConfigurationBus will contain one or more
+// ConfigurationColumns.
+class ConfigurationBus {
+       public:
+	ConfigurationBus() = default;
+
+	// Constructs a ConfigurationBus from iterators yielding
+	// FrameAddresses.  The frame address need not be contiguous or sorted
+	// but they must all have the same block type, row half, and row
+	// address components.
+	template <typename T>
+	ConfigurationBus(T first, T last);
+
+	// Returns true if the provided address falls into a valid segment of
+	// the address range on this bus.  Only the column and minor components
+	// of the address are considered as all other components are outside
+	// the scope of a bus.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next valid address on the bus in numerically increasing
+	// order. If the next address would fall outside this bus, no object is
+	// returned.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<ConfigurationBus>;
+
+	std::map<unsigned int, ConfigurationColumn> configuration_columns_;
+};
+
+template <typename T>
+ConfigurationBus::ConfigurationBus(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.block_type() == first->block_type() &&
+		            addr.row() == first->row());
+	    }));
+
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.column() < rhs.column();
+	          });
+
+	for (auto col_first = first; col_first != last;) {
+		auto col_last = std::upper_bound(
+		    col_first, last, col_first->column(),
+		    [](const unsigned int& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.column();
+		    });
+
+		configuration_columns_.emplace(
+		    col_first->column(),
+		    std::move(ConfigurationColumn(col_first, col_last)));
+		col_first = col_last;
+	}
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::ConfigurationBus> {
+	static Node encode(
+	    const prjxray::xilinx::xcupseries::ConfigurationBus& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcupseries::ConfigurationBus& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_BUS_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/configuration_column.h b/lib/include/prjxray/xilinx/xcupseries/configuration_column.h
new file mode 100644
index 0000000..33696c2
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/configuration_column.h
@@ -0,0 +1,78 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_COLUMN_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_COLUMN_H_
+
+#include <algorithm>
+#include <cassert>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+// ConfigurationColumn represents an endpoint on a ConfigurationBus.
+class ConfigurationColumn {
+       public:
+	ConfigurationColumn() = default;
+	ConfigurationColumn(unsigned int frame_count)
+	    : frame_count_(frame_count) {}
+
+	// Returns a ConfigurationColumn that describes a continguous range of
+	// minor addresses that encompasses the given
+	// FrameAddresses.  The provided addresses must only
+	// differ only by their minor addresses.
+	template <typename T>
+	ConfigurationColumn(T first, T last);
+
+	// Returns true if the minor field of the address is within the valid
+	// range of this column.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next address in numerical order.  If the next address
+	// would be outside this column, return no object.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<ConfigurationColumn>;
+
+	unsigned int frame_count_;
+};
+
+template <typename T>
+ConfigurationColumn::ConfigurationColumn(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.block_type() == first->block_type() &&
+		            addr.is_bottom_half_rows() ==
+		                first->is_bottom_half_rows() &&
+		            addr.row() == first->row() &&
+		            addr.column() == first->column());
+	    }));
+
+	auto max_minor = std::max_element(
+	    first, last, [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		    return lhs.minor() < rhs.minor();
+	    });
+
+	frame_count_ = max_minor->minor() + 1;
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::ConfigurationColumn> {
+	static Node encode(
+	    const prjxray::xilinx::xcupseries::ConfigurationColumn& rhs);
+	static bool decode(
+	    const Node& node,
+	    prjxray::xilinx::xcupseries::ConfigurationColumn& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_COLUMN_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/configuration_options_0_value.h b/lib/include/prjxray/xilinx/xcupseries/configuration_options_0_value.h
new file mode 100644
index 0000000..124d54a
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/configuration_options_0_value.h
@@ -0,0 +1,122 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
+
+#include <prjxray/bit_ops.h>
+#include <prjxray/xilinx/configuration_packet.h>
+#include <prjxray/xilinx/configuration_register.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+class ConfigurationOptions0Value {
+       public:
+	enum class StartupClockSource : uint32_t {
+		CCLK = 0x0,
+		User = 0x1,
+		JTAG = 0x2,
+	};
+
+	enum class SignalReleaseCycle : uint32_t {
+		Phase1 = 0x0,
+		Phase2 = 0x1,
+		Phase3 = 0x2,
+		Phase4 = 0x3,
+		Phase5 = 0x4,
+		Phase6 = 0x5,
+		TrackDone = 0x6,
+		Keep = 0x7,
+	};
+
+	enum class StallCycle : uint32_t {
+		Phase0 = 0x0,
+		Phase1 = 0x1,
+		Phase2 = 0x2,
+		Phase3 = 0x3,
+		Phase4 = 0x4,
+		Phase5 = 0x5,
+		Phase6 = 0x6,
+		NoWait = 0x7,
+	};
+
+	ConfigurationOptions0Value() : value_(0) {}
+
+	operator uint32_t() const { return value_; }
+
+	ConfigurationOptions0Value& SetUseDonePinAsPowerdownStatus(
+	    bool enabled) {
+		value_ = bit_field_set(value_, 27, 27, enabled ? 1 : 0);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetAddPipelineStageForDoneIn(bool enabled) {
+		value_ = bit_field_set(value_, 25, 25, enabled ? 1 : 0);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetDriveDoneHigh(bool enabled) {
+		value_ = bit_field_set(value_, 24, 24, enabled);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReadbackIsSingleShot(bool enabled) {
+		value_ = bit_field_set(value_, 23, 23, enabled);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetCclkFrequency(uint32_t mhz) {
+		value_ = bit_field_set(value_, 22, 17, mhz);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetStartupClockSource(
+	    StartupClockSource source) {
+		value_ = bit_field_set(value_, 16, 15,
+		                       static_cast<uint32_t>(source));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReleaseDonePinAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 14, 12, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetStallAtStartupCycleUntilDciMatch(
+	    StallCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 11, 9, static_cast<uint32_t>(cycle));
+		return *this;
+	};
+
+	ConfigurationOptions0Value& SetStallAtStartupCycleUntilMmcmLock(
+	    StallCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 8, 6, static_cast<uint32_t>(cycle));
+		return *this;
+	};
+
+	ConfigurationOptions0Value& SetReleaseGtsSignalAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 5, 3, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReleaseGweSignalAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 2, 0, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+       private:
+	uint32_t value_;
+};  // namespace xcupseries
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
diff --git a/lib/include/prjxray/xilinx/xcupseries/configuration_row.h b/lib/include/prjxray/xilinx/xcupseries/configuration_row.h
new file mode 100644
index 0000000..7b72655
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/configuration_row.h
@@ -0,0 +1,88 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_ROW_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_ROW_H_
+
+#include <algorithm>
+#include <cassert>
+#include <map>
+#include <memory>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcupseries/block_type.h>
+#include <prjxray/xilinx/xcupseries/configuration_bus.h>
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+class Row {
+       public:
+	Row() = default;
+
+	// Construct a row from a range of iterators that yield FrameAddresses.
+	// The addresses may be noncontinguous and/or unsorted but all must
+	// share the same row half and row components.
+	template <typename T>
+	Row(T first, T last);
+
+	// Returns true if the provided address falls within a valid range
+	// attributed to this row.  Only the block type, column, and minor
+	// address components are considerd as the remaining components are
+	// outside the scope of a row.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next numerically increasing address within the Row. If
+	// the next address would fall outside the Row, no object is returned.
+	// If the next address would cross from one block type to another, no
+	// object is returned as other rows of the same block type come before
+	// other block types numerically.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<Row>;
+
+	std::map<BlockType, ConfigurationBus> configuration_buses_;
+};
+
+template <typename T>
+Row::Row(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.row() == first->row());
+	    }));
+
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.block_type() < rhs.block_type();
+	          });
+
+	for (auto bus_first = first; bus_first != last;) {
+		auto bus_last = std::upper_bound(
+		    bus_first, last, bus_first->block_type(),
+		    [](const BlockType& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.block_type();
+		    });
+
+		configuration_buses_.emplace(
+		    bus_first->block_type(),
+		    std::move(ConfigurationBus(bus_first, bus_last)));
+		bus_first = bus_last;
+	}
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::Row> {
+	static Node encode(const prjxray::xilinx::xcupseries::Row& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcupseries::Row& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_ROW_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/crc.h b/lib/include/prjxray/xilinx/xcupseries/crc.h
new file mode 100644
index 0000000..3f0fd1c
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/crc.h
@@ -0,0 +1,40 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_CRC_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_CRC_H_
+
+#include <cstdint>
+
+constexpr uint32_t kCrc32CastagnoliPolynomial = 0x82F63B78;
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+// The CRC is calculated from each written data word and the current
+// register address the data is written to.
+
+// Extend the current CRC value with one register address (5bit) and
+// frame data (32bit) pair and return the newly computed CRC value.
+
+uint32_t icap_crc(uint32_t addr, uint32_t data, uint32_t prev) {
+	constexpr int kAddressBitWidth = 5;
+	constexpr int kDataBitWidth = 32;
+
+	uint64_t poly = static_cast<uint64_t>(kCrc32CastagnoliPolynomial) << 1;
+	uint64_t val = (static_cast<uint64_t>(addr) << 32) | data;
+	uint64_t crc = prev;
+
+	for (int i = 0; i < kAddressBitWidth + kDataBitWidth; i++) {
+		if ((val & 1) != (crc & 1))
+			crc ^= poly;
+
+		val >>= 1;
+		crc >>= 1;
+	}
+	return crc;
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_CRC_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/ecc.h b/lib/include/prjxray/xilinx/xcupseries/ecc.h
new file mode 100644
index 0000000..10f0173
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/ecc.h
@@ -0,0 +1,22 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_ECC_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_ECC_H_
+
+#include <cstdint>
+#include <vector>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+// Extend the current ECC code with one data word (32 bit) at a given
+// word index in the configuration frame and return the new ECC code.
+uint32_t icap_ecc(uint32_t idx, uint32_t data, uint32_t ecc);
+
+// Updates the ECC information in the frame.
+void updateECC(std::vector<uint32_t>& data);
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_ECC_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/frame_address.h b/lib/include/prjxray/xilinx/xcupseries/frame_address.h
new file mode 100644
index 0000000..9ceba19
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/frame_address.h
@@ -0,0 +1,68 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_FRAME_ADDRESS_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_FRAME_ADDRESS_H_
+
+#include <cstdint>
+#include <ostream>
+
+#include <prjxray/xilinx/xcupseries/block_type.h>
+#include <yaml-cpp/yaml.h>
+
+#ifdef _GNU_SOURCE
+#undef minor
+#endif
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+enum Ranges {
+ 	BLOCK_TYPE_HIGH = 26,
+	BLOCK_TYPE_LOW = 24,
+	ROW_HIGH = 23,
+	ROW_LOW = 18,
+	COLUMN_HIGH = 17,
+	COLUMN_LOW = 8,
+	MINOR_HIGH = 7,
+	MINOR_LOW = 0
+};
+
+class FrameAddress {
+       public:
+	FrameAddress() : address_(0) {}
+
+	FrameAddress(uint32_t address) : address_(address){};
+
+	FrameAddress(BlockType block_type,
+	             uint8_t row,
+	             uint16_t column,
+	             uint8_t minor);
+
+	operator uint32_t() const { return address_; }
+
+	BlockType block_type() const;
+	bool is_bottom_half_rows() const;
+	uint8_t row() const;
+	uint16_t column() const;
+	uint8_t minor() const;
+
+       private:
+	uint32_t address_;
+};
+
+std::ostream& operator<<(std::ostream& o, const FrameAddress& addr);
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::FrameAddress> {
+	static Node encode(const prjxray::xilinx::xcupseries::FrameAddress& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcupseries::FrameAddress& lhs);
+};
+
+} // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_FRAME_ADDRESS_H_
diff --git a/lib/include/prjxray/xilinx/xcupseries/part.h b/lib/include/prjxray/xilinx/xcupseries/part.h
new file mode 100644
index 0000000..5bad16d
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcupseries/part.h
@@ -0,0 +1,83 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUPSERIES_PART_H_
+#define PRJXRAY_LIB_XILINX_XCUPSERIES_PART_H_
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcupseries/configuration_row.h>
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+class Part {
+       public:
+	constexpr static uint32_t kInvalidIdcode = 0;
+
+	static absl::optional<Part> FromFile(const std::string& path);
+
+	// Constructs an invalid part with a zero IDCODE. Required for YAML
+	// conversion but shouldn't be used otherwise.
+	Part() : idcode_(kInvalidIdcode) {}
+
+	template <typename T>
+	Part(uint32_t idcode, T collection)
+	    : Part(idcode, std::begin(collection), std::end(collection)) {}
+
+	template <typename T>
+	Part(uint32_t idcode, T first, T last);
+
+	uint32_t idcode() const { return idcode_; }
+
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<Part>;
+
+	uint32_t idcode_;
+	std::map<unsigned int, Row> rows_;
+};
+
+template <typename T>
+Part::Part(uint32_t idcode, T first, T last) : idcode_(idcode) {
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.row() < rhs.row();
+	          });
+
+	for (auto row_first = first; row_first != last;) {
+		auto row_last = std::upper_bound(
+		    row_first, last, row_first->row(),
+		    [](const uint8_t& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.row();
+		    });
+
+		rows_.emplace(row_first->row(),
+		              std::move(Row(row_first, row_last)));
+		row_first = row_last;
+	}
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcupseries::Part> {
+	static Node encode(const prjxray::xilinx::xcupseries::Part& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcupseries::Part& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUPSERIES_PART_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/block_type.h b/lib/include/prjxray/xilinx/xcuseries/block_type.h
new file mode 100644
index 0000000..de473e9
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/block_type.h
@@ -0,0 +1,34 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_BLOCK_TYPE_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_BLOCK_TYPE_H_
+
+#include <ostream>
+
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+enum class BlockType : unsigned int {
+	CLB_IO_CLK = 0x0,
+	BLOCK_RAM = 0x1,
+	CFG_CLB = 0x2,
+	/* reserved = 0x3, */
+};
+
+std::ostream& operator<<(std::ostream& o, BlockType value);
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::BlockType> {
+	static Node encode(const prjxray::xilinx::xcuseries::BlockType& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcuseries::BlockType& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_BLOCK_TYPE_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/command.h b/lib/include/prjxray/xilinx/xcuseries/command.h
new file mode 100644
index 0000000..50a92d7
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/command.h
@@ -0,0 +1,34 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_COMMAND_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_COMMAND_H_
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+enum class Command : uint32_t {
+	NOP = 0x0,
+	WCFG = 0x1,
+	MFW = 0x2,
+	LFRM = 0x3,
+	RCFG = 0x4,
+	START = 0x5,
+	RCAP = 0x6,
+	RCRC = 0x7,
+	AGHIGH = 0x8,
+	SWITCH = 0x9,
+	GRESTORE = 0xA,
+	SHUTDOWN = 0xB,
+	GCAPTURE = 0xC,
+	DESYNC = 0xD,
+	IPROG = 0xF,
+	CRCC = 0x10,
+	LTIMER = 0x11,
+	BSPI_READ = 0x12,
+	FALL_EDGE = 0x13,
+};
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_COMMAND_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/configuration_bus.h b/lib/include/prjxray/xilinx/xcuseries/configuration_bus.h
new file mode 100644
index 0000000..64b19a9
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/configuration_bus.h
@@ -0,0 +1,91 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_BUS_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_BUS_H_
+
+#include <algorithm>
+#include <cassert>
+#include <map>
+#include <memory>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcuseries/configuration_column.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+// ConfigurationBus represents a bus for sending frames to a specific BlockType
+// within a Row.  An instance of ConfigurationBus will contain one or more
+// ConfigurationColumns.
+class ConfigurationBus {
+       public:
+	ConfigurationBus() = default;
+
+	// Constructs a ConfigurationBus from iterators yielding
+	// FrameAddresses.  The frame address need not be contiguous or sorted
+	// but they must all have the same block type, row half, and row
+	// address components.
+	template <typename T>
+	ConfigurationBus(T first, T last);
+
+	// Returns true if the provided address falls into a valid segment of
+	// the address range on this bus.  Only the column and minor components
+	// of the address are considered as all other components are outside
+	// the scope of a bus.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next valid address on the bus in numerically increasing
+	// order. If the next address would fall outside this bus, no object is
+	// returned.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<ConfigurationBus>;
+
+	std::map<unsigned int, ConfigurationColumn> configuration_columns_;
+};
+
+template <typename T>
+ConfigurationBus::ConfigurationBus(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.block_type() == first->block_type() &&
+		            addr.row() == first->row());
+	    }));
+
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.column() < rhs.column();
+	          });
+
+	for (auto col_first = first; col_first != last;) {
+		auto col_last = std::upper_bound(
+		    col_first, last, col_first->column(),
+		    [](const unsigned int& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.column();
+		    });
+
+		configuration_columns_.emplace(
+		    col_first->column(),
+		    std::move(ConfigurationColumn(col_first, col_last)));
+		col_first = col_last;
+	}
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::ConfigurationBus> {
+	static Node encode(
+	    const prjxray::xilinx::xcuseries::ConfigurationBus& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcuseries::ConfigurationBus& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_BUS_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/configuration_column.h b/lib/include/prjxray/xilinx/xcuseries/configuration_column.h
new file mode 100644
index 0000000..da054bb
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/configuration_column.h
@@ -0,0 +1,78 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_COLUMN_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_COLUMN_H_
+
+#include <algorithm>
+#include <cassert>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+// ConfigurationColumn represents an endpoint on a ConfigurationBus.
+class ConfigurationColumn {
+       public:
+	ConfigurationColumn() = default;
+	ConfigurationColumn(unsigned int frame_count)
+	    : frame_count_(frame_count) {}
+
+	// Returns a ConfigurationColumn that describes a continguous range of
+	// minor addresses that encompasses the given
+	// FrameAddresses.  The provided addresses must only
+	// differ only by their minor addresses.
+	template <typename T>
+	ConfigurationColumn(T first, T last);
+
+	// Returns true if the minor field of the address is within the valid
+	// range of this column.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next address in numerical order.  If the next address
+	// would be outside this column, return no object.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<ConfigurationColumn>;
+
+	unsigned int frame_count_;
+};
+
+template <typename T>
+ConfigurationColumn::ConfigurationColumn(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.block_type() == first->block_type() &&
+		            addr.is_bottom_half_rows() ==
+		                first->is_bottom_half_rows() &&
+		            addr.row() == first->row() &&
+		            addr.column() == first->column());
+	    }));
+
+	auto max_minor = std::max_element(
+	    first, last, [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		    return lhs.minor() < rhs.minor();
+	    });
+
+	frame_count_ = max_minor->minor() + 1;
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::ConfigurationColumn> {
+	static Node encode(
+	    const prjxray::xilinx::xcuseries::ConfigurationColumn& rhs);
+	static bool decode(
+	    const Node& node,
+	    prjxray::xilinx::xcuseries::ConfigurationColumn& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_COLUMN_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/configuration_options_0_value.h b/lib/include/prjxray/xilinx/xcuseries/configuration_options_0_value.h
new file mode 100644
index 0000000..d759e53
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/configuration_options_0_value.h
@@ -0,0 +1,122 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
+#define PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
+
+#include <prjxray/bit_ops.h>
+#include <prjxray/xilinx/configuration_packet.h>
+#include <prjxray/xilinx/configuration_register.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+class ConfigurationOptions0Value {
+       public:
+	enum class StartupClockSource : uint32_t {
+		CCLK = 0x0,
+		User = 0x1,
+		JTAG = 0x2,
+	};
+
+	enum class SignalReleaseCycle : uint32_t {
+		Phase1 = 0x0,
+		Phase2 = 0x1,
+		Phase3 = 0x2,
+		Phase4 = 0x3,
+		Phase5 = 0x4,
+		Phase6 = 0x5,
+		TrackDone = 0x6,
+		Keep = 0x7,
+	};
+
+	enum class StallCycle : uint32_t {
+		Phase0 = 0x0,
+		Phase1 = 0x1,
+		Phase2 = 0x2,
+		Phase3 = 0x3,
+		Phase4 = 0x4,
+		Phase5 = 0x5,
+		Phase6 = 0x6,
+		NoWait = 0x7,
+	};
+
+	ConfigurationOptions0Value() : value_(0) {}
+
+	operator uint32_t() const { return value_; }
+
+	ConfigurationOptions0Value& SetUseDonePinAsPowerdownStatus(
+	    bool enabled) {
+		value_ = bit_field_set(value_, 27, 27, enabled ? 1 : 0);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetAddPipelineStageForDoneIn(bool enabled) {
+		value_ = bit_field_set(value_, 25, 25, enabled ? 1 : 0);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetDriveDoneHigh(bool enabled) {
+		value_ = bit_field_set(value_, 24, 24, enabled);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReadbackIsSingleShot(bool enabled) {
+		value_ = bit_field_set(value_, 23, 23, enabled);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetCclkFrequency(uint32_t mhz) {
+		value_ = bit_field_set(value_, 22, 17, mhz);
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetStartupClockSource(
+	    StartupClockSource source) {
+		value_ = bit_field_set(value_, 16, 15,
+		                       static_cast<uint32_t>(source));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReleaseDonePinAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 14, 12, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetStallAtStartupCycleUntilDciMatch(
+	    StallCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 11, 9, static_cast<uint32_t>(cycle));
+		return *this;
+	};
+
+	ConfigurationOptions0Value& SetStallAtStartupCycleUntilMmcmLock(
+	    StallCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 8, 6, static_cast<uint32_t>(cycle));
+		return *this;
+	};
+
+	ConfigurationOptions0Value& SetReleaseGtsSignalAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 5, 3, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+	ConfigurationOptions0Value& SetReleaseGweSignalAtStartupCycle(
+	    SignalReleaseCycle cycle) {
+		value_ =
+		    bit_field_set(value_, 2, 0, static_cast<uint32_t>(cycle));
+		return *this;
+	}
+
+       private:
+	uint32_t value_;
+};  // namespace xcuseries
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_CONFIGURATION_OPTIONS_0_VALUE_H
diff --git a/lib/include/prjxray/xilinx/xcuseries/configuration_row.h b/lib/include/prjxray/xilinx/xcuseries/configuration_row.h
new file mode 100644
index 0000000..f8566ca
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/configuration_row.h
@@ -0,0 +1,88 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_ROW_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_ROW_H_
+
+#include <algorithm>
+#include <cassert>
+#include <map>
+#include <memory>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcuseries/block_type.h>
+#include <prjxray/xilinx/xcuseries/configuration_bus.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+class Row {
+       public:
+	Row() = default;
+
+	// Construct a row from a range of iterators that yield FrameAddresses.
+	// The addresses may be noncontinguous and/or unsorted but all must
+	// share the same row half and row components.
+	template <typename T>
+	Row(T first, T last);
+
+	// Returns true if the provided address falls within a valid range
+	// attributed to this row.  Only the block type, column, and minor
+	// address components are considerd as the remaining components are
+	// outside the scope of a row.
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	// Returns the next numerically increasing address within the Row. If
+	// the next address would fall outside the Row, no object is returned.
+	// If the next address would cross from one block type to another, no
+	// object is returned as other rows of the same block type come before
+	// other block types numerically.
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<Row>;
+
+	std::map<BlockType, ConfigurationBus> configuration_buses_;
+};
+
+template <typename T>
+Row::Row(T first, T last) {
+	assert(
+	    std::all_of(first, last, [&](const typename T::value_type& addr) {
+		    return (addr.row() == first->row());
+	    }));
+
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.block_type() < rhs.block_type();
+	          });
+
+	for (auto bus_first = first; bus_first != last;) {
+		auto bus_last = std::upper_bound(
+		    bus_first, last, bus_first->block_type(),
+		    [](const BlockType& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.block_type();
+		    });
+
+		configuration_buses_.emplace(
+		    bus_first->block_type(),
+		    std::move(ConfigurationBus(bus_first, bus_last)));
+		bus_first = bus_last;
+	}
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::Row> {
+	static Node encode(const prjxray::xilinx::xcuseries::Row& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcuseries::Row& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_ROW_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/crc.h b/lib/include/prjxray/xilinx/xcuseries/crc.h
new file mode 100644
index 0000000..3b11929
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/crc.h
@@ -0,0 +1,40 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_CRC_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_CRC_H_
+
+#include <cstdint>
+
+constexpr uint32_t kCrc32CastagnoliPolynomial = 0x82F63B78;
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+// The CRC is calculated from each written data word and the current
+// register address the data is written to.
+
+// Extend the current CRC value with one register address (5bit) and
+// frame data (32bit) pair and return the newly computed CRC value.
+
+uint32_t icap_crc(uint32_t addr, uint32_t data, uint32_t prev) {
+	constexpr int kAddressBitWidth = 5;
+	constexpr int kDataBitWidth = 32;
+
+	uint64_t poly = static_cast<uint64_t>(kCrc32CastagnoliPolynomial) << 1;
+	uint64_t val = (static_cast<uint64_t>(addr) << 32) | data;
+	uint64_t crc = prev;
+
+	for (int i = 0; i < kAddressBitWidth + kDataBitWidth; i++) {
+		if ((val & 1) != (crc & 1))
+			crc ^= poly;
+
+		val >>= 1;
+		crc >>= 1;
+	}
+	return crc;
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_CRC_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/ecc.h b/lib/include/prjxray/xilinx/xcuseries/ecc.h
new file mode 100644
index 0000000..ad704a2
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/ecc.h
@@ -0,0 +1,22 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_ECC_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_ECC_H_
+
+#include <cstdint>
+#include <vector>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+// Extend the current ECC code with one data word (32 bit) at a given
+// word index in the configuration frame and return the new ECC code.
+uint32_t icap_ecc(uint32_t idx, uint32_t data, uint32_t ecc);
+
+// Updates the ECC information in the frame.
+void updateECC(std::vector<uint32_t>& data);
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_ECC_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/frame_address.h b/lib/include/prjxray/xilinx/xcuseries/frame_address.h
new file mode 100644
index 0000000..45358ec
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/frame_address.h
@@ -0,0 +1,71 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_FRAME_ADDRESS_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_FRAME_ADDRESS_H_
+
+#include <cstdint>
+#include <ostream>
+
+#include <prjxray/xilinx/xcuseries/block_type.h>
+#include <yaml-cpp/yaml.h>
+
+#ifdef _GNU_SOURCE
+#undef minor
+#endif
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+enum Ranges {
+ 	BLOCK_TYPE_HIGH = 25,
+	BLOCK_TYPE_LOW = 23,
+
+	ROW_HIGH = 22,
+	ROW_LOW = 17,
+
+	COLUMN_HIGH = 16,
+	COLUMN_LOW = 7,
+
+	MINOR_HIGH = 6,
+	MINOR_LOW = 0
+};
+
+class FrameAddress {
+       public:
+	FrameAddress() : address_(0) {}
+
+	FrameAddress(uint32_t address) : address_(address){};
+
+	FrameAddress(BlockType block_type,
+	             uint8_t row,
+	             uint16_t column,
+	             uint8_t minor);
+
+	operator uint32_t() const { return address_; }
+
+	BlockType block_type() const;
+	bool is_bottom_half_rows() const;
+	uint8_t row() const;
+	uint16_t column() const;
+	uint8_t minor() const;
+
+       private:
+	uint32_t address_;
+};
+
+std::ostream& operator<<(std::ostream& o, const FrameAddress& addr);
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::FrameAddress> {
+	static Node encode(const prjxray::xilinx::xcuseries::FrameAddress& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcuseries::FrameAddress& lhs);
+};
+
+} // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_FRAME_ADDRESS_H_
diff --git a/lib/include/prjxray/xilinx/xcuseries/part.h b/lib/include/prjxray/xilinx/xcuseries/part.h
new file mode 100644
index 0000000..219b9b1
--- /dev/null
+++ b/lib/include/prjxray/xilinx/xcuseries/part.h
@@ -0,0 +1,83 @@
+#ifndef PRJXRAY_LIB_XILINX_XCUSERIES_PART_H_
+#define PRJXRAY_LIB_XILINX_XCUSERIES_PART_H_
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <absl/types/optional.h>
+#include <prjxray/xilinx/xcuseries/configuration_row.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+#include <yaml-cpp/yaml.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+class Part {
+       public:
+	constexpr static uint32_t kInvalidIdcode = 0;
+
+	static absl::optional<Part> FromFile(const std::string& path);
+
+	// Constructs an invalid part with a zero IDCODE. Required for YAML
+	// conversion but shouldn't be used otherwise.
+	Part() : idcode_(kInvalidIdcode) {}
+
+	template <typename T>
+	Part(uint32_t idcode, T collection)
+	    : Part(idcode, std::begin(collection), std::end(collection)) {}
+
+	template <typename T>
+	Part(uint32_t idcode, T first, T last);
+
+	uint32_t idcode() const { return idcode_; }
+
+	bool IsValidFrameAddress(FrameAddress address) const;
+
+	absl::optional<FrameAddress> GetNextFrameAddress(
+	    FrameAddress address) const;
+
+       private:
+	friend struct YAML::convert<Part>;
+
+	uint32_t idcode_;
+	std::map<unsigned int, Row> rows_;
+};
+
+template <typename T>
+Part::Part(uint32_t idcode, T first, T last) : idcode_(idcode) {
+	std::sort(first, last,
+	          [](const FrameAddress& lhs, const FrameAddress& rhs) {
+		          return lhs.row() < rhs.row();
+	          });
+
+	for (auto row_first = first; row_first != last;) {
+		auto row_last = std::upper_bound(
+		    row_first, last, row_first->row(),
+		    [](const uint8_t& lhs, const FrameAddress& rhs) {
+			    return lhs < rhs.row();
+		    });
+
+		rows_.emplace(row_first->row(),
+		              std::move(Row(row_first, row_last)));
+		row_first = row_last;
+	}
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+template <>
+struct convert<prjxray::xilinx::xcuseries::Part> {
+	static Node encode(const prjxray::xilinx::xcuseries::Part& rhs);
+	static bool decode(const Node& node,
+	                   prjxray::xilinx::xcuseries::Part& lhs);
+};
+}  // namespace YAML
+
+#endif  // PRJXRAY_LIB_XILINX_XCUSERIES_PART_H_
diff --git a/lib/xilinx/xcupseries/block_type.cc b/lib/xilinx/xcupseries/block_type.cc
new file mode 100644
index 0000000..ced79e2
--- /dev/null
+++ b/lib/xilinx/xcupseries/block_type.cc
@@ -0,0 +1,62 @@
+#include <prjxray/xilinx/xcupseries/block_type.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+std::ostream& operator<<(std::ostream& o, BlockType value) {
+	switch (value) {
+		case BlockType::CLB_IO_CLK:
+			o << "CLB/IO/CLK";
+			break;
+		case BlockType::BLOCK_RAM:
+			o << "Block RAM";
+			break;
+		case BlockType::CFG_CLB:
+			o << "Config CLB";
+			break;
+	}
+
+	return o;
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+
+Node convert<prjxray::xilinx::xcupseries::BlockType>::encode(
+    const prjxray::xilinx::xcupseries::BlockType& rhs) {
+	switch (rhs) {
+		case prjxray::xilinx::xcupseries::BlockType::CLB_IO_CLK:
+			return Node("CLB_IO_CLK");
+		case prjxray::xilinx::xcupseries::BlockType::BLOCK_RAM:
+			return Node("BLOCK_RAM");
+		case prjxray::xilinx::xcupseries::BlockType::CFG_CLB:
+			return Node("CFG_CLB");
+		default:
+			return Node(static_cast<unsigned int>(rhs));
+	}
+}
+
+bool YAML::convert<prjxray::xilinx::xcupseries::BlockType>::decode(
+    const Node& node,
+    prjxray::xilinx::xcupseries::BlockType& lhs) {
+	auto type_str = node.as<std::string>();
+
+	if (type_str == "CLB_IO_CLK") {
+		lhs = prjxray::xilinx::xcupseries::BlockType::CLB_IO_CLK;
+		return true;
+	} else if (type_str == "BLOCK_RAM") {
+		lhs = prjxray::xilinx::xcupseries::BlockType::BLOCK_RAM;
+		return true;
+	} else if (type_str == "CFG_CLB") {
+		lhs = prjxray::xilinx::xcupseries::BlockType::CFG_CLB;
+		return true;
+	} else {
+		return false;
+	}
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcupseries/configuration_bus.cc b/lib/xilinx/xcupseries/configuration_bus.cc
new file mode 100644
index 0000000..cb7f5ea
--- /dev/null
+++ b/lib/xilinx/xcupseries/configuration_bus.cc
@@ -0,0 +1,78 @@
+#include <prjxray/xilinx/xcupseries/configuration_bus.h>
+
+#include <iostream>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+bool ConfigurationBus::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_column = configuration_columns_.find(address.column());
+	if (addr_column == configuration_columns_.end())
+		return false;
+
+	return addr_column->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> ConfigurationBus::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the column for the current address.
+	auto addr_column = configuration_columns_.find(address.column());
+
+	// If the current address isn't in a known column, no way to know the
+	// next address.
+	if (addr_column == configuration_columns_.end())
+		return {};
+
+	// Ask the column for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_column->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current column doesn't know what the next address is.  Assume
+	// that the next valid address is the beginning of the next column.
+	if (++addr_column != configuration_columns_.end()) {
+		auto next_address = FrameAddress(
+		    address.block_type(),
+		    address.row(),
+			addr_column->first, 0);
+		if (addr_column->second.IsValidFrameAddress(next_address))
+			return next_address;
+	}
+
+	// Not in this bus.
+	return {};
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcupseries = prjxray::xilinx::xcupseries;
+
+namespace YAML {
+
+Node convert<xcupseries::ConfigurationBus>::encode(
+    const xcupseries::ConfigurationBus& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcupseries/configuration_bus");
+	node["configuration_columns"] = rhs.configuration_columns_;
+	return node;
+}
+
+bool convert<xcupseries::ConfigurationBus>::decode(
+    const Node& node,
+    xcupseries::ConfigurationBus& lhs) {
+	if (!node.Tag().empty() &&
+	    node.Tag() != "xilinx/xcupseries/configuration_bus") {
+		return false;
+	}
+
+	lhs.configuration_columns_ =
+	    node["configuration_columns"]
+	        .as<std::map<unsigned int, xcupseries::ConfigurationColumn>>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcupseries/configuration_column.cc b/lib/xilinx/xcupseries/configuration_column.cc
new file mode 100644
index 0000000..5ca5e6a
--- /dev/null
+++ b/lib/xilinx/xcupseries/configuration_column.cc
@@ -0,0 +1,52 @@
+#include <prjxray/xilinx/xcupseries/configuration_column.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+bool ConfigurationColumn::IsValidFrameAddress(FrameAddress address) const {
+	return address.minor() < frame_count_;
+}
+
+absl::optional<FrameAddress> ConfigurationColumn::GetNextFrameAddress(
+    FrameAddress address) const {
+	if (!IsValidFrameAddress(address))
+		return {};
+
+	if (static_cast<unsigned int>(address.minor() + 1) < frame_count_) {
+		return address + 1;
+	}
+
+	// Next address is not in this column.
+	return {};
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcupseries = prjxray::xilinx::xcupseries;
+
+namespace YAML {
+
+Node convert<xcupseries::ConfigurationColumn>::encode(
+    const xcupseries::ConfigurationColumn& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcupseries/configuration_column");
+	node["frame_count"] = rhs.frame_count_;
+	return node;
+}
+
+bool convert<xcupseries::ConfigurationColumn>::decode(
+    const Node& node,
+    xcupseries::ConfigurationColumn& lhs) {
+	if (!node.Tag().empty() &&
+	    node.Tag() != "xilinx/xcupseries/configuration_column") {
+		return false;
+	}
+
+	lhs.frame_count_ = node["frame_count"].as<unsigned int>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcupseries/configuration_row.cc b/lib/xilinx/xcupseries/configuration_row.cc
new file mode 100644
index 0000000..258b0ff
--- /dev/null
+++ b/lib/xilinx/xcupseries/configuration_row.cc
@@ -0,0 +1,62 @@
+#include <prjxray/xilinx/xcupseries/configuration_row.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+bool Row::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_bus = configuration_buses_.find(address.block_type());
+	if (addr_bus == configuration_buses_.end())
+		return false;
+	return addr_bus->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> Row::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the bus for the current address.
+	auto addr_bus = configuration_buses_.find(address.block_type());
+
+	// If the current address isn't in a known bus, no way to know the next.
+	if (addr_bus == configuration_buses_.end())
+		return {};
+
+	// Ask the bus for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_bus->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current bus doesn't know what the next address is. Rows come next
+	// in frame address numerical order so punt back to the caller to figure
+	// it out.
+	return {};
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcupseries = prjxray::xilinx::xcupseries;
+
+namespace YAML {
+
+Node convert<xcupseries::Row>::encode(const xcupseries::Row& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcupseries/row");
+	node["configuration_buses"] = rhs.configuration_buses_;
+	return node;
+}
+
+bool convert<xcupseries::Row>::decode(const Node& node, xcupseries::Row& lhs) {
+	if (!node.Tag().empty() && node.Tag() != "xilinx/xcupseries/row") {
+		return false;
+	}
+
+	lhs.configuration_buses_ =
+	    node["configuration_buses"]
+	        .as<std::map<xcupseries::BlockType,
+	                     xcupseries::ConfigurationBus>>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcupseries/ecc.cc b/lib/xilinx/xcupseries/ecc.cc
new file mode 100644
index 0000000..9004438
--- /dev/null
+++ b/lib/xilinx/xcupseries/ecc.cc
@@ -0,0 +1,60 @@
+#include <prjxray/xilinx/xcupseries/ecc.h>
+#include <cassert>
+#include <cstdio>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+constexpr size_t kECCFrameNumber = 0x32;
+
+uint32_t icap_ecc(uint32_t idx, uint32_t data, uint32_t ecc) {
+	uint32_t val = idx * 32;  // bit offset
+
+	if (idx > 0x25)  // avoid 0x800
+		val += 0x1360;
+	else if (idx > 0x6)  // avoid 0x400
+		val += 0x1340;
+	else  // avoid lower
+		val += 0x1320;
+
+	if (idx == 0x32)  // mask ECC
+		data &= 0xFFFFE000;
+
+	for (int i = 0; i < 32; i++) {
+		if (data & 1)
+			ecc ^= val + i;
+
+		data >>= 1;
+	}
+
+	if (idx == 0x64) {  // last index
+		uint32_t v = ecc & 0xFFF;
+		v ^= v >> 8;
+		v ^= v >> 4;
+		v ^= v >> 2;
+		v ^= v >> 1;
+		ecc ^= (v & 1) << 12;  // parity
+	}
+
+	return ecc;
+}
+
+static uint32_t calculateECC(const std::vector<uint32_t>& data) {
+	uint32_t ecc = 0;
+	for (size_t ii = 0; ii < data.size(); ++ii) {
+		ecc = xcupseries::icap_ecc(ii, data[ii], ecc);
+	}
+	return ecc;
+}
+
+void updateECC(std::vector<uint32_t>& data) {
+	assert(data.size() >= kECCFrameNumber);
+	// Replace the old ECC with the new.
+	data[kECCFrameNumber] &= 0xFFFFE000;
+	data[kECCFrameNumber] |= (calculateECC(data) & 0x1FFF);
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
diff --git a/lib/xilinx/xcupseries/frame_address.cc b/lib/xilinx/xcupseries/frame_address.cc
new file mode 100644
index 0000000..f9a2ac5
--- /dev/null
+++ b/lib/xilinx/xcupseries/frame_address.cc
@@ -0,0 +1,101 @@
+#include <prjxray/xilinx/xcupseries/frame_address.h>
+
+#include <iomanip>
+
+#include <prjxray/bit_ops.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+FrameAddress::FrameAddress(BlockType block_type,
+                           uint8_t row,
+                           uint16_t column,
+                           uint8_t minor) {
+	address_ = bit_field_set(0,
+							 BLOCK_TYPE_HIGH,
+							 BLOCK_TYPE_LOW,
+							 block_type);
+
+	address_ = bit_field_set(address_,
+							 ROW_HIGH,
+							 ROW_LOW,
+							 row);
+
+	address_ = bit_field_set(address_,
+							 COLUMN_HIGH,
+							 COLUMN_LOW,
+							 column);
+
+	address_ = bit_field_set(address_,
+							 MINOR_HIGH,
+							 MINOR_LOW,
+							 minor);
+}
+
+BlockType FrameAddress::block_type() const {
+	return static_cast<BlockType>(bit_field_get(address_, BLOCK_TYPE_HIGH, BLOCK_TYPE_LOW));
+}
+
+bool FrameAddress::is_bottom_half_rows() const {
+	return bit_field_get(address_, ROW_HIGH, ROW_HIGH);
+}
+
+uint8_t FrameAddress::row() const {
+	return bit_field_get(address_, ROW_HIGH, ROW_LOW);
+}
+
+uint16_t FrameAddress::column() const {
+	return bit_field_get(address_, COLUMN_HIGH, COLUMN_LOW);
+}
+
+uint8_t FrameAddress::minor() const {
+	return bit_field_get(address_, MINOR_HIGH, MINOR_LOW);
+}
+
+std::ostream& operator<<(std::ostream& o, const FrameAddress& addr) {
+	o << "[" << std::hex << std::showbase << std::setw(10)
+	  << static_cast<uint32_t>(addr) << "] "
+	  << " Row=" << std::setw(2) << std::dec
+	  << static_cast<unsigned int>(addr.row()) << " Column=" << std::setw(2)
+	  << std::dec << addr.column() << " Minor=" << std::setw(2) << std::dec
+	  << static_cast<unsigned int>(addr.minor())
+	  << " Type=" << addr.block_type();
+	return o;
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+
+namespace xcupseries = prjxray::xilinx::xcupseries;
+
+Node convert<xcupseries::FrameAddress>::encode(
+    const xcupseries::FrameAddress& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcupseries/frame_address");
+	node["block_type"] = rhs.block_type();
+	node["row"] = static_cast<unsigned int>(rhs.row());
+	node["column"] = static_cast<unsigned int>(rhs.column());
+	node["minor"] = static_cast<unsigned int>(rhs.minor());
+	return node;
+}
+
+bool convert<xcupseries::FrameAddress>::decode(const Node& node,
+                                              xcupseries::FrameAddress& lhs) {
+	if (!(node.Tag() == "xilinx/xcupseries/frame_address" ||
+	      node.Tag() == "xilinx/xcupseries/configuration_frame_address") ||
+	    !node["block_type"] || !node["row"] ||
+	    !node["column"] || !node["minor"])
+		return false;
+
+	lhs = prjxray::xilinx::xcupseries::FrameAddress(
+	    node["block_type"].as<xcupseries::BlockType>(),
+	    node["row"].as<unsigned int>(), node["column"].as<unsigned int>(),
+	    node["minor"].as<unsigned int>());
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcupseries/part.cc b/lib/xilinx/xcupseries/part.cc
new file mode 100644
index 0000000..71ca577
--- /dev/null
+++ b/lib/xilinx/xcupseries/part.cc
@@ -0,0 +1,113 @@
+#include <prjxray/xilinx/xcupseries/part.h>
+
+#include <iomanip>
+#include <sstream>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcupseries {
+
+absl::optional<Part> Part::FromFile(const std::string& path) {
+	try {
+		YAML::Node yaml = YAML::LoadFile(path);
+		return yaml.as<Part>();
+	} catch (YAML::Exception& e) {
+		return {};
+	}
+}
+
+bool Part::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_row = rows_.find(address.row());
+	if (addr_row == rows_.end())
+		return false;
+	return addr_row->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> Part::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the row for the current address.
+	auto addr_row = rows_.find(address.row());
+
+	// If the current address isn't in a known row, no way to know the next.
+	if (addr_row == rows_.end())
+		return {};
+
+	// Ask the row for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_row->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current row doesn't know what the next address is.  Assume that
+	// the next valid address is the beginning of the next row.
+	if (++addr_row != rows_.end()) {
+		auto next_address = FrameAddress(address.block_type(), addr_row->first, 0, 0);
+		if (addr_row->second.IsValidFrameAddress(next_address))
+			return next_address;
+	}
+
+	// Block types are next numerically.
+	if (address.block_type() < BlockType::BLOCK_RAM) {
+		next_address =
+		    FrameAddress(BlockType::BLOCK_RAM, 0, 0, 0);
+		if (IsValidFrameAddress(*next_address))
+			return next_address;
+	}
+
+	if (address.block_type() < BlockType::CFG_CLB) {
+		next_address = FrameAddress(BlockType::CFG_CLB, 0, 0, 0);
+		if (IsValidFrameAddress(*next_address))
+			return next_address;
+	}
+
+	return {};
+}
+
+}  // namespace xcupseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcupseries = prjxray::xilinx::xcupseries;
+
+namespace YAML {
+
+Node convert<xcupseries::Part>::encode(const xcupseries::Part& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcupseries/part");
+
+	std::ostringstream idcode_str;
+	idcode_str << "0x" << std::hex << rhs.idcode_;
+	node["idcode"] = idcode_str.str();
+	node["rows"] = rhs.rows_;
+	return node;
+}
+
+bool convert<xcupseries::Part>::decode(const Node& node, xcupseries::Part& lhs) {
+	if (!node.Tag().empty() && node.Tag() != "xilinx/xcupseries/part")
+		return false;
+
+	if (!node["rows"] && !node["configuration_ranges"]) {
+		return false;
+	}
+
+	lhs.idcode_ = node["idcode"].as<uint32_t>();
+	if (node["rows"]) {
+		lhs.rows_ = node["rows"].as<std::map<unsigned int, xcupseries::Row>>();
+	} else if (node["configuration_ranges"]) {
+		std::vector<xcupseries::FrameAddress> addresses;
+		for (auto range : node["configuration_ranges"]) {
+			auto begin =
+			    range["begin"].as<xcupseries::FrameAddress>();
+			auto end = range["end"].as<xcupseries::FrameAddress>();
+			for (uint32_t cur = begin; cur < end; ++cur) {
+				addresses.push_back(cur);
+			}
+		}
+
+		lhs = xcupseries::Part(lhs.idcode_, addresses);
+	}
+
+	return true;
+};
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/block_type.cc b/lib/xilinx/xcuseries/block_type.cc
new file mode 100644
index 0000000..18cc8ff
--- /dev/null
+++ b/lib/xilinx/xcuseries/block_type.cc
@@ -0,0 +1,62 @@
+#include <prjxray/xilinx/xcuseries/block_type.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+std::ostream& operator<<(std::ostream& o, BlockType value) {
+	switch (value) {
+		case BlockType::CLB_IO_CLK:
+			o << "CLB/IO/CLK";
+			break;
+		case BlockType::BLOCK_RAM:
+			o << "Block RAM";
+			break;
+		case BlockType::CFG_CLB:
+			o << "Config CLB";
+			break;
+	}
+
+	return o;
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+
+Node convert<prjxray::xilinx::xcuseries::BlockType>::encode(
+    const prjxray::xilinx::xcuseries::BlockType& rhs) {
+	switch (rhs) {
+		case prjxray::xilinx::xcuseries::BlockType::CLB_IO_CLK:
+			return Node("CLB_IO_CLK");
+		case prjxray::xilinx::xcuseries::BlockType::BLOCK_RAM:
+			return Node("BLOCK_RAM");
+		case prjxray::xilinx::xcuseries::BlockType::CFG_CLB:
+			return Node("CFG_CLB");
+		default:
+			return Node(static_cast<unsigned int>(rhs));
+	}
+}
+
+bool YAML::convert<prjxray::xilinx::xcuseries::BlockType>::decode(
+    const Node& node,
+    prjxray::xilinx::xcuseries::BlockType& lhs) {
+	auto type_str = node.as<std::string>();
+
+	if (type_str == "CLB_IO_CLK") {
+		lhs = prjxray::xilinx::xcuseries::BlockType::CLB_IO_CLK;
+		return true;
+	} else if (type_str == "BLOCK_RAM") {
+		lhs = prjxray::xilinx::xcuseries::BlockType::BLOCK_RAM;
+		return true;
+	} else if (type_str == "CFG_CLB") {
+		lhs = prjxray::xilinx::xcuseries::BlockType::CFG_CLB;
+		return true;
+	} else {
+		return false;
+	}
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/configuration_bus.cc b/lib/xilinx/xcuseries/configuration_bus.cc
new file mode 100644
index 0000000..f960f8a
--- /dev/null
+++ b/lib/xilinx/xcuseries/configuration_bus.cc
@@ -0,0 +1,78 @@
+#include <prjxray/xilinx/xcuseries/configuration_bus.h>
+
+#include <iostream>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+bool ConfigurationBus::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_column = configuration_columns_.find(address.column());
+	if (addr_column == configuration_columns_.end())
+		return false;
+
+	return addr_column->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> ConfigurationBus::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the column for the current address.
+	auto addr_column = configuration_columns_.find(address.column());
+
+	// If the current address isn't in a known column, no way to know the
+	// next address.
+	if (addr_column == configuration_columns_.end())
+		return {};
+
+	// Ask the column for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_column->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current column doesn't know what the next address is.  Assume
+	// that the next valid address is the beginning of the next column.
+	if (++addr_column != configuration_columns_.end()) {
+		auto next_address = FrameAddress(
+		    address.block_type(),
+		    address.row(),
+			addr_column->first, 0);
+		if (addr_column->second.IsValidFrameAddress(next_address))
+			return next_address;
+	}
+
+	// Not in this bus.
+	return {};
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcuseries = prjxray::xilinx::xcuseries;
+
+namespace YAML {
+
+Node convert<xcuseries::ConfigurationBus>::encode(
+    const xcuseries::ConfigurationBus& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcuseries/configuration_bus");
+	node["configuration_columns"] = rhs.configuration_columns_;
+	return node;
+}
+
+bool convert<xcuseries::ConfigurationBus>::decode(
+    const Node& node,
+    xcuseries::ConfigurationBus& lhs) {
+	if (!node.Tag().empty() &&
+	    node.Tag() != "xilinx/xcuseries/configuration_bus") {
+		return false;
+	}
+
+	lhs.configuration_columns_ =
+	    node["configuration_columns"]
+	        .as<std::map<unsigned int, xcuseries::ConfigurationColumn>>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/configuration_column.cc b/lib/xilinx/xcuseries/configuration_column.cc
new file mode 100644
index 0000000..e767c57
--- /dev/null
+++ b/lib/xilinx/xcuseries/configuration_column.cc
@@ -0,0 +1,52 @@
+#include <prjxray/xilinx/xcuseries/configuration_column.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+bool ConfigurationColumn::IsValidFrameAddress(FrameAddress address) const {
+	return address.minor() < frame_count_;
+}
+
+absl::optional<FrameAddress> ConfigurationColumn::GetNextFrameAddress(
+    FrameAddress address) const {
+	if (!IsValidFrameAddress(address))
+		return {};
+
+	if (static_cast<unsigned int>(address.minor() + 1) < frame_count_) {
+		return address + 1;
+	}
+
+	// Next address is not in this column.
+	return {};
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcuseries = prjxray::xilinx::xcuseries;
+
+namespace YAML {
+
+Node convert<xcuseries::ConfigurationColumn>::encode(
+    const xcuseries::ConfigurationColumn& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcuseries/configuration_column");
+	node["frame_count"] = rhs.frame_count_;
+	return node;
+}
+
+bool convert<xcuseries::ConfigurationColumn>::decode(
+    const Node& node,
+    xcuseries::ConfigurationColumn& lhs) {
+	if (!node.Tag().empty() &&
+	    node.Tag() != "xilinx/xcuseries/configuration_column") {
+		return false;
+	}
+
+	lhs.frame_count_ = node["frame_count"].as<unsigned int>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/configuration_row.cc b/lib/xilinx/xcuseries/configuration_row.cc
new file mode 100644
index 0000000..3d47216
--- /dev/null
+++ b/lib/xilinx/xcuseries/configuration_row.cc
@@ -0,0 +1,62 @@
+#include <prjxray/xilinx/xcuseries/configuration_row.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+bool Row::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_bus = configuration_buses_.find(address.block_type());
+	if (addr_bus == configuration_buses_.end())
+		return false;
+	return addr_bus->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> Row::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the bus for the current address.
+	auto addr_bus = configuration_buses_.find(address.block_type());
+
+	// If the current address isn't in a known bus, no way to know the next.
+	if (addr_bus == configuration_buses_.end())
+		return {};
+
+	// Ask the bus for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_bus->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current bus doesn't know what the next address is. Rows come next
+	// in frame address numerical order so punt back to the caller to figure
+	// it out.
+	return {};
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcuseries = prjxray::xilinx::xcuseries;
+
+namespace YAML {
+
+Node convert<xcuseries::Row>::encode(const xcuseries::Row& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcuseries/row");
+	node["configuration_buses"] = rhs.configuration_buses_;
+	return node;
+}
+
+bool convert<xcuseries::Row>::decode(const Node& node, xcuseries::Row& lhs) {
+	if (!node.Tag().empty() && node.Tag() != "xilinx/xcuseries/row") {
+		return false;
+	}
+
+	lhs.configuration_buses_ =
+	    node["configuration_buses"]
+	        .as<std::map<xcuseries::BlockType,
+	                     xcuseries::ConfigurationBus>>();
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/ecc.cc b/lib/xilinx/xcuseries/ecc.cc
new file mode 100644
index 0000000..3f4455a
--- /dev/null
+++ b/lib/xilinx/xcuseries/ecc.cc
@@ -0,0 +1,60 @@
+#include <prjxray/xilinx/xcuseries/ecc.h>
+#include <cassert>
+#include <cstdio>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+constexpr size_t kECCFrameNumber = 0x32;
+
+uint32_t icap_ecc(uint32_t idx, uint32_t data, uint32_t ecc) {
+	uint32_t val = idx * 32;  // bit offset
+
+	if (idx > 0x25)  // avoid 0x800
+		val += 0x1360;
+	else if (idx > 0x6)  // avoid 0x400
+		val += 0x1340;
+	else  // avoid lower
+		val += 0x1320;
+
+	if (idx == 0x32)  // mask ECC
+		data &= 0xFFFFE000;
+
+	for (int i = 0; i < 32; i++) {
+		if (data & 1)
+			ecc ^= val + i;
+
+		data >>= 1;
+	}
+
+	if (idx == 0x64) {  // last index
+		uint32_t v = ecc & 0xFFF;
+		v ^= v >> 8;
+		v ^= v >> 4;
+		v ^= v >> 2;
+		v ^= v >> 1;
+		ecc ^= (v & 1) << 12;  // parity
+	}
+
+	return ecc;
+}
+
+static uint32_t calculateECC(const std::vector<uint32_t>& data) {
+	uint32_t ecc = 0;
+	for (size_t ii = 0; ii < data.size(); ++ii) {
+		ecc = xcuseries::icap_ecc(ii, data[ii], ecc);
+	}
+	return ecc;
+}
+
+void updateECC(std::vector<uint32_t>& data) {
+	assert(data.size() >= kECCFrameNumber);
+	// Replace the old ECC with the new.
+	data[kECCFrameNumber] &= 0xFFFFE000;
+	data[kECCFrameNumber] |= (calculateECC(data) & 0x1FFF);
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
diff --git a/lib/xilinx/xcuseries/frame_address.cc b/lib/xilinx/xcuseries/frame_address.cc
new file mode 100644
index 0000000..e59c93b
--- /dev/null
+++ b/lib/xilinx/xcuseries/frame_address.cc
@@ -0,0 +1,101 @@
+#include <prjxray/xilinx/xcuseries/frame_address.h>
+
+#include <iomanip>
+
+#include <prjxray/bit_ops.h>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+FrameAddress::FrameAddress(BlockType block_type,
+                           uint8_t row,
+                           uint16_t column,
+                           uint8_t minor) {
+	address_ = bit_field_set(0,
+							 BLOCK_TYPE_HIGH,
+							 BLOCK_TYPE_LOW,
+							 block_type);
+
+	address_ = bit_field_set(address_,
+							 ROW_HIGH,
+							 ROW_LOW,
+							 row);
+
+	address_ = bit_field_set(address_,
+							 COLUMN_HIGH,
+							 COLUMN_LOW,
+							 column);
+
+	address_ = bit_field_set(address_,
+							 MINOR_HIGH,
+							 MINOR_LOW,
+							 minor);
+}
+
+BlockType FrameAddress::block_type() const {
+	return static_cast<BlockType>(bit_field_get(address_, BLOCK_TYPE_HIGH, BLOCK_TYPE_LOW));
+}
+
+bool FrameAddress::is_bottom_half_rows() const {
+	return bit_field_get(address_, ROW_HIGH, ROW_HIGH);
+}
+
+uint8_t FrameAddress::row() const {
+	return bit_field_get(address_, ROW_HIGH, ROW_LOW);
+}
+
+uint16_t FrameAddress::column() const {
+	return bit_field_get(address_, COLUMN_HIGH, COLUMN_LOW);
+}
+
+uint8_t FrameAddress::minor() const {
+	return bit_field_get(address_, MINOR_HIGH, MINOR_LOW);
+}
+
+std::ostream& operator<<(std::ostream& o, const FrameAddress& addr) {
+	o << "[" << std::hex << std::showbase << std::setw(10)
+	  << static_cast<uint32_t>(addr) << "] "
+	  << " Row=" << std::setw(2) << std::dec
+	  << static_cast<unsigned int>(addr.row()) << " Column=" << std::setw(2)
+	  << std::dec << addr.column() << " Minor=" << std::setw(2) << std::dec
+	  << static_cast<unsigned int>(addr.minor())
+	  << " Type=" << addr.block_type();
+	return o;
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace YAML {
+
+namespace xcuseries = prjxray::xilinx::xcuseries;
+
+Node convert<xcuseries::FrameAddress>::encode(
+    const xcuseries::FrameAddress& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcuseries/frame_address");
+	node["block_type"] = rhs.block_type();
+	node["row"] = static_cast<unsigned int>(rhs.row());
+	node["column"] = static_cast<unsigned int>(rhs.column());
+	node["minor"] = static_cast<unsigned int>(rhs.minor());
+	return node;
+}
+
+bool convert<xcuseries::FrameAddress>::decode(const Node& node,
+                                              xcuseries::FrameAddress& lhs) {
+	if (!(node.Tag() == "xilinx/xcuseries/frame_address" ||
+	      node.Tag() == "xilinx/xcuseries/configuration_frame_address") ||
+	    !node["block_type"] || !node["row"] ||
+	    !node["column"] || !node["minor"])
+		return false;
+
+	lhs = prjxray::xilinx::xcuseries::FrameAddress(
+	    node["block_type"].as<xcuseries::BlockType>(),
+	    node["row"].as<unsigned int>(), node["column"].as<unsigned int>(),
+	    node["minor"].as<unsigned int>());
+	return true;
+}
+
+}  // namespace YAML
diff --git a/lib/xilinx/xcuseries/part.cc b/lib/xilinx/xcuseries/part.cc
new file mode 100644
index 0000000..277ba7c
--- /dev/null
+++ b/lib/xilinx/xcuseries/part.cc
@@ -0,0 +1,113 @@
+#include <prjxray/xilinx/xcuseries/part.h>
+
+#include <iomanip>
+#include <sstream>
+
+namespace prjxray {
+namespace xilinx {
+namespace xcuseries {
+
+absl::optional<Part> Part::FromFile(const std::string& path) {
+	try {
+		YAML::Node yaml = YAML::LoadFile(path);
+		return yaml.as<Part>();
+	} catch (YAML::Exception& e) {
+		return {};
+	}
+}
+
+bool Part::IsValidFrameAddress(FrameAddress address) const {
+	auto addr_row = rows_.find(address.row());
+	if (addr_row == rows_.end())
+		return false;
+	return addr_row->second.IsValidFrameAddress(address);
+}
+
+absl::optional<FrameAddress> Part::GetNextFrameAddress(
+    FrameAddress address) const {
+	// Find the row for the current address.
+	auto addr_row = rows_.find(address.row());
+
+	// If the current address isn't in a known row, no way to know the next.
+	if (addr_row == rows_.end())
+		return {};
+
+	// Ask the row for the next address.
+	absl::optional<FrameAddress> next_address =
+	    addr_row->second.GetNextFrameAddress(address);
+	if (next_address)
+		return next_address;
+
+	// The current row doesn't know what the next address is.  Assume that
+	// the next valid address is the beginning of the next row.
+	if (++addr_row != rows_.end()) {
+		auto next_address = FrameAddress(address.block_type(), addr_row->first, 0, 0);
+		if (addr_row->second.IsValidFrameAddress(next_address))
+			return next_address;
+	}
+
+	// Block types are next numerically.
+	if (address.block_type() < BlockType::BLOCK_RAM) {
+		next_address =
+		    FrameAddress(BlockType::BLOCK_RAM, 0, 0, 0);
+		if (IsValidFrameAddress(*next_address))
+			return next_address;
+	}
+
+	if (address.block_type() < BlockType::CFG_CLB) {
+		next_address = FrameAddress(BlockType::CFG_CLB, 0, 0, 0);
+		if (IsValidFrameAddress(*next_address))
+			return next_address;
+	}
+
+	return {};
+}
+
+}  // namespace xcuseries
+}  // namespace xilinx
+}  // namespace prjxray
+
+namespace xcuseries = prjxray::xilinx::xcuseries;
+
+namespace YAML {
+
+Node convert<xcuseries::Part>::encode(const xcuseries::Part& rhs) {
+	Node node;
+	node.SetTag("xilinx/xcuseries/part");
+
+	std::ostringstream idcode_str;
+	idcode_str << "0x" << std::hex << rhs.idcode_;
+	node["idcode"] = idcode_str.str();
+	node["rows"] = rhs.rows_;
+	return node;
+}
+
+bool convert<xcuseries::Part>::decode(const Node& node, xcuseries::Part& lhs) {
+	if (!node.Tag().empty() && node.Tag() != "xilinx/xcuseries/part")
+		return false;
+
+	if (!node["rows"] && !node["configuration_ranges"]) {
+		return false;
+	}
+
+	lhs.idcode_ = node["idcode"].as<uint32_t>();
+	if (node["rows"]) {
+		lhs.rows_ = node["rows"].as<std::map<unsigned int, xcuseries::Row>>();
+	} else if (node["configuration_ranges"]) {
+		std::vector<xcuseries::FrameAddress> addresses;
+		for (auto range : node["configuration_ranges"]) {
+			auto begin =
+			    range["begin"].as<xcuseries::FrameAddress>();
+			auto end = range["end"].as<xcuseries::FrameAddress>();
+			for (uint32_t cur = begin; cur < end; ++cur) {
+				addresses.push_back(cur);
+			}
+		}
+
+		lhs = xcuseries::Part(lhs.idcode_, addresses);
+	}
+
+	return true;
+};
+
+}  // namespace YAML
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 69c5508..3ea6ef0 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -7,8 +7,11 @@
 add_executable(bittool bittool.cc)
 target_link_libraries(bittool libprjxray gflags absl::strings absl::span)
 
-add_executable(frame_address_decoder frame_address_decoder.cc)
-target_link_libraries(frame_address_decoder libprjxray)
+add_executable(xc7_frame_address_decoder xcu_frame_address_decoder.cc)
+target_link_libraries(xc7_frame_address_decoder libprjxray)
+
+add_executable(xcu_frame_address_decoder xc7_frame_address_decoder.cc)
+target_link_libraries(xcu_frame_address_decoder libprjxray)
 
 add_executable(gen_part_base_yaml gen_part_base_yaml.cc)
 target_link_libraries(gen_part_base_yaml
@@ -18,16 +21,16 @@
 	yaml-cpp
 	gflags
 )
-add_executable(xc7patch xc7patch.cc)
-target_link_libraries(xc7patch
+add_executable(xcpatch xcpatch.cc)
+target_link_libraries(xcpatch
 	absl::strings
 	absl::time
 	gflags
 	libprjxray
 )
 
-add_executable(xc7frames2bit xc7frames2bit.cc)
-target_link_libraries(xc7frames2bit
+add_executable(xcframes2bit xcframes2bit.cc)
+target_link_libraries(xcframes2bit
 	absl::strings
 	absl::time
 	gflags
diff --git a/tools/frame_address_decoder.cc b/tools/xc7_frame_address_decoder.cc
similarity index 88%
rename from tools/frame_address_decoder.cc
rename to tools/xc7_frame_address_decoder.cc
index b3c21e9..79c429e 100644
--- a/tools/frame_address_decoder.cc
+++ b/tools/xc7_frame_address_decoder.cc
@@ -6,7 +6,7 @@
 
 namespace xc7series = prjxray::xilinx::xc7series;
 
-void frame_address_decode(std::istream* input_stream) {
+void xc7_frame_address_decode(std::istream* input_stream) {
 	for (uint32_t frame_address_raw;
 	     (*input_stream) >> std::setbase(0) >> frame_address_raw;) {
 		xc7series::FrameAddress frame_address(frame_address_raw);
@@ -29,12 +29,12 @@
 	if (argc > 1) {
 		std::ifstream file_stream(argv[1]);
 		if (file_stream) {
-			frame_address_decode(&file_stream);
+			xc7_frame_address_decode(&file_stream);
 			return 0;
 		}
 	}
 
-	frame_address_decode(&std::cin);
+	xc7_frame_address_decode(&std::cin);
 
 	return 0;
 }
diff --git a/tools/xc7frames2bit.cc b/tools/xcframes2bit.cc
similarity index 100%
rename from tools/xc7frames2bit.cc
rename to tools/xcframes2bit.cc
diff --git a/tools/xc7patch.cc b/tools/xcpatch.cc
similarity index 95%
rename from tools/xc7patch.cc
rename to tools/xcpatch.cc
index 369ddac..6069584 100644
--- a/tools/xc7patch.cc
+++ b/tools/xcpatch.cc
@@ -8,7 +8,7 @@
 #include <prjxray/xilinx/configuration.h>
 
 DEFINE_string(part_name, "", "");
-DEFINE_string(part_file, "", "Definition file for target 7-series part");
+DEFINE_string(part_file, "", "Definition file for target part.");
 DEFINE_string(bitstream_file,
               "",
               "Initial bitstream to which the deltas are applied.");
@@ -76,8 +76,8 @@
 		    xilinx::BitstreamReader<ArchType>::InitWithBytes(
 		        bitstream_file->as_bytes());
 		if (!bitstream_reader) {
-			std::cout << "Bitstream does not appear to be a "
-			             "7-series bitstream!"
+			std::cout << "Bitstream does not appear to be compatible"
+			             "with the current architecture!"
 			          << std::endl;
 			return 1;
 		}
@@ -128,7 +128,7 @@
 		    xilinx::BitstreamWriter<ArchType>(configuration_package);
 		if (bitstream_writer.writeBitstream(
 		        configuration_package, FLAGS_part_name, FLAGS_frm_file,
-		        "xc7patch", FLAGS_output_file)) {
+		        "xcpatch", FLAGS_output_file)) {
 			std::cerr << "Failed to write bitstream" << std::endl
 			          << "Exitting" << std::endl;
 		}
diff --git a/tools/frame_address_decoder.cc b/tools/xcu_frame_address_decoder.cc
similarity index 66%
copy from tools/frame_address_decoder.cc
copy to tools/xcu_frame_address_decoder.cc
index b3c21e9..ffe02e8 100644
--- a/tools/frame_address_decoder.cc
+++ b/tools/xcu_frame_address_decoder.cc
@@ -2,18 +2,16 @@
 #include <iomanip>
 #include <iostream>
 
-#include <prjxray/xilinx/xc7series/frame_address.h>
+#include <prjxray/xilinx/xcuseries/frame_address.h>
 
-namespace xc7series = prjxray::xilinx::xc7series;
+namespace xcuseries = prjxray::xilinx::xcuseries;
 
-void frame_address_decode(std::istream* input_stream) {
+void xcu_frame_address_decode(std::istream* input_stream) {
 	for (uint32_t frame_address_raw;
 	     (*input_stream) >> std::setbase(0) >> frame_address_raw;) {
-		xc7series::FrameAddress frame_address(frame_address_raw);
+		xcuseries::FrameAddress frame_address(frame_address_raw);
 		std::cout << "[" << std::hex << std::showbase << std::setw(10)
 		          << frame_address_raw << "] "
-		          << (frame_address.is_bottom_half_rows() ? "BOTTOM"
-		                                                  : "TOP")
 		          << " Row=" << std::setw(2) << std::dec
 		          << static_cast<unsigned int>(frame_address.row())
 		          << " Column=" << std::setw(2) << std::dec
@@ -29,12 +27,12 @@
 	if (argc > 1) {
 		std::ifstream file_stream(argv[1]);
 		if (file_stream) {
-			frame_address_decode(&file_stream);
+			xcu_frame_address_decode(&file_stream);
 			return 0;
 		}
 	}
 
-	frame_address_decode(&std::cin);
+	xcu_frame_address_decode(&std::cin);
 
 	return 0;
 }