Merge pull request #55 from kc8apf/xc7patch

FASM and xc7patch proof of concept using partial reconfig flow
diff --git a/Makefile b/Makefile
index c34ebed..297b666 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,6 @@
 	cd build; cmake ..; make -j$(JOBS)
 
 format:
-	find . -name \*.cc -and -not -path './third_party/*' -exec $(CLANG_FORMAT) -style=file -i {} \;
-	find . -name \*.h -and -not -path './third_party/*' -exec $(CLANG_FORMAT) -style=file -i {} \;
-	find . -name \*.py -and -not -path './third_party/*' -exec yapf -p -i {} \;
+	find . -name \*.cc -and -not -path './third_party/*' -and -not -path './.git/*' -exec $(CLANG_FORMAT) -style=file -i {} \;
+	find . -name \*.h -and -not -path './third_party/*' -and -not -path './.git/*' -exec $(CLANG_FORMAT) -style=file -i {} \;
+	find . -name \*.py -and -not -path './third_party/*' -and -not -path './.git/*' -exec yapf -p -i {} \;
diff --git a/lib/include/prjxray/xilinx/xc7series/bitstream_writer.h b/lib/include/prjxray/xilinx/xc7series/bitstream_writer.h
index 73d6c13..ae0634d 100644
--- a/lib/include/prjxray/xilinx/xc7series/bitstream_writer.h
+++ b/lib/include/prjxray/xilinx/xc7series/bitstream_writer.h
@@ -25,8 +25,8 @@
 	typedef std::array<uint32_t, 6> header_t;
 	typedef std::vector<ConfigurationPacket> packets_t;
 	// Only defined if a packet exists
-	typedef absl::optional<absl::Span<uint32_t>> op_data_t;
-	typedef absl::Span<uint32_t>::iterator data_iterator_t;
+	typedef absl::optional<absl::Span<const uint32_t>> op_data_t;
+	typedef absl::Span<const uint32_t>::iterator data_iterator_t;
 	using itr_value_type = uint32_t;
 
 	class packet_iterator
diff --git a/lib/include/prjxray/xilinx/xc7series/configuration.h b/lib/include/prjxray/xilinx/xc7series/configuration.h
index fa10a19..20dea9c 100644
--- a/lib/include/prjxray/xilinx/xc7series/configuration.h
+++ b/lib/include/prjxray/xilinx/xc7series/configuration.h
@@ -16,7 +16,7 @@
 
 class Configuration {
        public:
-	using FrameMap = std::map<FrameAddress, absl::Span<uint32_t>>;
+	using FrameMap = std::map<FrameAddress, absl::Span<const uint32_t>>;
 
 	template <typename Collection>
 	static absl::optional<Configuration> InitWithPackets(
@@ -28,7 +28,7 @@
 	    : part_(part) {
 		for (auto& frame : *frames) {
 			frames_[frame.first] =
-			    absl::Span<uint32_t>(frame.second);
+			    absl::Span<const uint32_t>(frame.second);
 		}
 	}
 
diff --git a/lib/include/prjxray/xilinx/xc7series/configuration_packet.h b/lib/include/prjxray/xilinx/xc7series/configuration_packet.h
index 80cfe9b..f60d370 100644
--- a/lib/include/prjxray/xilinx/xc7series/configuration_packet.h
+++ b/lib/include/prjxray/xilinx/xc7series/configuration_packet.h
@@ -27,7 +27,7 @@
 	ConfigurationPacket(unsigned int header_type,
 	                    Opcode opcode,
 	                    ConfigurationRegister address,
-	                    const absl::Span<uint32_t>& data)
+	                    const absl::Span<const uint32_t>& data)
 	    : header_type_(header_type),
 	      opcode_(opcode),
 	      address_(address),
@@ -47,13 +47,13 @@
 	unsigned int header_type() const { return header_type_; }
 	const Opcode opcode() const { return opcode_; }
 	const ConfigurationRegister address() const { return address_; }
-	const absl::Span<uint32_t>& data() const { return data_; }
+	const absl::Span<const uint32_t>& data() const { return data_; }
 
        private:
 	unsigned int header_type_;
 	Opcode opcode_;
 	ConfigurationRegister address_;
-	absl::Span<uint32_t> data_;
+	absl::Span<const uint32_t> data_;
 };
 
 std::ostream& operator<<(std::ostream& o, const ConfigurationPacket& packet);
diff --git a/lib/xilinx/xc7series/configuration_packet.cc b/lib/xilinx/xc7series/configuration_packet.cc
index f95fc4a..bb24171 100644
--- a/lib/xilinx/xc7series/configuration_packet.cc
+++ b/lib/xilinx/xc7series/configuration_packet.cc
@@ -25,7 +25,11 @@
 			// NOPs.  Since Type 0 packets don't exist according to
 			// UG470 and they seem to be zero-filled, just consume
 			// the bytes without generating a packet.
-			return {words.subspan(1), {}};
+			return {words.subspan(1),
+			        {{header_type,
+			          Opcode::NOP,
+			          ConfigurationRegister::CRC,
+			          {}}}};
 		case 0x1: {
 			Opcode opcode = static_cast<Opcode>(
 			    bit_field_get(words[0], 28, 27));
@@ -73,6 +77,10 @@
 }
 
 std::ostream& operator<<(std::ostream& o, const ConfigurationPacket& packet) {
+	if (packet.header_type() == 0x0) {
+		return o << "[Zero-pad]" << std::endl;
+	}
+
 	switch (packet.opcode()) {
 		case ConfigurationPacket::Opcode::NOP:
 			o << "[NOP]" << std::endl;
diff --git a/minitests/partial_reconfig_flow/Makefile b/minitests/partial_reconfig_flow/Makefile
new file mode 100644
index 0000000..75916b3
--- /dev/null
+++ b/minitests/partial_reconfig_flow/Makefile
@@ -0,0 +1,95 @@
+# Top-level target for generating a programmable bitstream.  Given a .fasm
+# file, calling make with the .fasm extension replaced with .hand_crafted.bit
+# will generate a bitstream that includes both the harness and the .fasm design
+# ready for programming to a board.  For example, 'make
+# roi_noninv.hand_crafted.bit' will generate a bitstream that includes the
+# design from roi_noninv.fasm. 
+%.hand_crafted.bit: init_sequence.bit %.no_headers.bin final_sequence.bin
+	cat $^ > $@
+
+%.no_headers.bin: %.patched.bin
+	# WARNING: these values need to be tweaked if anything about the
+	# Vivado-generated design changes.
+	xxd -p -s 0x18 $< | xxd -r -p - $@
+
+%.patched.bin: %.frm harness_routed.bit
+	${XRAY_TOOLS_DIR}/xc7patch \
+		--part_file ${XRAY_PART_YAML} \
+		--bitstream_file harness_routed.bit \
+		--frm_file $< \
+		--output_file $@
+
+# xc7patch currently only generates the actual frame writes which is
+# insufficient to program a device.  Grab the initialization and finalization
+# sequences from the harness bitstream so they can be tacked on to the
+# xc7patch-generated bitstream to create a programmable bitstream.
+#
+# The offsets used below were determined by manually inspecting
+# harness_routed.bit with a hex editor.  init_sequence.bit is the beginning of
+# the file until just before the actual frame data is sent via a write to FDRI.
+# final_sequence.bin is from just after the frame data write to the end of the
+# file.  Note that final_sequence.bin normally includes at least one CRC check.
+# The sed command replaces any CRC checks with a Reset CRC command which is the
+# same behavior as setting BITSTREAM.GENERAL.CRC to Disabled.  These offset
+# should not change unless you alter the bitstream format used (i.e. setting
+# BITSTREAM.GENERAL.DEBUGBITSTREAM or BITSTREAM.GENERAL.PERFRAMECRC to YES).
+init_sequence.bit: harness_routed.bit
+	# WARNING: these values need to be tweaked if anything about the
+	# Vivado-generated design changes.
+	xxd -p -l 0x147 $< | xxd -r -p - $@
+
+final_sequence.bin: harness_routed.bit
+	# WARNING: these values need to be tweaked if anything about the
+	# Vivado-generated design changes.
+	xxd -p -s 0x216abf $< | \
+		tr -d '\n' | \
+		sed -e 's/30000001.\{8\}/3000800100000007/g' | \
+		fold -w 40 | \
+		xxd -r -p - $@
+
+# Generate a suitable harness by using Vivado's partial reconfiguration
+# feature.  roi_inv is used as a sample reconfiguration design as one is
+# required to generate a partial reconfiguration design.
+harness.dcp: harness.tcl top.v roi_base.v
+	vivado -mode batch -source harness.tcl
+
+roi_inv.dcp: roi_inv.tcl roi_inv.v
+	vivado -mode batch -source roi_inv.tcl
+
+roi_inv_routed.dcp roi_inv_w_harness_routed.dcp harness_routed.dcp: harness.dcp roi_inv.dcp roi_inv_routed.tcl
+	vivado -mode batch -source roi_inv_routed.tcl
+
+# Conversions between various formats.
+%.bit: %.dcp write_bitstream.tcl
+	vivado -mode batch -source write_bitstream.tcl -tclargs $< $@
+
+%.bits: %.bit
+	${XRAY_BITREAD} -y -o $@ $<
+
+# Extract only bits that are within the ROI.
+%.roi.bits: %.bit
+	${XRAY_BITREAD} -F ${XRAY_ROI_FRAMES} -z -y -o $@ $<
+
+%.segp: %.roi.bits
+	${XRAY_SEGPRINT} -zd $< > $@
+
+%.fasm: %.segp
+	${XRAY_DIR}/tools/segprint2fasm.py $< $@
+
+%.frm: %.fasm
+	${XRAY_DIR}/tools/fasm2frame.py $< $@
+
+# This format is a human-readable representation of the configuration packets
+# used to interact with 7-series chips over JTAG.
+%.packets: %.bit
+	${XRAY_TOOLS_DIR}/bittool list_config_packets $< > $@
+
+clean:
+	rm -rf specimen_[0-9][0-9][0-9]/ seg_clblx.segbits vivado*.log vivado_*.str vivado*.jou design *.bits *.dcp *.bit design.txt .Xil
+	rm -rf out_* *~
+	rm -rf *.frm *.segp *.packets *.bin
+	rm -rf harness_routed.fasm roi_inv_w_harness_routed.fasm
+	rm -rf hd_visual
+
+.PHONY: clean
+
diff --git a/minitests/partial_reconfig_flow/README.md b/minitests/partial_reconfig_flow/README.md
new file mode 100644
index 0000000..8eac3df
--- /dev/null
+++ b/minitests/partial_reconfig_flow/README.md
@@ -0,0 +1,53 @@
+# FASM Proof of Concept using Vivado Partial Reconfig flow
+
+top.v is a top-level design that routes a variety of signal into a black-box
+region of interest (ROI).  Vivado's Partial Reconfiguration flow (see UG909
+and UG947) is used to implement that design and obtain a bitstream that
+configures portions of the chip that are currently undocumented.
+
+Designs that fit within the ROI are written in FASM and merged with the above
+harness into a bitstream with fasm2frame and xc7patch.
+
+# Usage
+
+make rules are provided for generating each step of the process so that
+intermediate forms can be analyzed.  Assuming you have a .fasm file, invoking
+the %.hand\_crafted.bit rule will generate a merged bitstream:
+
+```
+make foo.hand\_crafted.bit # reads foo.fasm
+```
+
+# Using Vivado to generate .fasm
+
+Vivado's Partial Reconfiguration flow can be used to synthesize and implement a
+design that is then converted to .fasm.  The basic process is to write a module
+that _exactly_ matches the roi blackbox in the top-level design.  Note that
+even the name of the module must match exactly.  Once you have a design, the
+first step is to synthesize the design with -mode out\_of\_context:
+
+```
+read_verilog <design>.v
+synth_design -mode out_of_context -top roi -part $::env(XRAY_PART)
+write_checkpoint -force <design>.dcp
+```
+
+Next, implement that design within the harness.  Run 'make harness\_routed.dcp'
+if it doesn't already exist.  The following TCL will load the fully-routed
+harness, load your synthesized design, and generate a bitstream containing
+both:
+```
+open_checkpoint -force harness_routed.dcp
+read_checkpoint -cell <design>.dcp
+opt_design
+place_design
+route_design
+write_checkpoint -force <design>_routed.dcp
+write_bitstream -force <design>_routed.bit
+```
+
+'make <design>\_routed.fasm' will run a sequence of tools to extract the bits
+that are inside the ROI and convert them to FASM.  The resulting .fasm can be
+used to generate a marged bitstream using
+'make <design>\_routed.hand\_crafted.bit'.  The resulting bitstream should be
+equivalent to <design>\_routed.bit.
diff --git a/minitests/partial_reconfig_flow/defines.v b/minitests/partial_reconfig_flow/defines.v
new file mode 100644
index 0000000..1008d55
--- /dev/null
+++ b/minitests/partial_reconfig_flow/defines.v
@@ -0,0 +1,8 @@
+`ifndef DIN_N
+`define DIN_N 8
+`endif
+
+`ifndef DOUT_N
+`define DOUT_N 8
+`endif
+
diff --git a/minitests/partial_reconfig_flow/harness.tcl b/minitests/partial_reconfig_flow/harness.tcl
new file mode 100644
index 0000000..14be2e3
--- /dev/null
+++ b/minitests/partial_reconfig_flow/harness.tcl
@@ -0,0 +1,5 @@
+read_verilog top.v
+read_verilog roi_base.v
+
+synth_design -top top -part $::env(XRAY_PART)
+write_checkpoint -force harness.dcp
diff --git a/minitests/partial_reconfig_flow/roi_base.v b/minitests/partial_reconfig_flow/roi_base.v
new file mode 100644
index 0000000..ec69340
--- /dev/null
+++ b/minitests/partial_reconfig_flow/roi_base.v
@@ -0,0 +1,9 @@
+//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;
+endmodule
diff --git a/minitests/partial_reconfig_flow/roi_inv.tcl b/minitests/partial_reconfig_flow/roi_inv.tcl
new file mode 100644
index 0000000..a963234
--- /dev/null
+++ b/minitests/partial_reconfig_flow/roi_inv.tcl
@@ -0,0 +1,3 @@
+read_verilog roi_inv.v
+synth_design -mode out_of_context -top roi -part $::env(XRAY_PART)
+write_checkpoint -force roi_inv.dcp
diff --git a/minitests/partial_reconfig_flow/roi_inv.v b/minitests/partial_reconfig_flow/roi_inv.v
new file mode 100644
index 0000000..7e575cb
--- /dev/null
+++ b/minitests/partial_reconfig_flow/roi_inv.v
@@ -0,0 +1,53 @@
+//Connect the switches to the LEDs, inverting the signal in the ROI
+//Assumes # inputs = # outputs
+
+`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;
+    wire [DIN_N-1:0] internal;
+
+    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
+            //Very expensive inverter
+            (* 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(internal[i]));
+        end
+
+        //DOUT
+        for (i = 0; i < DOUT_N; i = i+1) begin:outs
+            //Very expensive buffer
+            (* KEEP, DONT_TOUCH *)
+            LUT6 #(
+                .INIT(64'b010)
+            ) lut (
+                .I0(internal[i]),
+                .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/partial_reconfig_flow/roi_inv_routed.tcl b/minitests/partial_reconfig_flow/roi_inv_routed.tcl
new file mode 100644
index 0000000..0fee8ca
--- /dev/null
+++ b/minitests/partial_reconfig_flow/roi_inv_routed.tcl
@@ -0,0 +1,149 @@
+open_checkpoint harness.dcp
+
+create_pblock roi
+add_cells_to_pblock [get_pblocks roi] [get_cells roi]
+resize_pblock [get_pblocks roi] -add "$::env(XRAY_ROI)"
+
+set_property CFGBVS VCCO [current_design]
+set_property CONFIG_VOLTAGE 3.3 [current_design]
+
+# Number of package inputs going to ROI
+set DIN_N 8
+# Number of ROI outputs going to package
+set DOUT_N 8
+
+set part $::env(XRAY_PART)
+set pincfg $::env(XRAY_PINCFG)
+
+# Map of top level net names to IOB pin names
+array set net2pin [list]
+
+# Create pin assignments based on what we are targetting
+# A50T I/O Bank 16 sequential layout
+if {$part eq "xc7a50tfgg484-1"} {
+    # Partial list, expand as needed
+    set bank_16 "F21 G22 G21 D21 E21 D22 E22 A21 B21 B22 C22 C20 D20 F20 F19 A19 A18"
+    set banki 0
+
+    # CLK
+    set pin [lindex $bank_16 $banki]
+    incr banki
+    set net2pin(clk) $pin
+
+    # DIN
+    for {set i 0} {$i < $DIN_N} {incr i} {
+        set pin [lindex $bank_16 $banki]
+        incr banki
+        set net2pin(din[$i]) $pin
+    }
+
+    # DOUT
+    for {set i 0} {$i < $DOUT_N} {incr i} {
+        set pin [lindex $bank_16 $banki]
+        incr banki
+        set net2pin(dout[$i]) $pin
+   }
+} elseif {$part eq "xc7a35tcsg324-1"} {
+    # Arty A7 switch, button, and LED
+    if {$pincfg eq "ARTY-A7-SWBUT"} {
+        # https://reference.digilentinc.com/reference/programmable-logic/arty/reference-manual?redirect=1
+        # 4 switches then 4 buttons
+        set sw_but "A8 C11 C10 A10  D9 C9 B9 B8"
+        # 4 LEDs then 4 RGB LEDs (green only)
+        set leds "H5 J5 T9 T10  F6 J4 J2 H6"
+
+        # 100 MHz CLK onboard
+        set pin "E3"
+        set net2pin(clk) $pin
+
+        # 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
+       }
+    # Arty A7 pmod
+    # Disabled per above
+    } elseif {$pincfg eq "ARTY-A7-PMOD"} {
+        # https://reference.digilentinc.com/reference/programmable-logic/arty/reference-manual?redirect=1
+        set pmod_ja "G13 B11 A11 D12  D13 B18 A18 K16"
+        set pmod_jb "E15 E16 D15 C15  J17 J18 K15 J15"
+        set pmod_jc "U12 V12 V10 V11  U14 V14 T13 U13"
+
+        # CLK on Pmod JA
+        set pin [lindex $pmod_ja 0]
+        set net2pin(clk) $pin
+
+        # DIN on Pmod JB
+        for {set i 0} {$i < $DIN_N} {incr i} {
+            set pin [lindex $pmod_jb $i]
+            set net2pin(din[$i]) $pin
+        }
+
+        # DOUT on Pmod JC
+        for {set i 0} {$i < $DOUT_N} {incr i} {
+            set pin [lindex $pmod_jc $i]
+            set net2pin(dout[$i]) $pin
+       }
+    } else {
+        error "Unsupported config $pincfg"
+    }
+} elseif {$part eq "xc7a35tcpg236-1"} {
+    if {$pincfg eq "BASYS3-SWBUT"} {
+        # https://raw.githubusercontent.com/Digilent/digilent-xdc/master/Basys-3-Master.xdc
+
+        # Slide switches
+        set sws "V17 V16 W16 W17 W15 V15 W14 W13 V2 T3 T2 R3 W2 U1 T1 R2"
+        set leds "U16 E19 U19 V19 W18 U15 U14 V14 V13 V3 W3 U3 P3 N3 P1 L1"
+
+        # 100 MHz CLK onboard
+        set pin "W5"
+        set net2pin(clk) $pin
+
+        # DIN
+        for {set i 0} {$i < $DIN_N} {incr i} {
+            set pin [lindex $sws $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]
+}
+
+set_property HD.RECONFIGURABLE TRUE [get_cells roi]
+
+read_checkpoint -cell roi roi_inv.dcp
+
+opt_design
+place_design
+route_design
+
+write_checkpoint -force roi_inv_w_harness_routed.dcp
+
+# Routed design of roi cell only
+write_checkpoint -force -cell roi roi_inv_routed.dcp
+
+# Replace roi cell with a black box and write the rest of the design
+update_design -cell roi -black_box
+lock_design -level routing
+write_checkpoint -force harness_routed.dcp
diff --git a/minitests/partial_reconfig_flow/roi_noninv.fasm b/minitests/partial_reconfig_flow/roi_noninv.fasm
new file mode 100644
index 0000000..7eaf102
--- /dev/null
+++ b/minitests/partial_reconfig_flow/roi_noninv.fasm
@@ -0,0 +1,228 @@
+INT_L_X10Y107.CENTER_INTER_L.SE6BEG0 NN6END0
+INT_L_X10Y104.CENTER_INTER_L.WL1BEG0 NW2END2
+INT_L_X10Y103.CENTER_INTER_L.EL1BEG0 NR1END1
+INT_L_X10Y103.CENTER_INTER_L.EL1BEG_N3 NW2END0
+INT_L_X10Y103.CENTER_INTER_L.FAN_ALT4 NR1END0
+INT_L_X10Y103.CENTER_INTER_L.NE2BEG3 NL1BEG_N3
+INT_L_X10Y103.CENTER_INTER_L.NL1BEG_N3 NL1END0
+INT_L_X10Y103.CENTER_INTER_L.SE2BEG2 EL1END2
+INT_L_X10Y103.CENTER_INTER_L.WW2BEG2 WL1END2
+CLBLM_L_X10Y102.SLICE_X13Y102.ALUT.INIT[01] 1
+CLBLM_L_X10Y102.SLICE_X13Y102.BLUT.INIT[01] 1
+CLBLM_L_X10Y102.SLICE_X12Y102.ALUT.INIT[01] 1
+CLBLM_L_X10Y102.SLICE_X12Y102.BLUT.INIT[01] 1
+INT_L_X10Y102.CENTER_INTER_L.BYP_ALT5 NN2END2
+INT_L_X10Y102.CENTER_INTER_L.EE2BEG2 NL1END2
+INT_L_X10Y102.CENTER_INTER_L.EL1BEG_N3 NL1END0
+INT_L_X10Y102.CENTER_INTER_L.GFAN0 GND_WIRE
+INT_L_X10Y102.CENTER_INTER_L.GFAN1 GND_WIRE
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L0 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L1 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L10 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L11 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L12 GFAN1
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L13 GFAN1
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L14 NL1BEG_N3
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L15 FAN_BOUNCE_S3_4
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L16 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L17 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L18 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L19 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L2 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L24 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L25 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L26 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L27 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L3 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L4 GFAN1
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L5 GFAN1
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L6 WR1END3
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L7 BYP_BOUNCE5
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L8 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.IMUX_L9 GFAN0
+INT_L_X10Y102.CENTER_INTER_L.NL1BEG0 LOGIC_OUTS_L9
+INT_L_X10Y102.CENTER_INTER_L.NL1BEG_N3 LOGIC_OUTS_L8
+INT_L_X10Y102.CENTER_INTER_L.NR1BEG0 LOGIC_OUTS_L12
+INT_L_X10Y102.CENTER_INTER_L.NR1BEG1 LOGIC_OUTS_L13
+INT_L_X10Y102.CENTER_INTER_L.SE2BEG2 EL1END2
+INT_L_X10Y102.CENTER_INTER_L.SR1BEG2 WL1END1
+INT_L_X10Y102.CENTER_INTER_L.SW2BEG3 WL1END3
+INT_L_X10Y102.CENTER_INTER_L.WW2BEG2 WL1END2
+CLBLM_L_X10Y101.SLICE_X13Y101.ALUT.INIT[01] 1
+CLBLM_L_X10Y101.SLICE_X13Y101.BLUT.INIT[01] 1
+CLBLM_L_X10Y101.SLICE_X12Y101.ALUT.INIT[01] 1
+CLBLM_L_X10Y101.SLICE_X12Y101.BLUT.INIT[01] 1
+INT_L_X10Y101.CENTER_INTER_L.BYP_ALT3 NL1BEG_N3
+INT_L_X10Y101.CENTER_INTER_L.BYP_ALT4 NW2END1
+INT_L_X10Y101.CENTER_INTER_L.ER1BEG2 LOGIC_OUTS_L13
+INT_L_X10Y101.CENTER_INTER_L.GFAN0 GND_WIRE
+INT_L_X10Y101.CENTER_INTER_L.GFAN1 GND_WIRE
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L0 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L1 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L10 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L11 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L12 GFAN1
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L13 GFAN1
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L14 SR1END2
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L15 BYP_BOUNCE3
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L16 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L17 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L18 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L19 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L2 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L24 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L25 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L26 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L27 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L3 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L4 GFAN1
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L5 GFAN1
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L6 BYP_BOUNCE4
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L7 NW2END_S0_0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L8 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.IMUX_L9 GFAN0
+INT_L_X10Y101.CENTER_INTER_L.LV_L18 SR1BEG_S0
+INT_L_X10Y101.CENTER_INTER_L.NE2BEG1 NR1END1
+INT_L_X10Y101.CENTER_INTER_L.NE2BEG3 NN6END3
+INT_L_X10Y101.CENTER_INTER_L.NL1BEG0 LOGIC_OUTS_L9
+INT_L_X10Y101.CENTER_INTER_L.NL1BEG2 WL1END2
+INT_L_X10Y101.CENTER_INTER_L.NL1BEG_N3 LOGIC_OUTS_L8
+INT_L_X10Y101.CENTER_INTER_L.NN6BEG0 LOGIC_OUTS_L12
+INT_L_X10Y101.CENTER_INTER_L.SE2BEG2 EE2END2
+INT_L_X10Y101.CENTER_INTER_L.SR1BEG_S0 WL1END3
+CLBLM_L_X10Y100.SLICE_X13Y100.ALUT.INIT[01] 1
+CLBLM_L_X10Y100.SLICE_X13Y100.BLUT.INIT[01] 1
+CLBLM_L_X10Y100.SLICE_X12Y100.ALUT.INIT[01] 1
+INT_L_X10Y100.CENTER_INTER_L.BYP_ALT1 LOGIC_OUTS_L8
+INT_L_X10Y100.CENTER_INTER_L.BYP_ALT2 BYP_BOUNCE1
+INT_L_X10Y100.CENTER_INTER_L.BYP_ALT3 NL1BEG_N3
+INT_L_X10Y100.CENTER_INTER_L.EE2BEG3 NN6END3
+INT_L_X10Y100.CENTER_INTER_L.ER1BEG1 SR1BEG_S0
+INT_L_X10Y100.CENTER_INTER_L.GFAN0 GND_WIRE
+INT_L_X10Y100.CENTER_INTER_L.GFAN1 GND_WIRE
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L0 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L1 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L10 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L11 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L13 GFAN1
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L14 BYP_BOUNCE2
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L16 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L19 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L2 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L25 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L26 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L3 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L4 GFAN1
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L5 GFAN1
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L6 SW2END2
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L7 BYP_BOUNCE3
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L8 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.IMUX_L9 GFAN0
+INT_L_X10Y100.CENTER_INTER_L.NE2BEG0 LOGIC_OUTS_L12
+INT_L_X10Y100.CENTER_INTER_L.NE2BEG3 NE6END3
+INT_L_X10Y100.CENTER_INTER_L.NL1BEG_N3 WL1END_N1_3
+INT_L_X10Y100.CENTER_INTER_L.NN2BEG2 WL1END1
+INT_L_X10Y100.CENTER_INTER_L.NR1BEG1 LOGIC_OUTS_L9
+INT_L_X10Y100.CENTER_INTER_L.SR1BEG_S0 WL1END3
+INT_R_X11Y104.CENTER_INTER_R.SL1BEG3 NE2END3
+INT_R_X11Y103.CENTER_INTER_R.NW2BEG2 NN2END2
+INT_R_X11Y103.CENTER_INTER_R.SL1BEG0 EL1END0
+INT_R_X11Y103.CENTER_INTER_R.WL1BEG2 SL1END3
+INT_R_X11Y103.CENTER_INTER_R.WL1BEG_N3 WR1END1
+CLBLM_R_X11Y102.SLICE_X14Y102.ALUT.INIT[01] 1
+CLBLM_R_X11Y102.SLICE_X14Y102.BLUT.INIT[01] 1
+INT_R_X11Y102.CENTER_INTER_R.EL1BEG0 LOGIC_OUTS13
+INT_R_X11Y102.CENTER_INTER_R.GFAN0 GND_WIRE
+INT_R_X11Y102.CENTER_INTER_R.GFAN1 GND_WIRE
+INT_R_X11Y102.CENTER_INTER_R.IMUX1 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX11 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX12 GFAN1
+INT_R_X11Y102.CENTER_INTER_R.IMUX15 EL1END3
+INT_R_X11Y102.CENTER_INTER_R.IMUX17 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX18 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX2 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX24 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX27 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.IMUX4 GFAN1
+INT_R_X11Y102.CENTER_INTER_R.IMUX7 WR1END3
+INT_R_X11Y102.CENTER_INTER_R.IMUX8 GFAN0
+INT_R_X11Y102.CENTER_INTER_R.NW2BEG0 LOGIC_OUTS12
+INT_R_X11Y102.CENTER_INTER_R.SE2BEG1 NE2END1
+INT_R_X11Y102.CENTER_INTER_R.SL1BEG2 SE2END2
+INT_R_X11Y102.CENTER_INTER_R.SL1BEG3 NE2END3
+INT_R_X11Y102.CENTER_INTER_R.WL1BEG1 NW2END3
+INT_R_X11Y102.CENTER_INTER_R.WL1BEG2 WL1END3
+INT_R_X11Y102.CENTER_INTER_R.WL1BEG_N3 SL1END0
+INT_R_X11Y102.CENTER_INTER_R.WR1BEG3 NR1END2
+CLBLM_R_X11Y101.SLICE_X14Y101.ALUT.INIT[01] 1
+CLBLM_R_X11Y101.SLICE_X14Y101.AMUX.O6 1
+CLBLM_R_X11Y101.SLICE_X14Y101.BLUT.INIT[01] 1
+INT_R_X11Y101.CENTER_INTER_R.EE2BEG2 ER1END2
+INT_R_X11Y101.CENTER_INTER_R.EL1BEG0 LOGIC_OUTS13
+INT_R_X11Y101.CENTER_INTER_R.GFAN0 GND_WIRE
+INT_R_X11Y101.CENTER_INTER_R.GFAN1 GND_WIRE
+INT_R_X11Y101.CENTER_INTER_R.IMUX1 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX11 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX12 GFAN1
+INT_R_X11Y101.CENTER_INTER_R.IMUX15 EL1END3
+INT_R_X11Y101.CENTER_INTER_R.IMUX17 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX18 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX2 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX24 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX27 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.IMUX4 GFAN1
+INT_R_X11Y101.CENTER_INTER_R.IMUX7 NR1END3
+INT_R_X11Y101.CENTER_INTER_R.IMUX8 GFAN0
+INT_R_X11Y101.CENTER_INTER_R.NN2BEG2 LOGIC_OUTS20
+INT_R_X11Y101.CENTER_INTER_R.NR1BEG2 SE2END2
+INT_R_X11Y101.CENTER_INTER_R.NW2BEG0 NE2END0
+INT_R_X11Y101.CENTER_INTER_R.SL1BEG3 NE2END3
+INT_R_X11Y101.CENTER_INTER_R.SR1BEG_S0 SL1END3
+INT_R_X11Y101.CENTER_INTER_R.SW2BEG2 SL1END2
+INT_R_X11Y101.CENTER_INTER_R.WL1BEG2 WR1END_S1_0
+INT_R_X11Y101.CENTER_INTER_R.WL1BEG_N3 SR1BEG_S0
+INT_R_X11Y101.CENTER_INTER_R.WW2BEG0 WL1END0
+INT_R_X11Y101.CENTER_INTER_R.WW2BEG1 WL1END1
+CLBLM_R_X11Y100.SLICE_X14Y100.ALUT.INIT[01] 1
+INT_R_X11Y100.CENTER_INTER_R.BYP_ALT5 ER1END1
+INT_R_X11Y100.CENTER_INTER_R.GFAN0 GND_WIRE
+INT_R_X11Y100.CENTER_INTER_R.GFAN1 GND_WIRE
+INT_R_X11Y100.CENTER_INTER_R.IMUX1 GFAN0
+INT_R_X11Y100.CENTER_INTER_R.IMUX11 GFAN0
+INT_R_X11Y100.CENTER_INTER_R.IMUX2 GFAN0
+INT_R_X11Y100.CENTER_INTER_R.IMUX4 GFAN1
+INT_R_X11Y100.CENTER_INTER_R.IMUX7 BYP_BOUNCE5
+INT_R_X11Y100.CENTER_INTER_R.IMUX8 GFAN0
+INT_R_X11Y100.CENTER_INTER_R.NL1BEG_N3 LOGIC_OUTS12
+INT_R_X11Y100.CENTER_INTER_R.NR1BEG3 NL1BEG_N3
+INT_R_X11Y100.CENTER_INTER_R.NW2BEG1 WL1END0
+INT_R_X11Y100.CENTER_INTER_R.SR1BEG_S0 SL1END3
+INT_R_X11Y100.CENTER_INTER_R.WL1BEG1 SE2END2
+INT_R_X11Y100.CENTER_INTER_R.WL1BEG_N3 SR1BEG_S0
+INT_L_X12Y103.CENTER_INTER_L.WL1BEG_N3 SE6END0
+INT_L_X12Y103.CENTER_INTER_L.WR1BEG1 NR1END0
+INT_L_X12Y102.CENTER_INTER_L.NR1BEG0 EL1END0
+INT_L_X12Y102.CENTER_INTER_L.WR1BEG3 EE2END2
+INT_L_X12Y101.CENTER_INTER_L.NW2BEG3 NR1END3
+INT_L_X12Y101.CENTER_INTER_L.SE2BEG0 EL1END0
+INT_L_X12Y101.CENTER_INTER_L.WL1BEG0 SE2END1
+INT_L_X12Y101.CENTER_INTER_L.WL1BEG1 WR1END3
+INT_L_X12Y101.CENTER_INTER_L.WR1BEG_S0 NE6END3
+INT_L_X12Y100.CENTER_INTER_L.EL1BEG2 NE6END3
+INT_L_X12Y100.CENTER_INTER_L.LV_L18 WR1END0
+INT_L_X12Y100.CENTER_INTER_L.NR1BEG3 EE2END3
+INT_L_X12Y100.CENTER_INTER_L.SW6BEG3 LV_L18
+INT_L_X12Y100.CENTER_INTER_L.WL1BEG0 WR1END2
+INT_R_X13Y101.CENTER_INTER_R.WR1BEG3 EE2END2
+INT_R_X13Y100.CENTER_INTER_R.SL1BEG2 EL1END2
+INT_R_X13Y100.CENTER_INTER_R.WL1BEG_N3 SE2END0
+INT_R_X13Y100.CENTER_INTER_R.WR1BEG2 NR1END1
+INT_L_X16Y100.CENTER_INTER_L.ER1BEG1 SR1BEG_S0
+INT_L_X16Y100.CENTER_INTER_L.SR1BEG_S0 WL1END3
+INT_R_X17Y101.CENTER_INTER_R.WL1BEG_N3 WW2END0
+CLBLL_R_X17Y100.SLICE_X27Y100.AFF.DMUX.AX 1
+CLBLL_R_X17Y100.SLICE_X27Y100.AFF.ZINI 1
+CLBLL_R_X17Y100.SLICE_X27Y100.AFF.ZRST 1
+CLBLL_R_X17Y100.SLICE_X27Y100.FFSYNC 1
+INT_R_X17Y100.CENTER_INTER_R.BYP_ALT0 LOGIC_OUTS0
+INT_R_X17Y100.CENTER_INTER_R.CLK0 ER1END1
diff --git a/minitests/partial_reconfig_flow/top.v b/minitests/partial_reconfig_flow/top.v
new file mode 100644
index 0000000..997231a
--- /dev/null
+++ b/minitests/partial_reconfig_flow/top.v
@@ -0,0 +1,14 @@
+//See README and tcl for more info
+
+`include "defines.v"
+
+module top(input wire clk,
+        input [DIN_N-1:0] din, output [DOUT_N-1:0] dout);
+    parameter DIN_N = `DIN_N;
+    parameter DOUT_N = `DOUT_N;
+
+    roi #(.DIN_N(DIN_N), .DOUT_N(DOUT_N)) roi (
+        .clk(clk),
+        .din(din), .dout(dout));
+endmodule
+
diff --git a/minitests/partial_reconfig_flow/write_bitstream.tcl b/minitests/partial_reconfig_flow/write_bitstream.tcl
new file mode 100644
index 0000000..62beef9
--- /dev/null
+++ b/minitests/partial_reconfig_flow/write_bitstream.tcl
@@ -0,0 +1,27 @@
+open_checkpoint [lindex $argv 0]
+
+# Disabling CRC just replaces the CRC register writes with Reset CRC commands.
+# This seems to work via JTAG as it works in combination with PERFRAMECRC
+# either when applied via Vivado (this setting) or by manually patching the
+# bitstream later.
+#
+set_property BITSTREAM.GENERAL.CRC Disable [current_design]
+
+# Debug bitstreams write to LOUT which is only valid on serial master/slave
+# programming methods.  If those are replaced with NOPs, Reset CRC commands, or
+# removed entirely, the bitstream will program (DONE light goes active) but the
+# configuration doesn't start.  The JTAG status register shows BAD_PACKET_ERROR
+# when this happens.  I'm guessing that the individual frame writes require the
+# PERFRAMECRC approach to work at all via JTAG.
+#
+#set_property BITSTREAM.GENERAL.DEBUGBITSTREAM YES [current_design]
+
+# PERFRAMECRC bitstreams can be directly loaded via JTAG.  They also use an
+# undocumented bit to disable autoincrement which seems to be required if doing
+# individual frame writes instead of a bulk write.  The CRC chceks after each
+# frame are _required_ for this bitstream to program.
+#
+#set_property BITSTREAM.GENERAL.PERFRAMECRC YES [current_design]
+
+
+write_bitstream -force [lindex $argv 1]
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 558c7d8..06dc18c 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -17,3 +17,10 @@
 	libprjxray
 	yaml-cpp
 )
+
+add_executable(xc7patch xc7patch.cc)
+target_link_libraries(xc7patch
+	absl::strings
+	gflags
+	libprjxray
+)
diff --git a/tools/fasm2frame.py b/tools/fasm2frame.py
index c29b929..a21aa33 100755
--- a/tools/fasm2frame.py
+++ b/tools/fasm2frame.py
@@ -178,6 +178,8 @@
             return '%s.%s.%s' % (tilej['type'], suffix, value)
 
         tile2dbkey = {
+            'CLBLL_L': clb2dbkey,
+            'CLBLL_R': clb2dbkey,
             'CLBLM_L': clb2dbkey,
             'CLBLM_R': clb2dbkey,
             'INT_L': int2dbkey,
diff --git a/tools/segprint2fasm.py b/tools/segprint2fasm.py
index 4d4e37d..d073288 100755
--- a/tools/segprint2fasm.py
+++ b/tools/segprint2fasm.py
@@ -37,6 +37,8 @@
         which = m.group(1)
         value = m.group(2)
         site = {
+            'clbll_l': 'CENTER_INTER_L',
+            'clbll_r': 'CENTER_INTER_R',
             'clblm_l': 'CENTER_INTER_L',
             'clblm_r': 'CENTER_INTER_R',
             'hclk_l': 'HCLK_L',
@@ -56,6 +58,8 @@
         raise Exception("Couldn't find tile type %s" % tile_type)
 
     tag2asm = {
+        'CLBLL_L': clbf,
+        'CLBLL_R': clbf,
         'CLBLM_L': clbf,
         'CLBLM_R': clbf,
         'INT_L': intf,
diff --git a/tools/xc7patch.cc b/tools/xc7patch.cc
new file mode 100644
index 0000000..3e0c549
--- /dev/null
+++ b/tools/xc7patch.cc
@@ -0,0 +1,210 @@
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include <absl/strings/str_split.h>
+#include <gflags/gflags.h>
+#include <prjxray/memory_mapped_file.h>
+#include <prjxray/xilinx/xc7series/bitstream_reader.h>
+#include <prjxray/xilinx/xc7series/bitstream_writer.h>
+#include <prjxray/xilinx/xc7series/configuration.h>
+#include <prjxray/xilinx/xc7series/part.h>
+
+DEFINE_string(part_file, "", "Definition file for target 7-series part");
+DEFINE_string(bitstream_file,
+              "",
+              "Initial bitstream to which the deltas are applied.");
+DEFINE_string(
+    frm_file,
+    "",
+    "File containing a list of frame deltas to be applied to the base "
+    "bitstream.  Each line in the file is of the form: "
+    "<frame_address> <word1>,...,<word101>.");
+DEFINE_string(output_file, "", "Write patched bitsteam to file");
+
+namespace xc7series = prjxray::xilinx::xc7series;
+
+int main(int argc, char* argv[]) {
+	gflags::SetUsageMessage(argv[0]);
+	gflags::ParseCommandLineFlags(&argc, &argv, true);
+
+	auto part = xc7series::Part::FromFile(FLAGS_part_file);
+	if (!part) {
+		std::cerr << "Part file not found or invalid" << std::endl;
+		return 1;
+	}
+
+	auto bitstream_file =
+	    prjxray::MemoryMappedFile::InitWithFile(FLAGS_bitstream_file);
+	if (!bitstream_file) {
+		std::cerr << "Can't open base bitstream file: "
+		          << FLAGS_bitstream_file << std::endl;
+		return 1;
+	}
+
+	auto bitstream_reader = xc7series::BitstreamReader::InitWithBytes(
+	    bitstream_file->as_bytes());
+	if (!bitstream_reader) {
+		std::cout
+		    << "Bitstream does not appear to be a 7-series bitstream!"
+		    << std::endl;
+		return 1;
+	}
+
+	auto bitstream_config =
+	    xc7series::Configuration::InitWithPackets(*part, *bitstream_reader);
+	if (!bitstream_config) {
+		std::cerr << "Bitstream does not appear to be for this part"
+		          << std::endl;
+		return 1;
+	}
+
+	// Copy the base frames to a mutable collection
+	std::map<xc7series::FrameAddress, std::vector<uint32_t>> frames;
+	for (auto& frame_val : bitstream_config->frames()) {
+		auto& cur_frame = frames[frame_val.first];
+
+		std::copy(frame_val.second.begin(), frame_val.second.end(),
+		          std::back_inserter(cur_frame));
+	}
+
+	// Apply the deltas.
+	std::ifstream frm_file(FLAGS_frm_file);
+	if (!frm_file) {
+		std::cerr << "Unable to open frm file: " << FLAGS_frm_file
+		          << std::endl;
+		return 1;
+	}
+
+	std::string frm_line;
+	while (std::getline(frm_file, frm_line)) {
+		if (frm_line[0] == '#')
+			continue;
+
+		std::pair<std::string, std::string> frame_delta =
+		    absl::StrSplit(frm_line, ' ');
+
+		uint32_t frame_address =
+		    std::stoul(frame_delta.first, nullptr, 16);
+
+		auto& frame_data = frames[frame_address];
+		frame_data.resize(101);
+
+		std::vector<std::string> frame_data_strings =
+		    absl::StrSplit(frame_delta.second, ',');
+		if (frame_data_strings.size() != 101) {
+			std::cerr << "Frame " << std::hex << frame_address
+			          << ": found " << std::dec
+			          << frame_data_strings.size()
+			          << "words instead of 101";
+			continue;
+		};
+
+		std::transform(frame_data_strings.begin(),
+		               frame_data_strings.end(), frame_data.begin(),
+		               [](const std::string& val) -> uint32_t {
+			               return std::stoul(val, nullptr, 16);
+		               });
+
+		uint32_t ecc = 0;
+		for (size_t ii = 0; ii < frame_data.size(); ++ii) {
+			uint32_t word = frame_data[ii];
+			uint32_t offset = ii * 32;
+			if (ii > 0x25) {
+				offset += 0x1360;
+			} else if (ii > 0x6) {
+				offset += 0x1340;
+			} else {
+				offset += 0x1320;
+			}
+
+			// Mask out where the ECC should be.
+			if (ii == 0x32) {
+				word &= 0xFFFFE000;
+			}
+
+			for (int jj = 0; jj < 32; ++jj) {
+				if ((word & 1) == 1) {
+					ecc ^= offset + jj;
+				}
+				word >>= 1;
+			}
+		}
+
+		uint32_t v = ecc & 0xFFF;
+		v ^= v >> 8;
+		v ^= v >> 4;
+		v ^= v >> 2;
+		v ^= v >> 1;
+		ecc ^= (v & 1) << 12;
+
+		// Replace the old ECC with the new.
+		frame_data[0x32] &= 0xFFFFE000;
+		frame_data[0x32] |= (ecc & 0x1FFF);
+	}
+
+#if 0
+	for (auto& frame : frames) {
+		std::cout << "0x" << std::hex
+		          << static_cast<uint32_t>(frame.first) << " ";
+
+		for (auto& word : frame.second) {
+			std::cout << "0x" << std::hex << word << ",";
+		}
+
+		std::cout << std::endl;
+	}
+#endif
+	std::vector<xc7series::ConfigurationPacket> out_packets;
+
+	// Generate a single type 2 packet that writes everything at once.
+	std::vector<uint32_t> packet_data;
+	for (auto& frame : frames) {
+		std::copy(frame.second.begin(), frame.second.end(),
+		          std::back_inserter(packet_data));
+
+		auto next_address = part->GetNextFrameAddress(frame.first);
+		if (next_address &&
+		    (next_address->block_type() != frame.first.block_type() ||
+		     next_address->is_bottom_half_rows() !=
+		         frame.first.is_bottom_half_rows() ||
+		     next_address->row() != frame.first.row())) {
+			packet_data.insert(packet_data.end(), 202, 0);
+		}
+	}
+	packet_data.insert(packet_data.end(), 202, 0);
+
+	out_packets.push_back(xc7series::ConfigurationPacket(
+	    1, xc7series::ConfigurationPacket::Opcode::Write,
+	    xc7series::ConfigurationRegister::FDRI, {}));
+	out_packets.push_back(xc7series::ConfigurationPacket(
+	    2, xc7series::ConfigurationPacket::Opcode::Write,
+	    xc7series::ConfigurationRegister::FDRI, packet_data));
+
+#if 0
+	for (auto& packet : out_packets) {
+		std::cout << packet << std::endl;
+	}
+#endif
+
+	// Write bitstream.
+	xc7series::BitstreamWriter out_bitstream_writer(out_packets);
+	std::ofstream out_file(FLAGS_output_file);
+	if (!out_file) {
+		std::cerr << "Unable to open file for writting: "
+		          << FLAGS_output_file << std::endl;
+		return 1;
+	}
+
+	for (uint32_t word : out_bitstream_writer) {
+		out_file.put((word >> 24) & 0xFF);
+		out_file.put((word >> 16) & 0xFF);
+		out_file.put((word >> 8) & 0xFF);
+		out_file.put((word)&0xFF);
+	}
+
+	return 0;
+}