HLS Stream Library
Streaming data is a type of data transfer in which data samples are sent in sequential order starting from the first sample. Streaming requires no address management.
Modeling designs that use streaming data can be difficult in C. The approach of using pointers to perform multiple read and/or write accesses can introduce issues, because there are implications for the type qualifier and how the test bench is constructed.
Vitis HLS provides a C++ template class hls::stream<>
for modeling streaming data structures. The streams
implemented with the hls::stream<>
class have the
following attributes.
- In the C code, an
hls::stream<>
behaves like a FIFO of infinite depth. There is no requirement to define the size of anhls::stream<>
. - They are read from and written to sequentially. That is, after data is
read from an
hls::stream<>
, it cannot be read again. - An
hls::stream<>
on the top-level interface is by default implemented with anap_fifo
interface. - There are two possible stream declarations:
hls::stream<Type>
: specify the data type for the stream.An
hls::stream<>
internal to the design is implemented as a FIFO with a default depth of 2. The STREAM pragma or directive can be used to change the depth.hls::stream<Type, Depth>
: specify the data type for the stream, and the FIFO depth.Set the depth to prevent stalls. If any task in the design can produce or consume samples at a greater rate than the specified depth, the FIFOs might become empty (or full) resulting in stalls, because it is unable to read (or write).
This section shows how the hls::stream<>
class can more easily model designs with streaming data. The topics in this section provide:
- An overview of modeling with streams and the RTL implementation of streams.
- Rules for global stream variables.
- How to use streams.
- Blocking reads and writes.
- Non-Blocking Reads and writes.
- Controlling the FIFO depth.
hls::stream
class should always be passed
between functions as a C++ reference argument. For example,
&my_stream
.hls::stream
class is only used in C++
designs. Array of streams is not supported.C Modeling and RTL Implementation
Streams are modeled as an infinite queue in software (and in the test bench during RTL co-simulation). There is no need to specify any depth to simulate streams in C++. Streams can be used inside functions and on the interface to functions. Internal streams may be passed as function parameters.
Streams can be used only in C++ based designs. Each hls::stream<>
object must be written
by a single process and read by a single process.
If an hls::stream
is used on the top-level
interface, it is by default implemented in the RTL as a FIFO
interface (ap_fifo
) but may be
optionally implemented as a handshake interface (ap_hs
) or an AXI4-Stream interface (axis
).
If an hls::stream
is used inside the design
function and synthesized into hardware, it is implemented as a
FIFO with a default depth of 2. In some cases, such as when
interpolation is used, the depth of the FIFO might have to be
increased to ensure the FIFO can hold all the elements produced
by the hardware. Failure to ensure the FIFO is large enough to
hold all the data samples generated by the hardware can result
in a stall in the design (seen in C/RTL co-simulation and in the
hardware implementation). The depth of the FIFO can be adjusted
using the STREAM directive with the depth
option. An example of this is provided in the example design
hls_stream.
hls::stream
variables are correctly sized when used in the default
non-DATAFLOW regions.If an hls::stream
is used to transfer data between tasks (sub-functions or loops), you should immediately consider implementing the tasks in a DATAFLOW region where data streams from one task to the next. The default (non-DATAFLOW) behavior is to complete each task before starting the next task, in which case the FIFOs used to implement the hls::stream
variables must be sized to ensure they are large enough to hold all the data samples generated by the producer task. Failure to increase the size of the hls::stream
variables results in the error below:
ERROR: [XFORM 203-733] An internal stream xxxx.xxxx.V.user.V' with default size is
used in a non-dataflow region, which may result in deadlock. Please consider to
resize the stream using the directive 'set_directive_stream' or the 'HLS stream'
pragma.
This error informs you that in a non-DATAFLOW region (the default FIFOs depth is 2) may not be large enough to hold all the data samples written to the FIFO by the producer task.
Global and Local Streams
Streams may be defined either locally or globally. Local streams are always implemented as internal FIFOs. Global streams can be implemented as internal FIFOs or ports:
- Globally-defined streams that are only read from, or only written to, are inferred as external ports of the top-level RTL block.
- Globally-defined streams that are both read from and written to (in the hierarchy below the top-level function) are implemented as internal FIFOs.
Streams defined in the global scope follow the same rules as any other global variables.
Using HLS Streams
To use hls::stream<>
objects, include
the header file hls_stream.h. Streaming data objects are defined by
specifying the type and variable name. In this example, a 128-bit unsigned integer type is
defined and used to create a stream variable called my_wide_stream
.
#include "ap_int.h"
#include "hls_stream.h"
typedef ap_uint<128> uint128_t; // 128-bit user defined type
hls::stream<uint128_t> my_wide_stream; // A stream declaration
Streams must use scoped naming. Xilinx
recommends using the scoped hls::
naming shown in the
example above. However, if you want to use the hls
namespace, you can rewrite the preceding example as:
#include <ap_int.h>
#include <hls_stream.h>
using namespace hls;
typedef ap_uint<128> uint128_t; // 128-bit user defined type
stream<uint128_t> my_wide_stream; // hls:: no longer required
Given a stream specified as hls::stream<T>
, the type T can be:
- Any C++ native data type
- A Vitis HLS arbitrary precision type
(for example,
ap_int<>
,ap_ufixed<>
) - A user-defined struct containing either of the above types
A stream can also be specified as hls::stream<Type, Depth>
, where Depth indicates the depth of the FIFO
needed in the verification adapter that the HLS tool creates for RTL co-simulation.
Streams can be optionally named. Providing a name for the stream allows the name to be used in reporting. For example, Vitis HLS automatically checks to ensure all elements from an input stream are read during simulation. Given the following two streams:
stream<uint8_t> bytestr_in1;
stream<uint8_t> bytestr_in2("input_stream2");
WARNING: Hls::stream 'hls::stream<unsigned char>.1' contains leftover data, which
may result in RTL simulation hanging.
WARNING: Hls::stream 'input_stream2' contains leftover data, which may result in RTL
simulation hanging.
Any warning on elements left in the streams are reported as follows, where
it is clear that the bytetr_in2 must be bytestr_in2:
When streams are passed into and out of functions, they must be passed-by-reference as in the following example:
void stream_function (
hls::stream<uint8_t> &strm_out,
hls::stream<uint8_t> &strm_in,
uint16_t strm_len
)
Vitis HLS supports both blocking and non-blocking access methods.
A complete design example using streams is provided in the Vitis HLS examples. Refer to the hls_stream
example in the design examples available from the Vitis IDE welcome screen.
Blocking Reads and Writes
The basic accesses to an hls::stream<>
object are blocking reads and writes. These are accomplished using class
methods. These methods stall (block) execution if a read is attempted
on an empty stream FIFO, a write is attempted to a full stream FIFO,
or until a full handshake is accomplished for a stream mapped to an ap_hs
interface protocol.
A stall can be observed in C/RTL co-simulation as the continued execution of the simulator without any progress in the transactions. The following shows a classic example of a stall situation, where the RTL simulation time keeps increasing, but there is no progress in the inter or intra transactions:
// RTL Simulation : "Inter-Transaction Progress" ["Intra-Transaction Progress"] @
"Simulation Time"
///////////////////////////////////////////////////////////////////////////////////
// RTL Simulation : 0 / 1 [0.00%] @ "110000"
// RTL Simulation : 0 / 1 [0.00%] @ "202000"
// RTL Simulation : 0 / 1 [0.00%] @ "404000"
Blocking Write Methods
In this example, the value of variable src_var
is
pushed into the stream.
// Usage of void write(const T & wdata)
hls::stream<int> my_stream;
int src_var = 42;
my_stream.write(src_var);
The << operator is overloaded such that it may be used in a similar fashion
to the stream insertion operators for C++ stream (for example, iostreams and
filestreams). The hls::stream<>
object to be
written to is supplied as the left-hand side argument and the value to be written as
the right-hand side.
// Usage of void operator << (T & wdata)
hls::stream<int> my_stream;
int src_var = 42;
my_stream << src_var;
Blocking Read Methods
This method reads from the head of the stream and assigns the values to the variable dst_var
.
// Usage of void read(T &rdata)
hls::stream<int> my_stream;
int dst_var;
my_stream.read(dst_var);
Alternatively, the next object in the stream can be read by assigning (using for example =, +=) the stream to an object on the left-hand side:
// Usage of T read(void)
hls::stream<int> my_stream;
int dst_var = my_stream.read();
The >>
operator is overloaded to allow use
similar to the stream extraction operator for C++ stream (for example, iostreams and
filestreams). The hls::stream
is supplied as the LHS
argument and the destination variable the RHS.
// Usage of void operator >> (T & rdata)
hls::stream<int> my_stream;
int dst_var;
my_stream >> dst_var;
Non-Blocking Reads and Writes
Non-blocking write and read methods are also provided. These allow execution to continue even when a read is attempted on an empty stream or a write to a full stream.
These methods return a Boolean value indicating the status of the access (true
if successful, false
otherwise). Additional methods are included for testing the status of an hls::stream<>
stream.
ap_fifo
protocol. More
specifically, the AXI-Stream standard and the Xilinx
ap_hs
IO protocol do not support non-blocking
accesses.During C simulation, streams have an infinite size. It is therefore not possible to validate with C simulation if the stream is full. These methods can be verified only during RTL simulation when the FIFO sizes are defined (either the default size of 1, or an arbitrary size defined with the STREAM directive).
Non-Blocking Writes
This method attempts to push variable src_var
into the stream my_stream
, returning a boolean true
if successful. Otherwise, false
is returned and the queue is unaffected.
// Usage of void write_nb(const T & wdata)
hls::stream<int> my_stream;
int src_var = 42;
if (my_stream.write_nb(src_var)) {
// Perform standard operations
...
} else {
// Write did not occur
return;
}
Fullness Test
bool full(void)
Returns true
, if and only if the
hls::stream<>
object is full.
// Usage of bool full(void)
hls::stream<int> my_stream;
int src_var = 42;
bool stream_full;
stream_full = my_stream.full();
Non-Blocking Read
bool read_nb(T & rdata)
This method attempts to read a value from the stream, returning
true
if successful. Otherwise, false
is returned and the queue is unaffected.
// Usage of void read_nb(const T & wdata)
hls::stream<int> my_stream;
int dst_var;
if (my_stream.read_nb(dst_var)) {
// Perform standard operations
...
} else {
// Read did not occur
return;
}
Emptiness Test
bool empty(void)
Returns true
if the hls::stream<>
is empty.
// Usage of bool empty(void)
hls::stream<int> my_stream;
int dst_var;
bool stream_empty;
stream_empty = my_stream.empty();
The following example shows how a combination of non-blocking accesses and full/empty tests can provide error handling functionality when the RTL FIFOs are full or empty:
#include "hls_stream.h"
using namespace hls;
typedef struct {
short data;
bool valid;
bool invert;
} input_interface;
bool invert(stream<input_interface>& in_data_1,
stream<input_interface>& in_data_2,
stream<short>& output
) {
input_interface in;
bool full_n;
// Read an input value or return
if (!in_data_1.read_nb(in))
if (!in_data_2.read_nb(in))
return false;
// If the valid data is written, return not-full (full_n) as true
if (in.valid) {
if (in.invert)
full_n = output.write_nb(~in.data);
else
full_n = output.write_nb(in.data);
}
return full_n;
}
Controlling the RTL FIFO Depth
For most designs using streaming data, the default RTL FIFO depth of 2 is sufficient. Streaming data is generally processed one sample at a time.
For multirate designs in which the implementation requires a FIFO with a depth greater than 2, you must determine (and set using the STREAM directive) the depth necessary for the RTL simulation to complete. If the FIFO depth is insufficient, RTL co-simulation stalls.
Because stream objects cannot be viewed in the GUI directives pane, the STREAM directive cannot be applied directly in that pane.
Right-click the function in which an hls::stream<>
object is declared (or is used, or exists in the argument list) to:
- Select the STREAM directive.
- Populate the
variable
field manually with name of the stream variable.
Alternatively, you can:
- Specify the STREAM directive manually in the
directives.tcl
file, or - Add it as a pragma in
source
.
C/RTL Co-Simulation Support
The Vitis HLS C/RTL co-simulation feature does not support structures or classes containing hls::stream<>
members in the top-level interface. Vitis HLS supports these structures or classes for synthesis.
typedef struct {
hls::stream<uint8_t> a;
hls::stream<uint16_t> b;
} strm_strct_t;
void dut_top(strm_strct_t indata, strm_strct_t outdata) {
}
These restrictions apply to both top-level function arguments and globally
declared objects. If structs of streams are used for synthesis, the design must be
verified using an external RTL simulator and user-created HDL test bench. There are no
such restrictions on hls::stream<>
objects with
strictly internal linkage.