FireSim Under Bazel
FireSim is an open-source tool from UC Berkeley that runs a chip
design, usually a RISC-V system written in Chisel, on an FPGA, so that it is
simulated cycle by cycle, far faster than in software.
FireSim 1.22.0 now builds under Bazel, from Chisel source to a bitstream
and a host driver, with no sbt, conda or Vivado project on the host.
Upstream, getting a machine ready to build FireSim takes dozens of pages of
setup instructions.
Under Bazel, all of that setup comes down to a single bazel_dep line in
MODULE.bazel.
It is a net of 22 small modules in my Bazel registry, most of them build
files laid over an upstream commit, plus one rules module,
rules_firesim.
On FireSim’s own Nitefury II recipe, a single Rocket core, the build gives a
bitstream that meets timing on Vivado 2025.2.
Three problems decided the design: Golden Gate needs Chisel 6’s FIRRTL, it
needs an older Scala and other dependency commits than the target, and Chisel
writes machine paths into the output.
What FireSim needs built
FireSim turns a RISC-V system written in Chisel into an FPGA design that a host program steps and inspects. A build has four stages:
- Elaboration. Chipyard’s generator runs a Scala program that writes the target design as FIRRTL.
- Golden Gate. FireSim’s compiler wraps the target in the simulator: the
clock control, the bridges to the host, and the control registers.
It writes
FireSim-generated.svand a C++ header for the driver. - The driver. A C++ program built from FireSim’s runtime, the bridges' host halves, and the header.
- The bitstream. The board’s Vivado project, with the generated Verilog in it.
Upstream, sbt builds the Scala, conda provides the tools, Make glues the steps together, and a Vivado project script builds the bitstream. Under Bazel, each stage is a rule:
load("@rules_firesim//firesim:defs.bzl",
"firesim_bitstream", "firesim_driver",
"firesim_elaborate", "firesim_goldengate")
firesim_elaborate(
name = "rocket",
config = "firechip.chip:FireSimRocket1GiBDRAMConfig",
generator = "@chipyard_firechip//:generator",
top_module = "firechip.chip.FireSim",
)
firesim_goldengate(
name = "rocket_nitefury",
platform_config = "firesim.configs:BaseNitefuryConfig",
target = ":rocket",
)
firesim_driver(
name = "rocket_nitefury_driver",
design = ":rocket_nitefury",
driver_lib = "@chipyard_firechip//:firechip_driver_lib",
)
firesim_bitstream(
name = "rocket_nitefury_bitstream",
design = ":rocket_nitefury",
tags = ["manual"],
)
One module per upstream project
Chipyard 1.15.0 pins about twenty submodules: rocket-chip, diplomacy, cde, hardfloat, BOOM, testchipip, icenet and others. None of them ship Bazel files. Each one became a registry entry of its own, with no new repository behind it:
source.jsonpoints at the upstream commit’s tarball, with its hash.overlay/BUILD.bazeldeclares the Scala library.MODULE.bazelnames the modules it depends on.
A consumer adds one bazel_dep and Bazel fetches the upstream source and lays
the build file over it.
Updating to a later Chipyard means new commits and hashes, which a small script
writes from Chipyard’s .gitmodules.
The Chisel modules share one small module, chipyard_scala.
It holds the macro that compiles a Chisel library, the one pinned Maven set
that every module compiles against, and a Scala toolchain that puts full
transitive classpaths on the compiler, as sbt does.
With that setting, a build file lists only its direct dependencies.
I first tried the existing rules_chisel module.
It resolves its own Maven set, which puts a second copy of the shared Scala
libraries, at other versions, on the classpath next to Chipyard’s.
chipyard_scala keeps a single set.
Problem one: Chisel 7 does not reach Golden Gate
The first trial build used Chisel 7, and the Chipyard libraries compiled and elaborated a Rocket system to SystemVerilog. FireSim needs something else: Golden Gate reads the target as legacy SFC FIRRTL. Only Chipyard’s Chisel 6 path produces that form. With Chisel 7, Chipyard drops the two components that write it, and none of Chipyard’s CI runs that combination. So the target side uses Chisel 6.7.0 on Scala 2.13.16, which is Chipyard’s default.
rules_scala ships Scala 2.13.18, and the Chisel 6.7.0 compiler plugin is
built for 2.13.16 exactly.
The root module overrides the three Scala artifacts to that version.
Problem two: Golden Gate is a separate Scala world
Golden Gate is built by FireSim’s own sbt project with Chisel 3.6.1, against
FireSim’s own pins of rocket-chip, diplomacy and hardfloat.
Those are different commits from Chipyard’s, and Chisel 3.6.1’s compiler plugin
goes up to Scala 2.13.14 only, against 2.13.16 on the target side.
One Bazel build cannot hold both sides.
rules_scala takes its Scala version from the root module only, and Bazel’s
module graph keeps one version of each module, so the two rocket-chip commits
could not both be in it.
So Golden Gate is built in a repository of its own,
bazel_firesim_goldengate, where it is the root module.
The build there produces one self-contained 48 MB jar, which each release
publishes.
The registry module firesim_goldengate pins that jar by its hash and wraps it
as a java_binary.
Consumers run Golden Gate as a tool and never compile it.
Problem three: machine paths in the output
Chisel records a source locator for each statement, and Golden Gate copies them into the generated Verilog as comments. Chisel writes a locator as the file’s path minus the project root. Bazel’s external repositories live in its repository cache, so the prefix never matched, and every locator held a full path on the build machine. The generated Verilog changed whenever that path changed, even with the same logic.
The rules now rewrite each locator to the file’s path inside its own repository, which is the form Chisel’s published jars use. They do it on the FIRRTL before Golden Gate, which sorts multi-file locators by path. They do it again on the Verilog after Golden Gate, for the locators of Golden Gate’s own hardware, which the jar recorded when it was built. No machine paths are left in either file.
Results
All numbers are from one 8-core machine.
| Stage | Result |
|---|---|
| The Chipyard generator, cold | about 6.5 min |
| Elaboration, Rocket | a 41 MB FIRRTL file |
| Golden Gate, Rocket | 59 s, 3 GB peak, a 20 MB .sv |
| Elaboration plus Golden Gate, warm cache | about 1.5 min, 9 actions |
| The driver, cold | about 13 min, mostly LLVM and Spike |
| The bitstream, Rocket on Nitefury II | 32 min, 4.6 GB peak |
The Rocket bitstream meets timing with 0.318 ns of setup slack.
On the Nitefury II’s XC7A200T, it uses 92,354 LUTs (69%) and 227.5 block RAM
tiles (62%).
These numbers match a run of the board’s own build script by hand on the same
machine, and the two .bit files differ only in the build timestamp in their
headers.
FireSim’s Nitefury II flow is pinned to Vivado 2022.1. On 2025.2, ten of its eleven IP cores keep their versions. The one change is XDMA, from 4.1 to 4.2, which is a patch in the platform module.
rules_firesim also has an opt-in second shell for the same board.
It replaces the board’s block design with one Vivado IP target per core and
the synthesis and place-and-route rules of rules_vivado.
Bazel then caches each core and the synthesis run separately.
On the Rocket recipe it meets timing with 0.152 ns of slack and 92,263 LUTs.
A full build takes 66 minutes against 32, most of it in synthesis.
A rebuild that changes only a constraint skips synthesis.
Limits
- The bitstreams have not run on a board. I don’t have a Nitefury II, and FireSim’s host reaches the board over PCIe, through XDMA.
- The Golden Gate jar holds source paths from the machine that built it. The rules remove them from the Verilog, but the jar itself is reproducible on that machine only.
- Only the Nitefury II platform is packaged.
The next post runs FireSim on a board that I do have, the Alinx AX7A200B, with Ethernet in place of PCIe.
Getting it
Point Bazel at both registries in .bazelrc:
common --registry=https://raw.githubusercontent.com/filmil/bazel-registry/main
common --registry=https://bcr.bazel.build
The root MODULE.bazel must also state what only a root module can: the Scala
version, the Scala toolchain, a C++ toolchain, Vivado, and a lock file for the
Maven set.
The integration/ workspace in the rules repository is a complete
example.
The rules are at filmil/bazel_rules_firesim, and Golden Gate’s build is at filmil/bazel_firesim_goldengate. Both are published in my Bazel registry.