The Python model

FIRAccel (fir.py) is a single HwComponent whose internal structure is three concurrent stage processes. The shape is the double-buffered timing model: you cannot model load ∥ compute ∥ store inside one coroutine, so each stage is its own process and they hand off through queues.

Three stage processes, kicked by run_proc

pre_sim spawns the three persistent processes; run_proc is the host-facing entry that pulls each command off the s_in control stream and hands it to the load stage without blocking — so successive commands (and the three stages) overlap:

def pre_sim(self):
    self.load_q    = self.transaction_queue()   # run_proc -> load
    self.compute_q = self.transaction_queue()   # load    -> compute
    self.store_q   = self.transaction_queue()   # compute -> store
    self.process(self.load()); self.process(self.compute()); self.process(self.store())

def run_proc(self):
    while True:
        cmd = yield from self.s_in.get(self.Cmd)
        self.load_q.put(cmd)          # kick the pipeline; do not wait for completion

The fictitious inter-stage messages

The load→compute and compute→store hand-offs are plain dataclasses (FIRCompMsg, FIRStoreMsg). They are never synthesized — in hardware the data moves through the BRAM ping-pong and the FIFO — so they need not be DataSchema types. They carry the data plus the timing tag (tstart) the stages thread through:

@dataclass
class FIRCompMsg:    # load -> compute
    tstart: float    # ABS sim-time the first input row is available
    X: ndarray; h: ndarray; cmd: FIRCmd

One golden, element-coordinate memory

Each stage does real numpy work and reads/writes shared memory through a Region — an element-coordinate view of the m_axi port (indices, not bytes; the region resolves element→byte). Compute calls the one shared golden, execute (= fir_golden), so the simulation’s Y is bit-exact with the kernel’s:

def load(self):    # ... read the row resident
    Xf, _ = yield from self._data.read_slice_pipelined(x0, x1, t_out_start=t_begin, num_trans=n_rows)
def compute(self): # ... the ONE golden
    Y = self.execute(msg.X, msg.h)
def store(self):   # ... write the output row
    yield from self._data.write_slice_pipelined(y0, Y.ravel(), t_out_start=anchor, num_trans=n_rows, ...)

The read_slice_pipelined / write_slice_pipelined calls are where the timing lives — the per-stage spans, the channel contention, and the store-hides-under-compute overlap. That is the next page.

Sim vs. hardware

Only the synthesizable kernel — the @synthesizable dataflow hook — becomes C++; the three stage processes are a sim-only device whose job is to produce the overlapped timeline. The functional truth (execute) is shared by both. So the same component is bit-exact-checked in Python and synthesized from the hand-written hook.


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