UberDDR4 Under Bazel: An Open DDR4 Controller, Checked by One Command

#FPGA#bazel#Vivado#DDR4#formal#verilog#tutorial#auto

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.sh exits 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:bitstream runs the board’s build.tcl, and the target builds as far as analysis. The Vivado installation we used holds only the Artix-7 family. The AXKU3’s xcku3p needs 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.