rules_openxc7: Open-Source 7-Series FPGA Toolchains in Bazel
rules_openxc7 brings open-source FPGA synthesis and place-and-route for
AMD 7-series chips into Bazel.
It drives Yosys, nextpnr-xilinx, Project X-Ray, and openFPGALoader through
the same target interface as rules_vivado.
This post covers the toolchain architecture, how the rules translate Xilinx
constraints, reproducible bitstreams, and how to swap between Vivado and
open tooling in one line.
Why open tooling in Bazel
Building for FPGAs usually means one of two extremes.
You either install a 100-gigabyte proprietary vendor suite, or you maintain
fragile shell scripts around open-source tools.
rules_openxc7 avoids both problems.
Bazel downloads pinned prebuilt binaries for Yosys, nextpnr, and Project X-Ray.
Every build runs in a sandbox with declared inputs and cached outputs.
You do not need to install Vivado or compile EDA tools by hand.
A new contributor can clone the repository and build a bitstream with
bazelisk build //....
The open toolchain pipeline
The rules connect four open-source projects into an automated pipeline:
- Yosys synthesizes Verilog and SystemVerilog with
synth_xilinx. It can also parse SystemVerilog viayosys-slangor VHDL via GHDL. - nextpnr-xilinx, the himbaechel architecture from the openXC7 project, places and routes against the Project X-Ray database.
- fpga-assembler (
fasm2framesandxc7frames2bit) converts the routing result into an FPGA frame configuration and a binary bitstream. - openFPGALoader programs the FPGA SRAM or SPI flash chip directly over JTAG.
The initial supported part is the xc7a200tfbg484-2, tested on the Alinx
AX7A200B development board.
Using the rules
You can declare FPGA targets using the same structure as rules_vivado:
# MODULE.bazel
bazel_dep(name = "rules_openxc7", version = "0.1.0")
# BUILD.bazel
load(
"@rules_openxc7//openxc7:defs.bzl",
"vivado_place_and_route",
"vivado_program_device",
"vivado_project",
"vivado_synthesis",
)
vivado_project(
name = "blinky",
srcs = ["blinky.sv"],
part = "xc7a200tfbg484-2",
top_level = "up_counter",
xdcs = ["blinky.xdc"],
)
vivado_synthesis(name = "blinky_synth", project = ":blinky")
vivado_place_and_route(name = "blinky_pnr", synthesis = ":blinky_synth")
vivado_program_device(name = "blinky_prog", deps = [":blinky_pnr"])
Build the bitstream:
bazel build //:blinky_pnr
Program the board:
bazel run //:blinky_prog
Bazel writes the bitstream to bazel-bin/blinky_pnr.bit.
openFPGALoader talks directly to the FTDI USB interface on the board.
You do not need a background hardware server daemon.
Drop-in compatibility with rules_vivado
The rules use the rule names and attribute names of rules_vivado.
A project can change its place-and-route engine by editing the load line.
rules_openxc7 also ships an emulated Vivado toolchain runner.
If your workspace already depends on rules_vivado, you can register the
open toolchain runner in your build configuration:
# BUILD.bazel
load("@rules_vivado//internal:toolchain.bzl", "vivado_toolchain")
vivado_toolchain(
name = "openxc7_vivado",
mode = "host",
runner = "@rules_openxc7//vivado:runner",
vivado_path = "/rules_openxc7/vivado",
vivado_version = "openxc7",
)
toolchain(
name = "openxc7_vivado_toolchain",
toolchain = ":openxc7_vivado",
toolchain_type = "@rules_vivado//toolchains:toolchain_type",
)
# MODULE.bazel
register_toolchains("//:openxc7_vivado_toolchain")
When you register this toolchain, rules_vivado targets execute using the
open toolchain.
The runner intercepts Vivado CLI invocations and Tcl commands such as
create_project, read_verilog, synth_design, and write_bitstream.
It translates those commands into calls to Yosys and nextpnr.
Designs build without modifying any existing BUILD files.
Translating XDC constraints
Vivado and nextpnr parse constraint files differently.
nextpnr understands only a subset of Xilinx Design Constraints (XDC).
It supports create_clock and basic set_property statements on ports.
It rejects [all_inputs] and [all_outputs].
It crashes when a constraint groups multiple ports in curly braces, such as
[get_ports {clk reset}].
rules_openxc7 adds an automatic constraint translation step
(xdc_translate.py).
Before place-and-route runs, the translator inspects the synthesized netlist
from Yosys.
It expands wildcards and multi-port groups into separate set_property
commands for each physical pin.
nextpnr receives single-port statements that it can parse cleanly.
Reproducible bitstreams
By default, Project X-Ray’s xc7frames2bit writes the current wall-clock
timestamp and the absolute file path into the bitstream header.
This behavior invalidates Bazel action caching and creates different
files across machines.
rules_openxc7 strips input file paths to base names and sets header
timestamps to fixed values.
Every build of the same RTL produces identical bits.
Bitstreams generated under fastbuild and -c opt compare equal byte for byte.
Limits of the open flow
The open 7-series flow has real technical limits compared to Vivado.
First, the open flow cannot use proprietary Vivado IP. It cannot synthesize encrypted IP cores, memory interface generators (MIG), integrated PCIe endpoints, or Vivado block designs. Designs must use open-source memory controllers and bus fabrics.
Second, timing analysis in nextpnr-xilinx is rudimentary.
Commands beyond create_clock produce warnings rather than full timing
verifications.
Third, upstream openXC7 tools are under active development. Known open issues include MMCM clock generator locking problems on some configurations, silicon capture defects on IDDR primitives, and incomplete DSP48 and LUTRAM packing.
For pure RTL designs on 7-series silicon, the open flow offers fast build times, zero license fees, and complete build hermeticity.
Repository and links
The source code and examples live in filmil/bazel_rules_openxc7. You can inspect the public rule definitions in docs/defs.md.