The read_lock
and write_lock
are like while(1)
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, strm; // 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.
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 (i.e. stating) 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.