rules_vivado: FPGA Synthesis and Place-and-Route in Bazel
rules_vivado drives the AMD/Xilinx Vivado FPGA toolchain from
Bazel: compile, simulate, synthesize, place-and-route, generate a
bitstream, and program the device, all as ordinary build actions instead of
clicks in a GUI or a pile of ad-hoc TCL. This post kicks off a series on the
Bazel modules behind cocoapuffs-fpga, the SoC repo from the
Zircon-on-FPGA bring-up. I have covered some modules before
(rules_ghdl, rules_shar, fshlib, bazel-ebook,
and build-in-docker); the coming posts cover the rest: this one, then
grlib, rules_fusesoc, rules_vunit, rules_dtc, vhdl_ls_gen, OpenSBI, and
rules_osvvm.
Why fight this fight at all
A place-and-route pass for the Cocoapuffs design takes about an hour and a
half. When a build step costs that much, you want two guarantees before you
start it: that its inputs are exactly what you think they are, and that you
never run it when nothing changed. The Vivado project flow gives you neither.
A .xpr project accumulates state as you click; two engineers with the
“same” project routinely hold different in-memory settings; and the standard
recovery advice for a confusing failure is to delete the project directory
and rebuild everything from scratch.
Bazel’s contract is exactly the missing piece: declared inputs, cached outputs, nothing rebuilt without a reason. The catch is that Vivado violates every assumption a build system makes about a well-behaved compiler, so the rules spend most of their effort on containment.
Containing a tool that hates sandboxes
Vivado wants a writable $HOME (it drops ~/.Xilinx state on every run),
writes logs and journals next to wherever it was started, expects absolute
paths, and comes as a multi-hundred-gigabyte licensed installation that
cannot be redistributed. The original answer, borrowed from my
build-in-docker rules, was to run every Vivado command inside a
locally built Docker container: the action’s working directory is mounted at
a fixed path, $HOME points inside the sandbox, and whatever Vivado
scribbles stays in the action.
That answer later generalized into a Bazel toolchain with four modes, selected by one flag:
docker(the default) runs the locally builtxilinx-vivadoimage;hostruns a Vivado installed on the machine, with$HOMEredirected into the action’s working directory so the build stays self-contained;hermetichas Bazel install Vivado itself (next section);customselects a toolchain registered by you or by a dependency module, so an infrastructure module can ship a ready-made Vivado setup for all of its dependents.
The Bazel-managed Vivado installation
The newest mode is my favorite. In hermetic mode, Bazel downloads AMD’s
unified installer archive (on the order of 100 GB; you mirror it locally,
since AMD downloads sit behind a login), runs an unattended batch install of
only the device families you list, and stores the result in a
content-addressed install cache keyed by the archive checksum and the
component selection. Every machine that builds the workspace gets the same
Vivado, provisioned on first use.
Two details took real effort. First, the installer’s component menu differs
per version, so the rules match your modules list against the menu and
fail with the full menu printed when a name is ambiguous or unknown;
requesting a nonexistent module is the documented way to discover what an
archive offers. Second, a naive implementation reinstalls 100 GB every time
Bazel decides to refetch the external repository, which happens far more
often than you would like (an edited .bzl file is enough). The
installation therefore lives outside the workspace’s output base, in
Bazel’s per-user cache: a refetch that finds the completed install
regenerates two small files and finishes in seconds, and even
bazel clean --expunge leaves the installation alone.
What the rules cover
The surface area is what you would expect from a full flow:
vivado_library for compiling VHDL and Verilog into named logical
libraries, vivado_simulation and vivado_test for XSim runs (the latter
as a self-checking bazel test), vivado_synthesis and
vivado_place_and_route down to the bitstream, and
vivado_program_device / vivado_program_flash to put it on the board.
vivado_ip configures a parameterized AMD IP block from its vlnv and a
config dict, and vivado_unisims_library builds the Xilinx simulation
primitives.
Two conveniences deserve a mention. vivado_repl and vivado_gui open an
interactive TCL prompt or the full GUI inside the same containment as the
build, so “poke at it interactively” and “build it reproducibly” use the
same Vivado and the same environment. And vivado_ila with
vivado_read_ila automate the Integrated Logic Analyzer: inserting an ILA
core and reading it back are usually GUI operations, and during a bring-up
(when the serial console is exactly the thing that does not work yet) a
scripted, headless path to on-chip visibility matters. The
Cocoapuffs bring-up ran for weeks in the “no working console”
state; the debugging machinery earned its permanent place in the rules.
In cocoapuffs-fpga
This module is the synthesis and simulation backend for the Artix-7
xc7a200t. The board flow lives in boards/noelv/tool.vivado:
vivado_library targets assemble the SoC from grlib and local IP,
then :sim, :synth, :pnr, and :prog form the chain from simulation
to bitstream to programming. Board-level generics splice the firmware
images (the SERV loader and NOEL-V boot images) into the RTL. Across the
repo there are dozens of vivado_library targets and a couple dozen
vivado_simulation runs, many of them boot-debug traces from the Zircon
work: with an hour-long place-and-route, the working rule is to reproduce
every problem in simulation first and touch the bitstream last.
Prior art and references
- agoessling/rules_vivado predates my rules and requires a host Vivado install; its container setup was the starting inspiration.
- hw-bzl/rules_vivado is an independent set of Vivado Bazel rules.
- bazel_rules_hdl is the broad “EDA under Bazel” precedent (Verilator, Yosys, OpenROAD).
- FuseSoC and Edalize solve the same “assemble and drive an FPGA flow” problem outside Bazel.
For the payoff, see From zero to RISC-V in hardware in 6 minutes and the Cocoapuffs post. The source is at filmil/bazel_rules_vivado, published through my Bazel registry.