6. Implementing Hardware Component#
6.1. Introduction#
SpaceStudio takes an executable high-level specification of a system’s functionalities and architecture and generates a hardware and software implementation that can be run on FPGA. This can be done by using the Architecture Implementation feature part of the SpaceStudio environment. This document explains how the user -either directly or through an automated process- defines the implementation of modules and devices mapped to hardware during Architecture Implementation. In many ways, this document is complementary to SpaceStudio’s Architecture Implementation User’s Guide and does not repeat its content.
- There are two ways of defining the implementation for hardware module/device components:
By using a behavioral high-level synthesis (HLS) tool to automatically transform the C/C++ code of the module into an implementation. This is described in High-Level Synthesis (HLS).
By manually providing HDL code (i.e., VHDL, Verilog). This is described in Manual Instance Implementation, and is currently the only supported implementation for device components.
Note
Architecture Implementation automatically handles the synthesis of software modules. Hence, this document does not apply to them.
6.2. High-Level Synthesis (HLS)#
High-level synthesis (HLS) tools automatically transform a module instance into a hardware implementation. This section explains their usage in SpaceStudio and gives insights on allowed C/C++ constructs.
6.2.1. Supported HLS tools#
SpaceStudio supports the following High-Level Synthesis (HLS) tools:
Vitis HLS 2025.2
Siemens Catapult HLS 2022.2
6.2.2. Enabling and Configuring#
HLS tools are enabled from SpaceStudio’s preferences. Before enabling an HLS tool, make sure it is a version supported by SpaceStudio and is correctly installed. To enable the HLS tool:
Click Tools
From the submenu, click Preferences…
From the left pane, expand the following nodes: SpaceStudio → EDA
Select the relevant EDA (for example, Xilinx – Vivado 2018.3)
Check the EDA is enabled and HLS tool is enabled checkboxes and configure the required preferences (e.g. installation path, associated EDA, …). This is depicted in Figure 6.1.
Figure 6.1 Enabling an HLS tool#
6.2.3. Invoking HLS on Instances#
To invoke the HLS tool during the Architecture Implementation workflow, on the Architecture Implementation export window that appears at the beginning of Architecture Implementation, check the checkboxes of the hardware instances to synthesize, as shown in Figure 6.2. Note that the bottom of the Figure 6.2 (“High-level synthesis” and “Modules instances to synthesize”) appear only when there is at least one module instance mapped to hardware in the architecture.
Figure 6.2 Architecture Implementation export#
SpaceStudio will automatically invoke the HLS tool on the chosen instances and integrate the result into the system. Should the HLS tool report errors, they will be printed back in SpaceStudio’s console.
6.2.4. Limitations and Guidelines of High-Level Synthesizers#
C/C++/SystemC HLS tools allow software developers to implement hardware code and allow making iterations on designs significantly easier and faster. However, there are inherent differences between C/C++ and a real hardware implementation [1]. As a result, the HLS tools stop with an error on unsupported scenarios or make up for these differences by attempting to automatically determine the context of the C/C++ statements [2].
Doing so is not always possible however, which can cause the underlying HLS tool to stop with an error or sometimes even generate improper hardware code despite the input C/C++ code being functionally correct. In such scenarios, the HLS tool’s warning and error messages (which are displayed in SpaceStudio’s console) may be of help. While every HLS tool has its own capabilities, in general a fully synthesizable module:
- Can use C/C++ integer, floating-point and boolean primitive data types
char,short,int,float,double,long,bool,enum, etc.
Can use arrays of such data types
Can use data structures (
struct) composed of the above types or, recursively, of sub-data structures (structwithinstruct) of these typesMay support variables holding pointers to above types, although usage of such pointers is discouraged unless trivial use-case
May support certain basic C/C++ APIs (such as
math.hfunctions); such support is currently delegated to the underlying HLS tool, and is not implemented by SpaceStudio directly- Cannot use dynamic memory allocation
No
new,malloc, etc…
- Cannot use dynamic typing
No
dynamic_cast,reinterpret_cast, etc…
For extensive HLS coding guidelines :
Vitis HLS : Vitis HLS Coding Guidelines
Siemens Catapult HLS : Catapult HLS Coding Guidelines
While SpaceStudio’s API is no exception to these problems, it informs (via a warning message) and/or enforces (via an error) the user to write code in such a way that the context can be determined by SpaceStudio and the underlying HLS tool, as in the error below with Vivado HLS:
[1/1] ERROR: The data buffer argument of a DeviceRead contains a cast of an array/pointer expression.
SpaceStudio disallows this because the communication functions need to know the true type of the array/pointer they are using.
NOTE: Here: DeviceRead(ZYNQ_DDR0_ID, IMG_IN_IFRANGE_OFFSET, (uint32_t*)imgin, IMGSZ)
6.3. Manual Instance Implementation#
This section defines a hardware module or device instance’s interconnect interfaces with the goal of allowing the end-user to implement them manually. This can, for example, be useful in a context where precise control over the module/device instance’s implementation is desired, or when an existing hardware IP implementation must be used on a physical board with SpaceStudio.
The instances interfaces are determined from the user code’s usage of SpaceStudio’s API, however note that this document does not explain how to use this API in C/C++ code. Refer to the Introduction to System Design manual for that.
6.3.1. Manual Instance Implementation Workflow#
The overall workflow of manual instance implementation is as follows:
The user requests a module instance to be manually implemented by unchecking the corresponding checkbox on the Architecture Implementation export window, which is depicted in Figure 6.2 . Currently, Device instances need to be implemented manually in all cases, but can be altogether ignored during Architecture Implementation by setting its “Only used for simulation” flag to true, as described in the Architecture Implementation User Guide.
For each module/device instance, SpaceStudio determines its communication interfaces, and eventually creates a template VHDL file with these interfaces.
The user provides the implementation of each instance in the VHDL file, potentially adding other VHDL/Verilog files along with them.
The user completes the Architecture Implementation process.
The below sections focus on steps 2 and 3. Step 4 is the subject of the Architecture Implementation User Guide.
6.3.2. Communication Interface Inference#
A user requests communication interfaces by using SpaceStudio’s communication API, except for devices, which additionally always have a slave memory-mapped interface. Just like in simulation or in implementation with an HLS tool, SpaceStudio will first analyze the user’s C/C++ code, searching for communication functions, to determine which interfaces the hardware instances need. The number of interfaces generated will be explained in each of the corresponding sections.
Table 6.1 describes which case generates which interface kind. This allows to know what interfaces to expect for a given module/device instance.
Case |
Type of generated interface |
Described in |
|---|---|---|
Call to |
Streaming |
|
Call to |
Register |
|
Call to |
Memory-Mapped (master) |
|
Device component |
Memory-Mapped (slave) |
|
Call to |
N/A [3] |
N/A |
6.3.3. Template Hardware File to Complete#
Once the communication interfaces of all needed instances are determined, eventually Architecture Implementation produces a template VHDL file for each instance to be manually implemented and will stop to let the user complete these files. They will be created at:
${arch_impl_dir}/application_repository/core/${component}/src/${component}.vhd
Where ${arch_impl_dir} is the directory chosen by the user in the Architecture Implementation export window and ${component} is the name of the instance in question.
The purpose of a hardware IP is normally to react to and drive its interfaces. The below sections focus on how the interfaces’ definition.
6.3.4. Global Ports#
In addition to an instance’s communication interfaces, the signals given in Table 6.2 are always generated in the instance’s template VHDL file.
PORT NAME |
DIRECTION |
NOTES |
|---|---|---|
|
|
|
|
|
Active-high |
6.3.5. Streaming Interfaces#
In our context, a streaming interface is a hardware interface which has no concept of specifying a certain address or offset; data is merely pushed from one hardware location and pulled at another.
Calls to SpaceStudio’s ModuleRead/ModuleWrite API, cause SpaceStudio to generate a streaming interface in the instance’s template VHDL file. This includes all cases described in “Introduction to System Design” document:
the writing instance writes to a FIFO, which in turn is read from the reading instance
the software instance writes to the hardware instance via a DMA
the writer and reader perform a direct-streaming communication.
One streaming read interface is generated per module that calls ModuleRead, and one streaming write interface is generated per module that calls ModuleWrite. A streaming interface is generated for the triplet (source_id, destination_id, channel_width). The channel_width is the width of the communication. For both source and destination module mapped in hardware, the channel_width is the size of the communication data type. For a software-mapped module, the channel_width is the interconnect data width.
6.3.5.1. AXI4-Stream Interface Signals#
Currently, SpaceStudio only supports the ubiquitous AXI4-Stream interface, as streaming interface. More specifically, SpaceStudio generates the signals described in Table 6.3 and Table 6.4 in its template VHDL file. The actual port names used in the VHDL file should be easily identifiable given the names given in Table 6.3 and Table 6.4. Refer to the AXI4-Stream specification, section “Signal List”, for a description of these signals.
Tip
To distinguish each generated AXI4-Stream interface, a comment above each such interface (in the template VHDL file) will explain the purpose of that interface.
PORT NAME |
DIRECTION |
NOTES |
|---|---|---|
|
|
|
|
|
Unused, present for future use. Should be set to |
|
|
To avoid deadlocks, if there is data to send, the implementation must eventually assert this signal, and do so until |
|
|
PORT NAME |
DIRECTION |
NOTES |
|---|---|---|
|
|
|
|
|
Unused, present for future use. |
|
|
|
|
|
Waiting until |
6.3.6. Register-Based Interfaces#
Calls to SpaceStudio’s RegisterRead/RegisterWrite API cause SpaceStudio to generate a register-based interface in the instance’s template VHDL file. A register-based communication uses SpaceStudio’s register_file component, which is a table of 32-bit integer registers sharable between modules (either hardware or software).
For each (register_file_id, register_id) pair written to via RegisterWrite, the hardware instance will have a write interface to that register. Similarly, for each (register_file_id, register_id) pair read from to via RegisterRead, the hardware instance will have a read interface from that register.
6.3.6.1. Register Read/Write Interface Signals#
Figure 6.3 Module with one Register Read and Register Write interfaces#
PORT NAME |
DIRECTION |
NOTES |
|---|---|---|
|
|
State/value of the register. |
PORT NAME |
DIRECTION |
NOTES |
|---|---|---|
|
|
Data to be written in the register. |
|
|
Asserting this signal causes the data to be written in the register at the next clock rising edge. |
6.3.6.1.1. Write Operation#
Registers behave as a vector of 32 D flip-flops. Each write operation overwrites the contents of the register with the new data. When the register_write_enable is asserted, the data is written into the register from the register_write_data signal. Writing into a register is always successful.
Figure 6.4 represents a module with a register write interface.
Figure 6.4 Register write operation#
6.3.6.1.2. Read Operation#
Registers behave as a vector of 32 D flip-flops which always return their current state. Reading from a register is always successful.
Figure 6.5 represents a module with a register read interface. The value on the register_read_data signal varies in time depending on the content of that register.
Figure 6.5 Register read operation#
6.3.7. Memory-Mapped Interfaces#
In our context, a memory-mapped interface is a hardware interface where transactions contain both data and an address or offset with a slave memory-mapped device (such as a DDR, a BRAM, a custom Device instance, etc.). The template VHDL file to complete may contain two different kinds of memory-mapped interfaces: master and slave memory-mapped interfaces.
Calls to SpaceStudio’s DeviceRead/DeviceWrite API, which can be done both from modules and Device instances, cause SpaceStudio to generate a master memory-mapped interface in the instance’s template VHDL file and connect that interface to the call’s target device. For a module, multiple master interfaces can be generated if the module communicates to multiple different devices (e.g., two separate BRAM components), however SpaceStudio may find that several of them can be reached with a single master interface (such as when multiple of them are reachable via an interconnect hierarchy), in which case SpaceStudio would use a single master interface for these devices.
A device always has a single slave memory-mapped interface. Modules never have a slave memory-mapped interface. Instead, modules rely on Register-based communication.
As a note, it is recommended to set fixed address ranges for devices to make sure Architecture Implementation maps the device to the expected address range. As shown in :Figure 6.6, in the SpaceStudio GUI, this can be done by clicking the device in the diagram, then going to the Properties tab, and then clicking the lock button in the Parameter page so that the icon is in the locked position. Note that this applies both to Device instances, and to other kinds of devices, such as BRAMs.
Figure 6.6 Fixing Device Address#
6.3.7.1. AXI4 Interface Signals#
Certain AXI4 transaction features are either not used by target devices, or certain values fit many common situations. More specifically, the signals described in Table 6.7, when present, should be safe to write trivially when outgoing, or assumed to given value when incoming. These values are, in part, taken from the AXI specification, section “A10.3 - Default signal values”.
PORT NAME |
SET BY |
VALUE |
|---|---|---|
|
Master |
All zeros |
|
Master |
|
|
Master |
|
|
Master |
|
|
Master |
|
|
Master |
All zeros: not participating in QoS scheme |
|
Master |
All zeros |
|
Master |
Any value |
|
Master |
All ones: all bytes used |
|
Master |
Any value |
|
Slave |
Assigned to incoming |
|
Slave |
Any value |
|
Master |
All zeros |
|
Master |
|
|
Master |
|
|
Master |
|
|
Master |
|
|
Master |
All zeros: not participating in QoS scheme |
|
Master |
All zeros |
|
Master |
Any value |
|
Slave |
Assigned to incoming |
|
Slave |
Any value |
For instance, AXI4 interfaces are not expected to send or be given strobed transactions; WSTRB bits are always HIGH.
Footnotes
As explained in [5], Memory2Stream and Stream2Memory are called from a software module, which indicates a hardware component as the target. The hardware module is required to have a corresponding ModuleRead or ModuleWrite, respectively, therefore Memory2Stream and Stream2Memory never affect the generated interfaces.
RID and BID cannot be assigned to any static value; they must be assigned to the same value as ARID and AWID signals, respectively. The AXI4 standard states: “Slaves are required to reflect on the appropriate BID or RID response an AXI ID received from a master”