Introduction
Taking a disciplined approach to managing an FPGA project may ever-so-slightly slow you down at the beginning stages, but it pays dividends through the life of the project. There is never just one released version of a professional FPGA project. Projects evolve over time, and your version-tracking scheme should support that. Tracking FPGA build and version information in a clear and repeatable way takes all the guesswork out of trying to remember if the bitstream that you currently have loaded is the one from yesterday or the one from today. If you have a separate software team writing code for your FPGA, they’ll thank you for including precise versioning data with each build.
Version Information to Track
- Semantic version number This number communicates how the FPGA’s user API has changed since the last release. In most cases, this will be with respect to the higher-level software API, meaning, if an FPGA change is made that is incompatible with the last version of the higher-level software, that that would be considered a breaking (major) change; for example, modifying the bit fields of an existing register such that software that used to work with the register would no longer function properly. A minor change would be adding a new feature from the higher level software’s perspective; for example, adding a new control or status register. A patch change would be fixing a firmware bug in a backwards-compatible way; for example, fixing a math error in an HDL module without touching the register interface. This number should be manually updated by the FPGA designer at the time of release.
- Build Date and Time Storing the build date and time as a software-accessible register is a great sanity-check.
- Git Hash Storing the git hash is another great sanity check. This number communicates the exact source code commit that was used to generate the build.
- Git Dirty Status A git repo is said to be “dirty” if it has uncommitted changes. If a build was generated with uncommitted changes, then we know that the reported git hash does not fully define the state of the source files at the time of the build. An official release build should never be built from a dirty repository, but sometimes it could make sense to generate a dirty build while doing quick engineering build iterations.
- Local or CI Build The build info should clearly show whether the build was generated on a CI server or on an engineer’s local PC. If you wanted to go one step further, you could include the PC hostname that was used to run the build.
- Engineering Development or Release Build This should communicate whether the bitstream is an official release build, intended to be fielded, or if it is an engineering development build.
- A Unique Device ID Number This helps identify the exact device that is being accessed. This number should be assigned at the start of the project and never touched again. If there are several design variants within the same project that target different board platforms, each of those variants should have their own unique device ID. I usually give development platform projects their own device ID as well. This helps to unambiguously define the exact combination of PCB / FPGA firmware.
A Reuseable Version Module
Since this version info is the same for essentially every FPGA design, it made sense to write a small reusable axi-lite version module to be dropped into the top level. Shown below is the entity definition for the standard version module. You can take a deeper look at it in the sblib-open project.
entity stdver_axil is
generic (
G_DEVICE_ID : std_logic_vector(31 downto 0) := x"DEAD_BEEF";
G_VER_MAJOR : natural range 0 to 255 := 0;
G_VER_MINOR : natural range 0 to 255 := 1;
G_VER_PATCH : natural range 0 to 255 := 0;
G_LOCAL_BUILD : boolean := true;
G_DEV_BUILD : boolean := true;
G_BUILD_DATE : std_logic_vector(31 downto 0) := x"DEAD_BEEF";
G_BUILD_TIME : std_logic_vector(23 downto 0) := x"DE_BEEF";
G_GIT_HASH : std_logic_vector(31 downto 0) := x"DEAD_BEEF";
G_GIT_DIRTY : boolean := true
);
port (
clk : in std_logic;
srst : in std_logic;
--
s_axil_req : in axil_req_t;
s_axil_rsp : out axil_rsp_t
);
end entity;
So its fairly straightforward - the module just provides an axi-lite access interface to the build-time settable values. As a small bonus, I also included a scratchpad register in stdver_axil, which can help with verifying register read/write access in a standardized way during initial board bring-up.
If you want to see an example design that includes stdver_axil, check out one of the top-level platforms from Shrikebyte’s template_fpga project.
Now that we’ve decided on the information that we want track with each build and now that we have a reuseable module to present that information to software, how do we ensure that this information is inserted into the design each time a new build is kicked off? That’s where the TCL build script comes in.
The Build Script
I primarily use Vivado, so that’s what this script was written for, but most of the TCL should be portable to other tools. This script also has some assumptions about where other files are based on the directory structure I usually use, so make any necessary changes to fit your project.
The date, time, git hash, git dirty status, and CI environment variables are determined by the script, but the version and device ID are passed in as input arguments because these are manually set by the user.
Here’s the article that initially inspired my stdver block. They take a slightly different approach, by setting their script as a pre-synthesis hook and dropping their block into IP Integrator, whereas I prefer using a standalone build script outside of the GUI and instantiating my block in RTL. Regardless of the particular details, I wanted to give credit to them for providing the datetime and git procedures.
# Current date, time, and seconds since epoch
# 0 = 4-digit year
# 1 = 2-digit year
# 2 = 2-digit month
# 3 = 2-digit day
# 4 = 2-digit hour
# 5 = 2-digit minute
# 6 = 2-digit second
# 7 = Epoch (seconds since 1970-01-01_00:00:00)
proc getDateTime {} {
# Array index 0 1 2 3 4 5 6 7
set datetime_arr [clock format [clock seconds] -format {%Y %y %m %d %H %M %S 00}]
# Example :
# 2020 20 05 27 13 45 45 00
# Get the datecode in the yyyy-mm-dd format
set datecode [lindex $datetime_arr 0][lindex $datetime_arr 2][lindex $datetime_arr 3]
# Get the timecode in the hh-mm-ss format
set timecode [lindex $datetime_arr 4][lindex $datetime_arr 5][lindex $datetime_arr 6]
# Show this in the log
puts "Build Date = $datecode"
puts "Build Time = $timecode"
return [ list $datecode $timecode ]
}
# Returns the git hash for this project
# Also determine if the repo is "dirty" (has uncommitted changes)
proc getGitHash {} {
set saved_dir [pwd]
cd [file dirname [info script]]
if { [catch {exec git rev-parse --short=8 HEAD}] } {
puts "#########################################################################"
puts "## WARNING: No git version control in $proj_dir directory"
puts "#########################################################################"
set git_hash DEADBEEF
set git_dirty 1
} else {
set git_hash [exec git rev-parse --short=8 HEAD]
set git_dirty [catch { exec git diff-index --quiet HEAD -- }]
}
if {$git_dirty} {
puts "#########################################################################"
puts "## WARNING: Build started with a dirty repository."
puts "#########################################################################"
} else {
puts "#########################################################################"
puts "## INFO: Build started with a clean repository"
puts "#########################################################################"
}
puts "Git Hash = $git_hash"
cd $saved_dir
return [ list $git_dirty $git_hash ]
}
# Figure out if this build has been initiated from a local engineer's PC or
# from a CI server
proc getLocalBuildStatus {} {
if { [info exists ::env(CI)] } {
set local_build 0
puts "#########################################################################"
puts "## INFO: This is a CI build"
puts "#########################################################################"
} else {
set local_build 1
puts "#########################################################################"
puts "## INFO: This is a local build"
puts "#########################################################################"
}
return $local_build
}
# Figure out if this is a release or development build. The CI server should set
# this env variable when a new tag is committed.
proc getDevBuildStatus {} {
if { [info exists ::env(FPGA_RELEASE_BUILD)] } {
set dev_build 0
puts "#########################################################################"
puts "## INFO: This is an official release build"
puts "#########################################################################"
} else {
set dev_build 1
puts "#########################################################################"
puts "## INFO: This is a development build"
puts "#########################################################################"
}
return $dev_build
}
################################################################################
# Setup variables
################################################################################
# Input arguments
if { $argc != 7 } {
puts "ERROR: Script usage - build.tcl <project_name> <platform_name> <device_id> <ver_maj> <ver_min> <ver_pat> <jobs>"
exit
}
set proj_name [lindex $argv 0]
set plat_name [lindex $argv 1]
set device_id [lindex $argv 2]
set ver_major [lindex $argv 3]
set ver_minor [lindex $argv 4]
set ver_patch [lindex $argv 5]
set num_cpus [lindex $argv 6]
# Script variables
set build_time_start [clock seconds]
set host_name [info hostname]
set script_dir [file dirname [info script]]
set build_date_time [getDateTime]
set build_date [lindex $build_date_time 0]
set build_time [lindex $build_date_time 1]
set git_dirty_hash [getGitHash]
set git_dirty [lindex $git_dirty_hash 0]
set git_hash [lindex $git_dirty_hash 1]
set local_build [getLocalBuildStatus]
set dev_build [getDevBuildStatus]
# Check validity of version info
puts "Major version: $ver_major"
puts "Minor version: $ver_minor"
puts "Patch version: $ver_patch"
set ver_string "v${ver_major}.${ver_minor}.${ver_patch}"
# Bounds checking on version numbers
foreach value [list $ver_major $ver_minor $ver_patch] {
if { $value < 0 || $value > 255 } {
puts "ERROR: version number out of range (0-255): $value"
exit 1
}
}
set proj_dir ${script_dir}/../build/vivado_out/${proj_name}_${plat_name}
################################################################################
# Run the build
################################################################################
puts "INFO: Starting build with $num_cpus jobs."
# Open the project if it's not already open
if {[catch current_project] != 0} {
open_project ${proj_dir}/${proj_name}_${plat_name}.xpr
}
set top_entity [lindex [find_top] 0]
# Create the build release directory
set build_name ${proj_name}_${ver_string}-${plat_name}
set release_dir [file normalize ${script_dir}/../build/${build_name}]
if {![file isdirectory $release_dir]} {
file mkdir $release_dir
}
puts "Build Directory = $release_dir"
# Set build-time generics
set_property generic " \
G_IS_SIM=1'b0 \
G_DEVICE_ID=32'h$device_id \
G_VER_MAJOR=$ver_major \
G_VER_MINOR=$ver_minor \
G_VER_PATCH=$ver_patch \
G_LOCAL_BUILD=1'b$local_build \
G_DEV_BUILD=1'b$dev_build \
G_GIT_DIRTY=1'b$git_dirty \
G_GIT_HASH=32'h$git_hash \
G_BUILD_DATE=32'h$build_date \
G_BUILD_TIME=24'h$build_time \
" [current_fileset]
# Synthesis
reset_runs synth_1
launch_runs synth_1 -jobs $num_cpus
wait_on_runs [get_runs synth_1] -quiet
# Exit failure if there was an error during synthesis
set synth_dir [get_property DIRECTORY [current_run -synthesis]]
if {[get_property STATUS [get_runs synth_1]] != "synth_design Complete!"} {
file copy -force ${synth_dir}/${top_entity}.vds ${release_dir}/${build_name}_synth.log
puts "ERROR: Synthesis FAILED. See ${release_dir}/${build_name}_synth.log."
exit 1
}
# Implementation
launch_runs impl_1 -to_step write_bitstream -jobs $num_cpus
wait_on_runs [get_runs impl_1] -quiet
# Exit failure if there was an error during implementation
set impl_dir [get_property DIRECTORY [current_run -implementation]]
if {[get_property STATUS [get_runs impl_1]] != "write_bitstream Complete!"} {
file copy -force ${synth_dir}/${top_entity}.vds ${release_dir}/${build_name}_synth.log
file copy -force ${impl_dir}/${top_entity}.vdi ${release_dir}/${build_name}_impl.log
puts "ERROR: Implementation FAILED. See ${release_dir}/${build_name}_impl.log."
exit 1
}
For the full version of this script in the context of a real project, see Shrikebyte’s template_fpga.
Now that the build script has been run and we have a deliverable bitstream ready to load, the next step would be to access the version registers from software.
Software Access
There isn’t really a standardized way to handle this - register-access software will be different from system to system. On a Zynq, you might access PL registers from the PS, but not always. You might access them from a Linux kernel driver or directly from Linux userspace or from a bare-metal no-os driver. You might access them from a host PC via PCIe or ethernet or UART or any other interface under the sun. Given the wide variety of potential access methods, I’ve provided a simple Stdver Python class for printing the version info. It gets initialized with external reg_rd and reg_wr functions, which would define how the FPGA’s registers are accessed for the specific implementation, and is likely different from platform to platform.
class Stdver:
def __init__(self, baseaddr, reg_rd, reg_wr):
self.reg_rd = reg_rd
self.reg_wr = reg_wr
self.SCRATCHPAD_ADDR = baseaddr + 0x00
self.ID_ADDR = baseaddr + 0x04
self.VERSION_ADDR = baseaddr + 0x08
self.DATE_ADDR = baseaddr + 0x0C
self.TIME_ADDR = baseaddr + 0x10
self.GITHASH_ADDR = baseaddr + 0x14
def get_scratchpad(self):
return self.reg_rd(self.SCRATCHPAD_ADDR)
def set_scratchpad(self, value):
self.reg_wr(self.SCRATCHPAD_ADDR, value)
def get_id(self):
return self.reg_rd(self.ID_ADDR)
def get_version(self):
raw = self.reg_rd(self.VERSION_ADDR)
return {
'patch': raw & 0xFF,
'minor': (raw >> 8) & 0xFF,
'major': (raw >> 16) & 0xFF,
'dev': (raw >> 29) & 0x1,
'local': (raw >> 30) & 0x1,
'dirty': (raw >> 31) & 0x1
}
def get_date(self):
raw = self.reg_rd(self.DATE_ADDR)
return {
'day': raw & 0xFF,
'month': (raw >> 8) & 0xFF,
'year': (raw >> 16) & 0xFFFF
}
def get_time(self):
raw = self.reg_rd(self.TIME_ADDR)
return {
'second': raw & 0xFF,
'minute': (raw >> 8) & 0xFF,
'hour': (raw >> 16) & 0xFF
}
def get_githash(self):
return self.reg_rd(self.GITHASH_ADDR)
def print_summary(self):
version = self.get_version()
date = self.get_date()
time = self.get_time()
print("---==== FPGA Build Info ====---")
print("Device ID : 0x{:08X}".format(self.get_id()))
print("Version : {}.{}.{}{}{}{}".format(
version['major'],
version['minor'],
version['patch'],
"-dev" if version['dev'] else "",
"-local" if version['local'] else "",
"-dirty" if version['dirty'] else ""
))
print("Build Date: {:04X}-{:02X}-{:02X}".format(date['year'], date['month'], date['day']))
print("Build Time: {:02X}:{:02X}:{:02X}".format(time['hour'], time['minute'], time['second']))
print("Git Hash : 0x{:08X}".format(self.get_githash()))
Calling print_summary() prints a nice little visual summary of the build’s properties:
---==== FPGA Build Info ====---
Device ID : 0x00000017
Version : 1.0.4-dev-local
Build Date: 2025-11-22
Build Time: 10:38:16
Git Hash : 0x7e609f0c1
Conclusion
That wraps up my strategy for keeping track of crucial version info in FPGA projects. Preparing to track this early on in the life of a new project is well worth the upfront investment, and after you get through the work of doing it the first time, it can easily be reused over and over again across projects.