Overview

ruckus is a firmware build framework developed at SLAC National Accelerator Laboratory. It wraps Vivado and other EDA tools (Vitis HLS, GHDL, Cadence Genus, Synopsys DC) behind a consistent GNU Make interface. Every firmware project that uses ruckus runs the same way: the engineer types make bit (or another target) and ruckus drives the EDA tool from project source description through to output artifacts.

Without ruckus, each FPGA firmware project would need its own Vivado project file, its own TCL scripts to load sources and run synthesis and implementation, and its own CI configuration. Because SLAC firmware projects share dozens of submodule libraries, maintaining all of that per-project tooling would multiply the maintenance burden across every project.

The Problem ruckus Solves

FPGA firmware projects at SLAC depend on shared submodule libraries — surf, lcls-timing-core, axi-pcie-core, and others. A given firmware target might include HDL sources from three or four submodule trees, plus local sources in the target directory. Integrating all of those into a single Vivado project requires explicitly enumerating every source file and constraint in a form Vivado can read.

The conventional solution — maintaining a Vivado project file (.xpr) in git — creates problems at scale. Vivado project files contain absolute paths, generated file lists, and tool-internal state. Every time source files are added or changed, the .xpr diff is large and hard to review. Merging .xpr changes across branches regularly causes conflicts that require manual resolution inside Vivado’s GUI.

ruckus replaces the .xpr file with a small ruckus.tcl script that describes the project’s sources declaratively, using ruckus-provided procedures (loadSource, loadConstraints, loadIpCore, etc.). The Vivado project is reconstructed from scratch on every build by running these scripts inside Vivado’s TCL interpreter. The project file is never committed to git; only the ruckus.tcl manifest is.

Because each submodule library also carries its own ruckus.tcl, the entire source tree can be described by loading ruckus.tcl files recursively — each library knows how to add its own files to the project. A top-level firmware target’s ruckus.tcl only needs to reference its immediate dependencies; ruckus traverses the rest.

How ruckus Works: The Makefile/TCL Hybrid

ruckus uses a two-layer model: Make orchestrates the build process at the outer level, and TCL runs inside Vivado at the inner level.

Layer 1 — Make: The engineer runs make bit from the firmware target directory. The project’s Makefile includes system_vivado.mk from the ruckus submodule, which provides all build targets. system_vivado.mk drives two Vivado invocations in sequence:

  1. vivado -source $(RUCKUS_DIR)/vivado/sources.tcl — assembles the Vivado project by recursively loading all ruckus.tcl files and adding their declared sources.

  2. vivado -source $(RUCKUS_DIR)/vivado/build.tcl — runs synthesis, implementation, bitstream generation, and copies output artifacts to images/.

Layer 2 — TCL: The ruckus.tcl file in each firmware target directory is the project manifest. It uses ruckus-provided procedures to declare what belongs in the project. These procedures run inside Vivado’s TCL interpreter during the first Vivado invocation (sources.tcl). The most common procedures are:

  • loadSource — add HDL source files to the Vivado sources_1 fileset

  • loadConstraints — add XDC/TCL constraint files to the Vivado constrs_1 fileset

  • loadIpCore — import Vivado IP core (.xci) files

  • loadRuckusTcl — recursively load another directory’s ruckus.tcl

A real-world example shows how compact this model is. The Simple-10GbE-RUDP-KCU105-Example project’s Makefile is three lines:

export TOP_DIR = $(abspath $(PWD)/../..)
include ../shared_version.mk
include $(TOP_DIR)/submodules/ruckus/system_vivado.mk

The first line sets TOP_DIR to the firmware/ root. The second line pulls in PRJ_VERSION, PRJ_PART, and the default build target from a shared version file. The third line provides the entire ruckus build system.

The corresponding ruckus.tcl for that project is equally concise:

# Load RUCKUS environment
source $::env(RUCKUS_PROC_TCL)

# Check for version 2023.1 of Vivado (or later)
if { [VersionCheck 2023.1] < 0 } {exit -1}

# Load shared and sub-module ruckus.tcl files
loadRuckusTcl $::env(TOP_DIR)/submodules/surf
loadRuckusTcl $::env(TOP_DIR)/shared

# Load local source code and constraints
loadSource      -dir "$::DIR_PATH/hdl"
loadConstraints -dir "$::DIR_PATH/hdl"

# Load local simulation source code
loadSource -sim_only -dir "$::DIR_PATH/tb"
set_property top {Simple10GbeRudpKcu105ExampleTb} [get_filesets sim_1]

This ruckus.tcl loads the surf library and the project’s local shared code by delegating to their own ruckus.tcl files, then adds the target’s own HDL sources and constraints. The $::DIR_PATH/hdl syntax ensures that paths are always anchored to the directory containing this specific ruckus.tcl — not to Vivado’s working directory.

The rest of this section explains the $::DIR_PATH mechanism that makes $::DIR_PATH/hdl work correctly, the full build pipeline, and the output artifact naming convention.