Integration with hdl-registers

The tsfpga module and source code handling is tightly integrated with its sister project hdl-registers (https://hdl-registers.com, https://github.com/hdl-registers/hdl-registers), a register code generator. To use it simply create a file regs_<name>.toml in the root of a module (see module structure). It is fast enough that before each build and each simulation run, the modules will re-generate their VHDL register artifacts making them always up-to-date. Creating documentation and headers, which are typically distributed as part of FPGA release artifacts, is simple and easy to integrate in a build script.

Releases to PyPI of tsfpga list hdl-registers as a dependency, so it will be installed as well.

Example usage in tsfpga

The tsfpga/examples/modules/ddr_buffer example module is heavily reliant on generated register information, both in the implementation and testbench.

Default registers

A lot of projects use a few default registers in standard locations that shall be present in all modules. For example, very commonly the first register of a module is an interrupt status register and the second one is an interrupt mask. In order to achieve this, without having to duplicate names and descriptions in many places, there is a default_registers flag to the get_modules() function. Passing a list of hdl_registers.register.Register objects will insert them in the register list of all modules that use registers.

Manipulating registers from Python

The ddr_buffer example module also showcases how to manipulate registers from Python via tsfpga’s module system. This method for manipulating registers can be very useful for information that is known in the Python realm, but is not convenient to add to the TOML file.

module_ddr_buffer.py
from tsfpga.module import BaseModule


class Module(BaseModule):
    version = 3

    def registers_hook(self) -> None:
        # Should have some registers already from the TOML file.
        register_list = self.registers
        assert register_list is not None

        register_list.add_constant(
            "version", self.version, f"Version number for the {self.name} module."
        )
        register_list.get_register("version").get_field("version").default_value = self.version

Using BaseModule.registers_hook() we add a constant as well as a read-only register for the module’s version number. The idea behind this example is that a software that uses this module will read the version register and compare to the static constant that shows up in the header. This will make sure that the software is running against the correct FPGA with expected module version.

Choosing what artifacts to generate

Per default, the module will generate all register VHDL artifacts. Which includes register packages, AXI-Lite register file wrapper, and simulation support packages. The easiest way to disable either of these is to set up a module_foo.py in the root of your module and disable either of the class variables below. They all have default value True in BaseModule.

Build information for traceability

It is quite common to want to include build information in the FPGA bitstream. Such as what git commit SHA this FPGA was built from, etc. An example of this is available in the repository.

The artyz7 example module is the top module of an example Vivado project. It has a build_id register which has its value assigned via a generic . The example Vivado project class assigns a random value to this generic for each project that is built or created. Apart from that, it sets register constants with all the build trace information before each build. After a successful build, this information is available in the generated C/C++ header file:

Excerpt of i_artyz7.h with build information.
 1  class IArtyz7
 2  {
 3  public:
 4    // Register constant.
 5    static const int expected_build_id = 11311432;
 6    // Register constant.
 7    static constexpr auto build_project_name = "artyz7";
 8    // Register constant.
 9    static constexpr auto build_generics = "build_id=11311432, num_lanes=2";
10    // Register constant.
11    static constexpr auto build_vivado_version = "2025.1";
12    // Register constant.
13    static constexpr auto build_git_commit = "fd1631dfb1f7";
14    // Register constant.
15    static constexpr auto build_time = "2025-09-17 20:53:00";
16    // Register constant.
17    static constexpr auto build_hostname = "runnervmf4ws1";
18    // Register constant.
19    static constexpr auto build_operating_system = "Linux";
20    // Register constant.
21    static constexpr auto build_operating_system_info = "#18~24.04.1-Ubuntu SMP Sat Jun 28 04:46:03 UTC 2025 Linux-6.11.0-1018-azure-x86_64-with-glibc2.39";

HTML documentation.

The embedded software can check that the build_id of the register artifact matches the build ID it reads from the register in the FPGA. If they do not match, it indicates a build flow error, and that the embedded software and FPGA might be out of sync. If it does match, it can proceed to print the git SHA, date, etc.

Note that there are many other ways of achieving this. The example approach shown here has the advantage of very minimal resource utilization, and the fact that multiple builds can be run in parallel without interfering with each other in the file system.