Your First Vivado Firmware Build

This tutorial walks through setting up a Vivado firmware project that uses ruckus as its build system. You will clone a working example project, examine its structure, and run make bit to produce a bitstream.

The example project is Simple-10GbE-RUDP-KCU105-Example — a real SLAC firmware project for the KCU105 evaluation board. Its Makefile and ruckus.tcl are minimal and representative of how ruckus projects are structured.

By the end of this tutorial you will understand:

  • What the three mandatory files in a ruckus project are and what each does

  • How ruckus.tcl declares sources using $::DIR_PATH/

  • How to run a build from scratch and what the output artifacts look like

Prerequisites

Before you begin, ensure you have:

  • Linux — ruckus requires a Linux environment. macOS and Windows are not supported.

  • Vivado — Xilinx/AMD Vivado installed and on your PATH. The example project requires Vivado 2023.1 or later. Test with:

    vivado -version
    
  • git — version 2.9.0 or later. Test with:

    git --version
    
  • git-lfs — version 2.1.1 or later. Test with:

    git lfs version
    
  • Python 3 — required only for make release (not for make bit). Install pip packages if you plan to use release targets:

    pip install gitpython PyGithub pyyaml
    

Note

Vivado must be activated in your shell before running any make target. Source the Vivado settings script before proceeding:

source /path/to/Vivado/2023.1/settings64.sh

Replace /path/to/Vivado/2023.1 with your actual Vivado installation path. After sourcing, confirm Vivado is accessible: vivado -version.

Clone the Example Project

The example project uses git submodules to pull in its dependencies (including ruckus itself). Clone it recursively so that all submodules are populated:

git clone --recursive https://github.com/slaclab/Simple-10GbE-RUDP-KCU105-Example.git
cd Simple-10GbE-RUDP-KCU105-Example

If you forget --recursive, initialize submodules manually:

git submodule update --init --recursive

ruckus itself lives at firmware/submodules/ruckus/ inside the cloned repository — it is a git submodule, not a system-wide tool. Every ruckus-based project carries its own pinned copy this way.

Project Directory Structure

After cloning, the relevant structure under firmware/ is:

firmware/
├── shared/
│   ├── ruckus.tcl          <- shared library manifest
│   ├── rtl/                <- shared RTL source files
│   ├── ip/                 <- shared IP core files (.xci)
│   └── xdc/                <- shared constraint files (.xdc)
├── submodules/
│   ├── ruckus/             <- this build system (as a git submodule)
│   └── surf/               <- SLAC FPGA library (as a git submodule)
├── targets/
│   ├── shared_version.mk   <- firmware version and target-common settings
│   └── Simple10GbeRudpKcu105Example/
│       ├── Makefile         <- build entry point
│       ├── ruckus.tcl       <- target-specific source manifest
│       ├── hdl/             <- target-specific RTL source files
│       └── tb/              <- testbench files
└── build/                  <- created at first build (or symlinked to scratch disk)

The three mandatory files for any ruckus project are:

  1. Makefile — includes system_vivado.mk from the ruckus submodule

  2. ruckus.tcl — declares the project’s sources using ruckus procedures

  3. images/ directory — receives output artifacts (ruckus creates it automatically on the first successful build)

Everything else in the structure above (shared/, surf/, etc.) belongs to this particular example project. A minimal new project needs only the three items above.

Understanding the Makefile

Look at the Makefile for this target (firmware/targets/Simple10GbeRudpKcu105Example/Makefile):

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

Three lines. TOP_DIR points two levels up to the firmware/ root. shared_version.mk sets PRJ_VERSION, PRJ_PART, and the default build target. system_vivado.mk provides all build targets (bit, mcs, gui, sim, etc.) and the complete pipeline logic.

The shared_version.mk for this project:

export PRJ_VERSION = 0x02180000
target: prom
export PRJ_PART = XCKU040-FFVA1156-2-E
export USE_XVC_DEBUG = 1
ifndef RELEASE
export RELEASE = simple_10gbe_rudp_kcu105_example
endif

PRJ_PART identifies the target FPGA device (the Kintex UltraScale KU040 on the KCU105 board). PRJ_VERSION is a 32-bit firmware version number embedded in the output filename and in the bitstream. The target: prom line makes make prom the default target instead of make bit — for this project, the default build produces both a bitstream and a PROM programming file.

To understand the full list of variables that system_vivado.mk recognizes, see The Vivado Build Pipeline.

Understanding ruckus.tcl

The ruckus.tcl for this target (firmware/targets/Simple10GbeRudpKcu105Example/ruckus.tcl):

# 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]

Walk through each line:

  • source $::env(RUCKUS_PROC_TCL) — loads all ruckus procedures. This must appear first in every top-level ruckus.tcl. RUCKUS_PROC_TCL is set by system_vivado.mk and points to submodules/ruckus/vivado/proc.tcl.

  • VersionCheck 2023.1 — enforces the minimum Vivado version for this project. The build exits immediately with a non-zero status if the installed Vivado is older than 2023.1. Each project sets its own minimum; ruckus itself imposes no global minimum.

  • loadRuckusTcl $::env(TOP_DIR)/submodules/surf — loads the surf submodule’s ruckus.tcl, which recursively adds all of surf’s source files to the Vivado project.

  • loadRuckusTcl $::env(TOP_DIR)/shared — loads the shared library’s ruckus.tcl.

  • loadSource -dir "$::DIR_PATH/hdl" — adds all HDL files (VHDL, Verilog, SystemVerilog) in the hdl/ subdirectory of this target to the project. $::DIR_PATH is set by ruckus to the directory of the currently-executing ruckus.tcl, so this path always resolves correctly regardless of where the build was initiated. See The ruckus.tcl Recursive Loading Model for details.

  • loadConstraints -dir "$::DIR_PATH/hdl" — adds all XDC constraint files from the same directory. Constraint files sit alongside the RTL sources in this project.

  • loadSource -sim_only -dir "$::DIR_PATH/tb" — adds files in the tb/ directory as simulation-only sources. They are included in the simulation fileset but not the synthesis fileset.

  • set_property top {Simple10GbeRudpKcu105ExampleTb} [get_filesets sim_1] — sets the simulation top-level entity, which Vivado needs to know to run simulation.

Writing Your Own ruckus.tcl

For a minimal new project that has no submodule dependencies, the pattern is:

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

# Load source files from this module's hdl/ directory
loadSource      -dir "$::DIR_PATH/hdl"

# Load constraint files
loadConstraints -dir "$::DIR_PATH/xdc"

The $::DIR_PATH/ prefix is mandatory on every path argument to loadSource, loadConstraints, loadIpCore, and loadBlockDesign. Without it, the path is resolved against Vivado’s current working directory — the build output directory, not your module’s source directory — and the build will fail immediately with a directory-not-found error.

This is the most common mistake when writing a first ruckus.tcl. See The ruckus.tcl Recursive Loading Model for the full explanation of why $::DIR_PATH is necessary and how it works.

Running the Build

Navigate to the target directory:

cd firmware/targets/Simple10GbeRudpKcu105Example

Create the build directory (required before the first build):

make dir

This verifies that firmware/build/ exists and creates the per-project output directory inside it (firmware/build/Simple10GbeRudpKcu105Example/). If the build directory does not exist yet, make dir prints instructions:

Build directory missing!
You must create a build directory at the top level.

This directory can either be a normal directory:
   mkdir firmware/build

Or by creating a symbolic link to a directory on another disk:
   ln -s /scratch/disk/path firmware/build

Create the directory and re-run make dir:

mkdir firmware/build
make dir

On SLAC HPC systems with a /u1/ scratch disk, ruckus automatically creates /u1/$USER/build and symlinks firmware/build to it. On workstations, just mkdir firmware/build.

Run the full build:

make bit

This runs two Vivado invocations in sequence:

  1. Source setup — Vivado assembles the project by executing all ruckus.tcl files recursively. This creates the .xpr project file in the build directory.

  2. Build — Vivado runs synthesis, implementation, and bitstream generation.

A complete build takes 30-90 minutes depending on the design size and host machine. You will see Vivado log output scrolling in the terminal. The build is complete when the terminal prompt returns without error.

Interpreting the Output

When the build succeeds, output files appear in firmware/targets/Simple10GbeRudpKcu105Example/images/:

images/
└── Simple10GbeRudpKcu105Example-0x02180000-20240315143022-smith-a1b2c3d.bit

The filename encodes:

  • Project nameSimple10GbeRudpKcu105Example

  • Firmware version0x02180000 (from PRJ_VERSION in shared_version.mk)

  • Build timestamp20240315143022 (UTC, format YYYYMMDDHHMMSS)

  • Usernamesmith (the $USER shell variable at build time)

  • Git commit hasha1b2c3d (short hash of the HEAD commit)

If git shows uncommitted changes at build time, the git hash is replaced with dirty:

images/
└── Simple10GbeRudpKcu105Example-0x02180000-20240315143022-smith-dirty.bit

The dirty suffix is a signal that the bitstream was built from a modified working tree — it cannot be reproduced exactly from the git history. For reproducible builds, commit all changes before running make bit.

See Output Artifacts and Hook Scripts for the complete output artifact naming convention and the full list of file types generated (MCS, PDI, LTX debug probes, etc.).

Other Useful Make Targets

make gui       # Open Vivado GUI with the assembled project (without building)
make syn       # Run synthesis only
make sim       # Run simulation (XSIM)
make clean     # Delete the build output directory (keeps images/)
make test      # Print all resolved Makefile variables (useful for debugging)

make gui is particularly useful during development: it assembles the Vivado project from the ruckus.tcl declarations and then opens the Vivado GUI, so you can interactively explore the project, run synthesis, or modify constraints without leaving the GUI.

Next Steps

Now that you have a working build, explore the rest of the documentation:

  • The ruckus.tcl Recursive Loading Model — Deep explanation of $::DIR_PATH and the recursive loading model. Required reading before writing your own ruckus.tcl files.

  • The Vivado Build Pipeline — Full pipeline walkthrough showing every step from make bit to the images directory, including hook injection points.

  • Output Artifacts and Hook Scripts — Complete output artifact naming convention and the full list of file types generated per build.

  • Overview — Conceptual overview of what ruckus is, the problem it solves, and how it fits into a firmware repository.