08 - Hardware Buffers: DDR4 and MR
Objective: Learn how to use the DDR4 buffer (for long-duration captures) and the MR (Multi-Rate) buffer (for fast, tProc-accessible captures). Understand when to use each buffer type.
1. Setup
[1]:
# Standard imports
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import logging
from qick import *
from qick.asm_v2 import AveragerProgramV2, QickSweep1D
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
logging.getLogger("qick_processor").setLevel(logging.WARNING)
# Connect to the board (adjust the path to your firmware)
BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS
soc = QickSoc(BITSTREAM_PATH)
soccfg = soc
# Define hardware channels
GEN_CH = 0
RO_CH = 0
2. Understanding Hardware Buffers
QICK provides two types of hardware buffers for capturing readout data:
Buffer |
Size |
Access |
Best for |
|---|---|---|---|
DDR4 Buffer |
Very large (up to ~1 second of data) |
Read from Python after experiment |
Long acquisitions, many repetitions |
MR Buffer |
Small (typically 8192 samples) |
Read by tProc during experiment |
Fast feedback, real-time decision making |
Key concepts:
Both buffers capture decimated IQ data (after downconversion and filtering)
You can enable one or both in the same experiment
Data is written continuously; you must manage buffer overflow
3. Capturing Data to DDR4 Buffer
The DDR4 buffer is ideal for capturing long sequences or many repetitions. It stores data in the board’s DDR4 memory, which can hold millions of samples.
[2]:
class DDR4Program(AveragerProgramV2):
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=1)
self.declare_readout(ch=ro_ch, length=cfg['ro_len'])
# Define loops (outer = gain sweep, inner = phase sweep)
self.add_loop("gain_loop", self.cfg["steps_gain"])
self.add_loop("phase_loop", self.cfg["steps_phase"])
# Define a Gaussian envelope
ramp_len = 0.2
self.add_gauss(ch=gen_ch, name="ramp",
sigma=ramp_len/10, length=ramp_len, even_length=True)
# Define a flat-top pulse with sweeps
self.add_pulse(ch=gen_ch, name="swept_pulse",
style="flat_top", envelope="ramp",
freq=cfg['freq'], ro_ch=ro_ch, length=cfg['flat_len'],
phase=QickSweep1D("phase_loop", 0, 360),
gain=QickSweep1D("gain_loop", 0.1, 1.0))
self.add_readoutconfig(ch=ro_ch, name="my_ro",
freq=cfg['freq'], gen_ch=gen_ch)
self.send_readoutconfig(ch=ro_ch, name="my_ro", t=0)
def _body(self, cfg):
self.pulse(ch=cfg['gen_ch'], name="swept_pulse", t=0)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],
ddr4=True, mr=False) # Enable DDR4, disable MR
# Configuration for DDR4 capture
config_ddr4 = {
'steps_gain': 4,
'steps_phase': 5,
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'trig_time': 0.25,
'ro_len': 0.35,
'flat_len': 0.05,
'ramp_len': 0.2
}
# Arm the DDR4 buffer before running
# Each transfer (burst) is 256 decimated samples
n_transfers = 10000 # Total samples = 256 * n_transfers
soc.arm_ddr4(ch=RO_CH, nt=n_transfers)
# Run the program
prog = DDR4Program(soccfg, reps=3, final_delay=1.0, cfg=config_ddr4, reps_innermost=True)
iq_list = prog.acquire(soc, rounds=1, progress=True)
# Read data from DDR4 buffer
# Request more samples than we expect to ensure we get everything
iq_ddr4 = soc.get_ddr4(250)
t_ddr4 = prog.get_time_axis_ddr4(RO_CH, iq_ddr4)
plt.figure(figsize=(12, 4))
plt.plot(t_ddr4, iq_ddr4[:,0], label='I')
plt.plot(t_ddr4, iq_ddr4[:,1], label='Q')
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('DDR4 Buffer Capture - Continuous IQ Trace')
plt.legend()
plt.grid(True)
plt.show()
print(f"Captured {len(iq_ddr4)} IQ samples from DDR4 buffer")
print(f"Total duration: {t_ddr4[-1]:.2f} µs")
Captured 31599 IQ samples from DDR4 buffer
Total duration: 102.86 µs
Explanation:
arm_ddr4(ch, nt)prepares the buffer.ntis the number of transfers (each transfer = 256 decimated samples)Setting
ddr4=Trueintrigger()enables DDR4 captureget_ddr4(n)reads data from the buffer. Request extra samples to ensure you get everythingThe data is continuous across all shots and sweep steps, showing the loop order
4. Capturing Data to MR Buffer
The MR (Multi-Rate) buffer is smaller but accessible to the tProc, making it useful for feedback and fast processing.
[3]:
class MRBufferProgram(AveragerProgramV2):
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=1)
self.declare_readout(ch=ro_ch, length=cfg['ro_len'])
# Define a simple constant pulse
self.add_pulse(ch=gen_ch, name="test_pulse",
style="const",
freq=cfg['freq'], ro_ch=ro_ch, length=cfg['pulse_len'],
phase=0, gain=1.0)
self.add_readoutconfig(ch=ro_ch, name="my_ro",
freq=cfg['freq'], gen_ch=gen_ch)
self.send_readoutconfig(ch=ro_ch, name="my_ro", t=0)
def _body(self, cfg):
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
# Enable MR buffer only (ddr4=False, mr=True)
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],
ddr4=False, mr=True)
# Configuration
config_mr = {
'gen_ch': GEN_CH,
'ro_ch': RO_CH,
'freq': 500,
'trig_time': 0.25,
'ro_len': 0.35,
'pulse_len': 0.05
}
# Arm the MR buffer
soc.arm_mr(ch=RO_CH)
# Run the program
prog = MRBufferProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mr)
iq_list = prog.acquire(soc, rounds=1)
# Read data from MR buffer
iq_mr = soc.get_mr()
t_mr = prog.get_time_axis_mr(RO_CH, iq_mr)
plt.figure(figsize=(12, 4))
plt.plot(t_mr, iq_mr[:,0], label='I')
plt.plot(t_mr, iq_mr[:,1], label='Q')
plt.plot(t_mr, np.abs(iq_mr.dot([1,1j])), label='Magnitude')
plt.xlabel('Time (µs)')
plt.ylabel('Amplitude (a.u.)')
plt.title('MR Buffer Capture')
plt.legend()
plt.grid(True)
plt.show()
print(f"Captured {len(iq_mr)} IQ samples from MR buffer")
ro_cfg = soccfg['readouts'][RO_CH]
mr_key = [k for k in ro_cfg.keys() if 'mr' in k.lower() or 'buf' in k.lower()]
print(f"MR-related config keys: {mr_key}")
for k in mr_key:
print(f" {k}: {ro_cfg[k]}")
Captured 8192 IQ samples from MR buffer
MR-related config keys: ['buf_maxlen', 'avgbuf_version', 'avgbuf_revision', 'avgbuf_type', 'avgbuf_fullpath']
buf_maxlen: 4096
avgbuf_version: 1.2
avgbuf_revision: 2
avgbuf_type: axis_avg_buffer
avgbuf_fullpath: axis_avg_buffer_0
Explanation:
arm_mr(ch)prepares the MR bufferSetting
mr=Trueintrigger()enables MR captureget_mr()reads data from the bufferThe MR buffer is smaller but faster and accessible to the tProc
5. Using Both Buffers Simultaneously
You can capture to both buffers at the same time. DDR4 provides long-duration storage, while MR provides fast tProc access.
[4]:
class BothBuffersProgram(AveragerProgramV2):
def _initialize(self, cfg):
ro_ch = cfg['ro_ch']
gen_ch = cfg['gen_ch']
self.declare_gen(ch=gen_ch, nqz=1)
self.declare_readout(ch=ro_ch, length=cfg['ro_len'])
self.add_pulse(ch=gen_ch, name="test_pulse",
style="const",
freq=cfg['freq'], ro_ch=ro_ch, length=cfg['pulse_len'],
phase=0, gain=1.0)
self.add_readoutconfig(ch=ro_ch, name="my_ro",
freq=cfg['freq'], gen_ch=gen_ch)
self.send_readoutconfig(ch=ro_ch, name="my_ro", t=0)
def _body(self, cfg):
self.pulse(ch=cfg['gen_ch'], name="test_pulse", t=0)
# Enable both buffers
self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],
ddr4=True, mr=True)
# Arm both buffers
soc.arm_ddr4(ch=RO_CH, nt=10000)
soc.arm_mr(ch=RO_CH)
# Run 10 repetitions to fill buffers
prog = BothBuffersProgram(soccfg, reps=10, final_delay=0.5, cfg=config_mr)
iq_list = prog.acquire(soc, rounds=1)
# Read both buffers
iq_ddr4 = soc.get_ddr4(500)
iq_mr = soc.get_mr()
t_ddr4 = prog.get_time_axis_ddr4(RO_CH, iq_ddr4)
t_mr = prog.get_time_axis_mr(RO_CH, iq_mr)
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
axes[0].plot(t_ddr4, iq_ddr4[:,0], label='I (DDR4)')
axes[0].plot(t_ddr4, iq_ddr4[:,1], label='Q (DDR4)')
axes[0].set_xlabel('Time (µs)')
axes[0].set_ylabel('Amplitude (a.u.)')
axes[0].set_title('DDR4 Buffer - Long Capture')
axes[0].legend()
axes[0].grid(True)
axes[1].plot(t_mr, iq_mr[:,0], label='I (MR)')
axes[1].plot(t_mr, iq_mr[:,1], label='Q (MR)')
axes[1].set_xlabel('Time (µs)')
axes[1].set_ylabel('Amplitude (a.u.)')
axes[1].set_title('MR Buffer - Single Shot Detail')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.show()
print(f"DDR4 captured {len(iq_ddr4)} samples")
print(f"MR captured {len(iq_mr)} samples")
DDR4 captured 63599 samples
MR captured 8192 samples
6. Analyzing Buffer Data with FFT
You can perform frequency domain analysis on buffer data to examine signal spectra.
[5]:
from numpy.fft import fft, fftfreq, fftshift
import numpy as np
iq_mr = soc.get_mr()
# MR samples are at the full readout rate, the same rate get_time_axis_mr() uses
fs_mr = soccfg['readouts'][RO_CH]['fs'] # MHz
# Treat as true complex only if hardware produces quadrature —
# here col 0 and col 1 are two real ADC channels, so keep separate
signal_i = iq_mr[:, 0].astype(float)
signal_q = iq_mr[:, 1].astype(float)
# Remove DC
signal_i -= signal_i.mean()
signal_q -= signal_q.mean()
# Form complex and apply Hann window
N = len(signal_i)
window = np.hanning(N)
complex_signal = (signal_i + 1j * signal_q) * window
# FFT
spectrum = fftshift(fft(complex_signal))
freqs = fftshift(fftfreq(N, d=1/fs_mr)) # MHz
psd = np.abs(spectrum)**2 / (fs_mr * np.sum(window**2)) # proper normalization
plt.figure(figsize=(12, 5))
plt.semilogy(freqs, psd)
plt.xlabel('Frequency (MHz)')
plt.ylabel('Power Spectral Density')
plt.title(f'MR Buffer Spectrum (fs = {fs_mr} MHz, N = {N})')
plt.grid(True)
plt.tight_layout()
plt.show()
# Report peaks excluding DC
dc_mask = np.abs(freqs) > 10
peak_idx = np.argmax(psd[dc_mask])
peak_freq = freqs[dc_mask][peak_idx]
peak_power = psd[dc_mask][peak_idx]
# Find image peak (opposite sign)
image_mask = (np.abs(freqs) > 10) & (np.abs(freqs + peak_freq) < 5)
image_power = np.max(psd[image_mask]) if image_mask.any() else None
print(f"Signal peak: {peak_freq:+.3f} MHz")
print(f"Image peak: {-peak_freq:+.3f} MHz (expected conjugate)")
if image_power:
imbalance_db = 10 * np.log10(peak_power / image_power)
print(f"Image rejection: {imbalance_db:.1f} dB (higher = better IQ balance)")
print(f"\nAliased from: {peak_freq + fs_mr:.3f} or {peak_freq + 2*fs_mr:.3f} MHz")
Signal peak: -999.000 MHz
Image peak: +999.000 MHz (expected conjugate)
Image rejection: 40.0 dB (higher = better IQ balance)
Aliased from: 1458.600 or 3916.200 MHz
7. Buffer Management Tips
Buffer Sizing:
DDR4:
nt= number of transfers. Each transfer = 256 decimated samplesMR: Fixed size (varies by firmware, typically 8192 samples)
Both buffers are circular; old data is overwritten
Estimating buffer needs:
# For a program with:
# - reps = N
# - sweep_steps = M
# - readout_length = L (samples)
# Total samples = N * M * L
# Transfers needed = ceil(total_samples / 256)
Best practices:
Arm buffers before running the program - They start capturing immediately
Request extra samples when reading - Use
get_ddr4(n)with n larger than expectedUse ``get_time_axis_*`` methods - They compute the correct time axis for each buffer type
Clear buffers between experiments - Old data may persist
[6]:
# Example: Clearing buffers between experiments
def clear_buffers(soc, ro_ch):
"""Clear DDR4 and MR buffers by re-arming them with 0 transfers."""
soc.arm_ddr4(ch=ro_ch, nt=0)
soc.arm_mr(ch=ro_ch)
print("Buffers cleared")
# clear_buffers(soc, RO_CH)
8. Summary
You have learned:
The difference between DDR4 and MR buffers
How to capture data to the DDR4 buffer for long acquisitions
How to capture data to the MR buffer for fast, tProc-accessible data
How to use both buffers simultaneously
How to perform frequency analysis on buffer data
Best practices for buffer management
When to use each buffer:
DDR4: Long acquisitions, many repetitions, post-processing analysis
MR: Fast feedback, real-time decisions, single-shot analysis
Both: When you need both long-term storage and real-time access
Next steps: Proceed to `09_Appendix_Tips_And_Limits.ipynb <./09_Appendix_Tips_And_Limits.ipynb>`__ for a reference guide on common pitfalls, hardware limits, and debugging tips.