Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Streams

Purpose: Communicate between TAPA tasks using typed FIFO streams.

Prerequisites: Tasks


Why this exists

Streams are the primary inter-task communication mechanism in TAPA. They are typed, directional FIFOs that appear explicitly in task signatures, making data flow visible in the source code. Unlike shared memory, streams enforce a single-writer/single-reader discipline and make producer–consumer relationships unambiguous to both the programmer and the compiler.


Mental model

A stream instance lives in an upper-level task. Leaf tasks receive directional references to it:

// Upper-level task instantiates the stream and wires it to two leaf tasks
void Upper(/* ... */) {
  tapa::stream<float, 16> data_q("data_q");  // depth = 16 elements

  tapa::task()
      .invoke(Producer, data_q)   // Producer writes to data_q
      .invoke(Consumer, data_q);  // Consumer reads from data_q
}

// Leaf task signatures use directional references
void Producer(tapa::ostream<float>& out) { /* ... */ }
void Consumer(tapa::istream<float>& in)  { /* ... */ }

The stream<T, Depth> template parameter controls the hardware FIFO depth (default: 2). A larger depth reduces the chance of stalls at the cost of FPGA BRAM resources.


Blocking read and write

void Task(tapa::istream<int>& in, tapa::ostream<int>& out) {
  int data = in.read();   // blocks until data is available
  out.write(data);        // blocks until space is available
}

The << and >> operator aliases are equivalent:

out << data;   // same as out.write(data)
in >> data;    // same as data = in.read()

Non-blocking read and write

To read from multiple streams or achieve an initiation interval of one, use the non-blocking variants that return a bool indicating success:

void Task(tapa::istream<int>& in, tapa::ostream<int>& out) {
  int data;
  bool ok = in.try_read(data);   // returns false if stream is empty
  if (ok) {
    out.try_write(data);         // returns false if stream is full
  }
}

Readiness checks

Check stream state before committing to a read or write:

if (!in.empty())  { /* safe to read  */ }
if (!out.full())  { /* safe to write */ }

For non-destructive inspection, peek returns the front element and a validity flag without consuming it:

bool valid;
auto val = in.peek(valid);   // does not remove the token
if (valid && /* routing decision */) {
  in.read(nullptr);          // consume now
}

End-of-Transaction (EoT)

A producer signals the end of a data stream by calling close(). The consumer detects it with try_eot():

// Producer
void Mmap2Stream(tapa::mmap<const float> mem, uint64_t n,
                 tapa::ostream<float>& stream) {
  for (uint64_t i = 0; i < n; ++i) {
    stream.write(mem[i]);
  }
  stream.close();  // send EoT token
}

// Consumer
void Stream2Mmap(tapa::istream<float>& stream, tapa::mmap<float> mem) {
  for (uint64_t i = 0;;) {
    bool eot;
    if (stream.try_eot(eot)) {
      if (eot) break;
      mem[i++] = stream.read(nullptr);
    }
  }
}

EoT loop helper macros

TAPA provides macros that encapsulate the non-blocking EoT check pattern:

  • TAPA_WHILE_NOT_EOT(stream) — loops until stream delivers an EoT token; body executes only when a valid non-EoT token is available.
  • TAPA_WHILE_NEITHER_EOT(s1, s2) — loops until either stream delivers EoT; body executes only when both have a valid token.
  • TAPA_WHILE_NONE_EOT(s1, s2, s3) — three-stream variant.
void Consumer(tapa::istream<int>& in, tapa::ostream<int>& out) {
  TAPA_WHILE_NOT_EOT(in) {
    out.write(in.read(nullptr));
  }
  out.close();
}

Tip

A downstream task can reopen a closed stream with stream.open() to reuse it across multiple transactions.


Stream arrays

For parameterized designs, TAPA provides arrays of streams:

  • tapa::streams<T, N> — array of N streams (instantiation in upper-level task)
  • tapa::istreams<T, N>& / tapa::ostreams<T, N>& — directional array references in leaf task signatures

When invoking N parallel instances of a leaf task, use invoke<tag, N>(...) and TAPA distributes the array elements automatically:

void InnerStage(int b, tapa::istreams<pkt_t, kN / 2>& in_q0,
                tapa::istreams<pkt_t, kN / 2>& in_q1,
                tapa::ostreams<pkt_t, kN>& out_q) {
  tapa::task().invoke<tapa::detach, kN / 2>(Switch2x2, b, in_q0, in_q1, out_q);
}

Rules

  • Always pass streams by reference: istream<T>&, ostream<T>&. Never by value — the stream object is not copyable.
  • Each stream instance must have exactly one reader and exactly one writer.
  • TAPA software simulation respects stream depth: a full stream blocks the writer, matching hardware behavior.
  • Stream depth is a hardware FIFO size. The FPGA resource used depends on depth:
    • Depth < 128: synthesised from SRL shift-registers (no BRAM cost).
    • Depth ≥ 128: mapped to BRAM.
    • Depth ≥ 4096 and element width ≥ 36 bits: mapped to URAM.
    • Default depth is 2, which costs only SRL resources.

Common mistakes

Wrong — stream passed by value (drops the reference, triggers a copy):

void Leaf(tapa::istream<float> in) { /* ... */ }  // missing &

Right — stream passed by reference:

void Leaf(tapa::istream<float>& in) { /* ... */ }

See also


Next step: Memory Access: mmap