UberDDR4 Under Bazel: An Open DDR4 Controller, Checked by One Command
UberDDR4 is an open-source DDR4 SDRAM controller and PHY for FPGAs, written by Angelo C. Jacobo. It connects a Wishbone master to DDR4 memory. It handles initialization, command timing, bank scheduling, refresh and PHY calibration. Its native PHY runs the memory at DDR4-2400 on a Kintex UltraScale+ part. The project is the successor to UberDDR3, and the NGI0 Commons Fund of NLnet paid for it.
The repository is unusually well verified for an open hardware project. It
has Verilator lint, Icarus and Yosys compile checks, 34 SymbiYosys proofs, and
26 simulations against Micron’s DDR4 model. A shell script, run_compile.sh,
runs them in sequence. We converted all of it to Bazel, in a pull request on
our fork. This article shows what that took and what it measured.
What the repository runs
The upstream flow has four stages, and each needs its own tools:
| Stage | Tool | What it checks |
|---|---|---|
| Lint | Verilator | Each core module, and the top level over all four |
| Compile | Icarus Verilog, Yosys | The core parses, and Yosys can elaborate it |
| Formal | SymbiYosys | 4 proofs, or 30 in the regression set |
| Simulation | Vivado XSim | 26 configurations against the Micron model |
The board example for the ALINX AXKU3 adds a Vivado bitstream build, with
make. The Linux example builds a LiteX SoC with a VexRiscv CPU. It boots
Linux from the DDR4 that UberDDR4 drives.
Each stage expects its tools on the PATH. The simulation also expects the
Micron model files to be linked in from your Vivado installation, by a setup
script you run first.
Getting the tools without installing them
Bazel downloads the open-source tools itself, so a checkout needs only
Bazel. One dated release of OSS CAD Suite provides Verilator,
Icarus, Yosys, SymbiYosys and the Yices solver. MODULE.bazel pins that
archive by its SHA-256:
http_archive(
name = "oss_cad_suite",
build_file = "//tools:oss_cad_suite.BUILD",
sha256 = "639c8c0366d74dc56201729c9907bf2d8281db7f18bb025abe2e4f3f2ebaa24e",
strip_prefix = "oss-cad-suite",
urls = ["https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-10-18/oss-cad-suite-linux-x64-20261018.tgz"],
)
One archive keeps the tools at versions that YosysHQ built together. Its old releases stay downloadable, so the pin keeps working. The only thing Bazel takes from the machine is Perl, which Verilator’s front end needs.
Every check becomes a test
Each check is now its own Bazel test. Bazel runs them in parallel and caches
each verdict until one of its inputs changes. bazel test //... runs every
check that needs no Vivado:
| Old command | Bazel target | Measured |
|---|---|---|
run_compile.sh --lint |
//rtl:lint |
0 to 2 s per module |
run_compile.sh --compile |
//rtl:compile |
Icarus under 1 s, Yosys 23 s |
run_compile.sh --formal |
//formal:single |
26 and 37 s at depth 8, about 12 minutes at depth 28 |
run_compile.sh --formal-regr |
//formal:multi |
30 of 30 proofs pass |
The formal proofs gain the most from this. The upstream script hands
SymbiYosys a whole .sby file, and its tasks run one after another. Under
Bazel, each task is a separate test, such as
//formal:multi_ddr4_2400_x16_map1. The 30 tasks then run side by side, and
changing an unrelated file reruns none of them.
The conversion keeps the upstream scripts as the only source of their
details. run_compile.sh writes stub modules for the Xilinx primitives
before lint, from a heredoc in the script. A Bazel genrule cuts that
heredoc out of the script at build time, rather than keeping a second copy.
Simulation with Vivado
The 26 simulations are tests too, one per row of the ALL_TESTS table in
testbench/regression_test.sh. Bazel reads each row at build time and writes
the configuration header that the script would have written. Then it runs the
repository’s own run_xsim.sh in a scratch copy of the checkout. The
verdict uses the regression script’s rule:
run_xsim.shexits 0;- the log shows a pass marker and no failure marker;
- the Micron model reports no timing violation.
Vivado comes from rules_vivado, which can install Vivado from
the AMD installer archive itself. The Micron model ships with Vivado’s DDR4
IP, and the project does not redistribute it. A vivado_extract target
copies the nine model files out of the installation, which is the job the
setup script used to do.
Each simulation is a cached build step, and its test only reads the log. A case that already ran, passing or failing, costs nothing the next time. One baseline case took 177 s. All 26 took 24 minutes on an 8-core machine, four at a time, and all 26 passed with Vivado 2025.2.
They passed after one fix to the testbench. Vivado 2025.2 refused to elaborate it:
ERROR: [VRFC 10-2649] an enum variable may only be assigned the same enum
typed variable or one of its values
The testbench declared localparam TB_DENSITY = _8G; with no type. Vivado
2025.2 treats such a parameter as an int. It will not pass an int to the
Micron model’s CONFIGURED_DENSITY parameter, whose type is the enum
UTYPE_density. Declaring localparam UTYPE_density TB_DENSITY = _8G;
fixed it, and the older Vivado 2023.1 that the author used accepts that form
too.
What is not done yet
Three parts of the upstream flow are not checked under Bazel yet:
- The AXKU3 bitstream.
//example_demo/axku3:bitstreamruns the board’sbuild.tcl, and the target builds as far as analysis. The Vivado installation we used holds only the Artix-7 family. The AXKU3’sxcku3pneeds Kintex UltraScale+, which is a separate installation of about 50 GB. - The native PHY simulations. There is a target for each of the 26 cases, but none has run yet. The upstream script allows each one up to four hours.
- The Linux SoC. Bazel runs the Linux example’s 64 host tests, with Migen and LiteX at the commits the project pins. Building the SoC and booting it still goes through the project’s own script. The RISC-V GCC it pins is a Windows build.
Try it
You need Bazel and a Linux x86-64 machine. Clone the branch and run:
git clone -b bazel https://github.com/filmil/UberDDR4
cd UberDDR4
bazel test //...
The first run downloads about 750 MB of tools. The two depth-28 proofs of the
single set and the four of the regression set take the longest. With Vivado
on the machine, bazel test //testbench:sim_regression adds the 26
simulations. The Bazel guide in the repository lists the target for
each command of the upstream flow.
For the general approach to hardware builds in Bazel, see Hardware Development with Bazel.