Intel® High Level Synthesis Compiler Pro Edition: Reference Manual

ID 683349
Date 12/04/2023
Public
Document Table of Contents
Give Feedback

4.3.1. The pipe Class and Its Use

The pipe class exposes static methods for writing a data word to a pipe and reading a data word from a pipe. The reads and writes can be blocking or nonblocking, with the form chosen based on the overload resolution.

The pipe API is equivalent to the following class declaration:
template <class name,
          class dataT,
          size_t min_capacity = 0>
class pipe {
public:
  // Blocking
  static dataT read();
  static void write(dataT data);
  // Non-blocking
  static dataT read(bool &success);
  static void write(dataT data, bool &success);
}
Where the template parameters are defined as follows:
Table 12.   pipe API Template Parameters
Parameter Description
name The type that is used to create a unique identifier for the pipe.

It is typically a user-defined class, in a user namespace. Forward declaration of the type is enough, and the type need not be defined.

dataT The data type of the packet contained within a pipe.

This is the data type that is read during a successful pipe read() operation, or written during a successful pipe write() operation.

The type must have a standard layout and be trivially copyable.

min_capacity The minimum number of words (in units of dataT) that the pipe must be able to store without any being read out.

The compiler might create a pipe with a larger capacity due to performance considerations.

Example Using Pipes

The following code example shows a header file that you can use in a source code library or use in component or task functions.
#include "HLS/hls.h"

template<unsigned ID, class T, unsigned pipe_capacity> class TaskSystem {
private:
  template<unsigned SystemID> class InputPipeID {};
  template<unsigned SystemID> class TaskPipeID {};
  template<unsigned SystemID> class OutputPipeID {};

public:
  using input_pipe  = ihc::pipe<class InputPipeID<ID>, T, pipe_capacity>;
  using output_pipe = ihc::pipe<class OutputPipeID<ID>, T, pipe_capacity>;
  using task_pipe   = ihc::pipe<class TaskPipeID<ID>, T, pipe_capacity>;

  static void first_task(unsigned N) {
    T data;
    for(unsigned i=0; i<N; ++i) {
      data = input_pipe::read();
      task_pipe::write(data);
    }
  }

  static void second_task(unsigned N) {
    T data;
    for(unsigned i=0; i<N; ++i) {
      data = task_pipe::read();
      output_pipe::write(data);
    }
  }
};

With this header file, first_task and second_task can be called from separate task functions to achieve concurrency.

Assuming the header file in a file called task_system.h, you can create a system of tasks component like the following example:
#include "HLS/hls.h"
#include <iostream>

#include “task_system.h”

unsigned constexpr ID = 42; // can be any unique value
unsigned constexpr CAPACITY = 100;
using MySystem = TaskSystem<ID, int, CAPACITY>;

int main() {
  ihc::launch<MySystem::first_task>(CAPACITY);
  ihc::launch<MySystem::second_task>(CAPACITY);

  for(int i = 0; i < CAPACITY; ++i) {
    std::cout << "input: " << i << "\n";
    MySystem::input_pipe::write(i);
  }

  for(int i = 0; i < CAPACITY; ++i) {
    int data = MySystem::output_pipe::read();
    std::cout << "output: " << data << "\n";
  }

  return 0;
}

Example of an Array of Pipes

The following code example implements an array of pipes using templates, and includes functions to write to such an array.

#include "HLS/hls.h"

// PipeArray

template <class ArrayID, typename T, unsigned pipeCapacity, unsigned arraySize>
class PipeArray {
private:
  template <unsigned idx> struct StructIndex;
  template <unsigned idx> struct VerifyIndex {
    static_assert(idx < arraySize, "Index out of bounds");
    using VerifiedPipe = ihc::pipe<StructIndex<idx>, T, pipeCapacity>;
  };

public:
  template <unsigned idx>
  using pipe_at = typename VerifyIndex<idx>::VerifiedPipe;
  static void write_to_pipes(T *values);
};

// Write Unroller

template <class ArrayID, typename T, unsigned pipeCapacity, unsigned arraySize,
          unsigned idx>
struct WriteUnroller {
  using my_array = PipeArray<ArrayID, T, pipeCapacity, arraySize>;
  static void write_to_pipes_impl(T *values) {
    my_array::template pipe_at<idx>::write(values[idx]);
    WriteUnroller<ArrayID, T, pipeCapacity, arraySize,
                  idx + 1>::write_to_pipes_impl(values);
  }
};

template <class ArrayID, typename T, unsigned pipeCapacity, unsigned arraySize>
struct WriteUnroller<ArrayID, T, pipeCapacity, arraySize, arraySize> {
  static void write_to_pipes_impl(T *values) {}
};

// Write function

template <class ArrayID, typename T, unsigned pipeCapacity, unsigned arraySize>
void PipeArray<ArrayID, T, pipeCapacity, arraySize>::write_to_pipes(T *values) {
  WriteUnroller<ArrayID, T, pipeCapacity, arraySize, 0>::write_to_pipes_impl(
      values);
}

The function PipeArray::write_to_pipes takes an array of values to be written, and calls WriteUnroller::write_to_pipes_impl, which uses recursive templating to write to each pipe in the array. Reading from the array of pipes would have a similar implementation.