Averager + Buffer (axis_avg_buffer) - QICK Firmware
The Averager + Buffer (axis_avg_buffer) is the IP block that sits
downstream of a readout (Readout System - QICK Firmware) and turns its decimated I/Q stream
into the two kinds of data a QICK program can pull back over PS-PL DMA:
accumulated points (AcquireMixin.acquire()) and raw decimated
waveforms (AcquireMixin.acquire_decimated()) – see
Accumulated vs. decimated readout for the software-level distinction. The module
lives in the qick firmware repository under
firmware/ip/axis_avg_buffer/ and is exposed to Python through
qick.drivers.readout.AxisAvgBuffer (and its V1pt1/V1pt2/
AxisWeightedBuffer subclasses).
1. General Description
axis_avg_buffer (axis_avg_buffer.v) takes one AXI-Stream input
(s_axis, the readout’s decimated I/Q, 2*B bits wide – I in the low
B bits, Q in the high B bits) and drives three independent output
streams from a single hardware trigger:
m0_axis – the averaged buffer: one accumulated (I, Q) point per trigger, stored in a dedicated BRAM and DMA’d out on request. This is what
acquire()reads.m1_axis – the raw buffer: every decimated sample captured individually during the trigger window, stored in a second BRAM and DMA’d out separately. This is what
acquire_decimated()reads.m2_axis – the same accumulated point as m0, but streamed out live (via a small async FIFO, no BRAM round-trip) instead of waiting for a DMA request. This feeds an input port of the tProcessor directly, for real-time feedback (e.g. thresholding a readout result to steer the next part of the program without going through software).
Internally the block is two independent, symmetric halves – avg_top
(drives m0 + m2) and buffer_top (drives m1) – both built from the same
pattern: a small FSM that writes captured/accumulated data into a dual-port
BRAM, and a second FSM (data_reader) that reads it back out over AXI-Stream
on request. Both halves share the same trigger and are independently
enabled/addressed/sized – you can run one without the other.
2. Synthesis Parameters
Parameter |
RTL Default |
Description |
|---|---|---|
|
16 |
Bit width of one I or one Q sample coming in on |
|
10 |
Address width of the averaged-buffer BRAM. Depth (max number of
accumulated points that can be stored per trigger burst) is
|
|
10 |
Address width of the raw-buffer BRAM. Depth (max number of raw
samples per trigger) is |
The accumulator itself is 2*B bits signed per component (acc_i,
acc_q), and the averaged-buffer word is 4*B bits ({acc_q, acc_i},
i.e. 64 bits total for the default B=16) – this is why the Python driver
reads avg_buff as np.int64 and buf_buff (2*B bits per raw
sample) as np.int32/np.int16 (see 6. Python Interface). Real board
configurations typically instantiate larger N_AVG/N_BUF than the RTL
default (see soc.cfg['readouts'][i]['avg_maxlen']/buf_maxlen'] for
the values on your board – read them at runtime rather than assuming a
number, since they vary by board and by readout channel).
3. Averaged Buffer Datapath (m0_axis / m2_axis)
The avg FSM (avg.sv) has two mutually exclusive modes, selected by
AVG_PHOTON_MODE_REG:
Accumulate mode (
AVG_PHOTON_MODE_REG=0, the default): on every valid input sample while triggered,acc_i += Iandacc_q += Q(plain running sums, no length normalization in hardware – that division happens in software, seeAcquireMixin.acquire()’s length normalization). AfterAVG_LEN_REGsamples, the accumulated{acc_q, acc_i}word is written to BRAM atAVG_ADDR_REGand the FSM waits for the next trigger (auto-incrementing the write address each time, so consecutive triggers fill consecutive slots).Edge-counter / “photon” mode (
AVG_PHOTON_MODE_REG=1): instead of summing I/Q, the block compares the I component against two signed thresholds (AVG_H_THRSH_REG,AVG_L_THRSH_REG) and counts rising edges through the high threshold (a Schmitt-trigger-style hysteresis: a new edge only counts once I has dropped back below the low threshold and risen through the high threshold again). This is theEDGE_COUNTERfeature the Python driver flags viacfg['has_edge_counter']– useful for photon-counting / single-photon-detector style readouts where you want a pulse count rather than an integrated amplitude.
FSM states: INIT -> START -> TRIGGER -> AVG -> QOUT -> WRITE_MEM ->
WAIT_TRIGGER -> (TRIGGER or START). AVG_START_REG gates whether the
block is armed at all; the actual capture only begins once the external
trigger pulse arrives while armed, and the FSM returns to
WAIT_TRIGGER/TRIGGER afterward so back-to-back triggers (e.g. once
per rep in a loop) each get their own BRAM slot without re-arming from
software.
The averaged-buffer BRAM is read out by a data_reader FSM controlled by
AVG_DR_START_REG/AVG_DR_ADDR_REG/AVG_DR_LEN_REG, independent of
the write side – you can read back completed points while the FSM is
capturing new ones elsewhere in the buffer. m2_axis taps the same
accumulated word through a small async FIFO the instant it’s computed
(QOUT_ST/WRITE_MEM_ST), without going through the BRAM/data_reader
path at all – that’s what makes it usable for low-latency in-program
feedback.
4. Raw Buffer Datapath (m1_axis)
buffer_top/buffer.sv is structurally the same pattern (FSM + BRAM +
data_reader) but simpler: every individual decimated (I, Q) sample seen
while triggered (governed by BUF_START_REG/BUF_ADDR_REG/
BUF_LEN_REG) is written to BRAM as-is, one sample per address, no
accumulation. Read-back uses its own independent
BUF_DR_START_REG/BUF_DR_ADDR_REG/BUF_DR_LEN_REG registers. This
is the raw time-domain waveform AcquireMixin.acquire_decimated()
returns.
5. Register Map
Register |
Width |
Description |
|---|---|---|
|
1 bit |
0: averager disabled. 1: armed, waiting for/capturing on |
|
|
BRAM write address for the next accumulated point. |
|
32 bits |
Number of input samples to accumulate before writing out one point. |
|
1 bit |
0: accumulate I/Q. 1: edge-counter mode (see above). |
|
|
High/low thresholds for edge-counter mode. |
|
1 bit |
1: stream accumulated data out on |
|
|
Start address / sample count for the |
|
1 bit |
0: raw buffer disabled. 1: armed, waiting for/capturing on |
|
|
BRAM write address / number of raw samples to capture. |
|
1 bit |
1: stream raw data out on |
|
|
Start address / sample count for the |
6. Python Interface
Most QICK programs never call this driver directly – AcquireMixin.acquire()
and AcquireMixin.acquire_decimated() drive it for you. Use the driver
directly only when you need low-level control (custom triggering schemes,
reading a buffer mid-experiment, etc.). It is reached as
soc.avg_bufs[readout_index].
from qick import *
soc = QickSoc()
buf = soc.avg_bufs[0]
# Set the downconversion frequency on the readout that feeds this buffer.
buf.set_freq(100.0, gen_ch=0) # MHz
# --- Accumulated (averaged) capture ---
buf.config_avg(address=0, length=1) # 1 accumulated point per trigger
buf.enable(avg=True, buf=False)
# ... run your tProc program, which fires `trigger` ...
iq = buf.transfer_avg(address=0, length=1) # -> shape (1, 2): [[I, Q]]
# --- Raw (decimated) capture ---
buf.config_buf(address=0, length=1024)
buf.enable(avg=False, buf=True)
# ... run your tProc program ...
iq = buf.transfer_buf(address=0, length=1024) # -> shape (1024, 2)
transfer_avg/transfer_buf return the data already reshaped as
(n_samples, 2) arrays of raw integers (not length-normalized or
offset-corrected – that’s what AcquireMixin._average_buf() and
AcquireMixin._ro_offset() do on top of this). Both work around a
known IP bug (fixed in newer IP versions, tracked by the driver’s
FIRST_OUT_SAMPLE_BUG_FIX flag) where the first sample of a transfer
always reads back as the sample at address 0, by silently requesting two
extra samples and discarding them.