Block — load, compute, store

The simplest way to use a hook is to not let it touch the port at all. If your operand is a fixed-size block you can name up front, read the whole block into a buffer, hand the buffer to a pure-compute hook, and write the result back — all in the extractable kernel body. Codegen generates the load and the store for you; you write only the math.

This is the array generalization of the scalar regmap kernel (examples/regmap/simp_fun.py): there the register I/O is auto-generated by the s_axilite path and compute(x, a, b) is a pure scalar function. Here the array I/O is auto-generated by the m_axi path and compute(x, n) is a pure block function. The worked example is examples/block_scaley[i] = A*x[i] + B over a resident int32 block.

The split: movement in run_proc, math in the hook

run_proc is the extractable kernel body. It does the data movement with read_array / write_array on the m_axi master, and delegates the math to the compute hook:

def run_proc(self) -> ProcessGen[None]:
    cmd = yield from self.s_in.get(BlockCmd)
    x = yield from self.m_mem.read_array(Int32, cmd.n, cmd.x_addr, max_count=self.max_n)
    y = yield from self.compute(x, cmd.n)              # the hook — pure compute
    yield from self.m_mem.write_array(y, Int32, cmd.y_addr, cmd.n, max_count=self.max_n)

@synthesizable
def compute(self, x: BlockBuf, n: int) -> ProcessGen[BlockBuf]:
    return block_affine(np.asarray(x)[:int(n)])       # == the golden; C++ is hand-written
    yield                                              # unreachable — makes this a generator

max_count is the compile-time bound on the buffer (here the max_n HwParam); cmd.n is the runtime length actually transferred. Every buffer carries its own bound — there is no global fallback — so the generated kernel can size each static local array.

What codegen generates

The read_array / write_array calls are not hooks — they are in the synthesizable subset, so the extractor lowers them to read_array_slice / write_array_slice bursts over the resident [0, n) range. Only compute becomes a call to your C++:

// generated gen/block_scale.cpp — run_proc lowered
BlockCmd cmd;
cmd.read_axi4_stream<32>(s_in);
static ap_int<32> x[max_n];
int32_array_utils::read_array_slice<32>(
    m_mem + memmgr::byte_addr_to_word_index<32>(cmd.x_addr), 0, cmd.n, x);   // the load
static ap_int<32> y[256];
block_scale_impl::compute(x, cmd.n, y);                                      // your hook
int32_array_utils::write_array_slice<32>(
    y, m_mem + memmgr::byte_addr_to_word_index<32>(cmd.y_addr), 0, cmd.n);   // the store

Both static buffers carry the same compile-time bound MAX_N (256), shown two ways: the operand buffer as the max_n HwParam (it came from the max_count=self.max_n argument) and the result buffer as the resolved literal 256 (from the BlockBuf bound on compute’s return). cmd.n is the runtime length actually transferred.

read_array_slice<WORD_BW>(mem + word_index(addr), i0, i1, buf) moves an element range [i0, i1) between the port and a resident buffer, in element coordinates — the kernel never divides by the packing factor. It is the resident-range access shape; the reference lists it alongside the throughput read_array_lane loop (which complex.md uses for data-dependent addressing).

The hook is pure compute

Because the load and store already happened, the hook only sees a materialized C++ array. It never references m_mem:

// block_scale_compute_impl.cpp — the whole hook
namespace block_scale_impl {

void compute(ap_int<32> x[256], int n, ap_int<32> out[256]) {
#pragma HLS INLINE
    for (int i = 0; i < n; ++i) {
#pragma HLS PIPELINE II=1
        out[i] = ap_int<32>(3) * x[i] + ap_int<32>(-4);   // y = A*x + B
    }
}

}  // namespace block_scale_impl

An array-typed return lowers to a void function with an appended out-parameter (HLS can’t return an array by value), so the signature codegen calls is compute(x, n, out). The hook is non-templated — it has no stream argument carrying an HwParam width — so it lives in a plain .cpp, not a .tpp (see the .cpp vs .tpp rule). Its Python sibling (block_affine) is the bit-exact golden, so csim validates the .cpp against it.

When to use it

Reach for the block pattern when:

  • the operand is a fixed-size block whose length you know up front (bounded by a compile-time max_count);
  • you don’t need cycle-level streaming — the whole block is resident before compute;
  • random access within the block is fine (it is a plain C++ array).

Move on when the data won’t fit or arrives incrementally — process it as it streams (stream.md) — or when the access pattern itself depends on runtime command fields (gather/scatter, strided rows) — drive the port from the datapath (complex.md).

See also


This site uses Just the Docs, a documentation theme for Jekyll.