6. Python API#

6.1. Required files#

Script Template

6.2. Introduction#

This tutorial presents the capabilities of SpaceStudio’s Python API, SpacePy. SpacePy defines a complete set of methods allowing the usage of SpaceStudio functionalities through Python scripts.

In this tutorial, the attendee will generate and modify a typical SpaceStudio project using SpacePy only. The step by step approach demonstrates useful API methods used in a common development workflow, from creating the project to performing a complete architecture implementation. Basic knowledge of Python is assumed.

The SpacePy methods are documented in the Python API Reference Guide, available in the SpaceStudio help contents.

Important

The EDA and the HLS tools used by the Architecture Implementation need to be installed and, if applicable, a valid license for them is required. In this tutorial, the HLS tool used is Vitis HLS, and the EDA tool used is Vivado, both developed by Xilinx. These tools require a license.

6.2.1. SpacePy usage#

A SpacePy script is launched with the SpaceStudio executable located inside the SpaceStudio installation folder. This is the same executable used to start the graphical user interface.

SpaceStudio --batch <python_script> <arguments>

Within the script, SpacePy API entry point is the spacepy.Engine object, which is obtained like so:

import spacepy as spy
ENGINE = spy.Engine.instance()

Note

The SpacePy methods are documented in the Python API Reference Guide (available in the SpaceStudio help contents). SpacePy is based on Python 3.12, so scripts must use Python 3 syntax.

6.2.2. Application#

The application used for this tutorial uses a producer-consumer structure, it consists of two user-defined modules:

  • producer: Continuously sends data to a consumer via ModuleWrite(). The repeating data sequence is defined in a separate file, added to the imports of the module.

  • consumer: Receives data from the producer via ModuleRead() and writes it in a BRAM via DeviceWrite(). The module stops execution after receiving a number of elements, defined by a module parameter.

../../_images/application_data_flow.png

Figure 6.7 Application data flow#

6.2.3. Project template#

The script tutorial6_template.py is provided as a starting point for the tutorial. Running it with SpacePy will generate the producer-consumer project and create the user defined modules. The steps of the script are described in the following sections.

6.2.3.1. Initialize the SpacePy engine#

import spacepy as spy
ENGINE = spy.Engine.instance()

6.2.3.2. Create the project#

project = ENGINE.create(project_name, project_path)

6.2.3.3. Create the solution#

solution = project.create_solution("producer_consumer")

6.2.3.4. Create and configure the producer and consumer modules#

producer = solution.create_module("producer")
consumer = solution.create_module("consumer")

The create_module() method creates the modules with template source files. These need to be replaced with the source files implementing the functionality of the modules:

shutil.copyfile("{}/src_files/producer.cpp".format(script_dir_path), producer.get_source_path())
shutil.copyfile("{}/src_files/producer.h".format(script_dir_path), producer.get_header_path())

shutil.copyfile("{}/src_files/consumer.cpp".format(script_dir_path), consumer.get_source_path())

Note

No declaration is needed in the consumer.h file, the base template can be left as is.

The producer data sequence is defined in separate files, these need to be added to the module imports:

producer.add_import("{}/src_files/producer_data.h".format(script_dir_path))
producer.add_import("{}/src_files/producer_data.c".format(script_dir_path))

The consumer module number of reads is defined using a module parameter. The add_parameter() method takes the name and default value of the parameter as arguments:

consumer.add_parameter("NUM_VALUES_TO_READ", "15")

6.2.3.5. Add application definitions#

When creating the solution, a template application_definitions.h file is generated where application-wide defintions can be added. In this case, the output BRAM offset used inside the consumer module is defined:

//File: application_definitions.h

#define OUTPUT_MEM_OFFSET 0x100 // Address of first write within the output_mem BRAM

The generated template file is therefore replaced by our modified application_definitions.h:

application_definition_path = "{}/solutions/producer_consumer/src/application/application_definitions.h".format(project_path)

shutil.copyfile("{}/src_files/application_definitions.h".format(script_dir_path), application_definition_path)

6.2.3.6. Save the project#

solution.save()
project.save()

6.3. Manipulations#

Three manipulations are presented, following a typical development workflow. Each manipulation uses a new script and expects the project to be in the state resulting from the previous manipulation script execution.

6.3.1. Create and simulate an architecture#

The goal of this first manipulation is to create and simulate an all-hardware architecture to verify the application behaviour. The reader will develop a SpacePy script to add this architecture to the template project generated by tutorial6_template.py. The relevant steps are described in the following sections.

6.3.1.1. Open the template project#

project = ENGINE.open(project_file_path)

Here, project_file_path is the path of .spacestudio file of the project that will be updated.

6.3.1.2. Create the new architecture in the existing solution#

solution = project.get_solution("producer_consumer")
all_hw_architecture = solution.create_architecture("all_hw")

6.3.1.3. Add a producer and a consumer instance to the architecture#

producer_instance = all_hw_architecture.create_module_instance("producer")
consumer_instance = all_hw_architecture.create_module_instance("consumer")

6.3.1.4. Configure the module instances#

When a module instance is created, it is given a default name and its parameters are set to their default values. The following methods are used to change those if needed:

producer_instance.set_name("producer0")
consumer_instance.set_name("consumer0")

consumer_instance.get_parameter("NUM_VALUES_TO_READ").set_value("20")

6.3.1.5. Create a BRAM component#

SpaceStudio components can be retrieved with their name using the get_component() method of the Spacepy Engine:

bram_instance = all_hw_architecture.create_component_instance(ENGINE.get_component("bram"))

Tip

The list of instantiable components can be obtained using the get_components() method.

6.3.1.6. Configure the BRAM#

bram_instance.set_name("output_mem")
bram_instance.get_parameter("axi_slave_mm.range").set_value("4KB")
bram_instance.get_parameter("axi_slave_mm.base_address").set_value("0x40001000")

Tip

The list of configurable parameters for a component instance can be obtained with the get_parameters() method.

6.3.1.7. Set the producer-consumer communication to a streaming channel#

for communication in all_hw_architecture.get_communications():
    if communication.get_writer().get_name() == "producer0":
        communication.set_type("streaming")

Tip

The list of possible communication types for a communication can be obtained with the get_possible_types() method.

6.3.1.8. Compile the architecture#

all_hw_compilation = all_hw_architecture.compile()

The compilation result is stored in a compilation object.

6.3.1.9. Simulate the compiled architecture#

In SpacePy, executable processes are represented by a Workflow object. A Workflow can be one of the following types: simulation, estimation, implementation and fpga-in-the-loop. To simulate the compiled architecture, create a simulation workflow with the get_workflow() method and execute it:

simulation_workflow = all_hw_architecture.get_workflow("simulation")
simulation_workflow.execute()

Simulation results will be displayed in the console where the script is executed.

6.3.1.10. Save the architecture#

all_hw_architecture.save()
project.save()

The script is now complete and ready to be executed.

6.3.2. Include a processor to an architecture#

After confirming that the producer-consumer application behaves as expected, it can be configured to run on a physical board. In this tutorial, the application will be run on a ZedBoard, which contains a Zynq 7000 SoC.

The goal of this manipulation is to show how to instanciate a SoC instance and move a user-instantiated module to run on its processor, in software. The reader will develop a SpacePy script to modify the SpaceStudio project obtained from the previous manipulation. The relevant steps are described in the following sections.

6.3.2.1. Create a new architecture based on the all hardware architecture#

These are the same steps as used for the all_hw architecture:

project = ENGINE.open(project_file_path)

solution = project.get_solution("producer_consumer")
zynq_architecture = solution.create_architecture("zynq")

producer_instance = zynq_architecture.create_module_instance("producer")
consumer_instance = zynq_architecture.create_module_instance("consumer")

producer_instance.set_name("producer0")
consumer_instance.set_name("consumer0")

consumer_instance.get_parameter("NUM_VALUES_TO_READ").set_value("20")

bram_instance = zynq_architecture.create_component_instance(ENGINE.get_component("bram"))

bram_instance.set_name("output_mem")
bram_instance.get_parameter("axi_slave_mm.range").set_value("4KB")
bram_instance.get_parameter("axi_slave_mm.base_address").set_value("0x40001000")

As the producer instance will be moved to software, available types for the producer to consumer communication will be different. The communication change from the all_hw architecture is therefore not included.

6.3.2.2. Instantiate the SoC component#

zynq_instance = zynq_architecture.create_component_instance(ENGINE.get_component("zynq"))
zynq_instance.set_name("zynq0")

6.3.2.3. Set the Zynq processor operating system#

In SpaceStudio, an operating system is linked to a processor core. Here, the operating system is set for the single core of the Zynq processor, which is included in the Zynq SoC:

zynq_apu = zynq_instance.get_component_instance("zynq_apu0")
core0 = zynq_apu.get_cores()[0]
core0.set_operating_system("linux")

Tip

The list of supported operating systems for a core can be obtained with the get_supported_operating_systems() method.

6.3.2.4. Move the producer instance to software#

producer_instance.move_to_software(core0)

The producer instance is now set to execute on the Zynq processor core.

6.3.2.5. Compile, simulate and save the architecture#

zynq_architecture.compile()
zynq_architecture.get_workflow("simulation").execute()

zynq_architecture.save()
project.save()

The script is now complete and ready to be executed.

6.3.3. Launch an architecture implementation#

When an architecture is complete and behaves as expected in simulation, an implementation can be launched to run it on the actual target board. In this tutorial, Xilinx Vivado 2025.2 will be used as the implementation tool.

The goal of this manipulation is to develop a SpacePy script to configure and launch the implementation of the zynq architecture obtained from the previous manipulation. The relevant steps are described in the following sections.

6.3.3.1. Retrieve the architecture#

project = ENGINE.open(project_file_path)
solution = project.get_solution("producer_consumer")
zynq_architecture = solution.get_architecture("zynq")

6.3.3.2. Define a implementation workflow object#

6.3.3.3. Set the implementation output directory#

impl_directory = "{}/tuto6_impl".format(os.path.expanduser("~"))
# "/home/<user>/tuto6_impl" on Linux, "C:\\Users\\<user>\\tuto6_impl" on Windows
impl_workflow.get_parameter("impl.general.directory").set_value(impl_directory)

6.3.3.4. Set the EDA tool used for implementation#

impl_tool = zynq_architecture.get_implementation("vivado_2025-2")
impl_workflow.get_parameter("impl.general.eda").set_value(impl_tool)

Tip

The list of available implementation tools for the architecture can be obtained with the get_implementations() method.

6.3.3.5. Get the implementation target board#

impl_board = impl_tool.get_board("zed")
impl_workflow.get_parameter("impl.general.board").set_value(impl_board)

Tip

The list of boards supported by the implementation tool can be obtained with the get_boards() method.

6.3.3.6. Set the hardware module(s) to synthesize and the HLS tool#

If the architecture contains hardware modules, those need to be synthesized by an high-level synthesis (HLS) tool to run on the SoC programmable logic. This is the case for the consumer module in this tutorial.

hw_module_instance = zynq_architecture.get_module_instance("consumer0")
impl_workflow.get_parameter("impl.hls.modules").set_value(hw_module_instance)

impl_hls = impl_tool.get_synthesizer("vitis_hls_2025-2")
impl_workflow.get_parameter("impl.hls.name").set_value(impl_hls)

Tip

The list of available HLS tools can be obtained with the get_synthesizers() method of the implementation tool object.

6.3.3.7. Set the number of threads used for implementation#

impl_n_threads = 8
impl_workflow.get_parameter("impl.general.thread").set_value(impl_n_threads)

6.3.3.8. Launch the architecture implementation and check if it succeeds#

impl_metrics = impl_workflow.execute()

The script is complete and ready to be executed, the results of the implementation will be generated in the impl_directory folder.

6.4. Result files#

SpaceStudio Project