A memory reached three ways

Sharing memory between concurrent tasks arises in a multitude of applications — capture buffers, scoreboards, and storage of intermediate values. As the memory guide describes, there are three ways to share memory between hardware modules:

  • External memory, typically DDR, that exists on the board and reaches the programmable logic over an AXI-MM interface.
  • A ping-pong buffer (PIPO), which transfers blocks with a synchronization mechanism built in.
  • A dedicated BRAM (Block RAM), instantiated in the top-level design in the programmable logic.

This example demonstrates implementing, modelling and using a BRAM, where two hardware modules share one true-dual-port memory. For shared DDR see the histogram example; for the ping-pong buffer, the interleaver example.

The interface itself is documented in BRAM — memory between modules; this page set is the worked example that uses it.

Why a dedicated BRAM?

A BRAM can be given dedicated access to a small number of hardware modules. DDR usually has to be shared with other modules and with the PS, which can add substantial delay.

A PIPO is also typically built from a two-port BRAM, but it comes with a synchronization mechanism you do not choose, and it requires one module to write a different buffer segment than another reads. A BRAM gives you the memory and leaves the correctness argument to you — which is the whole subject of this example.

The example

Two hardware modules share one BRAM, and the memory is reached three ways — which is where the name comes from, and which lines up with the three access cases that every Waveflow interface is organised by:

transaction access case what it costs
WRITE a timed transfer into the memory 1 access per element, II=1
COMPUTE in place — no transfer at all 2 accesses per element, II=2
READ a timed transfer out of the memory 1 access per element, II=1

WRITE and COMPUTE go through the same port on the same task, so the only thing that differs between them is the access shape. That makes the II a measurement in one waveform rather than an argument, and it is why this example has two opcodes instead of two examples.

Two tasks, BramWriteCmd and BramReadCmd, inside the bram_access Vitis kernel; the bram_t2p memory beside the kernel but inside the bram_access_top wrapper, reached over buf_w and buf_r bram ports; six streams on the left carrying commands, payload and responses.

The nesting is the point. The memory is outside the kernel and inside the wrapper, because a memory shared between two tasks has no expression inside a Vitis kernel at all — the guide has the evidence, including the two things Vitis does instead when you try. Every transaction takes a command and answers with a (tid, status) response: a write has no return path of its own, and a refused read returns zero words, which on a stream is indistinguishable from “not yet”. The messages are declared once as schemas, and the C++ headers the kernel compiles against are generated from them.

What you will learn

  • How to create a memory and connect two HwModules to it through a BramIF — and why it is registered with add_rtl_if rather than add_if.
  • How to move a vector into it, and how to compute over it in place without inventing a transfer — and what declaring a port read-write costs you in cycles.
  • How to read and write it concurrently, and what actually guards the hazard — which is not the thing the memory’s own Verilog appears to promise.
  • How to run the design in Python simulation and record its timing.
  • How the read path’s fill is modelled, and why that number must never be typed into Python.
  • How to run an RTL simulation in which the memory is the real hand-written Verilog, with no BFM standing in for it.
  • How to verify throughput, overlap and the in-place cost from a timing diagram built out of the RTL trace.

Once you have this example, the same structure is what the RF shot buffer is built on.

The pages

  • Python model — the three transactions, the schemas, the two task bodies, and the top level with the memory beside it.
  • Python simulation — running it, the test vectors, and recording the timing.
  • Code generation — the kernel, bram_t2p.v, and the wrapper that joins them.
  • RTL simulation — running XSI, and producing the trace.
  • Reading the trace — the activity diagram, the hazard scan, the in-place cost, and the comparison to pysim.

See also


Table of contents