Writing the dataflow hook
The synthesizable side of the load-compute-store dataflow is one hand-written kernel,
fir_dataflow.tpp, attached to the dataflow method
with @synthesizable. It is the Dataflow custom-hook pattern
— the hook owns the whole pipeline, not just the compute.
Three functions in a #pragma HLS DATAFLOW region
The core is one row loop whose body is a DATAFLOW region wiring three sub-functions through two on-chip channels — a partitioned BRAM (windowed) and a FIFO (sequential):
static void fir_accel_core(const real_t* X, real_t* Y, const real_t* h, int n_rows, int n_cols) {
const int out_len = n_cols - T + 1;
ROW:
for (int r = 0; r < n_rows; ++r) {
#pragma HLS DATAFLOW
real_t xbuf[NCOL_MAX]; // load -> compute (ping-pong)
#pragma HLS ARRAY_PARTITION variable=xbuf cyclic factor=8 dim=1 // partitioned for the T-tap window
real_t htbuf[T];
#pragma HLS ARRAY_PARTITION variable=htbuf complete dim=1
hls::stream<real_t> yfifo; // compute -> store (FIFO)
#pragma HLS STREAM variable=yfifo depth=64
fir_load_row(X, h, r * n_cols, n_cols, xbuf, htbuf); // burst-read one row + taps
fir_compute_row(htbuf, xbuf, n_cols, yfifo); // FIR at II=1 from BRAM only
fir_store_row(Y, r * out_len, out_len, yfifo); // burst-write one output row
}
}
#pragma HLS DATAFLOWruns the three functions concurrently across iterations — load(r+1) while compute(r) while store(r−1).xbufdeclared inside the loop ⇒ HLS gives the region a depth-2 ping-pong (N_PINGPONG = 2): one copy filled by load, the other read by compute.ARRAY_PARTITION cyclic factor=8onxbuflets theT = 8-tap window read in parallel each cycle;htbufiscomplete-partitioned (the taps are all read at once).
The compute sustains one output per cycle, with the tap loop fully unrolled, accumulating in the golden’s order so csim/cosim are bit-exact:
static void fir_compute_row(const real_t htbuf[T], real_t xbuf[NCOL_MAX], int n_cols,
hls::stream<real_t>& yfifo) {
COMPUTE:
for (int j = 0; j < n_cols - T + 1; ++j) {
#pragma HLS PIPELINE II=1
real_t acc = 0;
for (int t = 0; t < T; ++t) {
#pragma HLS UNROLL
acc += htbuf[t] * xbuf[j + (T - 1) - t]; // left-to-right == fir_golden
}
yfifo.write(acc);
}
}
fir_load_row and fir_store_row are simple II=1 burst loops over X[base + j] / Y[obase + j],
which HLS coalesces into AXI bursts.
Interface pragmas live in the static top
The hook core takes plain pointers — no m_axi / axis pragmas — so it is reusable and the
interface is a codegen concern. The top is a static source file
(fir_top.cpp, copied verbatim into gen/fir.cpp by
fir_build.py) that adds AXI-stream control (the
command on s_in, the response on m_out, like shared_mem) around it:
// static fir_top.cpp -> gen/fir.cpp
void fir(hls::stream<ap_uint<32> >& s_in, hls::stream<ap_uint<32> >& m_out, real_t* gmem) {
#pragma HLS INTERFACE axis port=s_in
#pragma HLS INTERFACE axis port=m_out
#pragma HLS INTERFACE m_axi port=gmem bundle=gmem ...
#pragma HLS INTERFACE ap_ctrl_hs port=return
// read the FIRCmd off s_in (x_off, h_off, y_off, n_rows, n_cols), then:
fir_dataflow::fir_accel_core(gmem + x_off, gmem + y_off, gmem + h_off, n_rows, n_cols);
fir_dataflow::fir_respond(m_out, tx_id); // response hook (fir_respond_impl.tpp)
}
A single full-duplex m_axi bundle suffices — AXI read and write channels are independent, so the
X-read and Y-write never contend (proven in the Phase-1 sandbox).
Offsets are element indices, matching the Python Region.
Validated against one golden
The @synthesizable dataflow method’s Python body is never run in the sim (the
three stage processes model its timing), but the kernel is checked against the same
execute golden in C and RTL simulation — bit-for-bit, because
the tap order matches.
See also
- Dataflow custom hook — the pattern (when/why), in the hooks guide.
- The load-compute-store dataflow — the sim-side timing model this kernel realizes.
fir_dataflow.tpp— the full kernel source.