Signal Generator v6 (SG-v6) - QICK Firmware
The Signal Generator v6 (SG-v6) is the newest version of QICK’s
on-chip waveform engine. It supersedes SG-v5 with a richer feature set,
lower latency, and full compatibility with all QICK-compatible boards
(ZCU216, ZCU111, etc.). The module lives in the qick firmware
repository under firmware/ip/axis_signal_gen_v6/ and is exposed to Python
through the qick.drivers.generator class.
1. General Description
The Signal Generator v6 (SG-v6) is the QICK waveform generation module
implemented in the signal_gen_top firmware entity. Its architecture combines:
A dual-port memory (BRAM) to store envelope samples.
A FIFO to queue 160-bit waveform descriptors.
A central
signal_genblock that reads the FIFO, accesses memory, and generates the output through N parallel DDS channels.
The module supports two synthesis-configurable operating modes:
DDS Mode (
GEN_DDS = "TRUE"): generates complex tones with upconversion.Baseband Mode (
GEN_DDS = "FALSE"): outputs only the envelope, without DDS.
And two envelope types:
COMPLEX (
ENVELOPE_TYPE = "COMPLEX"): separate memories for real and imaginary parts.REAL (
ENVELOPE_TYPE = "REAL"): real part only; the imaginary part is forced to zero.
Top-level block diagram: ctrl_sgv6 reads the descriptor FIFO and
drives the bram_tdp read address, the N_DDS-wide DDS array, and
the output mux/gain, while axis_to_bram_sg_nt independently loads
envelope samples into the same memory. The three-way mux (envelope ×
DDS product / raw DDS / raw envelope) matches sgv6.sv’s src_la
select – verified directly against the RTL, not just the block names.
2. Synthesis Parameters
Parameter |
Default Value |
Description |
|---|---|---|
|
16 |
Memory address bus size (number of bits). The effective table depth is 2^N samples per bank. |
|
16 |
Number of parallel DDS blocks and, therefore, samples emitted per clock
cycle on the |
|
|
|
|
|
|
3. Interface Ports
3.1 Clock and Reset
Signal |
Direction |
Description |
|---|---|---|
|
input |
Active-low reset for the main clock domain ( |
|
input |
Main clock. Drives the FIFO, the |
3.2 S0_AXIS – Envelope Sample Loading
Slave AXI-Stream interface to transfer envelope samples to the internal BRAM.
It operates in the s0_axis_aclk domain, which can be different from aclk.
Signal |
Direction |
Width |
Description |
|---|---|---|---|
|
input |
1 |
Active-low reset for the S0 domain. |
|
input |
1 |
S0 domain clock. Can differ from |
|
input |
32 |
Sample data. Bits [15:0] = real part; bits [31:16] = imaginary part
(only written if |
|
input |
1 |
Indicates valid data on the bus (standard AXI-Stream protocol). |
|
output |
1 |
Module indicates availability to receive data. |
3.3 S1_AXIS – Waveform Queue
Slave AXI-Stream interface to queue 160-bit waveform descriptors. Each descriptor defines the parameters for a playback burst.
Signal |
Direction |
Width |
Description |
|---|---|---|---|
|
input |
160 |
160-bit waveform descriptor. The |
|
input |
1 |
Indicates a valid descriptor. Connected directly to |
|
output |
1 |
Asserted when the internal FIFO is not full ( |
3.4 M_AXIS – Data Output
Master AXI-Stream interface that delivers generated samples to the DAC or the next block in the datapath.
Signal |
Direction |
Width |
Description |
|---|---|---|---|
|
input |
1 |
Consumer indicates it can accept data. |
|
output |
1 |
Module indicates that |
|
output |
|
Output sample bus. With default values ( |
3.5 Control Registers
Signal |
Direction |
Width |
Description |
|---|---|---|---|
|
input |
32 |
Starting address in BRAM where the |
|
input |
1 |
BRAM write enable. When asserted, the |
4. Internal Architecture
The signal_gen_top module instantiates and connects four main blocks:
4.1 FIFO (fifo_xpm)
Data width: 160 bits.
Depth: 16 entries.
Writing occurs directly from S1_AXIS (
fifo_wr_en = s1_axis_tvalid_i).The
fifo_fullflag controls thes1_axis_tready_osignal (backpressure).The
signal_genblock manages reading (fifo_rd_en).
4.2 Data Writer (data_writer)
Receives samples via S0_AXIS and writes them to the correct BRAM according to the
START_ADDR_REG and WE_REG registers. It generates bank enable signals
(mem_ena, an N_DDS-bit vector), address (mem_addra), and data (mem_dia).
4.3 BRAM Memories (bram_dp_xpm)
N_DDS BRAM instances are generated (one per DDS channel), each 16 bits wide and 2^N positions deep.
Port A (Write):
s0_axis_aclkdomain. Receives data from thedata_writer.Port B (Read):
aclkdomain. Thesignal_genprovides the addressmem_addrband receives data in parallel.
If ENVELOPE_TYPE = "COMPLEX", a second BRAM per channel is instantiated for
the imaginary part (data in bits [31:16] of the S0 bus). If ENVELOPE_TYPE = "REAL",
the mem_dob_imag signal is forced to zero.
Note
Both BRAMs (real and imaginary) of a channel share the same read address
mem_addrb and enable pin mem_ena[i], ensuring coherence between
real and imaginary parts.
4.4 Signal Generator (signal_gen)
The core of the module. It reads descriptors from the FIFO, generates memory
addresses, and produces output samples by combining the envelope read from BRAM
with the DDS blocks. The GEN_DDS parameter controls whether internal DDS
blocks are instantiated.
5. Data Flow Diagram
S0_AXIS ──► data_writer ──► BRAM_real[0..N_DDS-1] ──┐
BRAM_imag[0..N_DDS-1] ──┤
▼
S1_AXIS ──► fifo_xpm ──────────────────────────► signal_gen ──► M_AXIS
(+ DDS if GEN_DDS=TRUE)
Registers: START_ADDR_REG, WE_REG ──► data_writer
6. Python Usage (QICK)
Note
Earlier revisions of this section showed a direct-register API
(gen.set_freq(), gen.load_waveform(), tproc.load_wave(), raw
WPORT_WR assembly) that does not match the current qick_lib
driver/assembler – there is no qick.SignalGenerator class, and none
of those methods exist on the actual generator driver
(qick.drivers.generator.AxisSignalGen). The workflow below was
verified against qick.qick_asm/qick.asm_v2 in this repository.
soc.gens[i] gives you the low-level driver for generator array index
i – this is the same i you pass to declare_gen(ch=i, ...) and
add_pulse(ch=i, ...), and the same index QickProgram code
always uses. It is not simply “tProc channel - 1”: which tProc
waveform port and DAC each array index maps to is board-specific and, on
some boards, not a 1:1 mapping at all (see the worked ZCU216 example in
Firmware’s Worked Example: ZCU216 tpv2_std). Normal programs never need
to know this mapping – declare the channel once by its soc.gens[]
index, register any envelopes, add named pulses, then play them by name at
whatever time you choose.
6.1. Basic Configuration and Immediate Playback
A const pulse needs no envelope – the DDS output is gated on for
length at a fixed gain.
from qick import *
soc = QickSoc()
class BasicPulse(AveragerProgramV2):
def _initialize(self, cfg):
# gen array index 0 -- see /firmware for how this maps to a
# DAC on your specific board
self.declare_gen(ch=0, nqz=1)
self.add_pulse(ch=0, name="tone", style="const",
freq=100.0, phase=0, gain=0.5, length=1.0) # MHz, deg, -1..1, us
def _body(self, cfg):
self.pulse(ch=0, name="tone", t=0)
prog = BasicPulse(soccfg, reps=1, final_delay=1.0, cfg={})
prog.acquire(soc) # no readouts declared -> just runs the pulse once per rep
6.2. Playing a Shaped Pulse with an Envelope
Shaped pulses (Gaussian, DRAG, etc.) go through
QickProgram.add_envelope() – the I/Q samples are uploaded to the
generator’s internal BRAM once, and referenced by name from any number of
arb (or flat_top) pulses. add_envelope handles the
w_env/length bookkeeping for you.
from qick import *
import numpy as np
soc = QickSoc()
class GaussianPulse(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=0, nqz=1)
n_samples = 1024
sigma = n_samples / 6
idata = 30000 * np.exp(-0.5 * ((np.arange(n_samples) - n_samples/2) / sigma)**2)
self.add_envelope(ch=0, name="gauss", idata=idata.astype(np.int16))
self.add_pulse(ch=0, name="shaped", style="arb",
freq=100.0, phase=0, gain=0.9, envelope="gauss")
def _body(self, cfg):
self.pulse(ch=0, name="shaped", t=0)
prog = GaussianPulse(soccfg, reps=1, final_delay=1.0, cfg={})
prog.acquire(soc)
6.3. Multiple Pulses and the SG-v6 FIFO
The SG-v6 can queue up to 16 waveform descriptors in hardware (see
“FIFO Capacity” under §7 below); in software you just call QickProgram.pulse()
multiple times with different (or repeated) pulse names and times – the
compiler emits one WPORT_WR per call, and the SG-v6’s own FIFO/playback
logic handles queuing and back-to-back timing.
class MultiPulse(AveragerProgramV2):
def _initialize(self, cfg):
self.declare_gen(ch=0, nqz=1)
self.add_pulse(ch=0, name="long", style="const",
freq=100.0, phase=0, gain=0.5, length=2.0)
self.add_pulse(ch=0, name="short", style="const",
freq=105.0, phase=0, gain=0.5, length=0.5)
def _body(self, cfg):
self.pulse(ch=0, name="long", t=0)
self.pulse(ch=0, name="short", t=3.0) # us, relative to this shot's t=0
6.4. Descriptor Fields Reference
The 168-bit r_wave bus (written via WPORT_WR) contains six fields.
When using the tProcessor, these fields are written individually via w_freq,
w_phase, etc., or as a group via r_wave.
Field |
Register |
Bits |
Description |
|---|---|---|---|
Frequency |
|
32 |
DDS frequency control word (0 = DC, 2^31 = Nyquist) |
Phase |
|
32 |
DDS phase offset (2^32 = 360°) |
Gain |
|
32 |
Amplitude scaling (signed 32-bit, max 2^31-1) |
Envelope Start |
|
24 |
Start address in envelope memory (for shaped pulses) |
Length |
|
32 |
Number of samples to output (for shaped pulses) |
Configuration |
|
16 |
Mode, output selection, etc. (see SG-v6 source) |
Note
The 160-bit descriptor sent via S1_AXIS is packed internally by
Waveform.compile() (qick.asm_v2) from the fields you pass to
QickProgram.add_pulse()/QickProgram.add_envelope() – you
don’t need to build it by hand.
6.5. Additional Resources
For more examples (multi-channel playback, real-time frequency hopping, envelope chaining, advanced tProc sequencing), refer to:
QICK Tutorial Notebooks: 06 - Generators and Readouts, 07 - Advanced Generators and Readouts in tProc v2
tProcessor Documentation: tProcessor v2 - reference manual
Firmware Overview: Firmware
Community Examples: Check the #qick channel on the Unitary Fund Discord
7. Implementation Considerations
- Clock Domains:
The module contains two clock domains (
s0_axis_aclkandaclk). Domain crossing is resolved via dual-clock BRAMs. Ensure appropriate timing constraints are applied in the XDC file.- BRAM Read Latency:
The
bram_dp_xpminstances haveOUT_REG_ENA = 1, adding one cycle of latency to the read port to improve timing.- Output Bus Width:
The
m_axis_tdata_obus isN_DDS × 16bits wide. With the defaultN_DDS = 16, this equals 256 bits (32 bytes) per transaction.- Memory Depth:
With parameter
N = 16, each BRAM has 65,536 positions of 16 bits, equivalent to 128 KB per bank (real or imaginary).- FIFO Capacity:
The waveform FIFO has a capacity for 16 descriptors of 160 bits. If the producer is faster than the consumer, the
s1_axis_tready_osignal must be managed.
8. Default Parameters – Quick Summary
Configuration |
Value |
|---|---|
Samples per cycle (M_AXIS) |
16 ( |
Output bus width |
256 bits |
Table depth per bank |
65,536 samples ( |
FIFO capacity |
16 descriptors of 160 bits |
Envelope type |
Complex (I + Q) |
DDS Mode |
Enabled (upconversion) |
9. Relevant Source Files
signal_gen_top.v– Top module described in this document.signal_gen.v– DDS core and playback logic.data_writer.v– BRAM sample writing from AXI-Stream.fifo_xpm.v– Parameterizable FIFO based on Xilinx XPM primitives.bram_dp_xpm.v– Parameterizable dual-port BRAM (Xilinx XPM).