tProcessor v1 (axis_tproc64x32_x8) - reference manual
The tProcessor v1 (axis_tproc64x32_x8) is the original QICK real-time
co-processor: an 8-channel, 64-bit-instruction / 32-bit-register core that
runs a user-written program to sequence pulses, trigger readouts, and react
to feedback with cycle-level timing. The module lives in the qick
firmware repository under firmware/ip/axis_tproc64x32_x8_v1/ and is
exposed to Python through qick.drivers.tproc.AxisTProc64x32_x8; programs
are written with qick.asm_v1.QickProgram.
Note
This is legacy hardware. tProc v1 is still real, supported hardware –
many older QICK boards run it, and this page documents it accurately for
that reason – but it is not the recommended starting point for a new
design. New designs should use the tProcessor v2 (qick_processor); see
tProcessor v2 - reference manual.
A few concrete, RTL-verified differences that explain why v2 superseded v1:
Instruction width / program memory. v1 instructions are 64 bits, and with the default
PMEM_N=16parameter the usable program memory is 213 = 8192 instructions (64 KB) – see 3.1. Program Memory and Program Counter for how that number is derived from the RTL. v2 instructions are 72 bits, with program memory depth set independently by thePMEM_AWparameter (default 28 = 256 words; production designs typically configure much more).Register organization. v1 has a single flat, paged register file: 8 pages x 32 general-purpose 32-bit registers (256 registers total, register 0 hardwired to 0 on every page), and no separate special-purpose register bank – see 3.3. Register File. v2 splits registers into three purpose-built banks (general-purpose
DREG, special-functionSREG, and waveform-parameterWREG) described in 4. Complete Register Bank Reference of the v2 manual.Instruction set richness. v1 has a fixed set of ~20 instructions (see 4. Instruction Set) covering arithmetic, bitwise ops, memory, stack, and timed output – there is no hardware division, no multiply-accumulate unit, no LFSR, and no subroutine
CALL/RET(only an 8-bit-deep, 256-word return/data stack shared for bothpushi/popiand loop bookkeeping). v2 adds a division unit, a multiply-accumulate unit, an LFSR, and a 256-deep PC-call stack (see the v2 manual’s 2. Quick Reference Card).
1. General Description
axis_tproc64x32_x8 (top-level RTL: axis_tproc64x32_x8.v) wraps the
core datapath (tproc64x32_x8.v) together with an AXI-Lite configuration
slave (axi_slv_custom.sv) and a data-memory arbiter (data_mem/) that
lets the PS access the tProc’s data memory both by single AXI-Lite
read/write and by bulk AXI-Stream DMA.
The core itself (tproc64x32_x8.v) is a single-issue, microcoded processor
built from separate blocks for each function:
Control FSM (
ctrl.sv) – a one-hot state machine that fetches, decodes, and sequences every instruction (most instructions take 2-4 clock cycles; see 4. Instruction Set). The instruction-format comments at the top ofctrl.svare the authoritative bit-level encoding reference and were cross-checked against theinstructionsdictionary inqick_lib/qick/asm_v1.pyfor this page.Register file (
regfile/regfile_8p.vhd+regfile.vhd) – 8 independent 32-register banks (“pages”), selected by a 3-bit page field in the instruction; see 3.3. Register File.ALU (
alu/alu.v+math.vhd+bitw.vhd) – bitwise and arithmetic operations on 32-bit operands, plus a second, independent adder (intproc64x32_x8.v, not a separate file) dedicated to computing absolute output times for the timed-instruction dispatcher.Stack (
stack.vhd) – a single 256-word x 32-bit BRAM-backed push/pop stack, used bypushi/popi(and internally byloopnzfor its decrement-and-branch loop counter).Per-channel timed-output dispatcher (
fifo/fifo.vhd+timed_ictrl.vhd, instantiated 8x, one per output channel) – see 3.6. Timed Output Port Mechanism.Data memory (
bram_dpinstance inaxis_tproc64x32_x8.v+ arbiter indata_mem/) – a dual-port, 32-bit BRAM shared between the core’s ownmemr/memwinstructions and PS access; see 3.8. Data Memory.
The processor has:
8 timed output channels (
m1_axis..m8_axis, 160 bits wide each) – typically wired to signal generators and/or trigger/marker pins. A 9th port pair (s0_axis/m0_axis, 32 bits) is dedicated to the data-memory DMA path, not to timed output.4 input channels (
s1_axis..s4_axis, 64 bits wide each) – typically wired to readout feedback data, read into a register with thereadinstruction.
2. Synthesis Parameters
Parameter |
RTL Default |
Description |
|---|---|---|
|
16 |
Program-memory address width, in the sense of the |
|
10 |
Data-memory address width (log2 of the number of 32-bit words). With
the default value, data memory is 210 = 1024 samples (4 KB).
Exposed to Python as |
Note
An older, public copy of the firmware overview (main_qick’s
docs/firmware.rst, explicitly flagged by that page’s own PDF link as
possibly out of date) cites “4096 samples, 16 KB” for the tProc v1 data
memory – inconsistent with the DMEM_N=10 default recorded in this
repository’s component.xml. This is most likely a different
per-board synthesis configuration (DMEM_N is a generic, so it can
legitimately differ between board builds), not an error in either
document, but it could not be verified from source in this session.
Always read soc.tproc.cfg['dmem_size'] at runtime rather than
assuming a number. By contrast, that same older document’s “8k
instructions / 64 KB” program-memory figure and “256 samples / 1 KB”
stack-size figure both match this session’s RTL analysis exactly (see
3.1. Program Memory and Program Counter and 3.3. Register File).
The AXI-Lite configuration slave (axi_slv_custom.sv) additionally has a
fixed, non-parameterized NREG = 64 (64 x 32-bit = 256 bytes) reserved for
registers at the bottom of its address space; see 5. Register Map.
3. Datapath / Architecture
3.1. Program Memory and Program Counter
The program counter (pc_r in tproc64x32_x8.v) is a 16-bit register
that increments by 1 each fetch (pc_i = pc_r + 1) or is loaded with a
jump target for loopnz/condj. Instructions are 64 bits wide and
fetched over a dedicated pmem_do bus.
The core’s pmem_addr output is pc_r directly (matching the
PMEM_N-bit port width when PMEM_N=16). However, the top-level
wrapper (axis_tproc64x32_x8.v) then re-derives the external memory
address as:
assign pmem_addr = {pmem_addr_int[PMEM_N-4:0], 3'b000}; // x8 (64-bit -> byte)
This keeps only the low (PMEM_N-3) bits of the program counter (13 bits
for the default PMEM_N=16) and left-shifts them by 3 to form a
byte-oriented address for an 8-byte-wide (64-bit) instruction word, so the
result still fits in a PMEM_N-bit bus. The top 3 bits of the 16-bit pc_r
are simply never forwarded to the external memory address. This is
confirmed by the testbench’s program-memory BRAM instantiation
(src/tb/tb.sv), which drives its address port with
{3'b000, pmem_addr[PMEM_N-1:3]} – i.e. the same low 13 bits, zero-padded
back up to 16. The practical consequence: with the default ``PMEM_N=16``,
only 213 = 8192 64-bit instructions (64 KB) of program memory are
actually addressable**, regardless of the nominal 16-bit range of the PC.
This matches the driver’s own comment that PMEM_N is merely “the log2 of
the number of 64-bit words, though the actual memory is usually smaller”
(qick_lib/qick/drivers/tproc.py), and matches the “8k instructions / 64
KB” figure in the older public firmware overview.
3.2. Instruction Fetch / Decode (Control FSM)
ctrl.sv implements a one-hot FSM with states for each instruction’s
multi-cycle sequence (e.g. MATHI0_ST .. MATHI3_ST for a 4-cycle
immediate math op, SETI0_ST/SETI1_ST for a 2-cycle timed-output
write). Most instructions take 2-4 clock cycles to decode, operate, and
write back; instructions that push into a per-channel output FIFO
(seti/set/setbi/setb, waiti/wait) additionally stall
in a wait state if that channel’s FIFO is full (or, for the blocking
setbi/setb/wait/waiti forms, until the dispatcher
acknowledges).
3.3. Register File
The register file (regfile/regfile_8p.vhd, wrapping 8 instances of
regfile.vhd) provides 8 pages x 32 registers = 256 total 32-bit
registers, selected by a 3-bit page field (instruction bits 55:53) and
5-bit register-address fields (e.g. bits 40:36 for ra). It has 7
parallel read ports and 1 write port, with one clock cycle of read latency;
the page-select input itself is pipelined by one cycle to stay aligned with
the muxed read data. Register 0 on every page is hardwired to always read
0 (regfile.vhd: if unsigned(addrN) = 0 then doutN <= (others => '0')),
which the Python driver and assembler exploit as a convenient “discard” /
“always zero” operand (e.g. in QickProgram’s pulse-register allocation,
unused fields of a set call are pointed at register 0 of the page).
3.4. ALU and Time Adder
alu.v combines two sub-blocks, muxed by the top bit of the 4-bit
oper field (delayed 3 cycles to match each sub-block’s own pipeline
latency):
bitw.vhd(oper[3]=0): and / or / xor / not / shift-left / shift-right.math.vhd(oper[3]=1): add / subtract / multiply.
A second, separate adder in tproc64x32_x8.v (alut_a + alut_b, not
part of alu.v) computes absolute output times for timed instructions:
it adds the free-running 48-bit master clock (t_cnt_sync, the
synchronization point most recently set by sync/synci) to either a
register value or an immediate (selected by alut_src_b), producing the
t field that gets pushed into a channel’s output FIFO alongside
seti/set/waiti/wait.
3.5. Stack
stack.vhd is a single 256-word x 32-bit BRAM-backed stack (8-bit stack
pointer sp, initialized to all-ones/255 on reset – i.e. “empty”; full
is asserted when sp reaches 0). It backs pushi/popi directly, and
is also used internally by loopnz as its decrement-and-test loop
register mechanism is register-file-based rather than stack-based (loopnz
decrements a register, not the stack – the stack itself is only touched by
explicit pushi/popi). This 1 KB stack is the same figure (“256
samples of 32 bits, 1k Byte total”) given in the older public firmware
overview.
3.6. Timed Output Port Mechanism
Each of the 8 output channels has its own 16-deep FIFO (fifo/fifo.vhd)
and its own timed_ictrl.vhd dispatcher instance. When the core executes
seti/set/setbi/setb, it packages an opcode byte, the
computed absolute output time (48 bits), and up to 5 register values (for
set/setb, corresponding to {re,rd,rc,rb,ra}; for seti/
setbi, only one register/immediate matters) into a 216-bit word
(FW = 5*32 + 48 + 8) and pushes it into that channel’s FIFO.
Independently, each channel’s timed_ictrl FSM continuously watches the
free-running master clock (t_cnt) against the timestamp at the head of
its FIFO; when the target time is reached, it drives the channel’s AXI-Stream
output (m1_axis .. m8_axis, 160 bits wide): the low 32 bits carry a
seti/setbi value (zero-padded to 160 bits), while a set/setb
drives the full 160-bit {p4,p3,p2,p1,p0} word. This is the mechanism
behind the “p.seti()”/”p.set()” style output calls referenced in the
older ASM cheatsheet: seti/set are non-blocking (the core only stalls
if the target FIFO is already full), while setbi/setb additionally
block the core until the dispatcher confirms the AXI-Stream write went out
(m_axis_tready).
waiti/wait use the same per-channel FIFO/dispatcher path, but instead
of driving an output, the dispatcher raises a waitt flag once t_cnt
reaches the target time, which the core FSM waits on (WAIT_ACK_ST)
before resuming – i.e. these instructions block core execution until a
given absolute time, without touching an output port.
3.7. Input Ports
4 slave AXIS input channels (s1_axis .. s4_axis at the top level,
64 bits wide) are read into a register with the R-type read instruction.
The oper field selects which half of the 64-bit input word is captured:
oper == 4'b1010 selects the upper 32 bits, any other value selects the
lower 32 bits (din_i = (oper==4'b1010) ? din_mux[63:32] : din_mux[31:0]
in tproc64x32_x8.v).
3.8. Data Memory
Data memory is a dual-port 32-bit BRAM (bram_dp, depth 2DMEM_N).
One port is dedicated to the core’s own memr/memw/memri/memwi
instructions. The other port is shared, through the arbiter in
data_mem/data_mem.v, between:
Single AXI-Lite access – addresses above the 256-byte register window (see 5. Register Map) map directly onto data-memory samples.
Bulk AXI-Stream DMA, gated by
MEM_MODE_REG(0=read from memory tom0_axis, 1=write froms0_axisto memory),MEM_START_REG,MEM_ADDR_REG(start address) andMEM_LEN_REG(sample count); the DMA transaction runs untilLEN_REGsamples have moved (read side) ors_axis_tlastis asserted (write side).
When neither AXI-Lite single access nor an active DMA transaction is in flight, the arbiter is idle and grants no memory port cycles beyond what the core itself uses.
4. Instruction Set
All instructions are 64 bits, in one of three formats (bit-field layout is
transcribed from the header comments in ctrl.sv and cross-checked
against the instructions dict in qick_lib/qick/asm_v1.py):
Format |
Bits |
Remaining fields |
|---|---|---|
I-Type |
opcode |
|
J-Type |
opcode |
|
R-Type |
opcode |
|
Mnemonic |
Format |
Opcode |
Description |
|---|---|---|---|
|
I |
|
Push register |
|
I |
|
Pop the stack into register |
|
I |
|
|
|
I |
|
Schedule register |
|
I |
|
Rebase the internal time-sync reference to immediate |
|
I |
|
Block the core on |
|
I |
|
|
|
I |
|
|
|
I |
|
|
|
I |
|
|
|
I |
|
Blocking variant of |
|
J |
|
If register |
|
J |
|
Jump to |
|
J |
|
Halt: jump to the terminal state until the tProc is restarted. |
|
R |
|
|
|
R |
|
Schedule 5 registers |
|
R |
|
Rebase the internal time-sync reference to register |
|
R |
|
Read input |
|
R |
|
Block the core on |
|
R |
|
|
|
R |
|
|
|
R |
|
|
|
R |
|
Blocking variant of |
Math/bitwise op codes (shared 4-bit field for mathi/math,
bitwi/bitw): +=0b1000, -=0b1001, *=0b1010 (math);
&=0b0000, \|=0b0001, ^=0b0010, ~=0b0011, <<=0b0100,
>>=0b0101 (bitwise). condj condition codes: >=0, >==1,
<=2, <==3, ===4, !==5 (implemented in cond.vhd,
comparing registers ra/rb on the current page).
5. Register Map
The AXI-Lite configuration slave (axi_slv_custom.sv) reserves a fixed
64-word (256-byte) window for registers at the bottom of its address space
(NREG = 64); addresses at or above 256 bytes are forwarded to data
memory instead (byte-address minus 256, converted to a sample address).
Only 6 of the 64 reserved words are actually wired to anything; the rest
read/write as plain, unused storage:
Register |
Word Offset |
Byte Address |
Description |
|---|---|---|---|
|
0 |
0x00 |
Bit 0: 0 = start from |
|
1 |
0x04 |
Bit 0: 0 = init/stopped, 1 = start (edge-triggered: the FSM only
fires on a low-to-high transition, per the Python driver’s
|
|
2 |
0x08 |
Bit 0: 0 = AXIS read (memory -> |
|
3 |
0x0C |
Bit 0: 0 = stopped, 1 = execute the configured AXIS DMA operation. |
|
4 |
0x10 |
Starting data-memory sample address for the AXIS DMA operation. |
|
5 |
0x14 |
Sample count for the AXIS DMA operation. |
Data memory (single-access mode) is addressed starting at byte offset 256
(word offset 64): mem[addr] <-> axi_addr = 256 + 4*addr. The Python
driver’s single_read/single_write implement this directly as
mmio.array[addr + NREG] (NREG=64 words).
6. Python Interface
6.1. Driver (qick.drivers.tproc.AxisTProc64x32_x8)
bindto = ['user.org:user:axis_tproc64x32_x8:1.0', 'QICK:QICK:axis_tproc64x32_x8:1.0']
– this identifies the v1 core in a QICK overlay’s block-design metadata and
is what makes pynq/qick.QickSoc attach this driver class rather than
the v2 driver (Axis_QICK_Proc, bindto matching qick_processor,
documented in tProcessor v2 - reference manual). It is reached as soc.tproc.
from qick import *
soc = QickSoc()
tproc = soc.tproc # AxisTProc64x32_x8 instance
# Load a compiled program (list/array of 64-bit instruction words).
tproc.load_bin_program(binprog, load_mem=True)
# Set the start trigger source, then start.
tproc.start_src("internal")
tproc.start()
# ... program runs in real time on the FPGA fabric ...
# Force-stop by overwriting program memory with `end` instructions.
tproc.reset()
# Restore the program that was running before reset().
tproc.reload_program()
Notable methods:
start()– pulsesstart_reg0 then 1 (edge-triggered; has no effect if the start source is set to external).reset()– force-stops the core by overwriting only the high 32-bit half of every program word (mem.mmio.array[1::2] = 0x3F000000) with theendopcode (0b00111111, matchingctrl.sv’sEND_STencoding), leaving the low half untouched. This is a fast (~1 ms), targeted overwrite rather than a full program-memory clear, and relies on the opcode living in bits63:56of the 64-bit instruction word (i.e. the high 32-bit half when the word is split for a 32-bit-wide memory port).load_bin_program()/reload_program()– write (or rewrite, after areset()) the cached program to program memory via a directmmioarray copy (not DMA – program memory is memory-mapped, unlike data memory).single_read(addr)/single_write(addr, data)– single-sample AXI-Lite access to data memory, offset byNREG(see 5. Register Map).load_dmem(buff_in, addr)/read_dmem(addr, length)– bulk data-memory transfer via the AXI-Lite/DMA arbiter (3.8. Data Memory): configuresmem_mode_reg/mem_addr_reg/mem_len_reg/mem_start_regand drives an AXI DMA channel.start_src(src)–"internal"/"external", setsstart_src_reg.
6.2. Assembler (qick.asm_v1.QickProgram)
QickProgram builds a program as a list of instruction dicts
(self.prog_list), with one Python method auto-generated per entry in the
instructions class dict via __getattr__ – e.g. calling
prog.regwi(page, reg, imm) looks up instructions['regwi'] and calls
append_instruction('regwi', page, reg, imm). This is a thin,
direct-to-bytecode assembler: compile() resolves label() targets and
packs each instruction dict into a 64-bit word per the same bit layout
documented in 4. Instruction Set, then checks the result against
tproccfg['pmem_size'] (read from the driver’s cfg['pmem_size'], i.e.
the true, board-specific memory size – not a hardcoded 8192).
Register allocation is done by _allocate_registers(): each generator or
tProc-controlled-readout channel is assigned a fixed block of registers
(PULSE_REGISTERS, 10 names such as freq/phase/addr/gain/
mode/t) on one of the 8 pages, packing 2 channels per page (3 if
there are more than 15 channels total), with channel 0 alone on page 0 to
leave room for loop/shot counters used by averaging program base classes.
ch_page()/sreg() (and the _ro variants for readout channels) look
up the page and register number for a given channel/parameter name.
safe_regwi() works around the 31-bit immediate limit of the regwi
I-type encoding (4. Instruction Set) by splitting values >= 230
into a regwi + bitwi (shift) + optional mathi (remainder)
sequence.
High-level helpers (set_pulse_registers/pulse, trigger,
measure, sync_all, wait_all, reset_phase, …) build on top
of the raw instructions to implement the same pulse-sequencing API surface
used across QICK’s higher-level QickProgram classes (v1 and v2 share
the AbsQickProgram/AcquireMixin base in qick.qick_asm, but the
instruction-level implementation, register layout, and compiled bytecode
are entirely different between the two).
6.3. Contrast with tProc v2
Where v2’s QickProgramV2 (qick.asm_v2) works with named registers
across three purpose-built banks (r0..r31, s0..s15, w0..w5 –
see the v2 manual’s 4. Complete Register Bank Reference) and a richer instruction set
(division, multiply-accumulate, an LFSR, subroutine calls via a 256-deep PC
stack), v1 programming is lower-level: a single flat, paged 32-register
file with no dedicated special-function registers, arithmetic limited to
add/subtract/multiply and the 6 bitwise ops in 4. Instruction Set, and no
call/return mechanism (only the single shared push/pop stack). Timed output
in v1 always goes through the seti/set/setbi/setb family
described in 3.6. Timed Output Port Mechanism, addressed by output-channel number,
whereas v2 uses named wave/trigger/data ports with a page-based waveform
register bank (r_wave) purpose-built for signal-generator parameters.