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:
  1. 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).

  2. 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:

  1. Click Tools

  2. From the submenu, click Preferences…

  3. From the left pane, expand the following nodes: SpaceStudioEDA

  4. Select the relevant EDA (for example, Xilinx – Vivado 2018.3)

  5. 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.

_images/hw_hls_preferences.png

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.

_images/hw_impl_export.png

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 (struct within struct) of these types

  • May 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.h functions); 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 :

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:

  1. 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.

  2. For each module/device instance, SpaceStudio determines its communication interfaces, and eventually creates a template VHDL file with these interfaces.

  3. The user provides the implementation of each instance in the VHDL file, potentially adding other VHDL/Verilog files along with them.

  4. 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.

Table 6.1 Generated Interfaces per Case#

Case

Type of generated interface

Described in

Call to ModuleRead/ModuleWrite

Streaming

Section 6.3.5

Call to RegisterRead/RegisterWrite

Register

Section 6.3.6

Call to DeviceRead/DeviceWrite

Memory-Mapped (master)

Section 6.3.7

Device component

Memory-Mapped (slave)

Section 6.3.7

Call to Memory2Stream/Stream2Memory

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.

Table 6.2 Generated Global Ports#

PORT NAME

DIRECTION

NOTES

clock

in

reset

in

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:

  1. the writing instance writes to a FIFO, which in turn is read from the reading instance

  2. the software instance writes to the hardware instance via a DMA

  3. 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.

Table 6.3 Generated AXI4-Stream Write Ports#

PORT NAME

DIRECTION

NOTES

TDATA

out

TLAST

out

Unused, present for future use. Should be set to HIGH.

TREADY

out

To avoid deadlocks, if there is data to send, the implementation must eventually assert this signal, and do so until TVALID is asserted, even if TVALID is LOW

TVALID

in

Table 6.4 Generated AXI4-Stream Read Ports#

PORT NAME

DIRECTION

NOTES

TDATA

in

TLAST

in

Unused, present for future use.

TREADY

in

TVALID

out

Waiting until TREADY is HIGH before asserting this signal is permitted

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#

_images/hw_reg_signals.png

Figure 6.3 Module with one Register Read and Register Write interfaces#

Registers are always writable within the next clock cycle, and the data sent by them is always valid. Hence, no handshake is necessary, as seen by the signals in Table 6.5 and Table 6.6.

Table 6.5 Generated Register Read Ports#

PORT NAME

DIRECTION

NOTES

register_read_data

in

State/value of the register.

Table 6.6 Generated Register Write Ports#

PORT NAME

DIRECTION

NOTES

register_write_data

out

Data to be written in the register.

register_write_enable

out

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.

_images/hw_reg_write_wave.png

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.

_images/hw_reg_read_wave.png

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.

_images/hw_fix_dev_addr.png

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”.

Table 6.7 Trivially Assignable AXI4 Signals#

PORT NAME

SET BY

VALUE

AWID

Master

All zeros

AWBURST

Master

0b01: INCR; incrementing addresses burst type

AWLOCK

Master

0b0: normal access

AWCACHE

Master

0b0011: normal non-cacheable bufferable

AWPROT

Master

0b000: unprivileged secure data access

AWQOS

Master

All zeros: not participating in QoS scheme

AWREGION

Master

All zeros

AWUSER

Master

Any value

WSTRB

Master

All ones: all bytes used

WUSER

Master

Any value

BID

Slave

Assigned to incoming AWID signal [4]

BUSER

Slave

Any value

ARID

Master

All zeros

ARBURST

Master

0b01: INCR; incrementing addresses burst type

ARLOCK

Master

0b0: normal access

ARCACHE

Master

0b0011: normal non-cacheable bufferable

ARPROT

Master

0b000: unprivileged secure data access

ARQOS

Master

All zeros: not participating in QoS scheme

ARREGION

Master

All zeros

ARUSER

Master

Any value

RID

Slave

Assigned to incoming ARID signal [4]

RUSER

Slave

Any value

For instance, AXI4 interfaces are not expected to send or be given strobed transactions; WSTRB bits are always HIGH.

Footnotes