Checking for Full and Empty Blocks - 2023.2 English

Vitis High-Level Synthesis User Guide (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 English

The read_lock and write_lock are like while(empty) or while (full) loops - they keep trying to acquire the resource until they get the resource - so the code execution will stall until the lock is acquired. You can use the empty() and full() methods as shown in the following example to determine if a call to read_lock or write_lock will stall due to the lack of available blocks to be acquired.

#include "hls_streamofblocks.h"
  
void reader(hls::stream_of_blocks<buf> &in1, hls::stream_of_blocks<buf> &in2, int out[M][N], int c) {
    for(unsigned j = 0; j < M;) {
        if (!in1.empty()) {
            hls::read_lock<ppbuf> arr1(in1);
            for(unsigned i = 0; i < N; ++i) {
                out[j][i] = arr1[N-1-i];
            }
            j++;
        } else if (!in2.empty()) {
            hls::read_lock<ppbuf> arr2(in2);
            for(unsigned i = 0; i < N; ++i) {
                out[j][i] = arr2[N-1-i];
            }
            j++;
        }
    }
}
  
void writer(hls::stream_of_blocks<buf> &out1, hls::stream_of_blocks<buf> &out2, int in[M][N], int d) {
    for(unsigned j = 0; j < M; ++j) {
        if (d < 2) {
            if (!out1.full()) {
                hls::write_lock<ppbuf> arr(out1);
                for(unsigned i = 0; i < N; ++i) {
                    arr[N-1-i] = in[j][i];
                }
            }
        } else {
           if (!out2.full()) {
               hls::write_lock<ppbuf> arr(out2);
               for(unsigned i = 0; i < N; ++i) {
                   arr[N-1-i] = in[j][i];
               }
           }
        }
    }
}
  
void top(int in[M][N], int out[M][N], int c, int d) {
#pragma HLS dataflow
    hls::stream_of_blocks<buf, 3> strm1, strm2; // Depth=3
    writer(strm1, strm2, in, d);
    reader(strm1, strm2, out, c);
}

The producer and the consumer processes can perform the following actions within any scope in their body. As shown in the various examples, the scope will typically be a loop, but this is not required. Other scopes such as conditionals are also supported. Supported actions include:

  • Acquire a block, i.e. an array of any supported data type.
    • In the case of the producer, the array will be empty, i.e. initialized according to the constructor (if any) of the underlying data type.
    • In the case of the consumer, the array will be full (of course in as much as the producer has filled it; the same requirements as for PIPO buffers, namely full writing if needed apply).
  • Use the block for both reading and writing as if it were private local memory, up to its maximum allocated number of ports based on a BIND_STORAGE pragma or directive specified for the stream of blocks, which specifies what ports each side can see:
    • 1 port means that each side can access only one port, and the final stream-of-blocks can use a single dual-port memory for implementation.
    • 2 ports means that each side can use 1 or 2 ports depending on the schedule:
      • If the scheduler uses 2 ports on at least one side, merging will not happen
      • If the scheduler uses 1 port, merging can happen
    • If the pragma is not specified, the scheduler will decide, based on the same criteria currently used for local arrays. Moreover:
      • The producer can both write and read the block it has acquired
      • The consumer can only read the block it has acquired
  • Automatically release the block when exiting the scope in which it was acquired. A released block:
    • If released by the producer, can be acquired by the consumer.
    • If released by the consumer, can be acquired to be reused by the producer, after being re-initialized by the constructor, if any. This initialization may slow down the design, hence often it is not desired. You may use the __no_ctor__ attribute (explained earlier for std::complex) to prevent calling the constructor for the array elements.
A stream-of-blocks is very similar in spirit to a PIPO buffer. In the case of a PIPO, acquire is the same as calling the producer or consumer process function, while the release is the same as returning from it. This means that:
  • the handshakes for a PIPO are
    • ap_start/ap_ready on the consumer side and
    • ap_done/ap_continue on the producer side.
  • the handshakes of a stream of blocks are
    • its own read/empty_n on the consumer side and
    • write/full_n on the producer side.