When handling FPGA bitstreams it would be nice to have a way to extract build and version information from them, especially when working with hardware-software designs.

After generating a few different versions of a bitstream for a project it can be easy to lose track, especially when the design changes are not just incremental but alternative, e.g., to compare performance between possible implementations.

In this post we will look at how to add build and version information to an FPGA design. Then we will look at how to implement a Linux device driver to keep track of this information through the file system.

Adding version information to a design can be as simple as having a register somewhere with a manually set value, accessible through JTAG or from a CPU.

The example below does a bit more and is targeted to a Zynq system but the ideas would also work for other targets (e.g., Intel FPGA devices).

Source code for this post is available here : https://github.com/rick-heig/xilinx_version_ip

Version IP

I created a simple Version IP with the Xilinx IP Packager. The IP is nothing more than an AXI-Lite slave that can be memory mapped and exposes 4 registers on the memory map. 

The IP is available on github.

The IP exposes the values passed as inputs, date, time, hash, and version on the AXI bus. The values can either be directly set as constants (e.g., version_in) or made available via external ports. For the external ports, they are initialized in an RTL file (e.g., Verilog file) that wraps the block design.

The first reason to use this IP is simply because it is easier to connect a single wire to the AXI interconnect than it is to wire everything by hand in RTL (okay, it would not be that bad with an AXI modport in SystemVerilog).

The second reason is because the block diagram address mapping will automatically generate a device tree entry when the hardware is exported to PetaLinux. This device tree entry can then be used to automatically load a Linux device driver at startup and create file system entries for easy reading of the values (okay, a devmem command also works).

A complete system could look like this :

Setting the values in the wrapper

The wrapper file uses generic parameters to set the values. (This example is based on a Verilog example but also works with a VHDL wrapper).

These values are set to default values for simulation (or co-simulation) purposes and are connected to the block design wrapper inputs.

This is enough to manually set values, but we will do better, we will see how to automate the generation of these values.

Getting the build date, time, and version control info

First, credit goes to this original post : https://forums.xilinx.com/t5/Vivado-TCL-Community/Vivado-TCL-set-generics-based-on-date-git-hash/td-p/426838 that showed how to get the date and set generics in TCL.

I updated this original script in order to generate the values I needed and also make it compatible with non git version controlled projects.

The updated script contains the following :

# Original Idea credit goes to nolaega and a_bert
# Original link : https://forums.xilinx.com/t5/Vivado-TCL-Community/Vivado-TCL-set-generics-based-on-date-git-hash/td-p/426838
# Wayback Machine archive : https://web.archive.org/web/20200417104004/https://forums.xilinx.com/t5/Vivado-TCL-Community/Vivado-TCL-set-generics-based-on-date-git-hash/td-p/426838
# The original script has been updated by Rick Wertenbroek to the version below.

# 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)
# 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-00 format
set timecode [lindex $datetime_arr 4][lindex $datetime_arr 5][lindex $datetime_arr 6][lindex $datetime_arr 7]
# Show this in the log
puts DATECODE=$datecode
puts TIMECODE=$timecode
# Get the git hashtag for this project
set curr_dir [pwd]
set proj_dir [get_property DIRECTORY [current_project]]
cd $proj_dir

if { [catch {exec git rev-parse --short=8 HEAD}] } {
    puts "No git version control in the $proj_dir directory"
    set git_hash 00000000
} else {
    set git_hash [exec git rev-parse --short=8 HEAD]
# Show this in the log
puts HASHCODE=$git_hash
# Update the generics
set initial_generics [get_property generic [current_fileset]]
set_property generic "$initial_generics G_DATE_CODE=32'h$datecode G_TIME_CODE=32'h$timecode G_HASH_CODE=32'h$git_hash" [current_fileset]

It will set the G_DATE_CODE value to YYYYMMDD in hexadecimal. And the G_TIME_CODE value to HHMMSS00 as well as set the G_HASH_CODE from the current git commit (HEAD).

The script (revision.tcl) is bound to be launched before synthesis of the project by setting the project synthesis parameters in Vivado as follows :

This means the script will be invoked automatically prior to a synthesis run. The synthesis log shows the script has been run and the values are shown in the log. This way we always get up to date values for each run.

The final command of the script allows to overwrite the generic parameters we set in the wrapper Verilog file :

set_property generic "$initial_generics G_DATE_CODE=32'h$datecode G_TIME_CODE=32'h$timecode G_HASH_CODE=32'h$git_hash" [current_fileset]

Note 1: The reason the script is not packaged with the IP is because the IPs can get synthesized out of context and would not necessarily reflect the current build date and time. Also the IP repository may not be in the same git repository than the project.

This script will set generic (parameter) values so it can be used in any RTL design not only through this IP in a Zynq design. So if you use a completely other bus (e.g., Wishbone) or processing system (e.g., RISC-V) you can simply adapt your RTL to expose theses values through your specific bus to your processing system.

Note 2 : This TCL script can be updated and used with Intel products. For sure the final line with “set_property generic” must be updated using “set_parameter”, other changes may also be needed, please refer to the Quartus II Scripting Reference Manual and Quartus Prime Scripting Manual

set_parameter [-comment <comment>] [-disable] [-entity <entity_name>] -name <name>
[-remove] [-tag <data>] [-to <destination>] <value>

Reading the values from the CPU

The IP is connected to a Zynq ARM CPU and once the bitstream has been generated and loaded it is already enough the get the values over AXI.

The address editor shows us where the IP is mapped and these registers can be accessed either through a bare-metal application with e.g., the Xil_In32 function.

static inline u32 Xil_In32(UINTPTR Addr)
    return *(volatile u32 *) Addr;

or in Linux, e.g., through a devmem command

A simple devmem command allows us to read the four registers and is enough to find out that the project has been built on 2020 05 27 (YYYY-MM-DD) at 15 h 46 min and 11 sec.

The hash can be used to retrieve the specific commit at which the project was built :

Linux Device Driver with File System Attributes

The advantage of having the IP in the block diagram and exporting the hardware from Vivado to PetaLinux is that we now have a device tree entry for the IP with our Linux Kernel.

version_ip@43c00000 {
	clock-names = "s_axi_aclk";
	clocks = <0x1 0xf>;
	compatible = "xlnx,version-ip-1.0";
	reg = <0x43c00000 0x10000>;

This can be used to automatically create a device with sysfs attributes when the kernel reads the device tree and encounters the version_ip using the “compatible” string. The “reg” attribute is then used to automatically get the actual address of the version_ip.

This removes the need for the software engineer(s) to know the memory map, it will be deduced from the device tree generated from the hardware project.


I wrote a driver to add the entries in the file system. The driver is available here.

On startup if a compatible entry is found in the device tree the device driver will be loaded and will show the values read from the FPGA.

This driver will instantiate four read-only attributes in the sysfs and these files can be used to retrieve the values.

The device entry is created under /sys/bus/platform/devices/ . The name is taken from the device tree entry (and therefore also contains the address).

The attribute files can be read from there in order to obtain the values.

This also works if the bitstream is updated e.g., through JTAG or /dev/xdevcfg , and unless the memory map changed, reading the files will reflect the new values of the programmed bitstream.


We now have a simple way to check the build date and time, as well as the version information of the current bitstream that is loaded. This can be very helpful to make sure the correct bitstream is currently loaded.

This is not only true for Zynq designs but any design can benefit from this. For example this would also work for a Microblaze design, another use case is for accelerator designs, the version values could be read by the host over PCIe through AXI e.g., in an Amazon F1 project (Although they already provide version info through their AFI).

The general idea is to provide this information where it could be useful and make the generation automated so that it is always up to date.

The build time and date or hash (git revision tag) allows the software and hardware developers to know which version of the bitstream they currently have loaded and this simple addition can save them from a lot of headaches.

Appendix A : Steps to add the driver to a PetaLinux project

At the root directory of the petalinux project type :

$ petalinux-create -t modules --name plversion --enable

This will create a directory name plversion


Replace the file


by the file provided here : https://github.com/rick-heig/xilinx_version_ip/blob/master/linux_driver/plversion.c

You will need to update the PetaLinux project with the new hardware file exported by Vivado (after including the Version IP). This will update the device tree

$ petalinux-config --get-hw-description=../path/to/directory/holding/exported/xsa_file.xsa

And build the project with

$ petalinux-build

Note : The device tree could also be updated manually by adding an entry with the correct “compatible” string (see possibilities in the driver file). The driver (kernel module) can also be compiled for non PetaLinux projects (e.g,, buildroot) you just have to create a makefile to build it with the correct toolchain.