Module structure

Source code management in tsfpga is centered around modules. This page describes how modules must be structured in the file system in order to use all available functions.

Some functions in tsfpga require that modules use a certain folder structure. For example, if we want to set up local test configurations we must use a file called exactly module_<name>.py in the root of the module. Additionally the get_modules() function in tsfpga, which creates module objects from a source tree, will look for source files only in certain sub-directories.

Below is a recommended folder structure. The different files and folders are explained further down.

├── foo
│   ├──
│   ├── regs_foo.toml
│   ├── ip_cores
│   │   ├── fifo.tcl
│   │   └── ...
│   ├── scoped_constraints
│   │   ├── sample_data.tcl
│   │   └── ...
│   ├── src
│   │   ├── foo_top.vhd
│   │   ├── foo_pkg.vhd
│   │   ├── sample_data.vhd
│   │   └── ...
│   └── test
│       ├── tb_foo_top.vhd
│       └── ...
├── bar
│   └── ...
└── ...

At the top level there is a folder called modules that contains all available modules. It does not have to be named that, but it is a name that fits well with the tsfpga nomenclature. Within this folder there are source code modules: foo, bar, etc.

Sources and testbenches

Source code and packages are recommended to be placed in the src folder. There is no distinction made between entity source files and packages in tsfpga. The corresponding test benches are recommended to use the test folder.

We don’t have to use these exact folders; BaseModule will look for files in many folders, to accommodate for different projects using different structures. For example, at the moment BaseModule.get_synthesis_files() will look for source files in

  • src

  • rtl

  • hdl/rtl

  • hdl/package


If your project uses a different folder structure, and is locked into using it, tsfpga can be updated to accommodate that as well. This goes for most of the folders within the module, described below. Feel free to create an issue or a pull request.

If we want to, e.g., set up FPGA build projects or do local test configurations we can use a file called module_<name>.py. The Python file shall contain a class definition called Module that inherits from BaseModule. Methods from BaseModule can then be overridden to achieve the desired behavior.

Extra files

An FPGA build project might need a lot extra files, such as TCL scripts for pinning, block design, etc. Or maybe some simulations need data files stored on disk. It is perfectly valid to create other folders within the module, e.g. tcl or test/data, and place files there. Extra folders like these can be used freely and will not have any significance to tsfpga.


The file regs_<module_name>.toml, if it exists, will be parsed with the hdl-registers register generator. It contains the registers that the module uses and the fields within those registers.

Per default, the module will generate all register VHDL artifacts. Which includes register packages, AXI-Lite register file wrapper, and simulation support packages. If want only a subset of these to be created, you can achieve that by creating a, see Choosing what artifacts to generate for details.

IP cores

In tsfpga, IP cores are handled using TCL files that contain the code snippet that generates the core. This TCL snippet can be found in the Vivado TCL console when creating or modifying the IP. It typically looks something like this:

Example TCL that creates an IP core
create_ip -vlnv "" -module_name "mult_u12_u5"
set_property -dict [list \
  CONFIG.PortAType "Unsigned" \
  CONFIG.PortAWidth "12" \
  CONFIG.PortBType "Unsigned" \
  CONFIG.PortBWidth "5" \
  CONFIG.OutputWidthHigh "16" \
] [get_ips "mult_u12_u5"]

These TCL files shall be place in the ip_cores folder within the module. The IP cores will be included in all build projects that include the module, and in the simulation project.

Using small TCL snippets like this is preferred to using the .xci file generated by Vivado, especially when it comes to version control. The .xci file is very large and contains much extraneous information, that tends to be updated depending on what computer and Vivado version you’re using. The .tcl file on the other hand contains only the few settings that are needed.

Another advantage of using TCL is that it is a full-fledged scripting language. We can use variables, loops and if/else branches to parameterize our IP core creation. In the TCL snippet above for example, the IP core name or some of the property values could be replaced by variables.

To find the settings of an existing IP, the following TCL code can be executed with an existing design open:

Finding the non-default settings of all IP cores.
foreach ip [get_ips] {
  puts "\n\n";

  set ipdef [get_property IPDEF ${ip}]
  puts "create_ip -vlnv ${ipdef} -module_name ${ip}"

  puts "set_property -dict \[list \\"
  foreach property [list_property ${ip} -regexp {^CONFIG\.\w+$}] {
    if {[get_property ${property}.value_src ${ip}] == "USER"} {
      set property_value [get_property $property ${ip}]
      puts "  ${property} \"${property_value}\" \\"
  puts "\] \[get_ips ${ip}\]"

Alternatively, the Vivado command write_ip_tcl [get_ips <name>] can be used and the generated .tcl file inspected. However the TCL format that write_ip_tcl produces does not allow variable usage.

Scoped constraints

Scoped constraints are constraint files that are applied in Vivado relative to a certain entity. This is handled in build projects using the Constraint class. Constraint files in the scoped_constraints directory will be automatically added to build projects as scoped constraints.

The name of a scoped constraint file must be the same as the entity name and source file name. In the example tree above there is a scoped constraint file sample_data.tcl that will be applied to sample_data.vhd, which presumably contains an entity called sample_data.