Dynamic Readouts (axis_readout_v3 / axis_dyn_readout_v1) - QICK Firmware
axis_readout_v3 and axis_dyn_readout_v1 are the dynamic counterparts
of Readout v2 (axis_readout_v2) - QICK Firmware: digital down-converter (DDC) IP blocks that sit
between the RFDC’s ADC output and Averager + Buffer (axis_avg_buffer) - QICK Firmware, but whose
frequency/phase/output-selection/length are set by the tProcessor, in real
time, once per shot, instead of being written ahead of time as PYNQ
registers from software. Both modules live under firmware/ip/ (in
axis_readout_v3/ and axis_dyn_readout_v1/ respectively) and are
exposed to Python through qick.drivers.readout.AxisReadoutV3 and
AxisDynReadoutV1.
They are covered together on one page because they share the same control
architecture (a nearly-identical FSM, ctrl.sv/ctrl_dyn_ro_v1.sv, and
the same tProc-driven configuration mechanism) and the same Python-driver
pattern (AbsDynReadout); the numeric parameters of their datapaths
(samples/clock, decimation ratio, rounding) differ, and those differences are
called out explicitly below.
1. General Description
Both blocks perform the same three-stage job as axis_readout_v2
(down-convert with an on-chip DDS, FIR low-pass filter, decimate), but they
have no AXI-Lite control interface at all – neither module’s top-level
port list (axis_readout_v3.v, axis_dyn_readout_v1.v) includes an
S_AXI/register bus. Instead, each block exposes an extra AXI-Stream slave
port, s0_axis, driven directly by a tProcessor output port. The
tProcessor pushes one 88-bit configuration word per s0_axis transaction;
the block’s control FSM decodes that word into DDS frequency/phase and
readout-length/output-select settings and applies them immediately. This is
the hardware side of what QickProgram.declare_readout() calls a
readout “controlled by tProc” (see 5. Python Interface below) –
configuration happens per-shot, at run time, as part of the tProc program
itself, rather than once via a PYNQ register write before the program starts.
axis_readout_v3 – the dynamic counterpart of
axis_readout_v2’s datapath: 4 real ADC samples/clock in, FIR decimation by 4, 1 complex sample/clock out (m_axis, the only output).axis_dyn_readout_v1 – a newer, wider block: 8 real ADC samples/clock in (
N_DDS=8), FIR decimation by 8. Unlike v3, it exposes two outputs:m0_axis, the down-converted-but-not-yet-decimated stream (8 complex samples/clock, still subject to the output-selection mux), andm1_axis, the final decimated stream (1 complex sample/clock) that feeds Averager + Buffer (axis_avg_buffer) - QICK Firmware.
Both are single-tone readouts (one DDS-driven downconversion frequency at a
time per channel) – like axis_readout_v2 and unlike the PFB-based
readouts, they do not channelize multiple simultaneous tones.
Block diagram of readout_top_dynro (axis_dyn_readout_v1 is a
thin pass-through wrapper around it, so its ports are effectively the
IP’s own). The ADC stream is down-converted once and fans out to both
outputs: straight to M0_AXIS for the pre-decimation view, and
through fir_decim8_dynro to M1_AXIS for the decimated stream
that feeds Averager + Buffer (axis_avg_buffer) - QICK Firmware. fifo_sync carries the tProc-pushed
config descriptor (freq/phase/mode) into down_conversion_dynro.
2. Synthesis Parameters
Parameter |
axis_readout_v3 |
axis_dyn_readout_v1 |
Notes |
|---|---|---|---|
Input samples/clock ( |
4 |
8 |
Parallel real ADC lanes into the down-conversion multiplier; also the
number of DDS instances ( |
FIR decimation ratio |
4 |
8 |
Xilinx FIR Compiler 7.2, |
FIR coefficients |
107 taps ( |
120 taps ( |
Symmetric low-pass, fixed/compiled into the bitstream (not runtime-loadable). |
FIR coefficient/data width |
16 bits |
16 bits |
|
FIR output width (per I/Q component) |
18 bits |
16 bits |
See 3.6 Rounding and the I/Q DC offset for how each top level turns this into the final 16-bit I/Q. |
DDS phase/output width |
32-bit phase, 16-bit output |
32-bit phase, 16-bit output |
Same on both ( |
DDS latency |
10 cycles |
8 cycles |
IP-configured pipeline latency of the DDS compiler core. |
Config-descriptor FIFO depth |
8 entries |
8 entries |
|
Config descriptor width |
88 bits |
88 bits |
3. Datapath
3.1 axis_readout_v3
axis_readout_v3.v instantiates a single 88-bit-wide, 8-deep FIFO (fed by
s0_axis) and one down_conversion_fir block:
down_conversion (
down_conversion.sv) – reads one descriptor at a time out of the FIFO via an internalctrlFSM (see 3.3 Shared control FSM (ctrl.sv / ctrl_dyn_ro_v1.sv)), drives 4 paralleldds_0DDS cores from it, and complex-multiplies each of the 4 real ADC samples/clock (s1_axis_tdata) against its DDS output. Latency-matched shift registers (latency_reg) keep the raw-input and DDS-only paths aligned in time with the product path so all three can be muxed together.down_conversion_fir (
down_conversion_fir.sv) – feeds the 4 parallel 32-bit (16+16 I/Q) product-domain samples/clock intofir_0(decimation by 4), producing one complex sample/clock onm_axis.
down_conversion_fir.sv documents the block’s overall latency in comments:
16 cycles through down_conversion, 114 more through the FIR.
3.2 axis_dyn_readout_v1
axis_dyn_readout_v1.v wraps a readout_top module, which instantiates
the same FIFO (88 bits x 8 deep) and a down_conversion_fir block built
from 8-lane versions of the same pieces:
down_conversion (
down_conversion.v) – same structure as v3’s (sharedctrl_dyn_ro_v1FSM, per-lane DDS + complex multiply), but withN_DDS=8lanes and no latency-matching shift registers on the DDS/input mux paths (the mux paths are only delay-matched by fixed pipeline register counts, not by a parametrizedlatency_regblock as in v3). This stage’s output is exposed directly asm0_axis(8 complex samples/clock, pre-filter/pre-decimation, still passed through the output-selection mux).fir_compiler_0, wrapped by
down_conversion_fir.v– an 8x decimation, multi-rate FIR core (Number_Paths=2, one path per I/Q component) that consumesm0_axisdirectly and produces the final 1-complex-sample/clock stream onm1_axis.
3.4 Configuration descriptor (the s0_axis word)
Both ctrl.sv and ctrl_dyn_ro_v1.sv carry an identical header comment
describing the 88-bit s0_axis_tdata/FIFO word layout:
Field |
Bits |
Description |
|---|---|---|
(reserved) |
|
Unused (“xxxx” in the source comment). |
|
|
1: reset the phase-coherent DDS accumulator when this descriptor is loaded. 0: keep accumulating phase from the previous descriptor. |
|
|
0: hold this descriptor’s settings for |
|
|
Output source select (see 3.5 Output source selection). |
|
|
Number of clock cycles (not decimated output samples) this descriptor’s settings are held for, in one-shot mode. |
|
|
DDS phase offset. |
|
|
DDS tuning word (downconversion frequency). |
This is the same freq/phase/nsamp/outsel/mode field set as
axis_readout_v2’s register-composed descriptor (see
Readout v2 (axis_readout_v2) - QICK Firmware’s 83-bit FIFO word), plus one extra field, phrst, that
axis_readout_v2 does not have – and it arrives as a single AXI-Stream
push from the tProcessor rather than being assembled from five separate
PYNQ registers and committed with a WE_REG pulse.
3.5 Output source selection
Both blocks’ output multiplexers (dout_mux in down_conversion.sv /
down_conversion.v) use the same encoding for the 2-bit outsel field:
|
Value |
Description |
|---|---|---|
0 |
product |
The demodulated signal: ADC input x DDS (the normal downconverted output). |
1 |
dds |
The raw local-oscillator (DDS) tone, delay-matched – for calibration or debugging. |
2 |
input |
Bypass: the raw real ADC sample (delay-matched), with the imaginary part forced to zero. |
3 |
zero |
Always outputs zero. |
This matches the outsel encoding used by QickProgram.cfg2reg() for
tProc-v2 pulses/readout configs ("product"/"dds"/"input"/"zero"
-> 0/1/2/3) – the same field format is shared between signal generators and
these dynamic readouts.
3.6 Rounding and the I/Q DC offset
The two blocks round their internal fixed-point products differently, and
this shows up directly in the Python driver’s IQ_OFFSET constant
(AbsReadout.IQ_OFFSET, used by AcquireMixin() for offset
correction):
axis_readout_v3 (
down_conversion.sv): the complex-multiply result is truncated with a plain bit-slice (prod_y_real_round[i] = prod_y_real[i][30 -: 16], no rounding correction added), and the FIR’s own output rounding mode isSymmetric_Rounding_to_Zero(18-bit output). The top level (down_conversion_fir.sv) then takes bits[16:1]of each 18-bit FIR output component to form the final 16-bit I/Q. This truncation-based chain is why the Python driver setsIQ_OFFSET = -0.5forAxisReadoutV3.axis_dyn_readout_v1 (
down_conversion.v): the complex-multiply explicitly adds a rounding constant before truncating (py_round_real[i] = py_full_real_r[i] + RND_0P5whereRND_0P5 = 2**15, then keeps bits[31:16]), and the FIR core’s output width is configured to 16 bits directly (no further truncation at the top level –m1_axis_tdata_ois wired straight fromfir_compiler_0’s output). This explicit rounding is consistent with the Python driver’sIQ_OFFSET = 0.0forAxisDynReadoutV1.
4. Why There’s No Register Map
Unlike axis_readout_v2 (Readout v2 (axis_readout_v2) - QICK Firmware), neither axis_readout_v3
nor axis_dyn_readout_v1 has an AXI-Lite slave port in its top-level
Verilog – there is no FREQ_REG/PHASE_REG/NSAMP_REG/
OUTSEL_REG/MODE_REG/WE_REG set to document, and no register map
section to write for this page. The blocks’ only I/O is AXI-Stream:
Port |
Direction |
Width |
Description |
|---|---|---|---|
|
slave |
88 bits |
Configuration descriptors, pushed by the tProcessor (see
3.4 Configuration descriptor (the s0_axis word)). Backpressured by the 8-entry FIFO
( |
|
slave |
4x16 bits |
ADC input, 4 real samples/clock. |
|
master |
32 bits |
Decimated complex (I, Q) output, 1 sample/clock, to Averager + Buffer (axis_avg_buffer) - QICK Firmware. |
Port |
Direction |
Width |
Description |
|---|---|---|---|
|
slave |
88 bits |
Configuration descriptors, pushed by the tProcessor. |
|
slave |
8x16 bits |
ADC input, 8 real samples/clock. |
|
master |
8x32 bits |
Down-converted output, before FIR/decimation, 8 complex samples/clock. |
|
master |
32 bits |
Decimated complex (I, Q) output, 1 sample/clock, to Averager + Buffer (axis_avg_buffer) - QICK Firmware. |
Because there is no register interface, there is also no PYNQ overlay driver in the usual sense – see 5. Python Interface.
5. Python Interface
5.1 No PYNQ driver: AbsDynReadout
Every other readout driver in qick.drivers.readout
(AxisReadoutV2, AxisPFBReadoutV2/V3/V4) inherits from SocIP,
which itself inherits from PYNQ’s DefaultIP and is matched to firmware
blocks by IP-registry bindto strings (e.g.
bindto = ['user.org:user:axis_readout_v2:1.0', ...]) when
Overlay.load() scans the bitstream. That mechanism only works for blocks
that expose AXI-Lite registers.
AxisReadoutV3 and AxisDynReadoutV1 instead inherit from
AbsDynReadout(AbsReadout, DummyIP) – neither class defines a
bindto, so PYNQ’s overlay loader never instantiates them automatically.
DummyIP (qick.ip.DummyIP) exists specifically for “firmware IP blocks
without register access” – it just swallows the constructor’s
description argument so the class can still fit into QickIP’s
__init__ chain. As the source comments in both
AbsDynReadout/AxisReadoutV3/AxisDynReadoutV1 put it: “This isn’t
a PYNQ driver, since the block has no registers for PYNQ control. We still
need this class to represent the block and its connectivity.”
Because these objects never get created by the overlay loader, something
else has to create them. That happens in
AxisAvgBuffer.configure_connections() (qick.drivers.readout): when an
axis_avg_buffer traces its upstream s_axis connection back to an
axis_readout_v3 or axis_dyn_readout_v1 block, it manually constructs
the matching AxisReadoutV3/AxisDynReadoutV1 instance and calls its
configure_connections() – the comment there reads “the dynamic readout
blocks have no registers, so they don’t get PYNQ drivers, so we initialize
them here.” AbsDynReadout.configure_connections() also traces the
block’s s0_axis connection back through the tProcessor (or an
intervening axis_tmux_v1 time-multiplexer) to determine which tProc
output port (tproc_ctrl, and optionally tmux_ch) drives this
readout’s configuration – this is what lets software later target the right
tProc port when it sends a configuration.
5.2 declare_readout(): static vs. dynamic
QickProgram.declare_readout() (qick.qick_asm) is where this
distinction surfaces at the program-authoring level. It looks at whether the
readout’s config dict has a tproc_ctrl entry (set by
AbsDynReadout.configure_connections() above, absent for
axis_readout_v2/PFB readouts):
if 'tproc_ctrl' not in ro_cfg: # readout is controlled by PYNQ
if freq is None:
raise RuntimeError("readout %d is static (PYNQ-configured) - "
"frequency must be set in declaration"%(ch))
cfg['freq'] = freq
cfg['gen_ch'] = gen_ch
cfg['ro_config'] = self.soccfg.calc_ro_regs(ro_cfg, phase, sel)
else: # readout is controlled by tProc
if phase!=0 or sel!='product' or freq is not None or gen_ch is not None:
raise RuntimeError("readout %d is dynamic (tProc-configured) - "
"freq/phase/sel parameters are set using tProc "
"instructions, not in declaration"%(ch))
For a dynamic readout, declare_readout() only reserves the channel
(readout length, edge-counting, weights) and deliberately refuses to accept
freq/phase/sel/gen_ch – those are set later, per-shot, by
tProc instructions, not once at declaration time.
5.3 Setting frequency/phase/outsel from a tProc v2 program
On tProc v2 (qick.asm_v2.QickProgramV2), the program builds one
ReadoutManager per dynamic readout channel
(self._ro_mgrs = [ReadoutManager(self, iCh) if 'tproc_ctrl' in ch else None
...]). Two program methods drive it:
QickProgramV2.add_readoutconfig()(ch,name,freq,phase,outsel,phrst, …) computes the register values (DDS frequency/phase words, and a packedoutsel/mode/phrstconfiguration word viacfg2reg) and stores them as a named “pulse” in the program.QickProgramV2.send_readoutconfig()(ch,name,t) emits the instructions (ConfigReadout) that push that pre-computed word to the readout’stproc_ctrlport at timet, using the sameWPORT_WRwave-memory-write mechanism used to schedule signal-generator pulses (see Signal Generator v6 (SG-v6) - QICK Firmware’s tProc-sequencing section). This is the run-time source of thes0_axisdescriptor word described in 3.4 Configuration descriptor (the s0_axis word).
from qick import *
soc = QickSoc()
prog = QickProgramV2(soc)
ro_ch = 0
prog.declare_readout(ch=ro_ch, length=1.0) # no freq/phase/sel here
# Define the readout config once, up front.
prog.add_readoutconfig(ch=ro_ch, name="myro", freq=100.0, phase=0, outsel="product")
# ... later, inside the timed part of the program ...
prog.send_readoutconfig(ch=ro_ch, name="myro", t=0) # push freq/phase/outsel to the readout
prog.trigger(ros=[ro_ch], t=0.5) # arm the buffer to capture
tProc v1 programs (qick.asm_v1) have an equivalent ReadoutManager and
follow the same two-step (build descriptor, then instruction-schedule it)
pattern; the descriptor format on the wire is identical since it’s produced
by the same firmware control FSM.