13 KiB
Reproducing the first working VC707 open-flow bitstream
This document captures the end-to-end recipe that produced the first
confirmed-working open-flow bitstream on a real Virtex-7 VC707 board on
2026-06-01. The design — IBUFDS + BUFG + FDRE + OBUF — runs from
SYSCLK_P/N at 200 MHz, toggles a flip-flop, and drives LED D0 (silk
GPIO_LED_0, pin AM39). Observed hardware behaviour: D0 at half
brightness from the FF toggling at 100 MHz, with reset (pin AV40)
visibly controlling the LED state.
This is the open-flow analogue of what Vivado would produce from the same RTL — nextpnr-xilinx places and routes, RapidWright converts to a DCP, Vivado writes the bitstream.
What you need
| Component | Where | Version / branch |
|---|---|---|
| prjxray (this repo) | https://github.com/openXC7/prjxray | virtex7-support branch |
| nextpnr-xilinx | https://github.com/openXC7/nextpnr-xilinx | a branch with the patches from xilinx/{fasm,pack_clocking_xc7,pack_io_xc7}.cc listed below; develops out of the atomic-carry4 tag |
| RapidWright | https://github.com/Xilinx/RapidWright | 2025.2.1 standalone jar (rapidwright-2025.2.1-standalone-lin64.jar, ~95 MB) plus gson-2.10.1.jar |
| json2dcp + WireOracle + BuildWireOracle | local glue, currently in /home/jonathan/rapidwright/build/ |
tracked in tools/rapidwright_glue/ (planned — see "Glue tree" below) |
| Vivado | for write_bitstream and as source of device data |
2020.1 (others 2018+ may work) |
| SystemVerilog Suite (SVS) | ~/System-Verilog-suite/_build/default/sv_suite.exe |
Verilog → EDIF → nextpnr JSON |
| openFPGALoader | https://github.com/trabucayre/openFPGALoader | built locally; loads via Digilent JTAG |
Vivado version.
2020.1is the validated target; the upstream prjxray default of2017.2is too old forxc7vx485tffg1761-2(HP-bank IOB18 behaviour,HCLK_IOI/CMTproperties, several Tcl helpers that the patchedutils.tclexercises). SetXRAY_VIVADO_SETTINGS=/opt/Xilinx/Vivado/2020.1/settings64.sh.
The design (min_ibufds_ff_led)
top.v — minimal IBUFDS + BUFG + FDRE + OBUF:
module top (
input wire sysclk_p, sysclk_n, rst,
output wire led
);
wire clk_raw, clk;
IBUFDS #(.DIFF_TERM("TRUE"), .IBUF_LOW_PWR("FALSE"), .IOSTANDARD("LVDS"))
sysclk_ibufds (.I(sysclk_p), .IB(sysclk_n), .O(clk_raw));
BUFG sysclk_bufg (.I(clk_raw), .O(clk));
reg q = 1'b0;
always @(posedge clk) begin
if (rst) q <= 1'b0;
else q <= ~q;
end
OBUF led_obuf (.I(q), .O(led));
endmodule
top.xdc — VC707 pin map:
set_property PACKAGE_PIN E19 [get_ports sysclk_p]
set_property PACKAGE_PIN E18 [get_ports sysclk_n]
set_property IOSTANDARD LVDS [get_ports sysclk_p]
set_property IOSTANDARD LVDS [get_ports sysclk_n]
create_clock -period 5.000 -name sysclk [get_ports sysclk_p]
set_property PACKAGE_PIN AV40 [get_ports rst]
set_property IOSTANDARD LVCMOS18 [get_ports rst]
set_property PACKAGE_PIN AM39 [get_ports led]
set_property IOSTANDARD LVCMOS18 [get_ports led]
set_property CFGBVS GND [current_design]
set_property CONFIG_VOLTAGE 1.8 [current_design]
Build prerequisites
1. nextpnr-xilinx
cd ~/nextpnr-xilinx
cmake -B build -DARCH=xilinx -DBUILD_GUI=OFF
cmake --build build --target nextpnr-xilinx -j$(nproc)
Critical patches (in this checkout, on the virtex7-support /
atomic-carry4 branch):
xilinx/fasm.cc— HP-bank glue (LIOB18 default emission,IBUF_HP_BANK_GLUE,OBUF_HP_BANK_GLUE,IBUFDS_BANK_GLUE), and the phantom-BUFGCTRL filter that suppressesCLK_BUFG_*_Rfeatures for unused slots.xilinx/pack_clocking_xc7.cc— preservesX_ORIG_TYPE=BUFGandX_ORIG_PORT_I0=Ietc. when packing BUFG → BUFGCTRL so downstream EDIF / DCP emission can re-create the BUFG cell.xilinx/pack_io_xc7.cc— acceptLIOB18_tiles in addition toRIOB18_everywhere IOB18-specific placement was hardcoded; expandIBUFDS_GTE2user check to allow BUFG / BUFH / BUFHCE / BUFR.
The xc7vx485t chipdb must already be built from the nextpnr-xilinx-meta
metadata tree (xilinx/xc7vx485t.bin).
2. RapidWright + the glue jar
Install RapidWright device data (extract rapidwright_data.zip and
rapidwright_data2.zip into ~/.local/share/RapidWright/data/). Then
build the glue:
cd ~/rapidwright/build
./build.sh # compiles WireOracle.java + json2dcp.java -> rapidwright_json2dcp.jar
build.sh expects:
~/rapidwright/rapidwright-2025.2.1-standalone-lin64.jar
~/rapidwright/jars/jars/gson-2.10.1.jar
3. Wire-name oracle (one-shot per part)
The oracle is a static per-tile-type table answering PIP-direction,
routethru, and site→tile questions that json2dcp consults at routing-
import time. Build it once for xc7vx485tffg1761-2:
mkdir -p ~/min_ibufds_ff_led/oracle
java -Xmx16g \
-cp ~/rapidwright/build/oracle_classes:~/rapidwright/rapidwright-2025.2.1-standalone-lin64.jar \
dev.fpga.rapidwright.BuildWireOracle \
xc7vx485tffg1761-2 \
~/min_ibufds_ff_led/oracle/xc7vx485tffg1761-2.oracle.txt.gz
Cost: ~1.5 s wall, ~2.5 MB gzipped output. json2dcp auto-discovers
the file by walking up from the JSON's directory looking for an
oracle/<part>.oracle.txt.gz sibling, or via
XRAY_WIRE_ORACLE=<path>. json2dcp bombs out if the oracle
file is missing (no fallback — silent fallback produced bitstreams
that loaded but didn't clock on hardware).
End-to-end flow
cd ~/min_ibufds_ff_led
# 1) SVS: top.v -> top_vivado_synth.edif (via Vivado synthesis)
# top_vivado_synth.edif -> top.json (via SVS)
# 2) nextpnr-xilinx: top.json -> top_routed.json + top.fasm
~/System-Verilog-suite/_build/default/sv_suite.exe script nextpnr_pass/build.lua
# 3) json2dcp: top_routed.json -> top_rw.dcp
java -jar ~/rapidwright/build/rapidwright_json2dcp.jar \
xc7vx485tffg1761-2 \
nextpnr_pass/top_routed.json \
nextpnr_pass/top_rw.dcp
build.lua is the SVS script that drives the Verilog→EDIF→JSON→nextpnr
pipeline. See nextpnr_pass/build.lua.
Expected json2dcp output (good run):
[wire-oracle] loaded …/xc7vx485tffg1761-2.oracle.txt.gz types=115 tiles=150380 pips=35126
json2dcp ROUTING net=q: imported=17 (oracle=10, oracle_rev_bidir=0) … lookup_failed=0
json2dcp ROUTING net=rst_IBUF: imported=12 (oracle=6, oracle_rev_bidir=0) … lookup_failed=0
json2dcp ROUTING net=clk: imported=13 (oracle=8, oracle_rev_bidir=3) … lookup_failed=0
json2dcp ROUTING net=clk_raw: imported=20 (oracle=16, oracle_rev_bidir=2) … lookup_failed=0
lookup_failed=0 on every user net is the success signal. The
oracle_rev_bidir count records how many PIPs needed
setIsReversed(true) — those are exactly the bidir-direction cases
that previously failed silently.
4. Vivado: DCP → bitstream
Vivado opens the DCP cleanly and reports pre-route_design:
| Net | ROUTE_STATUS |
|---|---|
clk, clk_raw, q, rst_IBUF |
ROUTED |
sysclk_n |
UNROUTED (Vivado fills in) |
led, rst, sysclk_p, q_i_1_n_0 |
INTRASITE |
<const0>, <const1> |
ROUTED |
We lock the four good user nets, route sysclk_n only, downgrade a few
non-essential DRC checks, and write the bitstream:
# /tmp/write_bit_force.tcl
open_checkpoint /home/jonathan/min_ibufds_ff_led/nextpnr_pass/top_rw.dcp
foreach n [get_nets -hier] {
if {[get_property ROUTE_STATUS $n] == "ROUTED"} {
set_property IS_ROUTE_FIXED 1 $n
}
}
catch {route_design -nets [get_nets sysclk_n]} rerr
puts "route_design (sysclk_n only): $rerr"
foreach chk {UCIO-1 RTSTAT-5 RTSTAT-2 RTSTAT-1 NSTD-1 LUTLP-1 CFGBVS-1} {
catch { set_property IS_ENABLED 0 [get_drc_checks $chk] }
}
write_bitstream -force /home/jonathan/min_ibufds_ff_led/nextpnr_pass/top_rw.bit
exit
vivado -mode batch -nojournal -nolog -source /tmp/write_bit_force.tcl
Why these flags:
IS_ROUTE_FIXED 1on ROUTED nets preventsroute_designfrom unrouting them while it works onsysclk_n. Without the lock,route_designregressesqandrst_IBUFtoANTENNASbecause of a separate SLICE pin-assignment CRITICAL ([Route 35-557] q_i_1/I0 A5LUT/A3 not found in siteAssignment). That's an open bug, not load-bearing for this design.route_design -nets [get_nets sysclk_n]is a per-net route so it doesn't touch the others.IS_ENABLED 0disables four DRC checks that fire on cosmetic issues we know about (XDC LOC application timing, CFGBVS prop, LUT loop default, no-IOSTD).
Result: a ~20 MB bitstream at nextpnr_pass/top_rw.bit.
5. Load on the board
~/openFPGALoader/build/openFPGALoader \
--freq 15000000 --cable digilent \
~/min_ibufds_ff_led/nextpnr_pass/top_rw.bit
--freq 15000000 is the validated VC707 JTAG clock; the openFPGALoader
default of 6 MHz works too but is slower. The board must be powered
and the Digilent JTAG cable enumerated (lsusb shows a Digilent device).
Expected progress: Load SRAM: 100.00% → Done → done 1. No errors.
Expected hardware:
- LED D0 (
GPIO_LED_0, AM39) dimly oscillating — the FF toggles atsysclk / 2≈ 100 MHz, far too fast to see as discrete blinks but at ~50 % duty cycle so the LED is at roughly half brightness. - Reset (
CPU_RESET, AV40) holds the FF in0while pressed; release returns to the toggling state.
If LED is stuck on, stuck off, or D1 (the next LED over) lights up too, see Known issues below.
What's actually doing the heavy lifting
Three pieces of glue, layered:
xilinx/fasm.ccphantom-BUFGCTRL filter (this is what made the FASM path work earlier, separately from the DCP work). Without it, the FASM emits unused-BUFGCTRL features atCLK_BUFG_BOT_R_X192Y204that program contending clock paths and the FF never clocks.- The wire-name oracle (
build/BuildWireOracle.java,build/WireOracle.java). Without it, json2dcp can't resolve bidir-direction PIPs in the clock spine (CLK_BUFG_REBUFBIDIR pips at Y221 / Y194 / Y169 / Y142) — silently picks the wrong direction, route ends up incomplete. - The SITEWIRE→SITEWIRE SitePIP handler in
json2dcp.java. Without it, the implicit intra-site selections (AOUTMUX:A5Q -> AMUX,CLKINV:CLK -> OUT,A5FFMUX:IN_A -> OUT, ...) aren't bound on the SiteInst, so the cell pin and the site pin are disconnected and Vivado reportsANTENNASon every affected net.
Known issues (do not block this design, will trip other designs)
-
SLICE pin-assignment CRITICAL —
[Route 35-557] resultsCompare: q_i_1/I0 A5LUT/A3 is not found in the siteAssignment results from RuleRoute. Vivado'sroute_designaborts mid-route onSLICE_X46Y122because the cell-pin → BEL-pin mapping it computes disagrees with what we wrote. Workaround: lock ROUTED nets beforeroute_design. -
IOB sitetype-net overwrite CRITICALs —
[Constraints 18-4866]fires onIOB_X0Y124andIOB_X1Y276for paths likeOUTBUF_DCIEN_OUT,DIFFI_IN,PADOUT. Default SitePIPs thatcreateAndPlaceCellbinds (e.g.OUSED:0 -> OUTon a pure IBUF) overlap with our explicit nets. Cosmetic for this design. -
UCIO-1 not auto-applied — XDC LOCs are present in
top_rw.dcp/top.xdcand on the placed cells (property LOCin EDIF), but Vivado's port-LOC DRC didn't pick them up. Worked around by disabling the DRC; root-cause likely an XDC application-order issue.
Glue tree
The json2dcp / WireOracle / BuildWireOracle sources are currently in
/home/jonathan/rapidwright/build/:
build/
build.sh # one-shot compile-and-jar
manifest.mf
WireOracle.java # in-process reader for the oracle file
BuildWireOracle.java # offline generator: Device.getDevice() -> oracle
json2dcp.java # nextpnr JSON -> RapidWright Design -> DCP
oracle_classes/ # compiled classes
rapidwright_json2dcp.jar # the deliverable
That directory is not yet in a git repository. Plan: move it into
this repo under tools/rapidwright_glue/ (or its own repo on the
openXC7 organisation) so future bisects of the open flow have all
three layers under version control.
Provenance
- Test design lives at
~/min_ibufds_ff_led/. - Bitstream artefact:
~/min_ibufds_ff_led/nextpnr_pass/top_rw.bit(20 MB). - Oracle artefact:
~/min_ibufds_ff_led/oracle/xc7vx485tffg1761-2.oracle.txt.gz(2.5 MB). - Hardware: VC707 Rev1.1 (
xc7vx485tffg1761-2). - Vivado: 2020.1.
- nextpnr-xilinx:
virtex7-supportderivative; key commits include4e6663e(BUFGCTRL default emission),10c7372(atomic CARRY4 packer, also taggedatomic-carry4), and the in-tree changes tofasm.cc+pack_clocking_xc7.cc+pack_io_xc7.ccreferenced above. - RapidWright:
2025.2.1.