FireSim Under Bazel

#bazel#FireSim#Chisel#RISC-V#Vivado#FPGA#auto

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:

  1. Elaboration. Chipyard’s generator runs a Scala program that writes the target design as FIRRTL.
  2. 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.sv and a C++ header for the driver.
  3. The driver. A C++ program built from FireSim’s runtime, the bridges' host halves, and the header.
  4. 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.json points at the upstream commit’s tarball, with its hash.
  • overlay/BUILD.bazel declares the Scala library.
  • MODULE.bazel names 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.