What we’re building

A finite impulse response (FIR) filter, applied independently to every row of an input matrix. For an input row X[i, :] of length n_col and T filter taps h:

Y[i, j] = Σ_{t=0}^{T-1}  h[t] · X[i, j + (T-1) - t],     j = 0 … n_col − T

Each output is a weighted sum of the last T inputs — a sliding window of width T. We use the valid edge: only outputs where the whole window lands inside the row are produced, so each row yields n_col − T + 1 outputs (no zero-padding, no edge cases). The example fixes T = 8.

The golden

The numerical truth is one small numpy function, fir_golden.py — vectorized over the matrix, with the tap accumulation ordered left-to-right in t so it matches the hardware datapath bit-for-bit (the C and RTL are float, and the order of float adds matters). Everything downstream — the simulation, the C kernel, the RTL — is checked against this one function.

The command

A host drives the accelerator with AXI-stream control, like shared_mem: it sends the command over s_in and reads the response from m_out, while the data rides AXI-MM. Each FIRCmd (fir.py) carries the taps h, the element offsets of X, h, and Y in shared memory, and the matrix shape n_rows / n_cols. Addresses are element coordinates (indices into the typed region), not bytes.

Why FIR forces a resident buffer

FIR is the minimal computation that makes the load-compute-store structure a necessity rather than a choice. The output Y[i, j] needs inputs X[i, j−t] for t = 0 … T−1 — a window that moves and overlaps. You cannot compute it from a pure stream of beats, because producing each output requires random access back into the row. So the row must be resident and randomly addressable: load the whole row into an on-chip buffer, compute the windowed sum over it, store the result.

That is the load-compute-store dataflow — and because it’s forced, it is the cleanest possible vehicle for teaching the pattern. The next page walks it.


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